Karp 的技术博客

在高性能服务中,Swoole 常被用于构建 TCP/HTTP 长连接服务。但最近我们在实际业务(撮合引擎)运行中,遇到一个隐蔽但影响极大的问题:客户端 TCP 请求出现粘包/串包现象,导致撮合逻辑异常

经过多轮排查,最终定位到问题根因竟是我们为了防止内存泄漏设置的一个“自杀式”机制——worker 每 6 小时定时 kill 重启。以下是详细的分析过程与解决方案。


背景

在撮合系统中,核心引擎是一个基于 Swoole 的 TCP 长连接服务。由于部分逻辑存在小量内存泄漏,为防止单个 worker 长时间运行导致内存持续膨胀,我们在 worker 中加入了定时 kill 逻辑:

\Swoole\Timer::after(6 * 3600 * 1000, function () use ($pid) {
    \swoole_process::kill($pid, SIGKILL);
});

每 6 小时主动结束进程,由主进程重新拉起新 worker。

问题现象

在部署上述机制后,服务运行一段时间后开始出现:

  • 撮合撮单失败、交易撮合顺序异常
  • TCP 粘包、乱序读取
  • 服务日志显示某些连接数据内容解析出错,甚至出现跨连接数据错乱

这些问题表现为概率性、重启后恢复、但持续重现,并严重影响业务正确性。


2025-08-04T08:23:26.png

初步排查

首先排除典型 TCP 粘包原因:

  • 包格式问题(有明确包头+长度)
  • 客户端发送速率过快?
  • 服务端解析逻辑有问题?

但问题不是出现在协议设计或解析,而是worker 被 kill 后再次拉起的进程中,继承到了旧连接的 socket,接收到的 TCP 包并不是新发起连接,而是“断头续传”的旧数据。


问题定位

Swoole 在 TCP 模式下,worker 进程直接处理连接,每个连接都绑定在具体的 worker 上。如果 worker 被 SIGKILL 杀掉:

  • 不触发 onWorkerExit 清理逻辑
  • 未关闭 fd/socket 连接
  • 内核仍保持连接句柄(TIME\_WAIT)
  • 新 worker 重启后,部分 fd 被重复使用,导致连接状态错乱

最终表现为:一个新连接的 fd 实际复用了一个已死掉但未释放的旧连接状态,进而发生串包、错包、脏读现象


解决方案

彻底解决问题的关键在于避免强杀 worker,改为优雅重启机制

✅ 正确做法:使用 max_request 自动重启

Swoole 提供原生的 worker 生命周期控制方式,可在 worker 处理完一定数量请求后,自动触发优雅退出并重启:

$server->set([
    'max_request' => 100000,   // 每个 worker 处理 10 万个请求后重启
    'max_wait_time' => 5,      // 等待协程退出最大时间(秒)
    'reload_async' => true,    // 异步 reload 避免阻塞
]);

这一方式的优势:

  • worker 在处理完请求后再退出,不中断连接
  • 可触发 onWorkerExit 做清理工作
  • 内存、连接、资源均能被安全释放

🛑 禁止使用 SIGKILL

\swoole_process::kill($pid, SIGKILL);

该方法属于强制终止行为,不会触发任何事件回调或资源回收机制,极易引发以下问题:

  • 文件描述符未关闭
  • 内存未释放
  • Socket 状态未清理
  • Reactor 层混乱
  • 导致 TCP 粘包/串包/连接混乱

实际验证效果

我们在生产环境中将原本定时 kill worker 的逻辑移除,并仅依赖 max_request 机制进行周期性重启。

上线后观察超过 7 天:

  • TCP 粘包/串包问题完全消失
  • worker 内存曲线稳定(结合调优也可使用 memory_limit
  • 撮合撮单行为恢复正常

总结

🧠 教训

在基于 Swoole 的 TCP 服务中,worker 生命周期的管理需格外谨慎:

  • 千万不要强制 kill worker
  • TCP 长连接服务尤其敏感,任何未清理的 socket 都可能影响新连接
  • 优雅退出是保障服务稳定的核心原则

🔧 推荐配置(TCP 服务通用)

[
    'max_request' => 100000,
    'max_wait_time' => 5,
    'reload_async' => true,
    'enable_coroutine' => true,
    'dispatch_mode' => 2, // 防止连接错乱(粘包时建议关闭抢占式调度)
]

💬 最后

这次问题虽源于一个看似“合理”的自动重启方案,但也提醒我们:高并发下的服务维护,不能忽视任何一个“异常退出”的边界场景。

优雅重启,不仅是性能优化,也是稳定性的基本保障。

如你也在使用 Swoole 构建 TCP 服务,建议立即排查是否存在 SIGKILL 相关逻辑,及时修正,防患于未然。

swoole

版权属于:karp
作品采用:本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。
更新于: 2025年08月04日 08:35
2

目录

来自 《[踩坑] Swoole 定时 Kill Worker 导致 TCP 粘包问题分析与解决》