一、问题现象
某天重启一个基于 Swoole 的 PHP 服务,直接挂了。日志里报:
[27-Apr-2026 09:37:03] PHP Fatal error: Swoole\Server::start():
failed to start server. Error: start: FactoryProcess_manager_start failed
in /opt/webserver/binance-uper/vendor/ZScript/Server/Server.php on line 124第一反应是端口被占了,但 netstat / ss 一看,端口干干净净,没人占用。
二、初步排查方向(都不是)
排查 FactoryProcess_manager_start failed 这个错时,常见怀疑对象有这几个:
- 残留的 master / manager 进程没退干净 ——
ps aux | grep php看一下,没有。 - pid_file / unix sock 残留文件 ——
/tmp下没有相关残留。 ulimit -u(nproc)太低,fork 不出新进程 —— 当前用户进程数离上限还远。ulimit -n(nofile)不够 —— 102400,绰绰有余。- 配置项冲突(比如开了 task_worker 但没注册
onTask)—— 配置没动过,排除。
全都不是。
三、打开 Swoole debug 日志,真相浮现
把 Swoole 日志级别开到 debug:
$server->set([
'log_file' => '/tmp/swoole.log',
'log_level' => SWOOLE_LOG_DEBUG,
]);再启动一次,日志里多出来几行关键信息:
WARNING MsgQueue(:52): msgget() failed, Error: No space left on device[28]
WARNING create_task_workers: [Master] create task_workers failed
WARNING start: FactoryProcess_manager_start failed核心错误是 msgget() failed, No space left on device。
这跟磁盘空间一点关系没有,翻译过来是:System V 消息队列(msg queue)的 ID 已经分配完了,内核拒绝再分配新的。
四、原理:Swoole 为什么会用消息队列
Swoole 的 task_worker 进程间通信(IPC)有几种模式,由 task_ipc_mode 控制:
| 取值 | 含义 |
|---|---|
| 1(默认) | unix socket |
| 2 | System V 消息队列 |
| 3 | 消息队列 + 抢占式分配 |
我们这台机器之前为了某些场景设成了 2,所以 task_worker 启动时会调用 msgget() 申请一块消息队列。
而 Linux 系统对消息队列总数有上限:
cat /proc/sys/kernel/msgmni默认值因发行版不同从 32 到几千不等。问题是:Swoole 进程异常退出(kill -9、OOM、崩溃)时,这些消息队列不会自动回收,会一直挂在系统里。
服务跑久了、重启多了,残留的队列越堆越多,最终把 msgmni 占满,新进程的 msgget() 直接失败,manager 进程起不来,服务就挂了。
可以用 ipcs -q 查看当前系统所有消息队列:
ipcs -q正常情况下应该只有几个。如果列出来几十上百条,而且 OWNER 都是你的服务用户,基本可以锤实是这个原因。
五、解决方案
5.1 紧急恢复:清理残留的消息队列
# 只删自己用户的(更安全)
ipcs -q | awk -v u=$(whoami) 'NR>3 && $3==u {print $2}' | xargs -r -n1 ipcrm -q⚠️ 警告:如果机器上还有别的服务也在用 SysV IPC,不要无脑全删。一定要按 OWNER 过滤,只删自己服务用户拥有的那些。
清完之后再启动服务,立刻恢复正常。
5.2 长期方案:从根上避免
推荐做法:把 task IPC 改成 unix socket
在 $server->set([...]) 里加一行:
$server->set([
// ... 其他配置
'task_ipc_mode' => 1, // 1 = unix socket,绕开 SysV 消息队列
]);这样根本不会用到消息队列,这个坑就彻底没了。大部分业务场景下,unix socket 的性能跟消息队列没差别,甚至更好。
备选:调高系统消息队列上限
如果确实有理由必须用消息队列模式,可以把 msgmni 调大:
# 临时生效
sudo sysctl -w kernel.msgmni=1024
# 永久生效
echo "kernel.msgmni = 1024" | sudo tee -a /etc/sysctl.conf
sudo sysctl -p但治标不治本——只要还有进程异常退出残留队列,迟早还会撞上来,只是周期长一点。
六、复盘:这类报错的排查思路
FactoryProcess_manager_start failed 本身是个挺笼统的错误,Swoole 把好几种 fork / IPC 失败都归到了这一条上。光看这一行根本无法定位。
正确的排查顺序应该是:
- 第一步永远是开 debug 日志,把
log_level调到SWOOLE_LOG_DEBUG,看 master/manager 退出前的真实错误。 看到具体的子错误后,再去对应的方向排查:
msgget() failed→ 消息队列耗尽,本文场景。fork() failed→ nproc 不够,或者内存爆了。bind() failed→ 端口或 unix sock 被占。chown/chmod failed→ 配置里 user/group 跟实际权限不匹配。
不要凭"端口没占用"或者"昨天还好好的"就开始猜,系统级资源(IPC、fd、进程数)的耗尽问题,只看进程列表是看不出来的。
七、一句话总结
看到 Swoole 的FactoryProcess_manager_start failed,先别盯着端口和进程,
打开 debug 日志看真实错误。
如果是msgget() No space left on device,
用ipcs -q+ipcrm -q清理残留队列,
然后把task_ipc_mode改成1,一劳永逸。