Go net/http 超时指导 【已翻译100%】

caotj72,caotj72山野痞夫,山野痞夫sheepbao,sheepbaoimqipanimqipan · · 1217 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

当使用Go开发HTTP服务器或客户端时,超时造成的错误,常常是简单而又微妙的:很多因素都可能产生超时。一个错误可以很长一段时间没有结果,直到网络故障,进程被挂起。

HTTP是一个复杂的多层协议,所以在超时这个问题上,并没有一个通用的解决方案。想一想:流媒体终端、JSON API、Comet终端。事实上,默认值往往不是你想要的。(译注:没理解Comet endpoint是什么意思。原文给出的链接是维基百科上天文意义的彗星。译者怀疑是支持BT协议的BitComet)

在这篇文章中,我将分别介绍,在那些阶段,你可能需要设置一个超时。而且在服务器和客户端上,也将采用不同的方式来处理超时。(译者:本文主要是基于Go标准库进行介绍的。Go标准库在超时定义上提供了很高的灵活性。译者在刚开始用Go开发时,很是被折腾了一把)


caotj72
 翻译得不错哦!

设置最后期限(超时)

首先,你需要理解Go提供的最初级的网络超时实现:Deadlines(最后期限)。

在Go标准库net.Conn中实现了Deadlines,通过 set[Read|Write]Deadline(time.Time)方法进行设置。Deadlines是一个绝对时间,一旦到时,将停止所有I/O操作,并产生一个超时错误。(译注:time.Time的精度是纳秒)

Deadlines本身是不会超时的。一旦被设置,将一直生效(直到再一次调SetDeadline),它并不关心在此期间链接是否存在以及如何使用。因此,你需要在每次进行读/写操作前,使用SetDeadline设定一个超时时长。

实际开发中,你并不需要直接调用SetDeadline,而是在标准库net/http中使用更高层次的超时设置。但需要注意的是,所有基于Deadlines的超时都会被执行,所以不需要在每次收/发操作前,重置超时。(译注:tcp、udp、unix-socket也是如此,参见标准库net)。

服务器超时

HTTP server phases

对于一个部署在Internet上的HTTP服务器来说,设置客户端链接超时,是至关重要的。否则,一个超慢或已消失的客户端,可能会泄漏文件描述符,并最终导致异常。如下所示:

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms
caotj72
 翻译得不错哦!

http.Server提供了两个超时实现ReadTimeoutWriteTimeout。你可以使用显式定义方式来设置它们:

srv := &http.Server{  
    ReadTimeout: 5 * time.Second,
    WriteTimeout: 10 * time.Second,
}
log.Println(srv.ListenAndServe())

ReadTimeout涵盖的时间范围是:从受理一个链接请求开始,到读取一个完整请求报文后结束(HTTP协议的请求报文,可能只有报文头,例如GET,所以,也可以是读取请求报文头后)。是在net/http的Accept方法中,通过调用SetReadline来设置的。

WriteTimeout涵盖的时间范围是:从读取请求报文头后开始,到返回响应报文后结束(也可以称为:ServeHTTP生命周期)。在readRequest方法结束前,通过SetWriteDeadline来设置。

然而,在使用HTTPS连接时,WriteTimeout是在Accept方法中,调用SetWriteDeadline来设置的。因为,它还需要涵盖TLS握手所用的时间。这意味着(仅在此情况下),在使用HTTPS时,WriteTimeout实际上包括了请求报文的获取/等待时间。

当你处理不可信的客户端以及网络时,应该将两种超时都设置上。以此来避免,一个客户端,因超慢的读/写操作,长时间占用一个链接资源。

最后是http.TimeoutHandler。它不是一个Server参数,但一个Handler封装,会用它来限制ServeHTTP调用的时长。当达到超时条件时,将缓存响应数据,并发送一个504 Gateway Timeout 。注意,1.6版本存在问题,1.6.2中被修复。

caotj72
 翻译得不错哦!

http.ListenAndServe的问题

不幸的是, http.ListenAndServe, http.ListenAndServeTLS及http.Serveare等经由http.Server的便利函数不太适合用于对外发布网络服务。
因为这些函数默认关闭了超时设置,也无法手动设置。使用这些函数,将很快泄露连接,然后耗尽文件描述符。对于这点,我至少犯了6次以上这样的错误。
对此,你应该使用http.server!在创建http.server实例的时候,调用相应的方法指定ReadTimeout(读取超时时间)和WriteTimeout(写超时时间),在以下会有一些案例。

关于流

比较恼火的是没法从ServerHttp访问net.Conn包下的对象,所以一个服务器想要响应一个流就必须解除WriteTimeout设置(这就是为什么默认值是0的原因)。因为访问不到net.Conn包,就无法在每个Write操作之前调用SetWriteDeadline设置一个合理的闲置超时时间。

imqipan
 翻译得不错哦!

同理,由于无法确认ResponseWriter.Close支持并发写操作,所以ResponseWriter.Write可能产生的阻塞,并且是无法被取消的。

(译者注:Go 1.6.2版本中 ,接口ResponseWriter定义中是没有Close方法的,需要在接口实现中自行实现。揣测是作者在开发中实现过该方法)

