搜索全站
插件 13 分钟阅读

WP-Cron 改系统 crontab 触发:WordPress 定时任务正确配置

定时发布总是显示"错过计划"?WP-Cron 靠页面访问触发,流量低就会漏跑。本文讲清它的触发与加锁原理,给出禁用内置触发、改用服务器 crontab 的完整配置,附 WP-CLI 排查命令与多机部署注意事项。

U
UFO_PF 2022-08-15 · 阅读 13 分钟
WP 插件
阅读提示
本文约 4,893 字,预计阅读 13 分钟。文中命令与配置均经过实测,可直接参考执行。

引言:定时文章显示”错过计划”,是谁的锅

最常见的场景是这样的:你在 WordPress 后台把一篇文章设成明早 8 点发布,第二天打开后台,文章状态是”已定时”,旁边挂着一行红字——”错过计划”。类似的还有:WooCommerce 的订阅扣款没执行、备份插件说好的每天凌晨两点跑却十天没出文件、邮件队列积压了几百封没人发。

这些功能的共同点是它们都挂在 WordPress 的定时任务系统上,而这套系统的名字 WP-Cron 很容易让人误会。它叫 Cron,但它完全不是 Unix 世界里那个常驻后台的 cron 守护进程。系统 cron 由操作系统按时间调度,到点就执行,跟有没有人访问网站毫无关系;WP-Cron 却是”被动触发”——只有当有人访问站点时,WordPress 才会回头检查有没有到期的任务。

于是流量越小、缓存越狠的站点,定时任务越不靠谱。一个每天只有几十个访客的企业站,白天没人访问就不触发,一旦有人来,积压的任务一次性全跑,数据库瞬间被打满。反过来,一个开了页面缓存的高流量站,请求根本不进 PHP,WP-Cron 同样没机会触发。理解了这一点,改造方案就很明确了:把触发权从”访客”手里拿回来,交给服务器的 crontab。

原理分析:触发、加锁与执行三步

WP-Cron 的完整链路分三步:

  1. 触发:任意前端请求进入 WordPress,加载 wp-includes/cron.php,其中 wp_cron() 被调用。它先判断 DISABLE_WP_CRON 常量,若为 true 直接返回;否则对比 wp_next_scheduled() 里最近的到期时间,没到期就不做后续动作。
  2. 加锁:真正要执行时,WordPress 会在 wp_options 里写入 cron 之外的锁记录(transient),锁的默认有效期由 WP_CRON_LOCK_TIMEOUT 控制,缺省 60 秒。加锁是为了防止两个并发请求同时跑同一批任务。
  3. 执行: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。数据库共享,谁跑都一样,但不能都跑。

常见踩坑

  1. 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 确认。
  2. 用 root 跑 crontab,会把上传目录、缓存目录的文件属主改成 root,之后后台上传图片报 “Unable to create directory”。务必用 sudo -u www-data crontab -e(换成你实际的 PHP-FPM 运行身份)。
  3. 忘了 DISABLE_WP_CRON 就得同时面对两套触发。两套并存不会让任务跑两遍(有锁),但会让你排查时搞不清到底是谁触发的,建议二选一。
  4. 5 分钟周期未必够快。 需要分钟级精度的业务(如订单同步),把 crontab 改成 * * * * *(每分钟)并接受多一次进程开销,或用 wp_schedule_single_event() 单独排期。
  5. 任务本身抛致命错误会静默失败。 CLI 模式下错误默认写 stderr,你重定向到 /dev/null 就啥也看不到。上线初期一定保留日志,稳定后再关。
  6. 插件被停用后残留的事件不会自动清理。 停用插件前先 wp cron event list 看它注册了哪些钩子,必要时 wp cron event delete hook_name 手动清掉。
  7. 案例: 某外贸站用备份插件每天凌晨两点备份,半年没换过配置。后来站点上了 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 邮件发送日志排查 的思路,确认任务真的走到了发信那一步。

U
UFO_PF作者

专注 WordPress 生态的实战作者,内容以「可复制、可验证」为第一原则。

留下你的想法

邮箱地址不会被公开。带 * 的为必填项。