≡articles //snippets ./categories $uses ~about /search

systemd timer 替代 cron 实战:从 crontab 静默清空到全量迁移

起点:一条凌晨 3 点的报警

凌晨 3 点收到监控报警:证书续期任务没跑。登录服务器一看,crontab 里那条 certbot renew 消失得干干净净。

不是被黑了。是上周同事改配置时 crontab -e 保存失败(vim 非正常退出),cron 静默清空了整个文件——没有报错,没有提示,没有备份。

这是 cron 最危险的设计:编辑即覆盖,失败即清空。也是我彻底切到 systemd timer 的导火索。

这篇文章从真实踩坑出发,不罗列参数表,每条命令都有实际输出和排查路径。

systemd timer 是什么

一句话:用 .timer + .service 两个文件替代 crontab 一行。timer 管“什么时候跑”,service 管“跑什么”。

和 cron 的核心差异不在语法,在架构:

$ systemctl list-timers --all
NEXT                         LEFT          LAST                         PASSED    UNIT                         ACTIVATES
Tue 2026-08-11 03:00:00 CST  1h 23min left Mon 2026-08-10 03:00:00 CST  22h ago   certbot-renew.timer          certbot-renew.service
Tue 2026-08-11 06:00:00 CST  4h 23min left Mon 2026-08-10 06:00:00 CST  19h ago   wp-backup.timer              wp-backup.service
Wed 2026-08-12 00:00:00 CST  22h left      Tue 2026-08-11 00:00:00 CST  1h ago    logrotate.timer              logrotate.service

每一个 timer 的状态、下一次触发时间、上次运行时间,都是可查询的。 cron 只有 crontab 文件和 syslog——前者可能被清空,后者全靠 grep。

第一个坑:crontab 静默消失 → systemd 的隔离设计

先看 cron 的致命问题:

$ crontab -l
no crontab for root     # ← 什么时候没的?不知道

$ grep CRON /var/log/syslog | tail -20
Aug 10 03:00:01 vps CRON[384712]: (root) CMD (certbot renew)
Aug 11 03:00:01 vps CRON[391203]: (root) CMD (certbot renew)
# 到这里还有
Aug 12 03:00:01 vps CRON[397851]: (root) CMD (certbot renew)
Aug 13 03:00:01 — 没了

crontab 文件的编辑完全依赖编辑器的正常退出。vim 崩溃、SSH 断连、:q! 误操作——任一种都可能导致整个文件变空。更糟的是,系统不会告诉你任务丢了。你只有在某个任务不再跑的时候才发现。

systemd timer 的解决方案是文件隔离:

# /etc/systemd/system/certbot-renew.service
[Unit]
Description=Certbot Renew
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
# /etc/systemd/system/certbot-renew.timer
[Unit]
Description=Run certbot-renew twice daily
Requires=certbot-renew.service

[Timer]
OnCalendar=03:00
RandomizedDelaySec=300
Persistent=true

[Install]
WantedBy=timers.target

这种“文件隔离 + 显式启用”的设计意味着:你永远不会在半夜发现之前配好的备份任务神秘消失了。每个 .service 和 .timer 文件都是独立的存在,互不干扰。

第二个坑:cron 的环境变量地狱

这是新手屡犯的老问题。cron 运行环境极其精简:

# 在 crontab 里加一条诊断
* * * * * env > /tmp/cron-env.txt

$ cat /tmp/cron-env.txt
HOME=/root
LOGNAME=root
PATH=/usr/bin:/bin
SHELL=/bin/sh
PWD=/root

没有 /usr/local/bin,没有 /snap/bin,没有 $NODE_PATH。很多人在终端能跑的脚本,放到 crontab 里就 127 command not found:

$ which node
/home/hermes/.local/bin/node

# 放到 crontab:
0 3 * * * node /opt/scripts/generate-report.js
# → /bin/sh: node: command not found

# 修复方式:在 crontab 文件顶部手动设 PATH
PATH=/usr/local/bin:/usr/bin:/bin:/home/hermes/.local/bin
0 3 * * * node /opt/scripts/generate-report.js

但这种方式有两个问题:一是容易忘,二是每个用户的 crontab 都要单独设。systemd 的做法:

# /etc/systemd/system/generate-report.service
[Service]
Type=oneshot
ExecStart=/home/hermes/.local/bin/node /opt/scripts/generate-report.js
Environment=NODE_ENV=production
Environment=DB_HOST=127.0.0.1
WorkingDirectory=/opt/scripts
User=hermes

路径用绝对路径,环境变量显式声明,工作目录显式指定。不会有“终端能跑 cron 不能跑”的玄学问题。

