Java ThreadLocal 导致的连接泄漏

问题描述

某系统接连出现连接泄漏问题。表现为:

  1. 使用 RabbitMQ 作为消息队列,使用 RabbitMQConnection 进行消息发送。
  2. 封装了 RabbitMQ 客户端,使用 ThreadLocal 保存 Connection

连接数持续增长,直到连接达到 RabbitMQ 的最大连接数,导致系统无法正常工作。

分析

此问题出现周期间隔较长,且每次出现时,系统日志中均无异常信息,无法通过日志分析问题。只有最终的连接泄漏报错处,打出了少量的日志信息。

一开始出现时,怀疑是网络问题,导致无法连接。但排查后,发现网络一切正常。

直至后来某次,日志明确报出:The channelMax limit is reached,才意识到是连接泄漏问题。

检索代码,发现 ThreadLocal 的使用方式如下:

code

这里使用了ThreadLocal,将 Connection 保存在 ThreadLocal 中,每个线程都拥有自己的 Connection。原设计是结合线程池使用的,基于一套完整的设计理念。但由于封装较好,且使用方式简单,所以直接使用在业务代码中。而业务代码没有严格约束在有限线程池内使用,既可能运行于 Http 线程,又可能运行于 MQ等第三方线程,还可能运行于自定义线程。且这些线程相当一部分可能是即用即弃的临时线程。从而导致连接泄漏问题。

这也契合了 RabbitMQ 管理界面上大量空闲连接的事实。

解决

为解决此问题,且不冲击整体代码结构,将 ThreadLocal 改为 Pooled 池化方式,即:使用 Apache Commons Pool2 进行连接池管理。

优点:

  1. 最小化改动,仅需要对封装处进行修改
  2. 沿袭原有设计理念,使用简单

后话

此问题在系统上线后,持续观察一段时间,未再出现。

问题的本质是两点,一,使用方对封装方设计理念理解不足,导致使用不当;二,封装方设计上考虑不足,过于特化,只考虑了框架适配场景,未考虑通用场景。

同时,此问题也体现了,合理的第三方封装,可以大大降低使用方的使用成本,提高开发效率。并在出问题需要修复时,可以最小化改动,降低风险。