Karp 的技术博客

一、问题现象

某天重启一个基于 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 这个错时,常见怀疑对象有这几个:

  1. 残留的 master / manager 进程没退干净 —— ps aux | grep php 看一下,没有。
  2. pid_file / unix sock 残留文件 —— /tmp 下没有相关残留。
  3. ulimit -u(nproc)太低,fork 不出新进程 —— 当前用户进程数离上限还远。
  4. ulimit -n(nofile)不够 —— 102400,绰绰有余。
  5. 配置项冲突(比如开了 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
2System 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 失败都归到了这一条上。光看这一行根本无法定位。

正确的排查顺序应该是:

  1. 第一步永远是开 debug 日志,把 log_level 调到 SWOOLE_LOG_DEBUG,看 master/manager 退出前的真实错误。
  2. 看到具体的子错误后,再去对应的方向排查:

    • 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,一劳永逸。

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

目录

来自 《[踩坑] Swoole 启动报错 `FactoryProcess_manager_start failed` 排查记录:System V 消息队列耗尽》