背景介绍:
这篇文章中,主要是记录了一次由于使用elasticsearch的golang客户端导致的tcp连接泄露问题,从最初的问题出现、中间的过程问题排查和最终的问题解决都做了详细的记录,方便以后有类似问题可以参考,其中特别是网络指令:netstat 指令可以让我们得知整个系统的网络情况,迅速排查问题。
接下来就是故事(事故)开始了:
某天运维大佬在运维群里@了我,并发了一张tcp连接图,并帮我定位到了一个消费者pod,这个pod的业务确实刚加的,也符合时间点
顿时心中一紧,这个肯定有问题的,明显是tcp泄露了,赶紧跑过去和运维一起看看问题,一顿操作后确定了的确是这个pod的连接数很高,遂决定
还是重启大法,tcp连接数立马下降到正常位置了
经过分析一波有理有据的分析后:该pod没有web接口业务,主要连接是和中间件的连接:数据库,es,rabbitmq,redis,而web服务也有连接es,redis的业务,但是没有连接数据库的,但是连接数也很正常,so怀疑默认的xorm不支持连接池的,因此增加了一个连接池配置并发布了
。
。
。
回去洗洗睡了
。
。
如果事情就这么结束了,那就太没有意义了吧
早上回来后,想确认下连接数是否正常,尝试进入容器,试试netstat是否可以使用
幸好,netstat是可以使用的,但是连接状态的却很多,有几千的established,这明显不正常
再看看他们是跟那些ip和端口进行通信的(在mac上模拟的情况)
端口9200,搞过es的都知道这是es的端口号
立马去代码里看了es的代码逻辑,通过和测试大佬的沟通,最终确认了需要对es的返回值进行close操作,否则,就会tcp泄露
一波操作之后,上线,观察
果然连接数正常了
终于可以洗洗睡了
你以为故事结束了吗?
没有,这才刚刚开始
为啥会有TIME_WAIT,而且一直在两位数上一直波动,不会没有,也不会很多
那TIME_WAIT是啥呢?
根据tcp的4次挥手状态转化图,可知主动关闭连接的一方会进入TIME_WAIT,停留2个MSL时间后关闭:
关闭就关闭了,TIME_WAIT状态还要存在的原因:
1、保证完整全双工关闭链接
BadCase:A最后发的ACK丢了,B会重发FIN,如果A没有TIME_WAIT则系统会直接回RST,导致B直接抛异常
RST包:表示复位,直接关闭异常连接,比如服务器没监听的端口收到数据包
2、防止有未接收完的数据包
BadCase:B发完FIN后,之前B的旧数据分片到达(网络波动等影响),这时A这个端口起了新连接,新连接收到上个连接的旧分片可能会导致异常
TIME_WAIT过多危害
- 网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包过来后会直接影响新的TCP连接;
- 同样网络情况不好并且无TIME_WAIT等待,关闭连接后无新连接,当接收到被动方重传或延迟的FIN包后,会给被动方回一个RST包,可能会影响被动方其它的服务连接。
- 过多的话会占用内存,一个TIME_WAIT占用4k大小
- 占用端口一定时间,直到TIME_WAIT结束
那现在情况就很明了,es没有使用长连接,都是去使用的短连接去请求es服务器的
这个。。。。。
不close就会造成es的连接一直established,再次查询的时候就去新建连接;我close吧,又使用短连接去请求,唉,好难
但是我发现web那边也有去es查询的语句,里面也有close的,但是他的time_wait很少,偶尔出现一两个,也是正常情况
百度了一波也没找到啥说法
通过抓包,我们发现有一个1a是一次http通讯的握手连接,发送数据,1b是1a的通信挥手
中间两次红色就是es请求进行数据插入,每次插入都是一次http短连接请求过程
这也就明确了我们确实没有使用es的http长连接
web那边没有抓包尝试,中间也测试分析了很https://github.com/elastic/go-elasticsearch多,尝试http长连接等操作 https://cloud.tencent.com/developer/article/1531722还是没有解决问题
看了一遍示例也没看出一个所以然https://github.com/elastic/go-elasticsearch
分析下web和srv的对立面,两者都使用body.close(),但是web确实使用了长连接,srv却没有
无奈又去翻了https://github.com/elastic/go-elasticsearch一遍,
对md中英文一段段翻译,终于
NOTE: It is critical to both close the response body and to consume it, in order to re-use persistent TCP connections in the default HTTP transport. If you're not interested in the response body, call io.Copy(ioutil.Discard, res.Body)
.
因为srv(消费者)的业务中只有插入和删除,且并不关心结果,因此,并没有读取body,且直接关闭,而web中使用
json.NewDecoder(res.Body).Decode(&response),因此是处理了body的,
会不会是这个原因导致的,立马使用io.Copy(ioutil.Discard,
res.Body)
处理
果然连接复用了
TIME_WAIT也没有了,established也正常了
你以为故事到这里结束了吗?
没错,结束了,再不结束
总结:
- es客户端使用的是go的http.client,应当对所有的返回值进行处理,如果不关心结果,请使用
io.Copy(ioutil.Discard, res.Body)进行清空
,可以参考golang http client连接池不复用的问题 - 连接等问题多去使用netstat进行分析,必要时候使用wireshark进行分析
- 仔细阅读md和查看他们的issues