发生什么了?
我们的服务器每时每刻都在发出对第三方的http请求;我们采用一个十分老版本的beego.httplib库来发送这些请求。
例如我们希望发起一次对{url}的POST请求,我们可以这样写
req := httplib.Post(url)
response, err := req.DoRequest()
一切都岁月静好,直到某天我认识到,“需要把对第三方http请求监控起来了”。不得不说,这肯定不是一个坏主意,这其实是应该做的,而且也很容易做到。谁又能想到,随之而来的EOF问题,让我们动用最聪慧的头脑也花了好几天才解决呢?
具体来说,加上对外http请求监控后,每隔一段时间,就会有一个EOF问题抛出来,时间间隔不定,触发的url,body也不定。最可恨的是,除了“EOF”三个字符以外,我们什么也不知道,到底哪里读写产生了EOF问题?
为什么EOF
在网上查找了大量相关信息后,我们基本可以判定,这个 EOF 问题应该是http/1.1 keep-alive机制带来的。
简单解释一下上面发生了什么,如果一个http请求打开了keep-alive,它所用的tcp连接会被持久化在本地,等待下一次请求来使用,直到server端关闭该连接。作为客户端无法知道连接什么时候会被关闭,于是存在一种情况,当一个新的请求获取到一个持久化连接的前后,服务器关闭了连接,此时使用该连接就会导致EOF问题。
看看我们做了什么
EOF问题已经很明确,接下来让我们fix它吧。okay,该怎么fix这个问题?系统库导致了这个问题,也许我们该修改系统库,编译它,然后发布属于我们的私有化golang?等等,仔细想想,在我们打算监控http请求前,是没有EOF问题的,也许我们可以把一切配置都改回来,同时继续监控http请求。
第一步,先看看我们到底做了什么改动:
// 去掉了无关代码片段
type LoggingRoundTripper struct {
Proxy http.RoundTripper
}
func (lrt LoggingRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
...... // 读取request body
res, err = lrt.Proxy.RoundTrip(req)
...... // 读取response body
...... // 日志记录 request body, response body等
return res, err
}
httplib.SetDefaultSetting(httplib.BeegoHTTPSettings{
......
Transport: LoggingRoundTripper{http.DefaultTransport},
})
我们编写了一个实现http.RoundTripper接口的日志类LoggingRoundTripper,它在请求前后读取对应信息,然后把信息输出到日志。接下来用http系统库默认的RoundTripper构造了一个LoggingRoundTripper,用于设置httplib默认的http.RoundTripper。
看起来我们改动了httplib默认的RoundTripper,也许是这里产生了问题,让我们深入看看。
func (b *BeegoHTTPRequest) DoRequest() (resp *http.Response, err error) {
......
if trans == nil {
// create default transport
trans = &http.Transport{
TLSClientConfig: b.setting.TLSClientConfig,
Proxy: b.setting.Proxy,
Dial: TimeoutDialer(b.setting.ConnectTimeout, b.setting.ReadWriteTimeout),
MaxIdleConnsPerHost: 100,
}
} else {
......
}
......
}
好吧,它其实没有默认设置,是个空值;可它每次进行请求时都匪夷所思地新建了一个http.Transport,用完及弃。上面提到的持久化连接正是通过http.Transport维护的,也就是说这些持久化连接根本没有第二次被使用的机会,所以不会有EOF问题。
真是令人头大的实现,即使在写这篇文章的时候,我也没想明白为什么要这么做?(我查阅了最新的beego.httplib代码,虽然代码结构变了,但transport依然在被日复一日地创建着)
该解决问题了
fix#1 关闭 keepalive
我们可不会采用httplib一样新建无数transport的办法。幸运的是,transport有个参数 DisableKeepAlives 可以控制keepalive开关。
fix#2 设置 keepalive header
不知道是谁发现了这样一个神秘header,它竟然可以设置keepalive时间,像这样 timeout=60 代表60秒的保活时间,简直是解决 EOF 问题的最好办法。立刻设置上,发布,然后,,,EOF又出现了。也许这里有一些复杂的历史原因,但实践告诉我们,别指望这个header。
fix#3 让 POST 请求重试
类似的 EOF 问题,golang 在 1.6 左右对 GET/HEAD/OPTION 等请求添加了重试,但别的method不行。也许是上天眷顾,写这篇文章时恰巧找到了一个issue,而且是POST相关的。如果你使用 golang 1.12或更新的版本,你打算开启 keepalive,而且你的请求是幂等的,那么为你的请求加上 header Idempotency-Key 或 X-Idempotency-Key,http系统库会帮你重试这些标记为幂等的请求。
重回岁月静好
还等什么呢,赶快去检查你的http请求有没有开启keepalive吧。
Ref
- 类似的 EOF issue: https://github.com/golang/go/issues/19943#issuecomment-421092421
- keepalive header: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Kee...
- golang改动支持任意method重试:https://go-review.googlesource.com/c/go/+/147457
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。