不要使用默认的HTTP client和 HTTP server
Go标准库中的http包提供了HTTP客户端和服务器实现。但是,开发人员很容易犯一个常见错误:最终部署到生产环境中的应用程序的上下文依赖于默认实现。本文将分析这会产生什么问题以及如何解决。
HTTP Client
首先,来看看HTTP客户端默认值的含义,这里以GET请求为例进行说明。客户端默认值就是创建一个http.Client的零值,像下面的程序,初始化时没有设置任何参数。
代码语言:javascript复制client := &http.Client{}
resp, err := client.Get("https://golang.org/")
或者直接使用http.Get方法进行请求
代码语言:javascript复制resp, err := http.Get("https://golang.org/")
这两种Get请求本质实现是一样的,像http.Get这样底层使用的是http.DefaultClient,它也是基于http.Client创建的一个零值对象。
代码语言:javascript复制// DefaultClient is the default Client and is used by Get, Head, and Post.
var DefaultClient = &Client{}
使用默认的HTTP客户端有什么问题吗?首先,默认客户端没有设置任何超时时长,在生产环境中没有超时限制是可怕的,可能会导致很多问题。例如无止境的请求可能耗尽系统资源。在深入研究请求超时问题之前,让我们先来回顾一下HTTP请求中涉及的五个步骤:
- 建立TCP连接
- 进行TLS握手(如果开启)
- 发送请求
- 读取响应消息头
- 读取响应消息体
下面这幅图描述了上面5个步骤与客户端超时参数的关系:
图中四个超时参数含义如下:
- net.Dialer.Timeout:等待建立TCP连接的最长时间
- http.Transport.TLSHandshakeTimeout:等待TLS握手的最长时间
- http.Transport.ResponseHeaderTimeout:等待服务器响应消息头的时间
- http.Client.Timeout:整个请求的时间,包含建立TCP连接、进行TLS握手、发送请求、等待响应消息头和消息体的时间。
「NOTE: http请求返回的第二参数error表示未能(按预期时间)收到服务端的响应,此错误来自对消息头的处理,因为等待读取响应消息头是等待响应的第一步。如果设置了http.Client.Timeout, 等待响应消息头时间过长时会遇到如下错误提示」
代码语言:javascript复制net/http: request canceled (Client.Timeout exceeded while awaiting headers)
下面是设置了四个超时时间的一个客户端程序示例,该客户端建立TCP连接、TLS握手和读取响应头的设置的超时时间均为1秒,每个请求总的超时时间为5秒。
代码语言:javascript复制client := &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: time.Second,
}).DialContext,
TLSHandshakeTimeout: time.Second,
ResponseHeaderTimeout: time.Second,
},
}
默认情况下,HTTP客户端带有连接池行为。可以重用客户端连接,通过设置http.Transport.DisableKeepAlives为true可以禁用重用功能。此外,还有一个额外的超时来指定空闲连接在连接池中保留的时间,该时间由http.Transport.IdleConnTimeout控制,默认值为90秒,意味着此期间内连接可以被其他请求重用,在90之后如果连接没有被重用,它将被关闭。
如果想要配置连接池中的参数,我们需要重新设置http.Transport.MaxIdleConns,通过下面的程序可以看到该参数的默认为100. 此外,还有一个参数需要注意,它就是http.Transport.MaxIdleConnsPerHost,该参数表示每个host的连接池最大空闲连接数,默认值为2,即如果向同一台主机触发100个请求,之后只有2个连接将保留在连接池中。因此,如果再次触发100个请求,将不得不重新建立至少98个连接。在生产级程序中,我们需要注意该参数配置,因为它会影响平均延迟当同一个主机存在大量并行请求时。
代码语言:javascript复制var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// DefaultMaxIdleConnsPerHost is the default value of Transport's
// MaxIdleConnsPerHost.
const DefaultMaxIdleConnsPerHost = 2
总结,对于生产级程序,我们通常会重新设置上述连接池参数,不会使用标准库中提供的默认值。同时需要注意,调整这些与连接池相关的参数会对延迟产生重大影响,所以在设置时要小心,需要设置合理的值。
HTTP Server
在实现HTTP服务器时,我们也应该小心谨慎。默认的HTTP server可以通过http.Server创建,代码如下:
代码语言:javascript复制server := &http.Server{}
server.Serve(listener)
HTTP Server也可以通过一些函数创建,像http.Serve、http.ListenAndServe和http.ListenAndServeTLS. 这些函数内部实现也是依赖于默认的http.Server.
接收客户端连接后,HTTP响应分为五个步骤:
- 等待客户端发生请求
- TLS握手(如果启用)
- 读取请求头(http header)
- 读取请求正文(http body)
- 写回复内容
「NOTE: 不必对已建立的连接重复TLS握手过程」
下面这幅图描述了上面步骤中与服务器超时参数的关系:
三个主要的超时参数/函数及含义如下:
- http.Server.ReadHeaderTimeout: 该参数表示读取请求头的最长时间。
- http.Server.ReadTimeout: 该参数表示读取整个请求的最长时间(包括等待客户端发送请求、TLS握手、读取请求头和请求正文)
- http.TimeoutHandler: 该函数是对handler的一个封装,表示处理程序完成读取请求正文和写回复内容的最长时间。
注意这三个参数/函数中的最后一个,它不是服务器参数,只是handler的一个封装,用于限制http处理请求的最长时间。如果处理程序未能按时响应,服务器将回复特定的503 Service Unavailable 消息。同时,传递给处理程序的上下文将被取消。
「NOTE: 上文省略了对http.Server.WriteTimeout 的介绍,因为在Go1.8版本中http.TimeoutHandler发布之后,该参数不是必须要设置的字段。实际上,http.Server.WriteTimeout在使用上有一些问题。首先,它的行为取决于是否启用了TLS, 使得它的理解和使用更加复杂。其次,如果达到超时时间,它会关闭TCP连接而不返回正确的HTTP状态码。此外,它不会将传递给处理程序的上下文取消,这会导致处理程序在不知道TCP连接已经关闭的情况下继续执行。」
如果我们的服务器需要接收来自不受信任的客户端连接时,最佳实践是至少要设置http.Server.ReadHeaderTimeout参数并使用http.TimeoutHandler包装函数。否则,如果客户端可能会利用它并创建大量的连接,从而耗尽服务器资源。
下面是一个设置带有超时服务器的程序示例,通过http.TimeoutHandler包装业务处理程序。在上面这个服务器中,如果处理程序在1秒内没有响应,将会返回HTTP 503状态码。
代码语言:javascript复制s := &http.Server{
Addr: ":8080",
ReadHeaderTimeout: 500 * time.Millisecond,
ReadTimeout: 500 * time.Millisecond,
Handler: http.TimeoutHandler(handler, time.Second, "foo"),
}
此外,同客户端配置一样,我们也可以对服务器配置启用keep-alives设置等待下一个请求的最长时间,具体通过http.Server.IdleTimeout参数进行设置。注意,如果没有设置http.Server.IdleTimeout,则会使用http.Server.ReadTimeout的值作为空闲超时时间。如果这两个参数都没有设置,则不会有任何超时,并且连接将保持打开状态,直到它们被客户端关闭。
代码语言:javascript复制s := &http.Server{
// ...
IdleTimeout: time.Second,
}
总结,对于生产级应用程序,我们不要使用默认的HTTP Client和 HTTP Server. 否则,由于没有设置超时,恶意用户利用服务器没有设置超时这个漏洞,可能会导致服务器卡住无法继续提供服务。