Go net 超时处理

这篇文章详细介绍了,net/http包中对应HTTP的各个阶段,如何使用timeout来进行读/写超时控制以及服务端和客户端支持设置的timeout类型。本质上,这些timeout都是代码层面对各个函数设置的处理时间。好比,读取客户端读取请求头、读取响应体的时间,本质上都是响应函数的超时时间。git

做者强烈不建议,在工做中使用net/http包上层封装的默认方法(没有明确设置timeout),很容易出现系统文件套接字被耗尽等使人悲伤的状况。好比:github

// 相信工做中也不会出现这样的代码
func main() {
    http.ListenAndServe("127.0.0.1:3900", nil)
}

正文

在使用Go开发HTTP Serverclient的过程当中,指定timeout很常见,但也很容易犯错。timeout错误通常还不容易被发现,可能只有当系统出现请求超时、服务挂起时,错误才被严肃暴露出来。golang

HTTP是一个复杂的多阶段协议,因此也不存在一个timeout值适用于全部场景。想一下StreamingEndpointJSON APIComet, 不少状况下,默认值根本不是咱们所须要的值。api

这篇博客中,我会对HTTP请求的各个阶段进行拆分,列举可能须要设置的timeout值。而后从客户端和服务端的角度,分析它们设置timeout的不一样方式。缓存

SetDeadline

首先,你须要知道Go所暴露出来的,用于实现timeout的方法:Deadline网络

timeout自己经过 net.Conn包中的Set[Read|Write]Deadline(time.Time)方法来控制。Deadline是一个绝对的时间点,当链接的I/O操做超过这个时间点而没有完成时,便会由于超时失败。app

Deadlines不一样于timeouts. 对一个链接而言,设置Deadline以后,除非你从新调用SetDeadline,不然这个Deadline不会变化。前面也提了,Deadline是一个绝对的时间点。所以,若是要经过SetDeadline来设置timeout,就不得不在每次执行Read/Write前从新调用它。tcp

你可能并不想直接调用SetDeadline方法,而是选择 net/http提供的更上层的方法。但你要时刻记住:全部timeout操做都是经过设置Deadline实现的。每次调用,它们并不会去重置的deadlineide

Server Timeouts

关于服务端超时,这篇帖子So you want to expose Go on the Internet也介绍了不少信息,特别是关于HTTP/2Go 1.7 bugs的部分.函数

HTTP server phases

对于服务端而言,指定timeout相当重要。不然,一些请求很慢或消失的客户端极可能致使系统文件描述符泄漏,最终服务端报错:

http: Accept error: accept tcp [::]:80: accept4: too many open files; retrying in 5ms

在建立http.Server的时候,能够经过ReadTimeoutWriteTimeout来设置超时。你须要明确的声明它们:

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

ReadTimeout指从链接被Accept开始,到request body被彻底读取结束(若是读取body的话,不然是读取完header头的时间)。内部是net/http经过在Accept调用SetReadDeadline实现的。

WriteTimeout通常指从读取完header头以后到写完response的时间(又称ServerHTTP的处理时间),内部经过在 readRequest以后调用SetWriteDeadline实现。

