开启 Keep-Alive 可能会导致http 请求偶发失败
大家好,我是蓝胖子,说起提高http的传输效率,很多人会开启http的Keep-Alive选项,这会http请求能够复用tcp连接,节省了握手的开销。但开启Keep-Alive真的没有问题吗?我们来细细分析下。
最大空闲时间造成请求失败
通常我们开启Keep-Alive后 ,服务端还会设置连接的最大空闲时间,这样能保证在没有请求发生时,及时释放连接,不会让过多的tcp连接白白占用机器资源。
问题就出现在服务端主动关闭空闲连接这个地方,试想一下这个场景,客户端复用了一个空闲连接发送http请求,但此时服务端正好检测到这个连接超过了配置的连接最大空闲时间,在请求到达前,提前关闭了空闲连接,这样就会导致客户端此次的请求失败。
过程如下图所示,
如何避免此类问题
上述问题在理论上的确是一直存在的,但是我们可以针对发送http请求的代码做一些加强,来尽量避免此类问题。来看看在Golang中,http client客户端是如何尽量做到安全的http重试的。
go http client 是如何做到安全重试请求的?
在golang中,在发送一次http请求后,如果发现请求失败,会通过
shouldRetryRequest
函数判断此次请求是否应该被重试,代码如下,
func (pc *persistConn) shouldRetryRequest(req *Request, err error) bool {
if http2isNoCachedConnError(err) {
// Issue 16582: if the user started a bunch of
// requests at once, they can all pick the same conn // and violate the server's max concurrent streams. // Instead, match the HTTP/1 behavior for now and dial // again to get a new TCP connection, rather than failing // this request.
return true
}
if err == errMissingHost {
// User error.
return false
}
if !pc.isReused() {
// This was a fresh connection. There's no reason the server
// should've hung up on us. // // Also, if we retried now, we could loop forever // creating new connections and retrying if the server // is just hanging up on us because it doesn't like // our request (as opposed to sending an error).
return false
}
if _, ok := err.(nothingWrittenError); ok {
// We never wrote anything, so it's safe to retry, if there's no body or we
// can "rewind" the body with GetBody.
return req.outgoingLength() == 0 || req.GetBody != nil
}
if !req.isReplayable() {
// Don't retry non-idempotent requests.
return false
}
if _, ok := err.(transportReadFromServerError); ok {
// We got some non-EOF net.Conn.Read failure reading
// the 1st response byte from the server.
return true
}
if err == errServerClosedIdle {
// The server replied with io.EOF while we were trying to
// read the response. Probably an unfortunately keep-alive // timeout, just as the client was writing a request.
return true
}
return false // conservatively
}
我们来挨个看看每个判断逻辑,
http2isNoCachedConnError
是关于http2的判断逻辑,这部分逻辑我们先不管。
err == errMissingHost
这是由于请求路径中缺少请求的域名或ip信息,这种情况不需要重试。
pc.isReused()
这个是在判断此次请求的连接是不是属于连接复用情况,因为如果是新创建的连接,服务器正常情况下是没有理由拒绝我们的请求,此时如果请求失败了,则新建连接就好,不需要重试。
if _, ok := err.(nothingWrittenError); ok
这是在判断此次的请求失败的时候是不是还没有向对端服务器写入任何字节,如果没有写入任何字节,并且请求的body是空的,或者有body但是能通过
req.GetBody
恢复body就能进行重试。