返回博客

ThinkPHP 5 异步任务锁文件「消失」之谜:RUNTIME_PATH 不在你以为的地方

某后台系统的全量数据同步任务,运行十几分钟后前端始终显示「进行中」,刷新状态接口只返回上一次的进度快照,不再更新。于是怀疑:进程是不是中途挂掉了?

第一步:上服务器找锁文件。ls application/runtime/ ——空目录。排查陷入了一条完全错误的路径,折腾了整整 30 分钟。

现象

异步任务在启动时写入锁文件,逻辑大致如下:

$lockFile = RUNTIME_PATH . 'sync.lock';
file_put_contents($lockFile, getmypid());

任务进行中,到服务器检查:

ls -la /www/wwwroot/myproject/application/runtime/
# No such file or directory

直觉判断:锁文件从未写入,进程在任务开始前就被 Kill 了。由此引出一整条误判链——怀疑 PHP-FPM request_terminate_timeout 设置过短、内存超限、OOM Killer 介入,依次调高 memory_limit、延长超时……全部无效。

根因

ThinkPHP 5 框架在 thinkphp/base.php 里这样定义 RUNTIME_PATH:

// thinkphp/base.php(TP 5.0.x / 5.1.x 默认值)
defined('RUNTIME_PATH') or define('RUNTIME_PATH', APP_PATH . '../runtime/');

注意那个 ../——RUNTIME_PATH 是 APP_PATH 向上一级再进入 runtime/,不是 APP_PATH 的子目录。

实际目录布局:

/www/wwwroot/myproject/
├── application/                  ← APP_PATH
│   ├── controller/
│   └── ... (这里没有 runtime/ 子目录!)
├── runtime/                      ← RUNTIME_PATH 实际落点(项目根级)
│   ├── sync.lock
│   ├── progress.json
│   └── cache/
├── public/
└── thinkphp/

PHP 文件系统调用会透明处理 application/../runtime/ 里的 ../,直接写到 /www/wwwroot/myproject/runtime/sync.lock。锁文件确实写进去了,只是写的位置比所有人预想的高一层。

坑:在 application/ 下找 runtime 文件,永远找不到

RUNTIME_PATH . 'foo.lock' 展开后是 application/../runtime/foo.lock,PHP 在实际 I/O 时会把 ../ 解析掉,最终落到项目根目录的 runtime/,而不是 application/runtime/。从 application/ 目录往下找,永远是空的——这不是进程被 Kill 的证据,只是找错地方了。

正解

排查时:先从项目根搜,别从 application/ 搜

# 不知道 RUNTIME_PATH 在哪?从项目根全局搜
find /www/wwwroot/myproject -name "sync.lock" 2>/dev/null
# 输出:/www/wwwroot/myproject/runtime/sync.lock
# 文件存在,mtime 是刚才,进程还活着

确认 RUNTIME_PATH 实际值

# 读框架默认定义
grep -n "RUNTIME_PATH" /www/wwwroot/myproject/thinkphp/base.php
 
# 检查项目有没有在 index.php 或 config 里覆盖
grep -rn "define.*RUNTIME_PATH" \
    /www/wwwroot/myproject/application/ \
    /www/wwwroot/myproject/public/index.php

用 PHP 直接验证解析结果

php -r "
  define('APP_PATH', '/www/wwwroot/myproject/application/');
  define('RUNTIME_PATH', APP_PATH . '../runtime/');
  echo realpath(RUNTIME_PATH) . PHP_EOL;
"
# 输出:/www/wwwroot/myproject/runtime
正解:find 从项目根出发,realpath() 验证路径

排查 TP5 运行时文件缺失问题,第一条命令应该是 find <project-root> -name <filename>,而不是 ls application/runtime/。确认真实路径后再判断文件是否存在、mtime 是否更新、进程是否存活。

附:还要小心 runtime/ 的文件属主

找到 runtime/ 后还有第二个坑:如果系统上有 root 身份运行的 cron job 触发了 TP5 的 think 命令,框架会以 root 身份在 runtime/cache/ 写缓存文件。PHP-FPM 进程(通常是 www 用户)在 TTL 到期后试图覆写这些文件时,会因权限不足直接报 Permission denied 导致业务中断。

# 快速检查 runtime/ 是否被 root 文件污染
find /www/wwwroot/myproject/runtime -uid 0 | wc -l
# 如果数字 > 0,立即执行修复:
chown -R www:www /www/wwwroot/myproject/runtime/
# 并把 cron 任务改为 www 用户身份执行:
# sudo -u www -- bash -c 'cd /www/wwwroot/myproject && php think your:command'
提示:TP6 改善了路径命名

ThinkPHP 6+ 把默认 RUNTIME_PATH 改为 ROOT_PATH . 'runtime/'(根目录相对,不再走 ../ 歧义)。这个陷阱主要发生在 TP 5.0.x / 5.1.x 以及基于它们的 FastAdmin 项目上。升级前确认版本。

一句话外卖

TP5 的 RUNTIME_PATH 默认在项目根 runtime/,不在 application/runtime/;调试运行时文件缺失问题,永远先从项目根 find,别把「找不到文件」当「进程被 Kill」的证据。