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的实现,只有两个地方可能会阻塞:
读取客户端数据;
回调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编程案例-分布式协调本人亲自反复验证通过分享