一个很常见的现场:备份插件写着「上次运行:3 天前」,可站点访问正常、后台也没报错。再一看,站点是企业站,一天没几个访客,还挂着整页缓存。插件本身没坏:WordPress 的定时任务压根没被触发。
WordPress 把「什么时候该做什么」记在数据库里(wp_options 的 cron 选项),本身没有守护进程。它靠的是:每次有 PHP 请求进来,在初始化之后顺手看一眼有没有到点的事件,有就发一个 HTTP 请求去「请求自己」的 wp-cron.php,由那个请求把到点的钩子跑掉(wp_cron() → spawn_cron())。
这套机制带来几个直接后果:
doing_cron 这个 transient 做锁,WP_CRON_LOCK_TIMEOUT(默认 60 秒)内不重复 spawn。它是个锁,不是队列——锁过期后同一条长任务可能被第二次启动。判断当前站点处在哪种状态,先跑这两条:
wp cron test
wp cron event listwp cron test 会告诉你 WP-Cron 是不是被禁用了、环回请求通不通。wp cron event list 的默认字段是 hook、next_run_relative、recurrence,盯着 next_run_relative 看:它一直停在 now 附近不动,就说明没人推动它。
该不该换,跟站点大小关系不大,看的是错过一次会不会有人来问你。
需要换:
可以先不换:个人博客、一天跑一次晚点也无所谓、主机面板已经统一接管了调度。
// wp-config.php,放在 /* That's all, stop editing! */ 之前
define('DISABLE_WP_CRON', true);只关这个常量是不够的:wp-cron.php 仍然是一个可以被外部直接访问的 URL,别人可以反复打它来触发你的任务。在 web 层把它挡掉,只留本机:
location = /wp-cron.php {
allow 127.0.0.1;
deny all;
}系统 cron 走 WP-CLI,不经过 web,不受影响。
写一个包装脚本,把时间戳和退出码一起记进日志——只重定向命令输出的话,没有事件可跑的那几分钟日志里什么都不会有,后面做「日志多久没更新」的监控就没法判断。
#!/usr/bin/env bash
# /usr/local/bin/wp-cron-example.sh
set -uo pipefail
cd /var/www/example.com/current || exit 1
printf '[%s] start\n' "$(date -u +%FT%TZ)"
rc=0
/usr/local/bin/wp cron event run --due-now || rc=$?
printf '[%s] exit=%d\n' "$(date -u +%FT%TZ)" "$rc"
exit "$rc"chmod +x /usr/local/bin/wp-cron-example.sh
crontab -u www-data -e*/5 * * * * /usr/local/bin/wp-cron-example.sh >> /var/log/wp-cron.log 2>&1几个容易踩的点:
www-data 的 crontab,比 sudo -u 干净。% 有特殊含义(会被当成换行),所以别把 date +%F 直接写进 crontab,塞进脚本里就没这个问题。# /etc/systemd/system/wp-cron-example.service
[Unit]
Description=WP-Cron for example.com
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/example.com/current
ExecStart=/usr/local/bin/wp cron event run --due-now
TimeoutStartSec=300# /etc/systemd/system/wp-cron-example.timer
[Unit]
Description=Run WP-Cron for example.com every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
[Install]
WantedBy=timers.targetsystemctl daemon-reload
systemctl enable --now wp-cron-example.timer
systemctl list-timers | grep wp-cron
journalctl -u wp-cron-example.service -n 50 --no-pager同一个 service 在上一次还没结束时不会再起一个,重叠问题天然被挡住;输出也直接进 journal,不用自己管日志轮转。用 crontab 的话,加一把 flock -n 也能防重叠。
上面三步自己走一遍大概半小时,站点一多,每个都要再改一遍路径、再配一次日志轮转和告警,容易漏。AumCreate 做 WordPress 代部署,是把模板装到客户自己的主机上、每一页换成客户的内容,这种「关掉请求触发的 spawn、改用系统调度、脚本用站点文件所有者跑」的活儿放在交付前一次做完,主机、域名、源码都在客户手里,之后要自己改也不会被谁卡着。代部署 $399,材料收齐后 5 个工作日内上线,细节和需要准备的材料在 setup 页。愿意自己动手的话,上面几段脚本改改路径就能用。
分三层看,只看其中一层容易误判。
调度层:wp cron event list --fields=hook,next_run_relative,recurrence,next_run_relative 应该持续推进。
执行层:tail -f /var/log/wp-cron.log,每 5 分钟应该有成对的 start / exit 行,exit 不为 0 就要查。
业务层:很多人到这一步就停了,但调度器跑了不等于你的回调跑了。放一个探针 mu-plugin:
<?php
// wp-content/mu-plugins/cron-probe.php
add_action('cron_probe_event', function () {
$line = sprintf("[%s] probe ok pid=%d\n", gmdate('Y-m-d\TH:i:s\Z'), getmypid());
file_put_contents(WP_CONTENT_DIR . '/cron-probe.log', $line, FILE_APPEND | LOCK_EX);
});wp cron event schedule cron_probe_event now hourly
wp cron event list --hook=cron_probe_event
# 等一个周期后
tail -n 5 wp-content/cron-probe.log探针立刻验证一次也行:wp cron event run cron_probe_event。验完记得 wp cron event delete cron_probe_event 并删掉 mu-plugin。
日志不更新就报警,用一个最小的检查脚本挂在另一台机器上(用 stat -c %Y 取修改时间,GNU coreutils 才有,BSD 上换成 stat -f %m):
#!/usr/bin/env bash
set -u
LOG=/var/log/wp-cron.log
MAX_AGE=900
age=$(( $(date +%s) - $(stat -c %Y "$LOG") ))
if [ "$age" -gt "$MAX_AGE" ]; then
echo "wp-cron log stale on $(hostname): ${age}s" | mail -s "wp-cron stale" ops@example.com
fi时区。事件在库里以 UTC 存,wp cron event list 按站点时区显示。拿它跟服务器日志对时间,先确认 date 和站点时区不是两套。探针里用 gmdate() 写 UTC,比较起来最省事。
CLI 的 PHP 和 FPM 的 PHP 不是一回事。版本、扩展、php.ini 都可能是两套。先 wp --info 看清楚用的是哪个二进制,别在浏览器里测得好好的,定时任务里报找不到扩展。
关掉了却没配系统 cron。这是最常见的一种:DISABLE_WP_CRON 写着,系统 cron 忘了加,所有定时任务静默停摆,表现就是「插件说上次备份是 N 天前」。上线前 grep 一遍 wp-config.php。
多台 web 节点。每台都跑系统 cron 且没有共享锁时,同一条到点的事件可能被跑两次。要么只让一台跑,要么在跑之前抢一个分布式锁(Redis 之类),别指望 NFS 上的 flock。
长时间任务撑满窗口。一条跑 20 分钟的事件会把后面所有事件一起拖后。把大任务拆小,或者单独给它一个低频 timer,用 wp cron event run <hook> 精确指定。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。