2014年是互联网红包大战的元年,当时我加入艺龙旅行网,负责的第一个核心系统便是红包系统。
本文将分享一次线上故障的排查与解决过程:艺龙红包领取接口在高并发场景下频繁超时,最终我们通过巧妙地使用线程池异步化处理,不仅快速解决了问题,还意外规避了一个更深层次的连接泄露Bug。

1. 系统架构与问题现象

如图所示,用户登录艺龙APP后,客户端会自动调用后台的红包领取接口。
红包服务的处理逻辑如下:
- 校验用户是否符合领取条件。
- 若符合条件,则向RabbitMQ发送一条消息。
- 消息发送成功后,接口立即返回成功响应给前端。
- 用户服务作为消费者,异步消费MQ消息,将红包金额计入用户账户。
整体流程清晰,伪代码逻辑如下:

然而,在我刚接手系统时,APP团队的负责人频繁反馈:领取接口每隔一段时间就会出现大面积超时,只有重启红包活动服务后才能暂时恢复。
通过查看服务器日志,我们迅速将问题定位在流程的第三步:发送消息到RabbitMQ时频繁失败。
2. 使用netstat命令排查连接状态
首先怀疑是MQ连接本身出了问题。我们使用Linux的netstat命令来查看服务器当前的网络连接状况,该命令常用于检查端口监听和连接状态。
常用命令示例:
- 查看所有TCP端口:
netstat -ntlp

- 查看所有UDP端口:
netstat -nulp

- 显示所有监听与非监听端口:
netstat -a

- 结合grep筛选特定进程:
netstat -nap | grep [PID]

在生产环境对红包服务执行netstat命令后,发现了关键线索:与RabbitMQ服务建立的连接数量异常多,几乎达到了本地系统允许的最大句柄数上限。 这强烈暗示存在连接泄露,于是我们将目光转向了RabbitMQ客户端的封装工具类。
3. 剖析RabbitMQ SDK的隐藏Bug

上图是当时架构组封装的RabbitMQ工具类核心逻辑。其发送消息的流程是:先检查缓存的longConnection对象是否有效,有效则复用,无效则创建新连接并发送消息。
看似合理的设计,在高并发场景下却存在严重隐患:
- 线程不安全的状态判断:通过一个对象属性(
longConnection)来判断连接是否过期,在多线程未加锁的情况下,该判断并不可靠。

- 并发创建导致连接泄露:当连接失效时,多个线程可能同时进入创建连接的分支,导致大量重复连接被创建,而这些多余的连接无法被有效回收。
综上,该SDK在高并发下极易引发连接泄露。当连接数耗尽后,新的请求因无法获取连接而长时间阻塞,最终导致接口超时。
4. 线程池异步化:快速止血方案
请求架构组修复SDK需要经过开发、测试周期,无法满足线上紧急恢复的需求。我们决定采用一个巧妙的线程池异步方案进行快速“止血”。
核心思路:将耗时的、易出错的MQ发送操作与主请求线程分离。接口接收到领取请求后,立即将任务提交给一个独立的线程池处理,并直接返回成功响应给前端。

伪代码实现如下:

我们定义了一个单线程的线程池。领取接口只需调用executor.execute()提交任务,便可立即返回。线程池会按顺序异步执行后续的红包资格校验和MQ发送逻辑。
此方案带来两大好处:
- 接口响应极速:主线程(Tomcat线程)几乎无阻塞,接口响应时间大幅下降,吞吐量得以保障。
- 规避并发Bug:使用单线程串行处理领取任务,从根本上避免了原RabbitMQ工具类因多线程竞争导致的连接泄露问题,同时满足了业务对顺序性的要求。
5. 总结与反思
本次艺龙红包接口超时故障,根源在于一个隐蔽的RabbitMQ客户端连接泄露Bug。通过netstat命令我们快速定位到连接数异常,并分析了SDK源码中的并发安全问题。
从架构完善的角度,修复该SDK有两条路:
- 在创建连接时增加完善的锁机制,确保线程安全。
- 使用如
commons-pool这样的连接池框架来管理MQ连接,提升可维护性和健壮性。
在时间紧迫的情况下,我们采用的线程池异步化方案,不仅迅速稳定了线上服务,还意外地通过“单线程串行”这一特性巧妙地绕开了原Bug,是一个典型的、以解决问题为导向的实战技巧。这种从现象追踪到根因,并结合现有条件设计快速解决方案的思路,在处理生产环境突发问题时尤为宝贵。
|