cron / crontab —— 定时任务
一句话定义
cron 是 Linux 上最经典的定时任务调度器——按时间表跑脚本。crontab 是它的配置工具。现代生产更推荐 systemd timer(更强、更可观察),但 cron 普及度第一、几乎任何系统都能用。
典型场景
- 每天凌晨 2 点 etcd 备份:
0 2 * * * /usr/local/bin/etcd-backup.sh - 每周清 docker 镜像:
0 3 * * 0 docker system prune -af - 每分钟监控脚本上报
- 不需要 K8s CronJob 的"宿主级"任务
K8s 里跑定时任务用
kind: CronJob(K8s 原生)。宿主机级别才用 cron / systemd timer。
配置位置
| 位置 | 谁的 | 怎么编辑 |
|---|---|---|
/var/spool/cron/<user> | 用户自己 | crontab -e |
/etc/crontab | 系统级 | 直接 vim |
/etc/cron.d/* | 系统级(多文件) | 直接 vim |
/etc/cron.{daily,weekly,monthly,hourly}/ | 系统级(按周期) | 把脚本扔进去 |
推荐用 crontab -e(用户级)—— 不需 root、不污染系统配置。
crontab 基础
crontab -e # 编辑当前用户的 crontab
crontab -l # 列已配置
crontab -r # **删除所有 crontab**(小心,没二次确认)
crontab -u root -e # 编辑别人的(要 root)
crontab -e 第一次会让选 editor:
Select an editor:
1. /usr/bin/vim.basic
2. /usr/bin/nano
选完保存即生效(不需要 systemctl)。
5 字段时间格式
┌──── 分 (0-59)
│ ┌── 时 (0-23)
│ │ ┌──── 日 (1-31)
│ │ │ ┌── 月 (1-12)
│ │ │ │ ┌── 周 (0-7,0/7 都是周日)
│ │ │ │ │
│ │ │ │ │
* * * * * command
例子:
| 表达式 | 含义 |
|---|---|
* * * * * | 每分钟 |
0 * * * * | 每小时整点 |
*/5 * * * * | 每 5 分钟 |
0 2 * * * | 每天凌晨 2 点 |
0 2 * * 0 | 每周日凌晨 2 点 |
0 0 1 * * | 每月 1 号 |
30 9 * * 1-5 | 工作日上午 9:30 |
0 */6 * * * | 每 6 小时 |
0 8,12,18 * * * | 每天 8/12/18 点 |
0 9-17 * * 1-5 | 工作日每天 9 点到 17 点的每个整点 |
快捷别名
@reboot 开机时跑一次
@yearly / @annually 每年(= 0 0 1 1 *)
@monthly 每月(= 0 0 1 * *)
@weekly 每周(= 0 0 * * 0)
@daily / @midnight 每天(= 0 0 * * *)
@hourly 每小时(= 0 * * * *)
@daily /usr/local/bin/cleanup.sh
@reboot /usr/local/bin/init-task.sh
一条完整的 crontab 例子
# m h dom mon dow command
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
# 每天凌晨 2 点备份 etcd
0 2 * * * /usr/local/bin/etcd-backup.sh >> /var/log/etcd-backup.log 2>&1
# 每 5 分钟健康检查
*/5 * * * * /usr/local/bin/healthcheck.sh
# 每周日清理 docker
0 3 * * 0 docker system prune -af --volumes >> /var/log/docker-clean.log 2>&1
# 开机时设置 sysctl(备份手段)
@reboot sleep 30 && /usr/local/bin/init-sysctl.sh
关键写法:
>> file 2>&1—— 重定向 stdout 和 stderr 到日志(强烈推荐)SHELL=/bin/bash—— 默认 cron 用/bin/sh,bash 特性用不了PATH=...—— cron 的 PATH 很短,命令找不到就要写绝对路径或自定义 PATHMAILTO=""—— 关掉 cron 自动邮件(默认会发,多数生产没配 mail)
cron 的几个坑爹默认
1. PATH 不完整
* * * * * kubectl get pods # 失败:kubectl not found
cron 的 PATH 默认只有 /usr/bin:/bin。要么:
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * kubectl get pods
或者用绝对路径:
* * * * * /usr/local/bin/kubectl get pods
2. 当前目录是 $HOME
不是你写 crontab 时的目录。脚本里需要 cd:
0 2 * * * cd /opt/myapp && ./backup.sh
3. 默认发邮件、没 mail 服务器导致日志炸
每次 cron 任务有 stdout/stderr 输出 → cron 发邮件到本地用户邮箱(/var/mail/<user>)。邮件文件越积越大。
修:
MAILTO="" # 关掉 cron 邮件
或者把任务输出重定向到日志文件:
0 * * * * /script.sh > /var/log/script.log 2>&1
4. % 在 crontab 里被替换
0 2 * * * mysql -e "INSERT VALUES(NOW(), '50%')"
cron 把 % 当 newline 处理 → 命令被截断。
% 必须转义 \%:
0 2 * * * mysql -e "INSERT VALUES(NOW(), '50\%')"
或者写脚本里、crontab 调脚本:
0 2 * * * /usr/local/bin/insert.sh
5. 时区是系统时区
0 2 * * * /backup.sh # 2 点是 / etc/timezone 里的 2 点
K8s 集群推荐用 UTC(见 hostnamectl.md)。crontab 里写时间也用 UTC:
date # 看现在的系统时间
调试 cron 任务(经典问题)
* * * * * 设置了但脚本没跑:
1. 看 cron 服务跑没
systemctl status cron # Debian / Ubuntu
systemctl status crond # CentOS / RHEL
2. 看 cron 日志
journalctl -u cron --since today # systemd
# 或者老式
tail -f /var/log/cron
tail -f /var/log/syslog | grep CRON
输出格式:
May 27 02:00:01 m1 CRON[12345]: (root) CMD (/usr/local/bin/etcd-backup.sh)
看到了 = cron 跑了。看不到 = crontab 没生效。
3. 模拟 cron 环境
cron 任务和你 shell 跑的环境不同(PATH、shell、cwd)。模拟:
# 切到 cron 环境
env -i /bin/sh -c '/usr/local/bin/myscript.sh'
env -i 清空环境变量、模拟 cron。
4. 加日志
* * * * * /script.sh >> /tmp/cron.log 2>&1
>> 追加、2>&1 把 stderr 也合并。
5. 检查 crontab 写错
crontab -l # 看自己的
sudo crontab -u root -l # 看 root 的
ls /etc/cron.d/ # 看系统级
systemd timer —— 现代替代品
systemd 提供 timer 单元,功能更强:
写 service + timer
# /etc/systemd/system/etcd-backup.service
[Unit]
Description=Etcd backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/etcd-backup.sh
# /etc/systemd/system/etcd-backup.timer
[Unit]
Description=Run etcd backup daily
[Timer]
OnCalendar=daily # 每天 0 点
# OnCalendar=02:00 # 每天 2 点
# OnCalendar=*-*-* 02:00:00 # 同上更明确
# OnCalendar=Mon..Fri 09:00 # 工作日 9 点
Persistent=true # 系统关机错过的任务、开机后补跑
RandomizedDelaySec=300 # 加 0-300 秒随机延迟
[Install]
WantedBy=timers.target
启动:
systemctl daemon-reload
systemctl enable --now etcd-backup.timer
# 看 timer 状态
systemctl list-timers --all
systemctl status etcd-backup.timer
journalctl -u etcd-backup.service # 看任务日志
cron vs systemd timer
| cron | systemd timer | |
|---|---|---|
| 普及 | 全 Linux | systemd 发行版 |
| 日志 | 自己 redirect | journalctl 自动收 |
| 错过任务补跑 | 不会 | Persistent=true 支持 |
| 随机延迟 | 不支持 | RandomizedDelaySec |
| 依赖管理 | 不支持 | Requires= After= |
| 时间格式 | 5 字段 | OnCalendar 更灵活 |
| 配置位置 | crontab | unit 文件 |
新写定时任务推荐 systemd timer。已有 cron 不用强迁。
K8s CronJob —— 容器化的"cron"
K8s 集群内的定时任务:
apiVersion: batch/v1
kind: CronJob
metadata:
name: hello
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
command: ["sh", "-c", "echo Hello at $(date)"]
restartPolicy: OnFailure
kubectl get cronjob
kubectl get jobs # 每次触发产生一个 job
kubectl logs job/hello-1234 # 看具体一次的日志
K8s CronJob 用 K8s 原生时间表(也是 5 字段)。集群内任务用 CronJob,不要 ssh 到节点用 cron。
实战例子
1. etcd 备份脚本
cat > /usr/local/bin/etcd-backup.sh <<'EOF'
#!/bin/bash
set -euo pipefail
DEST=/backup/etcd
TS=$(date +%F-%H%M)
mkdir -p "$DEST"
ETCDCTL_API=3 etcdctl snapshot save "$DEST/etcd-$TS.db" \
--endpoints=https://127.0.0.1:2379 \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--cert=/etc/kubernetes/pki/etcd/server.crt \
--key=/etc/kubernetes/pki/etcd/server.key
# 保留 14 天
find "$DEST" -name 'etcd-*.db' -mtime +14 -delete
EOF
chmod +x /usr/local/bin/etcd-backup.sh
cron:
0 2 * * * /usr/local/bin/etcd-backup.sh >> /var/log/etcd-backup.log 2>&1
或 systemd timer(更推荐,见上面)。
2. 节点磁盘清理
0 3 * * 0 docker system prune -af --volumes >> /var/log/docker-clean.log 2>&1
0 4 * * 0 journalctl --vacuum-size=500M >> /var/log/journal-clean.log 2>&1
3. 监控自检
*/5 * * * * /usr/local/bin/health-probe.sh || /usr/local/bin/alert.sh
常见踩坑
坑 1:crontab -r 误删
crontab -r
# crontab: 都没了,没二次确认
没有二次提示!手快可能误删。
预防:定期备份
crontab -l > ~/crontab.bak
或者用 git 管 crontab 文件、crontab ~/.crontab 加载。
坑 2:脚本里 PATH 找不到命令
# script.sh
kubectl get pods # 直接 cron 跑找不到 kubectl
# script.sh 头部加
#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
kubectl get pods
坑 3:脚本权限
0 2 * * * /home/user/script.sh
# script.sh 不可执行 → 不跑
chmod +x /home/user/script.sh
坑 4:日志没 2>&1,只看到 stdout
0 2 * * * /script.sh >> /var/log/script.log
# stderr 还是发邮件 / 默默丢
0 2 * * * /script.sh >> /var/log/script.log 2>&1
坑 5:daily 任务集中在凌晨同时段、I/O 爆炸
@daily 等价 0 0 * * *——所有 daily 任务都在 0 点 0 分跑。多个 daily 同一时间 → I/O 抖动。
# 分散执行时间
3 2 * * * task1
17 2 * * * task2
42 2 * * * task3
或者 systemd timer 用 RandomizedDelaySec=。
坑 6:cron 没启用
systemctl status cron
# inactive (dead)
systemctl enable --now cron
某些精简镜像 / 容器默认没装 / 没启 cron。
坑 7:crontab 文件最后没空行
某些 cron 实现要求最后必须空行 / 换行:
0 2 * * * /script.sh
← 最后一行可能要空行
crontab -e 通常自动处理、但手编辑 /etc/cron.d/foo 要注意。
坑 8:时间还没到 / 系统时间错
date
# 显示的时间是 2018 年 ... ?
先校时(见 chrony.md)。
坑 9:用户级 vs 系统级
crontab -e
# 编辑当前用户的
# 跑的时候是当前用户身份
sudo crontab -e
# 编辑 root 的
权限不一样。普通用户的 cron 跑不了需要 root 权限的命令。
坑 10:cron 已经触发但你看不到日志
journalctl -u cron --since today | grep my-script
# 空
可能:
- crontab 写错(minute / hour 颠倒)
- 任务跑了但 stdout / stderr 都没 redirect、被 cron 邮件吞了
ls /var/mail/<user> # 看是不是被邮件接收
关联命令
- systemctl —— systemd timer 用 systemd
- journalctl —— systemd timer 任务日志
at—— 一次性定时任务(cron 是周期、at 是单次)- hostnamectl —— 时区影响 cron 执行时间
anacron—— cron 的"补跑"版(笔记本 / 不 24x7 机器)- K8s
CronJob—— 集群内定时任务