Go HTTP Proxy: Too many Open files
起因
最近在用go写一个HTTP代理服务,集成测试的时候发现访问量增大到一定程度它就会抛出”Too many open files”异常,简单的通过ulimit增加最大文件描述符数量并不能彻底解决问题。
定位
使用lsof定位问题组件–HTTP代理服务
使用netstat查看连接信息,果然发现有大量的由代理发起的ESTABLISHED连接
分析
http请求的处理函数大概长这样:
func handleHTTP(w http.ResponseWriter, req *http.Request) {
resp, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
defer resp.Body.Close()
copyHeader(w.Header(), resp.Header)
w.WriteHeader(resp.StatusCode)
io.Copy(w, resp.Body)
}
每次handleHTTP函数被调用后,都会新建一个TCP连接,如果对端不先关闭该连接(对端发FIN包)的话,即便是调用了resp.Body.Close()函数仍然不会改变这些处于ESTABLISHED状态的连接。这种现象会涉及到源码:
transport层会首先找与该请求相关的已经缓存的连接,如果已经有可以复用的旧连接,就会在这个旧连接上发送和接受该HTTP请求,否则会新建一个TCP连接,然后在这个连接上读写数据。
在函数handleHTTP()的每次调用中,我们都会新创建transport和RoundTrip结构,当HTTP请求完成并且接收到响应后,如果对端的HTTP服务器没有关闭连接,那么这个连接会一直处于ESTABLISHED状态。
不幸的是,HTTP1.1默认都会启用Keep Alive,正常情况下服务端客户端都会根据首部字段Conent-Length或者Transfer-Encoding来判断是否接收完成,在完成之前,连接会一直保持。
(貌似)知道了原因就可以实施解决方案了。比较容易实现的有
1. 减少keep alive超时时间
2. 经过代理时,通过把首部字段Connection设置成false等手段以禁止长连接。
3. 增大文件句柄上限
4. 使用全局RoundTrip,每次都通过它发送
5. 使用nginx来作为网关,把问题丢给它处理
考虑到我的项目只是通过代理拉一些静态资源,所以采用了方案2。禁用Keep Alive后,连接就不会被丢到缓存了。修改后的代码长这样:
MyTransport := &http.Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: -1,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := MyTransport.RoundTrip(req)
编译执行!单元测试通过!提交集成测试!果然没有大量的ESTABLISHED了!
转折
本来以为这个BUG已经修了,然而测试的小伙伴又给我打了回来。不过这次的报错变成了Cannot assign requested address。经过分析,发现在测试环境上出现了大量TIME_WAIT连接,导致端口耗尽无法创建套接字
无论是设置net.ipv4.tcp_tw_recycle=1还是net.ipv4.tcp_tw_resue=1都没有作用,所以初步分析不是程序的问题,而是环境的问题
通过分析环境,发现代理和测试客户端部署到了一台终端机上,而且终端机开启了全局系统代理。由于Go的DefaultTransport会读取代理相关环境变量,导致发送时又提交给了自己,形成了代理回环
解决方案很简单,让Transport不使用代理,或者启动时加入env -i参数就可以了。修改后的代码长这样:
MyTransport := &http.Transport{
Proxy: nil,
DialContext: (&net.Dialer{
Timeout: 10 * time.Second,
KeepAlive: -1,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
resp, err := MyTransport.RoundTrip(req)
完美解决
参考
--转载请注明: https://momo.cool/go-http-proxy-too-many-open-files/
说点什么