workerman 无法直接利用 cgroup 控制资源,必须通过 systemd-run --scope 从进程启动第一刻即纳入受控 cgroup,否则 fork 子进程将脱离限制;cgroup v2 需启用控制器且 memorymax 不含 swap,需设 memory.swap.max=0 并监控 memory.stat。

Workerman 本身是 PHP 进程模型,不自带 cgroup 集成;它无法“直接利用”cgroup 控制自身资源——因为 cgroup 管的是进程组(task group),不是单个进程的运行时行为。真正起作用的,是启动 Workerman 的方式是否从一开始就在受控的 cgroup 中运行。
用 systemd-run 启动 Workerman 实例并设限
这是最可靠、最贴近生产环境的做法。Workerman 进程(含所有子进程,如 Worker::fork() 派生的子 worker)会完整继承 cgroup 限制。
systemd-run --scope -p CPUQuota=30% -p MemoryMax=512M php start.php start -d- 必须用
--scope(而非--unit),否则退出后 cgroup 不自动清理 -
CPUQuota=30%表示最多占用单核 30% 时间(多核下仍可跨核调度,但总带宽被 cap) -
MemoryMax=512M是 cgroup v2 语义,需确认系统启用 v2:mount | grep cgroup应显示cgroup2挂载在/sys/fs/cgroup - 如果 Workerman 使用了
pcntl_fork()或exec()启动外部命令(如 FFmpeg),它们也会被纳入同一 cgroup —— 这是优势,也是风险点(你得确保这些子进程也遵守内存边界)
为什么不能把已运行的 php start.php 塞进 cgroup?
常见错误:Workerman 已在前台或后台跑着,用户再手动 echo $PID > /sys/fs/cgroup/memory/mygroup/tasks —— 这看似成功,但立刻失效。
-
cgroup.procs只影响当前写入的 PID,其 fork 出的新进程**不会继承**该 cgroup,而是回到父 cgroup(通常是 root) - Workerman 默认会 fork 多个子进程处理连接,这些子进程一启动就脱离限制
- 即使你用
cgclassify批量绑定所有现有 PID,也无法覆盖未来新 fork 的进程 - 结论:cgroup 必须从进程诞生第一刻就生效,Workerman 不支持“热限流”
memory.max 设了却 OOM 了?检查 swap 和匿名映射
Workerman 常搭配 Redis、PDO、大数组缓存,容易触发 cgroup 内存边界外的隐式分配。
-
memory.max仅限制物理内存页分配,malloc()成功不代表实际内存已提交;OOM 杀手在分配失败时才介入 - 若未设
memory.swap.max=0,内核可能把部分内存页换出到 swap,memory.current看似未超限,但整体压力仍在 - PHP 的
mmap(MAP_ANONYMOUS)(如 OPcache 大页、某些扩展的共享内存)可能不计入memory.current,却真实消耗物理内存 - 验证方式:
cat /sys/fs/cgroup/mygroup/memory.stat | grep "^pgpgin\|^pgpgout\|swappiness",看是否有 swap 活动
不要手建 cgroup 目录 + echo 写参数
有人照旧文档 mkdir + echo 写 cpu.cfs_quota_us,这在 cgroup v2 下大概率失败。
- v2 要求先启用控制器:
echo "+cpu +memory" > /sys/fs/cgroup/cgroup.subtree_control,否则cpu.max根本不出现 - 手动创建的目录不会自动 delegate 给普通用户,
systemd-run会帮你处理权限和委派逻辑 - 忘记 cleanup?残留 cgroup 占用内核资源,尤其在频繁启停 Workerman 场景下,
systemd-run --scope自动回收 - 如果你非要用脚本控制,务必加
trap 'rmdir /sys/fs/cgroup/myworkerman 2>/dev/null' EXIT,但仍有 race condition 风险
Workerman 的资源控制难点不在 PHP 层,而在启动边界 —— 它的 fork 模型决定了你必须在第一个进程出生前就画好笼子。任何试图“中途加锁”的操作,都会被子进程绕开。真正要盯住的,是 systemd-run 的参数组合、memory.swap.max 是否为 0,以及 memory.stat 里那些不声不响的 pgpgin/pgpgout。











