Go语言中常见100问题-#81 Using the default HTTP client and server

2022-08-15 15:29:58 浏览数 (1)

不要使用默认的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. 否则,由于没有设置超时,恶意用户利用服务器没有设置超时这个漏洞,可能会导致服务器卡住无法继续提供服务。

0 人点赞