内容简介:最近在用go写一个HTTP代理服务,集成测试的时候发现访问量增大到一定程度它就会抛出”Too many open files”异常,简单的通过ulimit增加最大文件描述符数量并不能彻底解决问题。使用lsof定位问题组件–HTTP代理服务使用netstat查看连接信息,果然发现有大量的由代理发起的ESTABLISHED连接
起因
最近在用 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)
完美解决
参考
Post Views: 9
以上所述就是小编给大家介绍的《Go HTTP Proxy: Too many Open files》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
单元测试之道Java版
David Thomas、Andrew Hunt / 陈伟柱、陶文 / 电子工业 / 2005-1 / 25.00元
程序员修炼三部曲丛书包含了四本书,介绍了每个注重实效的程序员和成功团队所必备的一些工具。 注重实效的程序员都会利用反馈来指导开发,并驱动个人的开发流程。编码的时候,最有用的反馈来自于“单元测试”。 为了测试一座桥梁,不会只在晴朗的天气,开一辆汽车从桥中间穿过,就认为已经完成了对桥梁的测试。然而许多程序员却正在使用这种测试方法——把这种一次顺利通过称为“测试”。事实上,注重实效的程序员应......一起来看看 《单元测试之道Java版》 这本书的介绍吧!