Socket泄漏的案例分享

Posted 零君聊软件

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket泄漏的案例分享相关的知识,希望对你有一定的参考价值。

本文分享一个最近发生的socket泄漏的真实案例。


背景介绍

出问题的这个服务对外暴露了一些REST API,但它自己同时也会访问其它Remote Servcie,所以它既是REST Server端,也是REST Client端。大致图示如下:


客户遇到的问题就是这个服务无法处理REST请求了,在其log中发现了"too many open files"的错误消息。


该服务每收到一个HTTP请求之后,都会访问Remote Server。当它与Remote Server的网络不通时,REST Client就会超时,超时时间时30秒,而且还会重试5次,所以如果网络不通时,总耗时150秒左右。在这期间,最左侧的Client的访问早就超时了,所以Client会主动关闭连接;在REST Server这边就会看到大量的CLOSE-WAIT的TCP连接。这些CLOSE-WAIT的连接最后莫名其妙的变成了未知状态,而且越积累越多,最后达到了1024的上限,就出现了"too many open files"错误。


初步分析

用我上一篇文章介绍的手段,可以很快分析出的确有大量的socket泄漏。



这里要注意的是,在Linux系统中,一切皆文件。Socket也是当成文件来对待。默认情况下,每个进程能打开的最大文件数是1024,当达到这个数之后,再尝试创建新的socket时,就会出现“too many open files”错误。


因为该服务在作为REST Client访问其它Remote server时,每次都会创建一个新的http.Client和http.Transport,所以开始怀疑socket泄漏是这个原因导致的。这个问题其他人也报过,

https://github.com/golang/go/issues/24719


但是http.Transport里的DisableKeepAlives的值设置的是true,所以能规避上面这个问题。而且经过大量的测试和分析,发生泄漏的socket的确和这个问题无关。


所以最后将焦点放在了该服务作为REST Server的身份上。根据前面的分析,socket泄漏应该是由于REST Server并没有关闭CLOSE-WAIT状态的TCP连接。但是为什么没有关闭呢?


net/http源码分析

接着仔细分析了golang中net/http包中http.Server的处理流程。

https://github.com/golang/go/blob/master/src/net/http/server.go


处理流程并不复杂,net/http中的http.Server会为每一个accept的连接创建一个单独的goroutine,来读取并处理这个连接上的数据。注意到这个goroutine的方法里有下面代码:

 defer func() {    ...... if !c.hijacked() { c.close() c.setState(c.rwc, StateClosed, runHooks) }}()


那就意味着,当这个goroutine退出时,只要这个连接没有被hijack,那么肯定会关闭连接。而现在并没有关闭,那就说明这个goroutine一直没有退出;那八成是阻塞在某个地方了。


那么可能会阻塞在哪些地方呢?简单看了一下net/http/server.go的实现,只有两个地方可能会阻塞:

  1. 读取客户端数据;

  2. 回调handler的ServeHTTP;


因为Client已经关闭了连接,在REST Server端TCP连接的状态已经是CLOSE-WAIT,所以当REST Server尝试读取客户端数据时,肯定会返回错误,所以不可能阻塞在读取客户端数据。那么只剩下一种可能,就是阻塞在回调handler的ServeHTTP。

因为net/http中的http.Server会为每一个accept的连接创建一个单独的goroutine,所以socket leak必然伴随着goroutine leak。


pprof

最后给代码enable了pprof,具体怎么enable pprof,请参考下面这个repo。非常简单易用,我昨天晚上临时放到github上去的。

https://github.com/ahrtr/pprof


重新复现这个问题时分析每一个goroutine的调用栈。果然确认了是阻塞在回调handler的ServeHTTP这个方法上。

具体怎么分析goroutine的调用栈,请参考:

https://github.com/ahrtr/pprof


前面已经交代过,REST Client会与Remote Server交互,当网络不通时,会重试5次,总共会耗时150秒左右。而在这个过程中居然一直占着一个锁(mutex)。导致后续的所有goroutine都在等待锁,越积越多,最后达到上限从而出错。


经验教训

永远不要在执行耗时的网络操作或者I/O期间一直占着某个锁。一个典型的例子就是在生产者-消费者模式中,当生产者或消费者等待时,会释放锁,当其苏醒时,会重新获取锁。


--END--


推荐文章



以上是关于Socket泄漏的案例分享的主要内容,如果未能解决你的问题,请参考以下文章

2018-07-14期 ZK编程案例-分布式协调本人亲自反复验证通过分享

SysOM 案例解析:消失的内存都去哪了 !

为啥这段代码会泄露? (简单的代码片段)

避免android片段中内存泄漏的最佳方法是啥

带有 Socket.IO 1.0 的 NodeJS - 堆外的内存泄漏

FragmentStatePagerAdapter 内存泄漏(带有 viewpager 的嵌套片段)