对于提高分布式系统的可用性, 请求超时是非常重要的一个部分, 当系统某个部分出现故障的时超时机制可以降低故障对整个分布式系统的影响, 就如下面这条 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 https://godoc.org/net/http#Client
通过 http.Client 可以定义从三次握手 (Dialer) 到接收到响应体的一个完整的请求 - 响应周期的超时时间. http.Client 结构有一个可选的类型为 time.Duration 的 Timeout 字段
client := http.Client{Timeout: 5 * time.Second}
Context https://godoc.org/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 https://godoc.org/net/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 的文章.
来源: http://www.tuicool.com/articles/aU7zIrn