Category: /

[译]Linux系统管理员常用网络工具

Linux中用于网络管理、故障排除和调试的一些最常用的命令行工具和实用程序


来源: A Linux Sysadmin’s Guide to Network Management, Troubleshooting and Debugging

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)

完美解决

参考

  1. Golang HTTP to many open files
  2. How to close Golang’s HTTP connection
  3. Time Wait
  4. RoundTripper

Note:容器连接宿主机dbus

Continue reading…

Optimized with PageSpeed Ninja