一次tcp泄露的探秘

背景介绍

这篇文章中,主要是记录了一次由于使用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过多危害

  1. 网络情况不好时,如果主动方无TIME_WAIT等待,关闭前个连接后,主动方与被动方又建立起新的TCP连接,这时被动方重传或延时过来的FIN包过来后会直接影响新的TCP连接;
  2. 同样网络情况不好并且无TIME_WAIT等待,关闭连接后无新连接,当接收到被动方重传或延迟的FIN包后,会给被动方回一个RST包,可能会影响被动方其它的服务连接。
  3. 过多的话会占用内存,一个TIME_WAIT占用4k大小
  4. 占用端口一定时间,直到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也正常了

你以为故事到这里结束了吗?

没错,结束了,再不结束

总结:

  1. es客户端使用的是go的http.client,应当对所有的返回值进行处理,如果不关心结果,请使用io.Copy(ioutil.Discard, res.Body)进行清空,可以参考golang http client连接池不复用的问题
  2. 连接等问题多去使用netstat进行分析,必要时候使用wireshark进行分析
  3. 仔细阅读md和查看他们的issues
展示评论