Go HttpServer 最佳实践

发布于 2023-08-21 08:42:40 字数 10254 浏览 43 评论 0

这是 Cloudflare 的 Filippo Valsorda 2016 年发表在 Gopher Academy 的一篇 文章 , 虽然过去两年了,但是依然很有意义。​

先前 crypto/tls 太慢而 net/http 也很年轻, 所以对于 Go web server 来说, 通常我们明智的做法把它放在反向代理的后面, 如 nginx 等,现在不需要了。在 Cloudflare 我们最近试验了直接暴漏纯 Go 的服务作为主机。 Go 1.8 的 net/httpcrypto/tls 提供了稳定的、高性能并且灵活的功能。

然后,需要做一些调优的工作,本文我们将展示怎么去调优和使 web 服务器更稳定。

crypto/tls

2016 年了,你不会再运行一个不加密的 HTTP Server,所以你需要 crypto/tls 。好消息使这个库已经非常 了(我们的 测试 ),目前他的安全攻击追踪也很优秀。

缺省配置是使用 Mozilla 参考 中的中级推荐配置,但是 你仍然应该设置 PreferServerCipherSuites 以确保采用更快更安全的密码库, CurvePreferences 避免未优化的曲线。 客户端如果使用 CurveP384 算法回导致我们的机器多达 1 秒的 cpu 消耗。

&tls.Config{
	// Causes servers to use Go's default ciphersuite preferences,
	// which are tuned to avoid attacks. Does nothing on clients.
	PreferServerCipherSuites: true,
	// Only use curves which have assembly implementations
	CurvePreferences: []tls.CurveID{
		tls.CurveP256,
		tls.X25519, // Go 1.8 only
	},
}

如果你想配置兼容性, 你可以设置 MinVersionCipherSuites

MinVersion: tls.VersionTLS12,
CipherSuites: []uint16{
	tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
	tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, // Go 1.8 only
	tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,   // Go 1.8 only
	tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
	tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	// Best disabled, as they don't provide Forward Secrecy,
	// but might be necessary for some clients
	// tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
	// tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
   },

注意 Go 的 CBC 加密套件的实现(上面我们禁用了)很容易收到 Lucky13 攻击 , 即使 Go 1.8 实现了部分的 处理

最后需要注意的是, 所有这些建议仅适用 amd64 架构因为它可以实现快速的常数级的 加密原语 (AES-GCM, ChaCha20-Poly1305, P256), 其它架构可能不适合产品级应用。既然是服务要暴漏带互联网上, 它需要一个公开的可信的证书。通过 Let’s Encrypt 很容易申请, 可以使用 golang.org/x/crypto/acme/autocertGetCertificate 函数。

不要忘了将 HTTP 重定向到 HTTPS, 如果你的客户端是浏览器的话,可以考虑 HSTS

srv := &http.Server{
	ReadTimeout:  5 * time.Second,
	WriteTimeout: 5 * time.Second,
	Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		w.Header().Set("Connection", "close")
		url := "https://" + req.Host + req.URL.String()
		http.Redirect(w, req, url, http.StatusMovedPermanently)
	}),
}
go func() { log.Fatal(srv.ListenAndServe()) }()

你可以使用 SSL Labs test 检查配置是否正确。

net/http

net/http 包含 HTTP/1.1HTTP/2 。你一定已经熟悉了 Handler 的开发,所以本文不讨论它。我们讨论服务器端背后的一些场景。

Timeout

超时可能是最容易忽略的危险的场景。你的服务可能在受控网络中幸免于难,但是在互联网上就不会那么幸运了, 特别是(不仅仅)受到恶意攻击。有一系列的资源需要超时控制。尽管 goroutine 消耗很少,但文件描述符总是有限的。卡住的连接、不工作的连接甚至恶意断掉的连接不应该消耗它们。

一个超过最大文件符的服务器总是不能接受新的连接, 会报下面的失败:

http: Accept error: accept tcp [::]:80: accept: too many open files; retrying in 1s

一个缺省的 http.Server , 、就像包文档中的例子 http.ListenAndServehttp.ListenAndServeTLS , 没有设置任何超时控制, 你肯定不是你想要的。

http.Server 有三个参数控制 timeout: ReadTimeout , WriteTimeoutIdleTimeout ,你可以显示地设置它们:

srv := &http.Server{
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
    TLSConfig:    tlsConfig,
    Handler:      serveMux,
}
log.Println(srv.ListenAndServeTLS("", ""))

ReadTimeout 的时间范围起自连接备接受,止于请求的 body 完全读出。在 net/http 的实现中它在连接 Accept 后通过 SetReadDeadline 设置

ReadTimeout 最大的问题它不允许服务器给客户端更多的时间去请求的 body stream。 go 1.8 新引入了一个参数 ReadHeaderTimeout ,它止于读完请求头。然后一直有一些不清楚的方式去设置读超时,相关的设计讨论可以参考 #16100

WriteTimeout 超时正常起自读完请求头, 止于 response 写完(也就是 ServeHTTP 的生命周期), 通过 readRequest结尾 处的 SetWriteDeadline 设置。

