[译] 初学者需要了解的Go语言中的HTTP timeout

1,127 阅读3分钟

原文链接 itnext.io/http-reques…

​ 对于提高分布式系统的可用性,请求超时是非常重要的一个部分,当系统某个部分出现故障时超时机制可以降低故障对整个分布式系统的影响,就如下面这条twitter中提到的。

问题

在go语言中应该如何合理的模拟一个504 http.StatusGatewayTimeout响应呢?

之前在开发一个OAuth token授权功能的时候,我曾试着用httptest去模拟服务端超时并返回504 http.StatusGatewayTimeout响应,然而我实现的效果却是客户端由于没有在设定的时间内得到响应而超时退出,而不是服务端返回了504的status code。如同大多数,当时我像下面这样使用标准库的HTTP包去创建一个client对象并指定timeout属性:

client := http.Client{Timeout: 5 * time.Second}

需要发起http请求时,创建上面这样一个http client对象看起来是一个非常简单和直接的方式。然而很多关于请求超时的细节被忽视了,包括客户端超时、服务端超时和负载均衡器的超时。

客户端超时

在客户端,http请求超时有多种不同的定义方式,取决于你关注整个请求-响应周期的那个部分。具体说来,一个完整的请求-响应周期由Dialer(三次握手), TLS握手, 请求头及请求体的生成和发送,响应头及响应体的接收。除了定义一个完整的请求-响应周期的超时时间之外,go语言还支持定义这个周期的某个组成部分的超时时间,有如下三个常用的方式:

  • http.Client
  • context
  • http.Transport

http.Client

通过http.Client可以定义从三次握手(Dialer)到接收到响应体的一个完整的请求-响应周期的超时时间。http.Client结构有一个可选的类型为time.DurationTimeout字段

client := http.Client{Timeout: 5 * time.Second}

Context

go语言的context包提供了WithTimeout, WithDeadline, WithCancel三个实用的方法分别去实现具有超时时间的,具有过期时间的和可以手动取消的http请求。使用context包的WithTimeout方法,配合上http.Request对象的WithContext方法,我们可以控制从请求发送到到手响应之间超时时间(不包括TCP三次握手和TLS握手的耗时):

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
    t.Error("Request error", err)
}

resp, err := http.DefaultClient.Do(req.WithContext(ctx))

http.Transport

通过使用自定义的http.Transport并指定DialContext属性来创建http.Client对象,可以控制Dialer的超时时间(即三次握手的超时时间):

transport := &http.Transport{
    DialContext: (&net.Dialer{   
        Timeout: timeout,
    }).DialContext,
}
client := http.Client{Transport: transport}

解决方案

基于上面的问题分析和可选方案,我尝试通过context.WithTimeout来控制 http.Request的超时时间。然而得到了如下的error:

client_test.go:40: Response error Get http://127.0.0.1:49597: context deadline exceeded

这并没有解决我的问题,因为我想实现服务端返回504 http.StatusGatewayTimout的响应。

服务端超时

上述在客户端使用context.WithTimeout()的方案,当设定的时间内没有完成请求-响应时,客户端发起http请求的方法终止并且返回了一个error,而不是我想要的服务端返回了504的http status code。

通过下面的方式可以让httptest server每次都返回超时的状态码:

httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request){
    w.WriteHeader(http.StatusGatewayTimeout)
}))

然而如果想让服务端在处理客户端请求超时时返回504 status code,我们可以在服务端程序里用http.TimeoutHandler去装饰一下原本的handler来实现:

func TestClientTimeout(t *testing.T) {
    handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        d := map[string]interface{}{
            "id":    "12",
            "scope": "test-scope",
        }

        time.Sleep(100 * time.Millisecond) //<- Any value > 20ms
        b, err:= json.Marshal(d)
        if err != nil {
            t.Error(err)
        }
        io.WriteString(w, string(b))
        w.WriteHeader(http.StatusOK)
    })

    backend := httptest.NewServer(http.TimeoutHandler(handlerFunc, 20*time.Millisecond, "server timeout"))

    url := backend.URL
    req, err := http.NewRequest("GET", url, nil)
    if err != nil {
        t.Error("Request error", err)
        return
    }

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        t.Error("Response error", err)
        return
    }

    defer resp.Body.Close()
}

对于刚接触go语言的gopher来说,理解这些上层的http timeout的工作原理非常有用!如果你想了解更多go语言中关于http timeout的细节,一定要读一下这篇来自Cloudflare的文章。