第三个坑:cron 没有依赖管理

假设有两个任务:备份数据库 → 然后清理旧备份。在 cron 里,你只能手动错开时间:

0 3 * * * /opt/scripts/backup-db.sh
15 3 * * * /opt/scripts/cleanup-backups.sh   # 赌 15 分钟能跑完

备份跑 16 分钟怎么办?清理脚本就开始删正在生成中的备份文件。或者在备份脚本里手动加一行:

#!/bin/bash
/opt/scripts/backup-db.sh && /opt/scripts/cleanup-backups.sh

可行但脆弱——以后别人看 crontab 只看到一条任务,不会知道里面包了两步。systemd timer 原生支持 OnUnitActiveSec——在 service 完成后计时:

$ systemctl cat backup-db.service
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup-db.sh
$ systemctl cat cleanup-backups.timer
[Timer]
OnUnitActiveSec=5min    # backup-db 跑完后等 5 分钟再触发
Unit=cleanup-backups.service

这是一种“任务完成后 5 分钟”的语义,不是“每天 3:15”。无论备份跑了 3 分钟还是 30 分钟,清理任务永远在它真正完成后才触发。

第四个坑:cron 邮件是一坨垃圾

cron 默认行为:任务有 stdout/stderr 输出 → 发邮件给本地 root 用户。如果没配 MTA(绝大多数服务器不会配),这些邮件全堆在 /var/mail/root:

$ ls -lh /var/mail/root
-rw-rw---- 1 root mail 247M Aug 11 09:00 /var/mail/root

$ tail -5 /var/mail/root
From root@vps  Mon Aug 11 03:00:02 2026
Return-Path: <root@vps>
Subject: Cron <root@vps> certbot renew
Saving debug log to /var/log/letsencrypt/letsencrypt.log
No renewals were attempted.

247MB 的 cron 垃圾邮件。没人看,占磁盘。systemd 的方案:日志进 journald,结构化可查询:

$ journalctl -u certbot-renew.service --since "2026-08-01" --no-pager
Aug 01 03:00:02 vps systemd[1]: Starting Certbot Renew...
Aug 01 03:00:03 vps certbot[4123]: Saving debug log to /var/log/letsencrypt/letsencrypt.log
Aug 01 03:00:05 vps certbot[4123]: No renewals were attempted.
Aug 01 03:00:05 vps systemd[1]: certbot-renew.service: Deactivated successfully.

$ journalctl -u certbot-renew.service --since "2026-08-01" -p err
# 只显示 ERROR 级别日志,零噪音

输出自动带时间戳、带 service 名、可按级别过滤、可查指定时间范围。比 grep CRON /var/log/syslog 强一个数量级。

第五个坑:Persistent=true — 机器宕机后补跑

这是 systemd timer 最实用的功能,cron 做不到。

服务器凌晨 3 点因为 OOM 重启了,3 点到 4 点之间的所有 cron 任务直接跳过——第二天才会跑。certbot renew 跳过一天没问题。数据库备份跳过一天呢?

[Timer]
OnCalendar=03:00
Persistent=true    # ← 这一行

Persistent=true 的效果:当系统在 timer 触发时间点处于关机状态,下次开机时立即补跑。cron 没有这个概念——anacron 勉强类似但功能有限(只支持 daily/weekly/monthly 粒度)。

排查实录:timer 不触发,三分钟定位

配置完 timer 后 systemctl start 没报错,但任务不跑。排查过程:

$ systemctl status my-backup.timer
● my-backup.timer - Run daily backup
     Loaded: loaded (/etc/systemd/system/my-backup.timer; disabled; preset: disabled)
     Active: inactive (dead)
    Trigger: n/a
   Triggers: ● my-backup.service

Loaded 但 Active: inactive——timer 文件存在但没被启动。原因:

$ systemctl start my-backup.timer
# 没报错但没效果

$ systemctl enable my-backup.timer  # ← 关键
Created symlink /etc/systemd/system/timers.target.wants/my-backup.timer → /etc/systemd/system/my-backup.timer.

$ systemctl start my-backup.timer
$ systemctl status my-backup.timer
● my-backup.timer
     Active: active (waiting) since Tue 2026-08-11 10:23:05 CST
    Trigger: Wed 2026-08-12 03:00:00 CST

enable 创建符号链接,start 立即激活。只 start 不 enable → timer 被标记为一次性,下次 daemon-reload 后丢失。这是最常见的“配置正确但不触发”的根因。

cron → systemd timer 迁移清单

完整迁移一个 crontab 条目的检查清单:

# 1. crontab 原文
0 3 * * * /opt/scripts/backup-db.sh >> /var/log/backup.log 2>&1

# 2. 迁移到 systemd
# backup-db.service:
[Unit]
Description=Database Backup
After=network.target mysqld.service

[Service]
Type=oneshot
ExecStart=/opt/scripts/backup-db.sh
StandardOutput=journal     # 替代 >> log 文件
StandardError=journal
User=root

# backup-db.timer:
[Unit]
Description=Daily database backup

[Timer]
OnCalendar=03:00
Persistent=true

[Install]
WantedBy=timers.target

# 3. 部署与验证
$ systemctl daemon-reload
$ systemctl enable backup-db.timer
$ systemctl start backup-db.timer
$ systemctl list-timers --all | grep backup-db
$ journalctl -u backup-db.service -f   # 等触发时实时看日志

什么时候不用 systemd timer

不是所有的 cron 都值得迁移。以下场景保留 cron 更合理:

  • 容器环境:容器里通常没有 systemd(需要特权模式或特殊镜像),cron 是更轻量的选择
  • 秒级定时:OnUnitActiveSec=5s 在理论上是秒级,但 systemd 的调度精度约为 1 秒,超出这个精度应该用专门的调度器
  • 一次性临时任务:echo "rm -rf /tmp/*" | at now + 2 hours 这种用 at 比创建两个文件简单得多
  • 老旧系统兼容:CentOS 6 / RHEL 6 没有 systemd,只能 cron

真实感受:把 crontab 清空之后,看着 systemctl list-timers 一排整齐的 timer 状态,心里才踏实。每个任务什么时候触发、上次跑了多久、有没有报错——全是透明的,一目了然。

实战:把 WordPress 定时任务从 cron 迁到 systemd timer

拿一个真实场景收尾——WordPress 的 wp-cron.php。默认情况下每次页面请求都会触发 wp-cron,在高流量站点这就是一个性能黑洞。

禁用 WordPress 内置 cron:

# wp-config.php
define('DISABLE_WP_CRON', true);

然后创建 systemd timer 每 5 分钟触发一次:

# /etc/systemd/system/wp-cron.service
[Unit]
Description=WordPress Cron
After=network.target

[Service]
Type=oneshot
ExecStart=/usr/bin/curl -s -o /dev/null https://yoursite.com/wp-cron.php?doing_wp_cron
User=www
StandardOutput=journal
StandardError=journal
# /etc/systemd/system/wp-cron.timer
[Unit]
Description=WordPress Cron Timer

[Timer]
OnCalendar=*:*:0/300       # 每 5 分钟
RandomizedDelaySec=30       # 随机偏移 30 秒,避免惊群
Persistent=false            # wp-cron 跳过几次无所谓

[Install]
WantedBy=timers.target

部署后验证:

$ systemctl daemon-reload
$ systemctl enable --now wp-cron.timer
$ journalctl -u wp-cron.service -f
# 能看到每 5 分钟一条 "Deactivated successfully"

对比 cron 的方案——在 crontab 里写 */5 * * * * curl ...,你没办法知道它到底跑没跑、每次耗时多少、有没有报错。systemd timer 给你 systemctl status 和 journalctl,可观测性不在一个量级。

排查路径总结

回顾从踩坑到迁移的完整路径:

$ tree 排查路径/
├── 根因:crontab 静默清空 → systemd 文件隔离设计
├── 环境变量:127 command not found → Environment + 绝对路径
├── 任务依赖:手动错开时间赌博 → OnUnitActiveSec
├── 日志:247MB /var/mail/root 垃圾 → journalctl 结构化查询
├── 宕机补跑:cron 直接跳过 → Persistent=true
└── 陷阱:只 start 不 enable → timer 重启后丢失

核心收益不是“更现代”,是每一个坑 cron 都踩过了,而 systemd timer 从设计层面给你避开了。

© 2026 systemd timer 替代 cron 实战:从 crontab 静默清空到全量迁移 · 本文由 Charlie 原创撰写,发布于 sodebug.com。 未经授权禁止转载、洗稿、机器抓取。AI 训练数据使用需获得书面授权。
GK
独立开发者,在 WordPress、Nginx 和各种 API 之间切换。不写没用的东西。

related相关文章

#01Linux 放行端口指令3 min#02VPS 常用测试脚本大全5 min#03Linux 负载排查方法论:Load Average 8.5、CPU 空闲 88%,从数字到底层 IO7 min
$ echo "less bullshit, more debugging" | send-to-inbox
新文章直接发到邮箱,一个月 2-4 封。
$ subscribe →