然后,当通过 HTTPS 连接时, SetWriteDeadlineAccept 后立即设置, 所以它也包含 TLS 握手的 packet 的写。讨厌的是,这意味着 WriteTimeout 包含 http 头的读以及第一个字节的等待。

ReadTimeoutWriteTimeout 是绝对值,无法在 Handler 中更改它( #16100 )。

Go 1.8 还新引入了 IdleTimeout 参数, 用来限制服务端 Keep-Alive 连接在重用前 idle 的数量。

Go 1.8 之前的版本, ReadTimeout 在请求完成后又立即开始滴答(tick),这对 Keep-Alive 连接是不合适的: idle time 会消耗客户端允许发送请求的时间,导致一些快的客户端会有不期望的超时。

对于不可信的客户端和网络,你应该设置 Read , WriteIdle 超时, 这样一个读或者写很慢的客户端不会长时间占用一个连接。

对于 go 1.8 之前的 HTTP/1.1 超时的背景知识, 你可以参考 Cloudflare 的 博客

HTTP/2

HTTP/2 在 Go 1.6+中回自动启用, 只要它满足下面的条件:

  • 请求通过 TLS/HTTPS
  • Server.TLSNextProto 为 nil (如果设置一个空的 map,则禁止 HTTP/2)
  • Server.TLSConfig 已被设置, ListenAndServeTLS 被调用或者下一条
  • Serve 被调用,并且 tls.Config.NextProtos 包含 h2 (比如[]string{"h2", "http/1.1")

HTTP/2 和 HTTP/1.1 有些不同,因为同一个连接同时会服务多个请求,但是 Go 抽象了统一的超时控制接口。

遗憾的是, Go 1.7 中的 ReadTimeout 会打断 HTTP/2 连接,它不会为每一个连接重置,而是在连接初次建立时就设置而不会重置,当超时后就会断掉 HTTP/2 连接。 Go 1.8 修复了这个 问题

基于此和 ReadTimeout 的 idle time 问题,我强烈建议你尽快升级到 1.8。

TCP Keep-Alives

如果你使用 ListenAndServe (与传入 net.ListenerServe 不同,这个方法使用缺省值提供了零保护措施), 3 分钟的 TCP Keep-Alive 会 自动设置 ,它会让彻底消失的 client 有机会放弃连接, 我的经验是不要完全相信它, 无论如何也要设置超时。

首先, 3 分钟太长了,你可以使用你自己的 tcpKeepAliveListener 调整它。

更重要的是, Keep-Alive 只是保证 client 还活着,但不会设置连接存活的上限。恶意攻击的客户端会打开非常多的连接,导致你的服务器打开很多文件描述符, 通过未完成的请求, 会导致你的服务拒绝服务。

最后,我的经验是连接往往会导致泄漏,知道 超时起作用

ServeMux

包级别的 http.Handle[Func] (和你的 web 框架)注册 handler 到全局的 http.DefaultServeMux , 如果 Server.Handler 是 nil 的话, 你应该避免这样做。

任何你输入的包,不管是直接的还是间接的,都可以访问 http.DefaultServeMux ,可能会注册你不期望的 route。例如,包依赖中有任何一个库导入了 net/http/pprof ,客户端都能得到你的应用的 CPU 的 profile。 你可以使用 net/http/pprof 手工注册。

正确的是, 初始化你自己的 http.ServeMux ,把 handler 注册到它的上面, 设置它为 Server.Handler , 或者设置你自己的 web 框架为 Server.Handler

Logging

net/http 在调用你的 handler 之前做了大量的工作, 比如 接受连接 https://github.com/golang/go/blob/1106512db54fc2736c7a9a67dd553fc9e1fca742/src/net/http/server.go#L2631-L2653, TLS 握手 等等……

当任何一个步骤出错,它会写一行日志到 Server.ErrorLog 。其中一些错误, 比如超时和连接重置, 在互联网上是正常的。你可以连接大部分错误并把它们加入到 metric 中,这要归功于这个保证:

Each logging operation makes a single call to the Writer’s Write method.

如果在 handler 中你不想输出堆栈 log, 你可以使用 panic(nil) 或者使用 Go 1.8 的 panic(http.ErrAbortHandler)

Metrics

metric 可以帮助你监控打开的文件描述符。 Prometheus 使用 proc 文件系统来帮助你完成这些

如果你需要调研泄漏问题, 你可以使用 Server.ConnState 钩子来得到更多的连接的细节 metric。注意,不保持 state 就没有方式能保持一个正确的 StateActive 数量,所以你需要维护一个 map[net.Conn]ConnState

结论

使用 Nginx 做 Go 服务前端的日志一去不复返了, 但是面对互联网你仍然需要做一些额外的防护措施, 可能需要升级到新的 Go 1.8 版本。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

扎心

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

内心激荡

文章 0 评论 0

JSmiles

文章 0 评论 0

左秋

文章 0 评论 0

迪街小绵羊

文章 0 评论 0

瞳孔里扚悲伤

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文