凌晨三点,电话来了。现象很怪:数据只更新到周四。机器没挂,进程没挂,磁盘没满,网络通的。
排查了四十分钟。最后发现那台机器周一被例行重启过一次,而那个每周四凌晨跑的数据同步任务——cron 的——重启期间错过的就是错过了。
它不补,不说,甚至不留一行日志告诉你它没跑。
cron 这玩意儿,任务没跑的时候是静默的。这是它最大的罪,比语法丑严重多了。
所以前阵子看到有人写文章劝大家把 cron 任务迁到 systemd timers,我站他这边。理由不是什么「现代化」,是几个实打实的运维痛点。
先说日志。cron 任务的标准输出去哪了?老规矩,自己在 crontab 里加重定向,追加到某个文件,然后祈祷那个文件不被 logrotate 干掉、磁盘不写满。谁没见过 >> /var/log/xxx.log 2>&1 后面跟一个再也没人看过的文件。
systemd timer 不一样,输出直接进 journald,带时间戳带元数据,journalctl -fu 一条命令盯着看。就这一条,值得迁一半的任务。
然后是 Persistent=true。机器在计划时间关着,开机之后补跑。上面那个凌晨三点的电话,有这一行,根本不会响。
三是不跑的时候你知道它没跑。systemctl list-timers 列出所有 timer 和下次触发时间,配合 OnFailure 可以在任务挂掉时触发告警脚本。cron 任务挂了怎么知道?——你不知道。你等到下游数据断流才知道。
还有资源限制。CPU、内存、OOM 评分直接写在单元文件里。这个我劝各位认真考虑。多少台机器上跑着一个每五分钟一次、偶尔内存泄漏、把整个节点拖下水的 cron 脚本?在 cron 里你管不了它,在 systemd 里一行 MemoryMax 的事。
迁移本身不复杂。一个 .service 定义干什么——cron 场景基本就是 Type=oneshot——一个 .timer 定义什么时候干。OnCalendar=daily,Persistent=true,顺手加个 RandomizedDelaySec 把触发时间打散,免得一堆任务挤在同一秒。
然后 daemon-reload。enable 的是 .timer 不是 .service——这个坑新手必踩,enable 了 service,任务纹丝不动,还纳闷为什么没跑。
时间表达式不确定对不对,别猜:
systemd-analyze calendar "daily"
它会把后续几次的确切触发时间打给你看。比我这些年靠脑子算 cron 五段式的准确率高,这个我认。
但是。说句实话,这里有个坑——评论区有人指出来,作者也认了。我要是没看到那条评论,大概率也要栽进去:
systemd timer 默认不是精确触发的。AccuracySec 默认一分钟,系统会在窗口里合并触发,为了省电少唤醒。对大部分任务无所谓。
可你要是从 cron 迁过来一个依赖精确秒级调度的东西,或者真用上了 OnCalendar=*:*:0/30 这种每 30 秒一次的玩法(cron 做不到的粒度),默认那个一分钟窗口会给你表演「我明明设了每 30 秒,为什么一分多钟才跑一次」。
要精确,加 AccuracySec=1us,代价是放弃节能。各取所需,但你得知道这个开关存在。不知道的人,排查这个能排查一晚上——因为它「看起来在跑」,只是不准。这种故障比完全不跑更阴。
扯远了,拉回来。
我知道有人会说:两行 crontab 变成两个文件,一个任务两份配置,这也叫进步?字数上是退步,我认。
但你算账不能只算写的成本,得算半夜的成本。crontab 那一行之所以便宜,是因为故障成本全记在值班那个冤种头上了——日志没人管、错过的跑次没人知道、失控的进程没人拦。systemd timers 把这些成本前置到了配置里。
道理和 runbook 一样:写的时候多花十分钟,凌晨三点少熬一小时。
当然别一刀切。祖传 crontab 里那些跑了八年没人敢动的行,先弄明白它们干嘛的再说,别批量「现代化」。那里面说不准哪一行是某次事故之后加的补丁——你不知道,迁移脚本更不知道。让脚本去动屎山,等于让没踩过雷的人排雷。
我的做法:新任务一律 timer,老任务动到哪个迁哪个。
本期教训:cron 任务最危险的不是写错,是失败的时候不吭声——选调度工具,先看它挂了之后谁会知道。