37

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

 4 years ago
source link: https://studygolang.com/articles/26359
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

YjeqM3j.png!web

问题

在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 的文章。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK