我们排查了服务端代码,在客户端到代理服务握手的过程中,发现几处可能会存在连接泄露的点,也就是当客户端创建TCP连接并发出登录请求的时候,在某些极端情况下,代理服务会将这个TCP连接的引用遗失,没有显式调用TCP连接的关闭方法。修改后上线,问题依然存在。我们把目光转向了调查客户端是否保持了多余的连接没有关闭,由于我们的服务基础镜像是scratch,所以需要替换成某个linux发行版以便于进入容器进行调试;由于我们的容器运行在docker swarm overlay网络上,客户端IP无法直接在容器内看到,所以需要以host网络部署裸容器来进行调试。生产环境不便于直接排查,在测试环境的监控中我们观察到了同样的现象,所以选择在测试环境排查问题。通过在测试环境进入容器,并使用netstat -atp发现,确实存在许多 ESTABLISHED 状态的连接,这证明客户端发起了并建立了正常的连接,这些连接因为未知的原因并没有关闭,这个跟网络上普遍遇到的 TIME_WAIT 问题并不一样。对这些已经 ESTABLISHED 的连接通过IP进行统计,同一个IP维持了多个 ESTABLISHED 连接,和预期的1条TCP连接不一致。开始与客户端排查连接问题。客户端排查的结论是:由于聊天相关的模块设计问题,TCP连接的生命周期是跟随开发工具进程的生命周期,当开发工具中的客户端在进行开发调试,反复重启的情况下,确实会存在残留并维持多个连接的情况,客户端作为app发布运行的话不会出现这个问题。这个问题启发了我们,如果 ESTABLISHED 状态的连接不是客户端在“维持”,客户端直接消失,并没有发任何挥手包或者RST包,只是服务端“一厢情愿”的认为连接存在呢?我们设计了一个实验,通过nc命令手动创建TCP连接到服务端,在容器内确认连接后,客户端直接断网,不让挥手包发出来。结果是:服务端确实认为这个连接处于 ESTABLISHED 状态,并且至少持续2天没有改变。问题似乎清晰了一些:不断增长的连接都是 ESTABLISHED 状态,客户端在实际运行的过程中并不会“维持”多个TCP连接,是服务端“一厢情愿”的认为连接还存在。就算客户端因为各种原因已经消失了,服务端仍然保持了这个连接的状态,导致出现socket数量“泄露”的现象。这里需要简单说明一下TCP连接的状态转换:TCP连接的建立并不是真的有一个线把两端连接起来,一旦线断掉双方都有感知。TCP连接经过三次握手,客户端和服务端的TCP连接管理模块里把这个连接标记为“ESTABLISHED”,那么这个连接就被“建立”起来了,开始交换数据。但是如果建立连接后,没有数据交换,其中一方直接消失,或者网线直接物理断掉,另一方是没有任何感知的,“ESTABLISHED”状态会一直持续,直到超时。经过实际测试和抓包我们发现,ESTABLISHED至少会持续2天以上,这个是我们不能容忍的。在测试过程中我们意外发现,docker swarm的负载均衡机制也会加重这个问题的表现。docker swarm集群依赖IPVS实现容器的负载均衡,当客户端首次发送数据包到docker swarm节点时,会选择一个容器转发数据包,并写入一个新的转发规则到一个路由表中,后续的数据包按照路由表已有的记录进行转发。这条转发记录上存在超时,如果15min内没有任何数据包通过来重置超时,那么这个转发记录会被删除,一旦客户端有新的数据包发来,那么只会收到一个无情的RST,容器内服务收不到任何数据;服务端内这条连接以 ESTABLISHED 的状态被封闭在容器内,直到超时才被回收。客户端是移动端,网络环境十分不稳定,不能保证对于每个连接都能妥善关闭,现在需要有一个机制,让服务端主动发现客户端已经不在线了,把连接回收,其中一个方式就是修改 ESTABLISHED 超时时间。这种方式我们认为太hack,希望能通过业务层解决这个问题;另一个方式就是有一个心跳机制,连接建立后,服务端定期主动发出心跳包,探测客户端,如果客户端没有及时回复心跳包,则服务端认为客户端掉线,直接回收连接。所以最后的解决方案是:在业务层代码中手动开启TCP KeepAlive机制并设置合适的探测时间间隔,以及时回收连接。KeepAlive并不是TCP协议规范的一部分,但在几乎所有的TCP/IP协议栈(不管是Linux还是Windows)中,都实现了KeepAlive功能。部署后效果显著,socket连接数不再持续增长,而是跟随业务负载在一个区间内起伏。