2657 字
约 8 分钟
16
S15-X09 · 定时任务与 cron:从 crontab 到 systemd timer

"手动跑得好好的,放进 cron 就不行"——这句话背后九成是环境差异:PATH 少了、HOME 不对、locale 变成 POSIX、脚本没有执行位。剩下那一成是并发与重叠:上一轮还没跑完,下一轮已经起来,两份任务同时写同一张表。

下面按"怎么写 → 放哪里 → 环境差在哪 → 怎么防重叠 → 什么时候换 timer"走:字段语义 → 文件位置 → 环境变量 → 输出与日志 → 并发 → systemd → 判据 → 时区。


1. 五字段语义与写法陷阱

┌───────── 分钟 (0-59)
│ ┌─────── 小时 (0-23)
│ │ ┌───── 日 (1-31)
│ │ │ ┌─── 月 (1-12)
│ │ │ │ ┌─ 星期 (0-7,0 与 7 都是周日)
* * * * *  command
写法 实际行为 陷阱
*/5 * * * * 每 5 分钟 分片边界由 0 起算,*/7 会留下不整齐的空隙
0/5 * * * * 从 0 起每 5 分钟 与 */5 在分钟字段等价,在日字段不等价
*/5 写在"日"字段 1、6、11、16、21、26、31 号 跨月不连续:31 号到 6 号之间空 6 天
0 0 13 * 5 13 号或周五 日与周同时受限是"或",不是"且"
@daily 等价 0 0 * * * 只在 0 点整,不是"每天启动后第一个整点"
@reboot cron 守护启动时跑一次 容器里"重启容器"就算一次

常用简写:@hourly / @daily(@midnight) / @weekly / @monthly / @yearly(@annually) / @reboot。简写只解决可读性,语义与展开后的五字段完全相同。


2. 三个位置的差别

位置 是否需要用户字段 生效方式 适用
crontab -e(用户 crontab) 不需要 保存即生效 个人或单账号任务
/etc/crontab 需要(第 6 列) 由 cron 读取 系统级,但更容易被包管理覆盖
/etc/cron.d/<name> 需要(第 6 列) 文件放入即生效 应用自带的定时任务,随包分发
/etc/cron.{hourly,daily,weekly,monthly}/ 不需要 run-parts 调度 只放可执行脚本,不写时间
# /etc/crontab 与 /etc/cron.d/* 多一个用户字段
30 3 * * * root /usr/local/bin/backup.sh

# 用户 crontab(crontab -e)没有用户字段
30 3 * * * /usr/local/bin/backup.sh

两个容易忽略的细节:/etc/cron.d 只处理由字母、数字、连字符、下划线组成的文件名,带 . 的文件被直接跳过("backup.sh" 这种命名永远不生效);/etc/cron.{daily,...} 目录里的脚本必须可执行,参数由 run-parts 决定,不接受自定义命令行。


3. 环境变量:手工能跑、cron 不能跑

cron 启动的是非交互、非登录 shell,不读 .bashrc、.profile,环境极简:

变量 cron 里的值 后果
PATH 通常是 /usr/bin:/bin python3 在 /usr/local/bin 时直接 command not found
HOME 取自 /etc/passwd 相对路径 ~ 指向的目录与交互时可能不同
SHELL /bin/sh 脚本里用了 [[ ]]、<<< 等 bash 语法会报错
LANG/LC_* 常为 POSIX/C 中文乱码、日期格式与排序结果变化

排查最快的一招:把环境原样 dump 出来对比。

# crontab -e 里临时加一条
* * * * * env > /tmp/cron-env.txt 2>&1

# 交互 shell 里
env | sort > /tmp/login-env.txt
diff /tmp/cron-env.txt <(sort /tmp/login-env.txt)

修法优先级:脚本里写绝对路径(/usr/bin/python3)> crontab 顶部显式声明 PATH=/usr/local/bin:/usr/bin:/bin 与 LANG=zh_CN.UTF-8 > 用 bash -lc '...' 加载登录环境(最慢、最容易引入副作用,放最后)。


4. 输出与日志

cron 会把任务的 stdout/stderr 通过邮件发给任务所有者;机器没装 MTA 时这些输出进本地邮件队列,日积月累就是磁盘问题。

做法 写法 说明
丢弃全部输出 cmd >/dev/null 2>&1 最省事,出问题也没线索
落文件 cmd >> /var/log/job.log 2>&1 记得配 logrotate
只要错误 cmd >/dev/null 2>> /var/log/job.err 折中
关掉邮件 MAILTO=""(crontab 顶部) 关的是投递,不是输出产生
指定收件人 [email protected] 需要本机有可用 MTA
# 推荐:自己带时间戳,落文件并按天切
0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

MAILTO="" 只阻止投递,任务仍在产生输出、仍写管道;真正省资源的是把输出重定向掉。


5. 并发保护:别让两轮任务撞在一起

手段 写法 特点
flock 非阻塞 flock -n /var/lock/job.lock -c 'cmd' 拿不到锁立即退出,最常用
flock 等待 flock -w 600 /var/lock/job.lock -c 'cmd' 最多等 10 分钟
文件描述符式 flock -n 9; [ $? -ne 0 ] && exit 0 可在脚本内部加锁
systemd 单实例 service 默认不并发 timer 触发时上一个还在跑会被跳过或排队
# 每 5 分钟一轮,但同一时刻只允许一个实例
*/5 * * * * flock -n /var/lock/sync.lock -c '/usr/local/bin/sync.sh >> /var/log/sync.log 2>&1'

