Root Cron 污染 PHP-FPM 共享 runtime——看板绿灯、业务全断 38 小时
那天运营找过来,说某个推送功能"今天所有人都没收到"。我第一反应是看监控看板——24 小时失败 0 条,上次心跳时间显示正常。我差点认为运营搞错了。
好在我没相信看板。一条 SELECT MAX(sent_at) FROM xxx_send_log 打出来,最后一行是 38 小时前。看板在说谎。
现象
- cron 任务上线后最初几小时一切正常,业务表持续有写入
- 某个时间点之后业务表彻底停写,无任何 5xx,无报警
- 后台看板仍显示「24h 失败 0 条」「最近一次成功:X 分钟前」
ls -la runtime/cache/发现大量root:root拥有的文件
根因
这个坑是三层机制叠加造成的,缺任何一层都不会这么隐蔽:
crontab 默认以 root 身份运行(除非显式 -u 指定用户)
↓
0 2 * * * cd /www/wwwroot/project && php think xxx >> runtime/log/cron.log 2>&1
↑
重定向是 root 打开的文件描述符
↓ 触发三路 root 写入:
1. think 命令业务逻辑写 cache(如 access_token 缓存文件)
2. STDOUT/STDERR 重定向落 runtime/log/cron.log
3. ThinkPHP Log facade 内部 fwrite 写 runtime/log/202505/xx.log
↓
TTL 过期前:PHP-FPM (www) file_get_contents 能读 root:root 0644 文件 → 业务正常
TTL 过期那刻:PHP-FPM (www) file_put_contents 试图覆写 → Permission denied (EACCES)
→ PHP fatal error
→ 上层 catch 吞掉 → 不写业务日志 → 看板看不到失败
→ 业务静默全断
为什么这个坑极难发现:
- TTL 滞后:cron 跑完后不会立即崩,而是等 cache 过期才崩,与部署时间的关联性丢失
- catch 静默化:框架层 catch 只写 Log,不写业务表,看板由「业务表写入」驱动,所以永远成功
- 三路污染:cache + log STDOUT + log facade 三路同时被 root 污染,只治一路不够
坑:重定向写在 sudo 外层,cron.log 还是 root 拥有
# 看起来修了,其实没修——重定向仍是 root 打开 fd
0 2 * * * sudo -u www -- php /path/to/think xxx >> runtime/log/cron.log 2>&1外层 shell 在 cron 里是 root,>> 由 root 打开文件描述符,即使进程切换到 www,cron.log 这个文件本身的属主依然是 root。下次 www 想续写就 Permission denied。
正解
治本:把重定向包进 bash -c 内层
# 正确写法:整段命令(含重定向)都在 www 身份下执行
0 2 * * * sudo -u www -- bash -c 'cd /www/wwwroot/project && php think xxx >> runtime/log/cron.log 2>&1'如果环境没有 sudo(部分裸 CentOS),改用:
0 2 * * * su www -s /bin/bash -c 'cd /www/wwwroot/project && php think xxx >> runtime/log/cron.log 2>&1'注意 -s /bin/bash,防止 www 用户默认 shell 是 nologin 导致 su 失败。
最干净的方式是直接在 www 用户的 crontab 里加任务:
sudo crontab -u www -e上线前必须先清存量污染
只改 cron entry 不够,历史已写入的 root 文件还在。TTL 没过期前 www 还能读,过期那刻照样崩。
# 1. 整棵 runtime 树 chown 回 www
sudo chown -R www:www /www/wwwroot/project/runtime/
# 2. 验证干净(应该输出 0)
sudo find /www/wwwroot/project/runtime -uid 0 | wc -l
# 3. 删除 cache 让 www 重新生成
sudo rm -rf /www/wwwroot/project/runtime/cache/*
# 4. 如果 cron.log 也是 root 拥有,删掉让 www 重新创建
sudo rm /www/wwwroot/project/runtime/log/cron.logThinkPHP 5/6 的 runtime 树还包括 temp/、session/、lock/,全树 chown 一次最稳。
正解:三路同治——cron 改身份 + 清存量 + 加业务表写入告警
- cron entry 用
bash -c '...'把重定向包进 sudo 内层 - 上线前
chown -R www:www runtime/+ 删 root 残留文件 - 给业务关键表加「N 分钟无写入则告警」,不依赖「成功路径写日志」的看板
反模式备忘
chmod 0666临时扩权:下次 cron 跑完又变 root:root,没用- 只 chown 不改 cron:下次 cron 再污染回来
- 只改 cron 不清存量:TTL 过期前一切正常,过期后又崩
- 以为只污染了 log 没污染 cache:两路一起看
一句话外卖
root cron 跑 PHP CLI 时,STDOUT 重定向、Log facade、业务 cache 三路同时以 root 落盘;把整段命令用
bash -c '...'包进sudo -u www内层,上线前清存量,否则等 cache TTL 过期你才会知道它悄悄断了多久。