本文约 4,893 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:定时文章显示”错过计划”,是谁的锅
最常见的场景是这样的:你在 WordPress 后台把一篇文章设成明早 8 点发布,第二天打开后台,文章状态是”已定时”,旁边挂着一行红字——”错过计划”。类似的还有:WooCommerce 的订阅扣款没执行、备份插件说好的每天凌晨两点跑却十天没出文件、邮件队列积压了几百封没人发。
这些功能的共同点是它们都挂在 WordPress 的定时任务系统上,而这套系统的名字 WP-Cron 很容易让人误会。它叫 Cron,但它完全不是 Unix 世界里那个常驻后台的 cron 守护进程。系统 cron 由操作系统按时间调度,到点就执行,跟有没有人访问网站毫无关系;WP-Cron 却是”被动触发”——只有当有人访问站点时,WordPress 才会回头检查有没有到期的任务。
于是流量越小、缓存越狠的站点,定时任务越不靠谱。一个每天只有几十个访客的企业站,白天没人访问就不触发,一旦有人来,积压的任务一次性全跑,数据库瞬间被打满。反过来,一个开了页面缓存的高流量站,请求根本不进 PHP,WP-Cron 同样没机会触发。理解了这一点,改造方案就很明确了:把触发权从”访客”手里拿回来,交给服务器的 crontab。
原理分析:触发、加锁与执行三步
WP-Cron 的完整链路分三步:
- 触发:任意前端请求进入 WordPress,加载
wp-includes/cron.php,其中wp_cron()被调用。它先判断DISABLE_WP_CRON常量,若为 true 直接返回;否则对比wp_next_scheduled()里最近的到期时间,没到期就不做后续动作。 - 加锁:真正要执行时,WordPress 会在
wp_options里写入cron之外的锁记录(transient),锁的默认有效期由WP_CRON_LOCK_TIMEOUT控制,缺省 60 秒。加锁是为了防止两个并发请求同时跑同一批任务。 - 执行:
wp-cron.php通过wp_remote_post()向wp-cron.php?doing_wp_cron=时间戳发起一个回环 HTTP 请求(loopback),让任务在独立请求里跑完,避免拖慢当前用户的页面响应。这也是很多主机上”任务不执行”的根因——服务器禁止自己访问自己。
| 对比项 | WP-Cron(内置) | 系统 cron(crontab) |
|---|---|---|
| 触发来源 | 页面访问 | 操作系统时钟 |
| 低流量站点 | 漏跑、延迟 | 准时 |
| 高缓存站点 | 请求不进 PHP,漏跑 | 准时 |
| 锁超时 | WP_CRON_LOCK_TIMEOUT(默认 60 秒) |
无此概念 |
| 失败重试 | 靠下次访问 | 靠 crontab 周期 |
| 依赖网络 | 需要 loopback 请求 | 直接 CLI 执行,不需要 |
另外一个容易忽视的点:wp-cron.php 不检查 DISABLE_WP_CRON 常量。也就是说在 wp-config.php 里禁用内置触发后,手工或由 crontab 请求 wp-cron.php 仍然能正常执行任务——这正是改造方案成立的前提。
保姆级实操
第一步:先盘点站内到底有哪些定时任务
不要上来就改配置,先用 WP-CLI 看清楚现状:
# 列出所有已注册事件
wp cron event list --fields=hook,next_run_gmt,recurrence --format=table
# 列出系统支持的周期(hourly / twicedaily / daily / weekly 等)
wp cron schedule list
# 测试 WP-Cron 是否能正常跑通(会报告 loopback 是否可用)
wp cron test
wp cron test 会明确告诉你 loopback 请求是否被服务器配置阻断。若输出里提示 WP-Cron spawning is disabled 或 loopback 失败,就更该改用系统 cron。
第二步:在 wp-config.php 里禁用内置触发
/**
* 关闭"页面访问时自动触发 WP-Cron"
* 注意:这只关闭自动 spawn,不阻止手工/crontab 请求 wp-cron.php
*/
define( 'DISABLE_WP_CRON', true );
/* 可选:把并发锁的有效期从 60 秒放宽到 5 分钟,
任务耗时较长时避免"下个周期到点但锁还没释放" */
define( 'WP_CRON_LOCK_TIMEOUT', 300 );
这行常量要放在 /* That's all, stop editing! */ 之前,也就是 require_once ABSPATH . 'wp-settings.php'; 之前才生效。
第三步:在服务器上配置 crontab
推荐直接用 PHP CLI 执行,不经过 Web 服务器,也就绕开了 loopback 限制:
# 用站点运行身份编辑 crontab(避免用 root 跑出 root 属主的缓存文件)
sudo -u www-data crontab -e
# 每 5 分钟执行一次,日志丢弃(保持静默)
*/5 * * * * /usr/bin/php -q /home/www/mimangfei.com/public_html/wp-cron.php >/dev/null 2>&1
如果你的主机不给 CLI 权限,也可以用 wget 或 curl 走 HTTP:
*/5 * * * * /usr/bin/wget -q -O - https://mimangfei.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
第四步:验证任务真的跑起来了
# 手动跑一遍所有到期任务,观察有没有报错
wp cron event run --due-now
# 只跑某一个钩子
wp cron event run woocommerce_scheduled_subscription_payment --due-now
# 再次列出,确认 next_run 已经往后推
wp cron event list
想留证据的话,把 crontab 的输出重定向到日志文件,而不是丢进黑洞:
*/5 * * * * /usr/bin/php -q /home/www/mimangfei.com/public_html/wp-cron.php >> /var/log/wp-cron.log 2>&1
第五步:自定义一个执行周期
默认的 hourly、twicedaily、daily、weekly 常常不够用,用 cron_schedules 过滤器加一个即可:
add_filter( 'cron_schedules', function ( $schedules ) {
$schedules['every_10_minutes'] = array(
'interval' => 600,
'display' => '每 10 分钟',
);
return $schedules;
} );
// 注册一个每 10 分钟执行的事件(放在主题 functions.php 或插件里)
if ( ! wp_next_scheduled( 'mimangfei_pull_orders' ) ) {
wp_schedule_event( time(), 'every_10_minutes', 'mimangfei_pull_orders' );
}
add_action( 'mimangfei_pull_orders', 'mimangfei_do_pull_orders' );
第六步:多台服务器时只让一台跑
负载均衡下如果每台 Web 节点都配同样的 crontab,同一批任务会被执行多次。正确做法是只在其中一台(通常挂个”cron 节点”标签)配置 crontab,其余节点一律 define( 'DISABLE_WP_CRON', true ); 且不装 crontab。数据库共享,谁跑都一样,但不能都跑。
常见踩坑
- CLI 与 FPM 的
memory_limit不是同一份配置。 命令行用的是/etc/php/8.2/cli/php.ini,而 Web 用的是/etc/php/8.2/fpm/php.ini。CLI 默认内存常常更大,也可能更小,跑大数据量任务前先执行php -i | grep memory_limit确认。 - 用 root 跑 crontab,会把上传目录、缓存目录的文件属主改成 root,之后后台上传图片报 “Unable to create directory”。务必用
sudo -u www-data crontab -e(换成你实际的 PHP-FPM 运行身份)。 - 忘了
DISABLE_WP_CRON就得同时面对两套触发。两套并存不会让任务跑两遍(有锁),但会让你排查时搞不清到底是谁触发的,建议二选一。 - 5 分钟周期未必够快。 需要分钟级精度的业务(如订单同步),把 crontab 改成
* * * * *(每分钟)并接受多一次进程开销,或用wp_schedule_single_event()单独排期。 - 任务本身抛致命错误会静默失败。 CLI 模式下错误默认写 stderr,你重定向到
/dev/null就啥也看不到。上线初期一定保留日志,稳定后再关。 - 插件被停用后残留的事件不会自动清理。 停用插件前先
wp cron event list看它注册了哪些钩子,必要时wp cron event delete hook_name手动清掉。 - 案例: 某外贸站用备份插件每天凌晨两点备份,半年没换过配置。后来站点上了 Nginx FastCGI 缓存,访客请求不再进 PHP,WP-Cron 从此再没被触发过,备份全部停摆。直到服务器硬盘故障需要恢复,才发现最后一个备份文件是半年前的。改造办法就是本文这套:先
wp cron test确认 loopback 不可用,再DISABLE_WP_CRON+ crontab,最后用>> /var/log/wp-cron.log保留一周日志做验证。教训是:任何依赖 WP-Cron 的自动化,都必须有可观测的日志或告警,不能默认它”应该在跑”。
总结
判断标准很清晰:只要你的站点满足下面任意一条,就该把触发改成系统 cron——开启了页面缓存、日访问量低于几百、定时任务涉及收入或数据安全、部署在多节点上。改造成本只有一行常量加一行 crontab,收益是任务执行时间从”碰运气”变成”可预期”。
行动清单:先用 wp cron test 测 loopback,再用 wp cron event list 盘点事件,接着在 wp-config.php 加 DISABLE_WP_CRON,最后以站点运行身份配置 crontab 并保留日志。日常排查记住三个命令:wp cron event list、wp cron event run --due-now、wp cron event delete。如果你想在后台界面里可视化管理这些事件,可以配合 WP Crontrol 管理定时任务 那篇里讲的做法;定时任务涉及发信时,也建议顺手看下 WordPress 邮件发送日志排查 的思路,确认任务真的走到了发信那一步。