flock 的锁随进程退出自动释放,比"检查 pid 文件是否存在"可靠得多——pid 文件在 kill -9 后会残留,flock 不会。


6. systemd timer:结构对照

cron 的等价物是"一个 .service 加一个 .timer",两者默认同名配对:

# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
StandardOutput=append:/var/log/backup.log
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly

[Timer]
OnCalendar=*-*-* 03:30:00
Persistent=true
AccuracySec=1min
RandomizedDelaySec=2min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers --all | grep backup
journalctl -u backup.service -n 50 --no-pager
systemd-analyze calendar "*-*-* 03:30:00"   # 验证表达式与下次触发时间
能力 cron systemd timer
补跑错过的任务 不支持 Persistent=true
依赖其他服务 只能自己 sleep/重试 After=、Requires=、Wants=
日志 邮件或自行重定向 journalctl -u,带结构化字段
并发控制 需 flock service 天然单实例
随机抖动 自己做 RandomizedDelaySec
表达式校验 只能等触发 systemd-analyze calendar 离线验证
秒级精度 不支持 OnCalendar 支持秒

OnCalendar 的常用形态:*-*-* 03:30:00(每天)、Mon..Fri *-*-* 09:00:00(工作日)、*-*-01 00:00:00(每月 1 号)、Sun *-*-* 04:00:00(每周日)。AccuracySec 默认 1 分钟,它允许 systemd 在这个窗口内合并唤醒以省电,要求准点就调小;RandomizedDelaySec 用来打散一批同时触发的任务。

Persistent=true 只在有 OnCalendar 时有意义:它记录上次触发时间(/var/lib/systemd/timers/),开机后补跑错过的轮次。这正是 cron 最缺的能力——机器在 3:30 关机,cron 就永久丢了这一轮。


7. 选择判据

需求 选 cron 选 timer
一次性、简单的命令 ✅ —
需要补跑错过的轮次 — ✅
依赖网络/数据库就绪 — ✅(After=network-online.target)
需要精细日志与退出码 — ✅(journalctl)
秒级或复杂日历 — ✅
分发给非 systemd 环境 ✅ —
想一条命令改完就走 ✅ —

原则:任务本身有状态(要补跑、要依赖、要查日志)就用 timer;纯"到点跑一下"就用 cron。 同一台机器上两者可以共存,别为了一致性把简单的 flock 任务改写成三个文件。


8. 时区

位置 时区来源 注意
cron 系统时区(/etc/timezone、timedatectl) CRON_TZ=Asia/Shanghai 可改单条,支持情况按发行版核对
systemd timer 系统时区 跨时区机器先 timedatectl set-timezone
脚本内部 进程继承 TZ=UTC date 只影响该次调用

夏令时地区还有额外的坑:切换当天可能出现"这一小时不存在"或"这一小时出现两次",0 2 * * * 这类表达式在春季跳变的日期会跳过或重复执行。对时间敏感的任务,把触发点避开 1:00–3:00。


9. 排错

现象 原因 处理
手工能跑,cron 报 command not found PATH 不含自定义目录 脚本内写绝对路径,或 crontab 顶部设 PATH
脚本无输出、也不执行 没有执行位,或 shebang 缺失 chmod +x,首行写 #!/usr/bin/env bash
报 bad interpreter shebang 指向不存在的解释器 核对 which bash 的实际路径
/etc/cron.d 里的任务不生效 文件名带 .,或缺少用户字段 改名,补第 6 列
输出里出现意外换行/截断 命令里的 % 未转义 写成 \%(cron 把裸 % 当换行符)
同一任务重复执行 多份 crontab 或 cron 与 timer 同时配 grep -r 全盘找一遍,用 flock 兜底
任务重叠导致数据重复 上一轮未结束,下一轮已启动 flock -n 或改 timer 单实例
Persistent 没补跑 timer 没有 OnCalendar,或从未 enable systemctl enable --now x.timer
中文日志乱码 cron 的 locale 是 C/POSIX 脚本里显式 export LANG=zh_CN.UTF-8

10. 边界

场景 更合适的方案 理由
分钟级、跨多台机器 调度平台(XXL-JOB 等) 需要统一视图、失败重试、分片
需要任务依赖 DAG Airflow / DolphinScheduler cron 与 timer 都表达不了依赖图
高频(秒级、毫秒级) 常驻进程 + 内部调度 每次 fork 进程的成本吃不消
一次性延迟执行 systemd-run --on-active=30m 不必写单元文件
容器内定时 容器外调度,或应用内调度器 容器重启即丢 cron 状态

一句话:cron 解决"到点 fork 一个进程",timer 解决"到点 fork 一个进程并留下可查的证据";再往上就该换真正的调度系统。


11. 自测

  1. */5 写在"日"字段,为什么会出现跨月不连续的间隔?
  2. 日与星期同时写了具体值,是"且"还是"或"?
  3. /etc/cron.d 里的文件为什么不能叫 backup.sh?
  4. cron 任务的 PATH 与交互 shell 不同,最快的定位手段是什么?
  5. flock -n 比 pid 文件可靠在哪?
  6. Persistent=true 为什么必须配合 OnCalendar 才有意义?
S15-X09 · 定时任务与 cron:从 crontab 到 systemd timer
https://www.clxhxhhr.top/posts/5830/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。