然而,若是是HTTPS的话,SetWriteDeadline方法在Accept后就被调用,因此TLS handshake也是WriteTimeout的一部分。同时,这也意味着(仅仅HTTPSWriteTimeout包括了读header头以及握手的时间。

为了不不信任的client端或者网络链接的影响,你应该同时设置这两个值,来保证链接不被client长时间占用。

最后,介绍一下http.TimeoutHandler,它并非一个Server属性,它被用来Wrap http.Handler ,限制Handler处理请求的时长。它主要依赖缓存的response来工做,当超时发生时,响应503 Service Unavailable的错误。它在1.6存在问题,在1.6.2进行了修复

http.ListenAndServe is doing it wrong

顺带说一句,这也意味:使用一些内部封装http.Server的包函数,好比http.ListenAndServe, http.ListenAndServeTLS以及http.Serve是不正规的,尤为是直接面向外网提供服务的场合。

这种方法默认缺省配置timeout值,也没有提供配置timeout的功能。若是你使用它们,可能就会面临链接泄漏和文件描述符耗尽的风险。我也好几回犯过这样的错误。

相反的,建立一个http.Server应该像文章开头例子中那样,明确设置ReadTimeoutWriteTimeout,并使用相应的方法来使server更完善。

About streaming

Very annoyingly, there is no way of accessing the underlying net.Conn from ServeHTTPso a server that intends to stream a response is forced to unset the WriteTimeout (which is also possibly why they are 0 by default). This is because without net.Conn access, there is no way of calling SetWriteDeadline before each Write to implement a proper idle (not absolute) timeout.

Also, there's no way to cancel a blocked ResponseWriter.Write since ResponseWriter.Close (which you can access via an interface upgrade) is not documented to unblock a concurrent Write. So there's no way to build a timeout manually with a Timer, either.

Sadly, this means that streaming servers can't really defend themselves from a slow-reading client.

I submitted an issue with some proposals, and I welcome feedback there.

Client Timeouts

HTTP Client phases

client端的timeout能够很简单,也能够很复杂,这彻底取决于你如何使用。但对于阻止内存泄漏或长时间链接占用的问题上,相对于Server端来讲,它一样特别重要。

下面是使用http.Client指定timeout的最简单例子。timeout覆盖了整个请求的时间:从Dial(若是非链接重用)到读取response body

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

像上面列举的那些server端方法同样,client端也封装了相似的方法,好比http.Get。他内部用的就是一个没有设置超时时间的Client

下面提供了不少类型的timeout,可让你更精细的控制超时:

  • net.Dialer.Timeout用于限制创建TCP链接的时间,包括域名解析的时间在内(若是须要建立的话)
  • http.Transport.TLSHandshakeTimeout用于限制TLS握手的时间
  • http.Transport.ResponseHeaderTimeout用于限制读取响应头的时间(不包括读取response body的时间)
  • http.Transport.ExpectContinueTimeout用于限制从客户端在发送包含Expect: 100-continue请求头开始,到接收到响应去继续发送post data的间隔时间。注意:在1.6 HTTP/2 不支持这个设置(DefaultTransport从1.6.2起是一个例外 1.6.2).
c := &http.Client{
    Transport: &http.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来实现,由于这个过程是在client方法返回以后发生的(后面介绍如何取消一个请求)。

最后,在1.7的版本中增长了http.Transport.IdleConnTimeout,用于限制链接池中空闲连持的存活时间。它不能用于控制阻塞阶段的客户端请求,

注:客户端默认执行请求重定向(302等)。能够为每一个请求指定细粒度的超时时间,其中http.Client.Timeout包括了重定向在内的请求花费的所有时间。而http.Transport是一个底层对象,没有跳转的概念。

Cancel and Context

net/http 提供了两种取消客户端请求的方法: Request.Cancel以及在1.7版本中引入的Context.

Request.Cancel是一个可选的channel,若是设置了它,即可以经过关闭该channel来终止请求,就跟请求超时了同样(它们的实现机制是相同的。在写这篇博客的时候,我还发现了一个 1.7的 bug :全部被取消请求,返回的都是timeout超时错误)。

type Request struct {

    // Cancel is an optional channel whose closure indicates that the client
    // request should be regarded as canceled. Not all implementations of
    // RoundTripper may support Cancel.
    //
    // For server requests, this field is not applicable.
    //
    // Deprecated: Use the Context and WithContext methods
    // instead. If a Request's Cancel field and context are both
    // set, it is undefined whether Cancel is respected.
    Cancel <-chan struct{}
}

咱们可结合Request.Canceltime.Timertimeout进行更细的控制。好比,在咱们每次从response body中读取数据后,延长timeout的时间。

package main

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

func main() {
    
    //定义一个timer:5s后取消该请求,即关闭该channel
    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

    //执行请求,请求的时间不该该超过5s
    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)
        }
    }
}

上述例子中,咱们给Do设置了5s的超时,经过后续8个循环来读取response body的内容,这个操做至少花费了8s的时间。每次read的操做均设置了2s的超时。咱们能够持续这样读,不须要考虑任何阻塞的风险。若是在2s内没有接受到数据,io.CopyN将会返回net/http: request canceled

1.7的版本中context被引入到了标注库,此处是一些介绍。接下来咱们用它来替换 Request.Cancel,实现相同的功能。

使用context来取消一个请求,咱们须要获取一个Context类型,以及调用context.WithCancel返回的cancel()方法,并经过Request.WithContextcontext绑定到一个请求上。当咱们想取消这个请求时,只须要调用cancel()方法(代替上述关闭channel的作法)

//ctx是context.TODO()的子节点
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)

Contexts有不少优,好比一个parent(传递给context.WithCancel的对象)被取消,那么命令会沿着传递的路径一直向下传递,直到关闭全部子context

博客地址:neojos welcome you
阅读原文:The complete guide to Go net/http timeouts

相关文章
相关标签/搜索