令人遗憾的是,这意味着流媒体服务器面对一个低速客户端时,将无法有效保障自身的效率、稳定。

我已经提交了一些建议,并期待有所反馈。

客户端超时

HTTP Client phases

客户端超时,取决于你的决策,可以很简单,也可以很复杂。但同样重要的是:要防止资源泄漏和阻塞。

最简单的使用超时的方式是http.Client。它涵盖整个交互过程,从发起连接到接收响应报文结束。

c := &http.Client{  
    Timeout: 15 * time.Second,
}
resp, err := c.Get("https://blog.filippo.io/")

与服务端情况类似,使用http.Get等包级易用函数创建客户端时,也无法设置超时。应用在开放网络环境中,存在很大的风险。

caotj72
 翻译得不错哦!

还有其它一些方法,可以让你进行更精细的超时控制:

  • net.Dialer.Timeout 限制创建一个TCP连接使用的时间(如果需要一个新的链接)

  • http.Transport.TLSHandshakeTimeout 限制TLS握手使用的时间

  • http.Transport.ResponseHeaderTimeout 限制读取响应报文头使用的时间

  • http.Transport.ExpectContinueTimeout 限制客户端在发送一个包含:100-continue的http报文头后,等待收到一个go-ahead响应报文所用的时间。在1.6中,此设置对HTTP/2无效。(在1.6.2中提供了一个特定的封装DefaultTransport)

c := &http.Client{  
    Transport: &Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

据我了解,尚没有限制发送请求使用时间的机制。目前的解决方案是,在客户端方法返回后,通过time.Timer来个手工控制读取请求信息的时间(参见下面的“如何取消请求”)。

最后,在新的1.7版本中,提供了http.Transport.IdleConnTimeout。它用于控制一个闲置连接在连接池中的保留时间,而不考虑一个客户端请求被阻塞在哪个阶段。

注意,客户端将使用默认的重定向机制。由于http.Transport是一个底层的系统机制,没有重定向概念,因此http.Client.Timeout涵盖了用于重定向花费的时间,而更精细的超时控,可以根据请求的不同,进行定制。

caotj72
 翻译得不错哦!

Cancel 和 Context

net/http提供了两种用于撤销客户端请求的方法:Request.Cancel以及新的1.7版本中提供的Context。

Request.Cancel是一个可选channel。在Request.Timeout被触发时,Request.Cancel将被设置并关闭,进而促使请求中断(基本上“撤销”都采用相同的机制,在写此文时,我发现一个1.7中的bug,所有的撤销操作,都会当作一个超时错误返回)。

我们可以使用Request.Cancel和time.Timer,来构建一个超时更可控的,可用于流媒体的客户端。它可以在成功获响应报文体(Body)的部分数据后,重置deadline。

package main

import (  
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "time"
)

func main() {  
    c := make(chan struct{})
    timer := time.AfterFunc(5*time.Second, func() {
        close(c)
    })

        // Serve 256 bytes every second.
    req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)
    if err != nil {
        log.Fatal(err)
    }
    req.Cancel = c

    log.Println("Sending request...")
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    log.Println("Reading body...")
    for {
        timer.Reset(2 * time.Second)
                // Try instead: timer.Reset(50 * time.Millisecond)
        _, err = io.CopyN(ioutil.Discard, resp.Body, 256)
        if err == io.EOF {
            break
        } else if err != nil {
            log.Fatal(err)
        }
    }
}

在上面这个例子中,我们在请求阶段,设置了一个5秒钟的超时。但读取响应报文阶段,我们需要读8次,至少8秒钟的时间。每次读操作,设置2秒钟的超时。采用这样的机制,我们可以无限制的获取流媒体,而不用担心阻塞的风险。如果我们没有在2秒钟内读取到任何数据,io.CopyN将返回错误信息: net/http: request canceled。

在1.7版本标准库中的新增了context包。关于Contexts,我们有大量需要学习的东西。基于本文的主旨,你首先应该知道的是:Contexts将替代Request.Cancel,不再建议(反对)使用Request.Cancel。

caotj72
 翻译得不错哦!

为了使用Contexts来撤销一个请求,我们需要创建一个新的Context以及它的基于context.WithCancel的cancel()函数,同时还有创建一个基于Request.WithContext的Request。当我们要撤销一个请求时,我们其实际是通过cancel()函数撤销相应的Context(取代原有的关闭Cancel channel的方式):

ctx, cancel := context.WithCancel(context.TODO())  
timer := time.AfterFunc(5*time.Second, func() {  
    cancel()
})

req, err := http.NewRequest("GET", "http://httpbin.org/range/2048?duration=8&chunk_size=256", nil)  
if err != nil {  
    log.Fatal(err)
}
req = req.WithContext(ctx)

在上下文(我们提供给context.WithCancel的)已经被撤销的情况下,Contexts更具有优势。我们可以向整个管道发送命令。

就这些了。希望你对ReadDeadline理解比我更深刻。

caotj72
 翻译得不错哦!

有疑问加站长微信联系(非本文作者)

本文来自:开源中国翻译

感谢作者:caotj72,caotj72山野痞夫,山野痞夫sheepbao,sheepbaoimqipanimqipan

查看原文:Go net/http 超时指导 【已翻译100%】

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1217 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传