
服务器上很多事故,不是服务挂了,而是该跑的任务没跑。Cron 定时任务最尴尬的地方就在于它失败时往往没有任何告警,等你发现备份漏了一周、SSL 证书没自动续期、清理脚本没执行,损失已经造成。本文教你如何让 Cron 任务在服务器上稳定运行,并把日志监控做到能在第一时间发现异常,帮助你彻底解决悄悄失败的老问题。
Cron 为什么经常悄悄失败
写一行 crontab 看起来简单,真正在生产环境跑稳定,需要绕开几个长期被忽视的坑。最常见的原因是环境差异:你在交互 shell 里跑得好好的脚本,到了 cron 上下文里 PATH 变短、locale 不对、HOME 没设置,结果某条命令找不到。
还有几类常见原因导致定时任务在静默中失败:标准输出和错误没有重定向,失败信息直接被丢弃;没有锁机制,前一次任务还在跑下一次又起来导致互相踩;写硬编码路径,工作目录从用户家目录变成 /,相对路径全失效;没设置超时,单次卡死后所有后续调度都被堵;没有失败告警,连失败过这件事都不知道。
这些坑并不需要复杂工具就能避开,只要把脚本和 crontab 当成正式产物来对待。如果你正在选购一台新服务器跑批处理任务,可以先看看 云服务器选购指南 中关于 IO 与内存的部分,再决定规格。
让脚本本身可靠:环境、锁、超时
可靠的 Cron 任务,一半工作在脚本里。一个稳健的脚本至少应该包括:显式设定 PATH 与 LANG、用 set -euo pipefail 让失败立刻退出、加文件锁防止并发、用 timeout 给整体限定时间、用 trap 在失败时打印调用栈或上下文。
下面是一段可以照抄的骨架:
#!/usr/bin/env bash
set -euo pipefail
export PATH=/usr/local/bin:/usr/bin:/bin
export LANG=en_US.UTF-8
export HOME=/var/lib/myjob
LOCK=/var/lock/myjob.lock
LOG=/var/log/myjob/run.log
exec 9>"$LOCK"
flock -n 9 || { echo "$(date -Iseconds) already running" >> "$LOG"; exit 0; }
trap 'echo "$(date -Iseconds) failed at line $LINENO" >> "$LOG"' ERR
timeout 25m /usr/local/bin/do-the-work.sh >> "$LOG" 2>&1
几个细节值得强调。第一,flock 用文件描述符而不是文件名,可以避免 race。第二,timeout 的值要小于 cron 的调度间隔,给下一次留余地。第三,日志路径要预先创建并配 logrotate,否则一个高频任务能在一周内把磁盘写满。
crontab 行本身的几条最佳实践
脚本稳了,crontab 行也要稳。看似一两个字符的差异,决定了任务能不能在生产环境长期跑下去。推荐的写法包括:在文件头部写 SHELL=/bin/bash 与 PATH 覆盖默认的 /bin/sh 与短 PATH;用 MAILTO 把失败邮件发给一个团队邮箱而不是个人;时间字段写绝对而不是简写,便于审计;避开整点,错峰避免与备份、监控等系统任务争 IO;同一台机器避免上百行 cron,集中到一个调度框架更可维护。
举例:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
17 3 * * * johnson /usr/local/bin/myjob-wrapper.sh
27 */4 * * * johnson /usr/local/bin/syncjob-wrapper.sh
注意一定要写用户名(系统 crontab 在 /etc/cron.d 下时必须),并把脚本路径写成绝对路径。如果你的 VPS 是新装的,建议先按 美国 VPS 部署教程指南 做完基础加固再加任务。
systemd timer:现代替代方案
在较新的 Linux 发行版上,systemd timer 在很多场景下比 cron 更合适。它的优势是:可以指定 OnCalendar 表达式精确到秒、原生支持失败重试、日志自动落入 journal、可以通过 systemctl status 直接看到上次执行结果。
一个最小可用的 timer 单元如下:
; /etc/systemd/system/myjob.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/myjob-wrapper.sh
TimeoutStartSec=30min
User=johnson
; /etc/systemd/system/myjob.timer
[Timer]
OnCalendar=*-*-* 03:17:00
Persistent=true
[Install]
WantedBy=timers.target
Persistent=true 这一项很重要:如果机器在调度点关机了,下次开机会自动补跑一次。这点 cron 默认做不到。把这一套用起来后,定时任务的最后一次执行时间、状态、退出码都能用 systemctl list-timers 一目了然。
日志结构:让告警有据可查
很多团队的 cron 日志是一锅粥,所有输出混在一起,事后排查就像考古。建议从一开始就给日志加上几个字段:时间戳、任务名、阶段、状态、耗时。结构化之后,下一步无论是 grep、ELK 还是 Loki 都好处理。
一个简单的封装函数:
log() {
printf '%s [%s] [%s] %s\n' "$(date -Iseconds)" "$JOB" "$1" "$2" >> "$LOG"
}
log INFO "start"
log INFO "fetched 1280 rows"
log ERROR "upload failed: $err"
如果用 systemd timer,日志直接走 journal,可以用 journalctl -u myjob.service –since “1 hour ago” 拉出最近一次执行。再配合 journald 的转发能力,把关键事件推送到中心日志系统。
监控与告警:能感知没跑才有意义
监控 cron 最大的难点是:你监控的是没发生的事。常规 Prometheus 抓指标做不到这点,更合适的工具是死人开关:任务跑成功时主动 ping 一次,超时没收到 ping 就告警。开源方案 healthchecks 自部署或商用版都不错,几分钟就能接入。
具体可以这样组合:
- 任务结束时 curl 一次 ping URL,区分 success 与 fail
- 在监控平台配 schedule 与容忍窗口,超时自动告警
- 关键任务再叠加一个独立的 Prometheus blackbox 或 cron exporter
- 备份类任务额外校验产物(文件大小、行数、checksum)
- 失败告警走两条独立通道(邮件加 IM 机器人),避免单通道挂掉
把告警接到团队的 IM 群和值班机制里,整套链路才算闭环。配合 WordPress 数据库定时备份脚本与异地同步方案 中的备份产物校验,可以让备份相关的定时任务监控更扎实。
容易被忽视的几个坑
最后梳理几个我在不同项目里反复见到的坑:用 root 跑所有 cron 结果一个脚本写崩了整个系统;脚本里隐式依赖某个 Python 虚拟环境,cron 启动时根本没激活;日志全打到 stdout 被 cron 邮件机制塞满管理员邮箱;用相对时间做 SQL 查询跨时区跑出错;脚本里写了 sudo 但 cron 用户没免密配置。这些都可以靠 review 清单提前发现。
总结
要让 Cron 定时任务在服务器上长期可靠运行,关键是把脚本健壮、crontab 规范、日志结构化、监控有死人开关、告警有团队闭环这五件事都做到位。建议你把本文的脚本骨架和 timer 模板存进运维知识库,每次新增任务都按模板套。如果你需要扩展到更复杂的批处理或多机调度,可以考虑引入专门的工作流调度器;想看更多服务器运维实战,推荐继续阅读 WordPress 分类下与运维相关的文章 与 WordPress W3 Total Cache 调优,把性能、备份、调度这几块拼成完整的运维基线。