Hyperf秒级定时任务易引发OOM,根本原因是crontab-dispatcher进程不参与Swoole生命周期管理、持续new实例且不回收,导致内存缓慢泄漏;应改用beforeStart中启动的协程定时器,并配合适当资源限制与分布式锁机制。

Hyperf 做百万级日活系统,必须用常驻内存模型 + 秒级定时任务,但直接开 @Crontab 注解跑秒级任务,大概率在几天后触发 OOM —— 根本原因不是代码写错,而是 crontab-dispatcher 进程持续泄漏内存,且默认不回收。
为什么秒级定时任务会悄悄吃光内存
Hyperf 的 hyperf/crontab 组件启动后,会额外 fork 出一个 crontab-dispatcher 进程,它每秒轮询所有 @Crontab 任务并触发执行。问题在于:
- 该进程本身不参与 Swoole 的 worker 生命周期管理,不会随
max_request重启 - 每次调度都会 new 一个任务实例,若任务类里持有静态变量、全局缓存、未关闭的数据库连接或未释放的协程资源,就会累积泄漏
- 日志里看不到明显报错,只表现为 worker 进程 PID 持续飙升、
crontab-dispatcherRSS 内存缓慢上涨(如从 50MB → 800MB)
你本地复现不出,往往是因为开发环境关了定时任务,或 worker_num=1 掩盖了多进程下的泄漏叠加效应。
如何安全启用秒级调度(不改框架源码)
绕过 crontab-dispatcher 的默认行为,用原生协程定时器替代:
- 删掉所有
@Crontab注解,改用Coroutine::create(function () { while (true) { /* 业务逻辑 */ Coroutine::sleep(1); } });启动独立协程 - 务必在
ServerProvider的beforeStart钩子里启动,确保只执行一次 - 避免在循环内 new 大对象;若需 DB 操作,用
make()获取新实例,别复用单例 - 加兜底退出逻辑:监听信号或检查服务状态,防止协程无限存活
示例片段:
public function beforeStart()
{
Coroutine::create(function () {
while (ServerManager::isRunning()) {
try {
// 执行轻量检查,比如心跳上报、缓存刷新
$this->refreshCache();
} catch (\Throwable $e) {
Log::warning('secondly task error', ['exception' => $e->getMessage()]);
}
Coroutine::sleep(1);
}
});
}文件句柄和协程数必须同步调高
秒级任务 + 百万日活意味着每秒可能新建数百协程、发起上千连接。若系统限制没放开,会出现 Too many open files 或协程创建失败,但错误日志里只显示 fork() failed 或静默丢任务:
-
LimitNOFILE=65536:65536必须写进 systemd unit 文件,/etc/security/limits.conf对 Hyperf 无效 -
max_coroutine在server.php中设为 ≥ 100000,否则协程池满后新任务直接被丢弃 -
worker_num不宜设过高(建议 CPU 核心数 × 2),否则crontab-dispatcher轮询压力翻倍 - Redis 连接池大小要匹配:秒级任务频繁读写,
max_idle_time设太短会导致反复建连
分布式锁必须绑定 TTL 和唯一 value
秒级任务天然并发高,多个实例同时触发同一任务是常态。用 Redis 实现分布式锁时,仅靠 SETNX 不够:
- 必须传
['NX','EX'=>30,'PX'=>30000],用毫秒级PX防止秒级任务因网络抖动超时误删锁 - value 必须是当前进程+协程 ID 的组合(如
swoole_get_last_error().getmypid().Swoole\Coroutine::getuid()),否则释放锁时可能删掉别人刚加的锁 - 禁止用
$redis->del($key)粗暴释放,要用 Lua 脚本原子判断再删 - ZooKeeper 方案虽强一致,但秒级任务下节点创建/删除频率太高,ZK 会成为瓶颈,不推荐
真正容易被忽略的点是:crontab-dispatcher 进程的内存泄漏无法通过 max_request 控制,它不走 worker 生命周期;而协程定时器虽可控,但一旦写错退出条件,就会变成“永生协程”,比 dispatcher 更难排查。上线前务必用 ps aux --sort=-%mem | head -20 持续观察进程 RSS 变化。


















