Java ThreadLocal 导致的连接泄漏
问题描述
某系统接连出现连接泄漏问题。表现为:
- 使用
RabbitMQ
作为消息队列,使用RabbitMQ
的Connection
进行消息发送。 - 封装了
RabbitMQ
客户端,使用ThreadLocal
保存Connection
。
连接数持续增长,直到连接达到 RabbitMQ
的最大连接数,导致系统无法正常工作。
分析
此问题出现周期间隔较长,且每次出现时,系统日志中均无异常信息,无法通过日志分析问题。只有最终的连接泄漏报错处,打出了少量的日志信息。
一开始出现时,怀疑是网络问题,导致无法连接。但排查后,发现网络一切正常。
直至后来某次,日志明确报出:The channelMax limit is reached
,才意识到是连接泄漏问题。
检索代码,发现 ThreadLocal
的使用方式如下:
这里使用了ThreadLocal
,将 Connection
保存在 ThreadLocal
中,每个线程都拥有自己的 Connection
。原设计是结合线程池使用的,基于一套完整的设计理念。但由于封装较好,且使用方式简单,所以直接使用在业务代码中。而业务代码没有严格约束在有限线程池内使用,既可能运行于 Http
线程,又可能运行于 MQ
等第三方线程,还可能运行于自定义线程。且这些线程相当一部分可能是即用即弃的临时线程。从而导致连接泄漏问题。
这也契合了 RabbitMQ
管理界面上大量空闲连接的事实。
解决
为解决此问题,且不冲击整体代码结构,将 ThreadLocal
改为 Pooled
池化方式,即:使用 Apache Commons Pool2
进行连接池管理。
优点:
- 最小化改动,仅需要对封装处进行修改
- 沿袭原有设计理念,使用简单
后话
此问题在系统上线后,持续观察一段时间,未再出现。
问题的本质是两点,一,使用方对封装方设计理念理解不足,导致使用不当;二,封装方设计上考虑不足,过于特化,只考虑了框架适配场景,未考虑通用场景。
同时,此问题也体现了,合理的第三方封装,可以大大降低使用方的使用成本,提高开发效率。并在出问题需要修复时,可以最小化改动,降低风险。