ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Linux定时任务全解:crontab、systemd timer与at的选型与排查

Linux定时任务全解:crontab、systemd timer与at的选型与排查

1. 例行性工作:Linux 运维里最容易被低估的基础功

1.1 定时任务在真实环境里到底承担什么角色

我接触 Linux 系统管理这些年,最大的感受是:很多人对定时任务(例行性工作)的理解停留在"会用 crontab 写个备份脚本"这个层面,但真正到了线上环境,定时任务几乎是无处不在的底层设施。日志切割、监控数据采集、证书续期、数据库备份、临时缓存清理、报表定时生成,这些都是靠定时任务在背后默默跑着的。

举个最直观的例子:某公司内部有一个业务库,每天凌晨两点要做全量备份,备份完成之后还要把备份文件同步到远程存储,再保留最近 7 天的备份、清理更早的文件。这一整条链路如果全靠人肉盯着,不仅累,而且还容易出纰漏。用定时任务把备份、同步、清理串成一套脚本,每天自动执行,才是正经的运维做法。

RHCE 考试里,定时任务属于必考的基础技能。它考的不是"你会不会用 crontab -e",而是你知不知道任务执行的底层逻辑、环境差异、日志排查,以及不同场景下该选哪种工具。很多人在考试里能写对 crontab 语法,但一遇到"为什么我的脚本手动执行正常、定时执行就失败"这种经典问题就懵了。这说明对定时任务的理解还停留在表面。

这篇内容我想系统地把 Linux 例行性工作拆开讲清楚:cron 系、at 系、systemd timer 这三套工具各自适合什么场景,配置时有哪些坑,出了问题怎么排查。不管你是备考 RHCE,还是日常做系统维护,这套东西都能直接用上。

1.2 学习路线:先分清"一次性"和"周期性"

例行性工作按调度方式分两大类:一类是周期性的,比如每天、每周、每隔几分钟执行一次,这类用 cron 或者 systemd timer;另一类是一次性的,比如"五分钟后重启服务""下午三点执行某个脚本",这类用 at 命令最合适。

很多人一开始会把这两类混在一起,觉得反正都能定时执行。但实际使用中,一次性任务如果用 cron 来做,会留下一个永远不再需要执行的残留条目,之后还得记得清理。而 at 任务执行完就自动消失,不需要管理残渣。

我建议的学习路径是:先精通 cron(这是最常用、考试必考的),再掌握 systemd timer(这是现代 Linux 发行版主推的替代方案),最后熟悉 at(一次性任务场景)。下面我按这个顺序把每套工具的用法、原理、坑都过一遍。

2. 工具选型:crontab、systemd timer、at 怎么选

2.1 三套工具的定位差异

先做一张对比表,方便大家直观感受三套工具的差异。

维度cron(crond)systemd timerat
任务类型周期性执行周期性执行一次性执行
最小粒度分钟级分钟级(可到秒级)分钟级
配置方式crontab 文件 / 系统级 /etc/crontab.timer 单元 + .service 单元at 命令交互式输入
日志位置/var/log/cronjournalctl 统一日志/var/log/cron
依赖管理不支持支持,可定义服务依赖不支持
错过补跑不补,错过就错过Persistent=true 可补跑不适用
适合场景传统运维、脚本类任务需要更精细控制的服务型任务临时、一次性操作

从这张表能看出,cron 的优势是简单、普及率高、任何 Linux 发行版默认都有;systemd timer 是面向服务化场景设计的,能管理依赖、能在错过后补跑、日志统一;at 则是专门为一次性任务设计的工具。三者不是竞争关系,而是互补关系。

2.2 实际场景下的选型建议

我自己的选型习惯是这样的:

  • 简单的周期脚本,比如每天备份、每 5 分钟做一次健康检查,直接用 crontab,配置简单、排查问题也容易。
  • 如果任务需要保证"这次没跑成,下次开机要补上",比如补课式的数据同步,用 systemd timer 的 Persistent 特性。
  • 如果任务需要精细控制执行条件,比如"某个服务起来之后 10 分钟再执行",systemd timer 的 After、OnUnitActiveSec 能实现,cron 做不到。
  • 临时性的、只跑一次的任务,比如"一个小时后给我发个通知",用 at 一行命令搞定,不用污染 crontab。

RHCE 考试对工具选型的要求,其实就是要求你能根据需求描述选择合适的工具。比如题目说"设置一个每 30 分钟执行一次的任务",你写 cron;说"在晚上 11 点执行一次临时任务",你用 at;说"要求任务在系统启动后延迟执行,并允许错过补跑",你就得想到 systemd timer。这些场景我在后面逐个展开。

3. crontab 配置拆解与实战要点

3.1 时间字段:五分钟学会,但要想清楚细节

crontab 的语法核心是五个时间字段加一条命令,顺序是分、时、日、月、周。对应取值范围是:

  • 分:0-59
  • 时:0-23
  • 日:1-31
  • 月:1-12
  • 周:0-7,其中 0 和 7 都表示周日

举个例子,30 2 * * *表示每天凌晨 2 点 30 分执行;*/5 * * * *表示每 5 分钟执行一次;0 9-18 * * 1-5表示周一到周五的 9 点到 18 点整点执行。星号、逗号、减号、斜杠这四个通配符分别表示任意值、枚举多个值、连续范围、步进间隔,组合起来非常灵活。

但这里有个非常典型的坑:当"日"和"周"字段同时被设置为非星号时,cron 的处理逻辑是"或",而不是"与"。也就是说,0 8 1 * 1的意思是"每月 1 号的早上 8 点,或者每周一的早上 8 点"执行,而不是"每个月 1 号且恰好是周一"才执行。这一点和很多人的直觉不符,考试里也经常拿这个做文章。

还有一个细节:分钟字段一定要写,很多人写* 2 * * *以为意思是"凌晨 2 点执行一次",实际上是"凌晨 2 点的每一分钟都执行",也就是会连跑 60 次。这是一个非常昂贵的误会,之前真有同事在凌晨两点把服务器打挂了。想要"每天两点执行一次",必须写成0 2 * * *,分钟位填 0。

3.2 用户级与系统级:权限边界要分清

crontab 分两个层级:用户级和系统级。用户用crontab -e编辑的是自己的任务表,存放在 /var/spool/cron/ 目录下,文件名就是用户名。系统级任务可以写在 /etc/crontab 文件里,也可以在 /etc/cron.d/ 目录下新建文件。

两者的区别在于:用户级的 crontab 不用写执行用户,因为默认就是当前用户;而 /etc/crontab 和 /etc/cron.d/ 里的条目在时间字段后面必须写执行用户,格式是分 时 日 月 周 用户 命令。比如系统级任务写备份,应该是0 2 * * * root /opt/scripts/backup.sh。

另外,如果用户被列在 /etc/cron.deny 文件里,就无法使用 crontab 命令;如果存在 /etc/cron.allow 文件,则只有该文件里列出的用户才能使用。默认情况下这两个文件可能都不存在,所有用户都能配置,在安全要求严格的机器上,建议显式创建 /etc/cron.allow 来限制使用者范围。

考试里经常考 /etc/crontab 和用户 crontab 的格式差异,我见过不少人在用户 crontab 里写了用户名,导致整行被当成命令执行,任务根本不会按预期运行。记住:用户 crontab 五段时间字段后面直接跟命令,系统 crontab 六段后面跟用户再跟命令。

3.3 环境变量:定时任务失败的"头号嫌疑人"

我排查过非常多的定时任务问题,其中至少三分之一是环境变量导致的。手动在终端里执行脚本一切正常,但 cron 一跑就报错,最常见的报错就是command not found。

原因很简单:cron 执行命令时用的是精简环境,PATH 一般只有 /usr/bin:/bin,不包含 /usr/local/bin,更不包含你自己加进去的自定义路径。手动登录终端时,shell 会加载 /etc/profile 和 ~/.bash_profile,把完整 PATH 配好;但 cron 是非交互式、非登录式执行,这些文件不会被加载。

解决方案有两个思路。第一个是脚本内自包含:在脚本开头硬编码 PATH,比如export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin,再在后面加一行export LANG=en_US.UTF-8,避免字符集导致的脚本异常。第二个是在 crontab 文件顶部追加环境变量配置:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin MAILTO=admin@example.com

另外要注意,cron 的工作目录默认是用户家目录,脚本里如果有相对路径引用,一定要先cd到目标目录,或者在脚本里用绝对路径定位文件。最稳妥的做法是:脚本里所有路径全部写绝对路径,包括日志文件的输出路径。

3.4 输出日志与重定向:别让邮件把系统塞满

默认情况下,crontab 任务执行时产生的标准输出和标准错误会通过邮件发送给用户。这里有两个隐患:一是服务器上如果没配邮件服务,输出会积压在本地邮件队列里,时间长了占磁盘空间;二是即使配了邮件,也没人真的会去看邮件,日志就相当于丢了。

所以我的习惯是,每个 cron 任务都显式重定向输出。标准写法是:

0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

这条命令把标准输出追加到备份日志,把标准错误也重定向到同一个文件。如果你希望"成功时安静、失败时通知",可以用更精细的写法:

0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1 || echo "backup failed" | mail -s "backup alert" admin@example.com

||左边的命令失败时会执行右边的告警命令,这样既不丢日志,又能在异常时主动报警。至于那种完全不需要输出的任务,可以全部丢到 /dev/null,但我不建议真的这么做,因为之后排查问题时没有任何线索,会很痛苦。

3.5 百分号转义:一个容易忽视的语法细节

如果你在 crontab 命令里用到%符号,比如 date 命令的格式化参数%Y%m%d,会有一个坑:在 crontab 文件中,%会被解析成特殊字符,表示换行,之后的内容会被作为标准输入传给命令。所以想输出带格式的日期,必须写成\%Y\%m\%d:

0 2 * * * /opt/scripts/backup.sh --prefix=$(date +\%Y\%m\%d) >> /var/log/backup.log 2>&1

这个细节很多人不知道,写进去之后任务报错或者行为诡异,还查不出原因。考试虽然不考这么细,但实际工作中碰到了,排查起来非常痛苦,我特意提一下。

4. at 一次性任务:临时需求的最佳拍档

4.1 at 命令的基本操作

at 的用法比 cron 简单得多。进入交互模式直接用at 时间表达式,比如:

at 14:30

输入 at 后会进入一个类似 shell 的提示符,你输入要执行的命令,以 Ctrl+D 结束。也可以不用交互模式,直接把命令从管道喂进去:

echo "/opt/scripts/oneoff.sh" | at 14:30

时间表达式非常灵活,支持绝对时间、相对时间、甚至模糊表达。比如at now + 10 minutes表示 10 分钟之后;at 17:00 tomorrow表示明天下午五点;at noon表示今天中午十二点;at teatime表示下午四点。相对时间的写法最常用,因为临时任务通常就是"再过多久跑一下"。

查看待执行的任务用atq,输出结果里会带任务编号和执行时间。删除任务用atrm 编号,比如atrm 5就把编号 5 的任务删掉。任务一旦被执行,条目就从队列里消失,不需要额外清理。

4.2 at 的安全与权限控制

at 的安全机制和 cron 类似,受 /etc/at.allow 和 /etc/at.deny 控制。存在 at.allow 时只有列表内的用户能用 at;否则所有用户都能用,再看 at.deny 里有没有被排除的用户。默认系统一般只有 at.deny 且内容是空,表示所有用户都能用。

权限这块在企业环境里值得认真对待。如果一台机器是多人共管的,临时任务又涉及重启服务、删文件这类高风险操作,最好限制只有 root 或指定运维组成员能使用 at,避免有人随手提交一个危险任务。

还要注意,at 任务执行时同样会继承精简环境,所以脚本里该写的绝对路径和 PATH 声明一样不能少。另外 at 任务执行输出默认也是发邮件,同样建议重定向到日志文件。

5. systemd timer:面向服务器时代的进阶方案

5.1 一次完整的 timer 配置拆解

systemd timer 由一对单元文件组成:一个 .timer 描述触发条件,一个 .service 描述要执行的内容。先用 timer 管理一个"每小时执行一次健康检查"的任务,整个过程分三步。

第一步,创建服务单元 /etc/systemd/system/healthcheck.service:

[Unit] Description=Run health check script [Service] Type=oneshot ExecStart=/opt/scripts/healthcheck.sh

Type=oneshot 表示执行完就退出,不是长驻服务。

第二步,创建配套的定时单元 /etc/systemd/system/healthcheck.timer:

[Unit] Description=Run health check every hour [Timer] OnCalendar=hourly Persistent=true [Install] WantedBy=timers.target

OnCalendar 的语法比 cron 更接近自然语言,hourly 就是每小时,还可以写Mon..Fri 09:00:00这种表达。Persistent=true 的含义是:如果到达执行时间时系统处于关机状态,开机后会自动补跑错过的任务。这是 cron 完全没有的能力。

第三步,启用并启动定时器:

systemctl daemon-reload systemctl enable healthcheck.timer systemctl start healthcheck.timer

注意,这里 enable/start 的对象是 .timer,不是 .service。查看当前系统上所有 timer 的状态用systemctl list-timers,它会列出下一次执行时间、上一次执行时间以及对应的 service 单元。

5.2 OnCalendar 语法与 cron 的对照

OnCalendar 语法看起来和 cron 差异大,但理解起来其实更直观。下面是几个常用对照:

需求crontab 写法OnCalendar 写法
每天凌晨 2 点0 2 * * *02:00:00
每 5 分钟*/5 * * * **:0/5
每周一 9 点0 9 * * 1Mon 09:00:00
每月 1 号和 15 号0 0 1,15 * **-*-01,15 00:00:00

OnCalendar 还支持every 5 minutes这种纯粹自然语言的写法,以及sec、min单位组合,灵活度很高。systemd timer 还支持 OnBootSec 表示开机后多久执行、OnUnitActiveSec 表示服务上一次启动后多久再执行,这是 cron 完全不支持的场景。

如果用 systemd timer 管理任务,查看日志直接走 journalctl,比如journalctl -u healthcheck.service就能看到上一次执行的全部输出。这和 cron 需要单独看 /var/log/cron 加脚本日志的方式相比,排查链路更统一。

5.3 什么场景值得从 cron 迁移到 timer

我的建议是不要为了用新工具而用新工具,但以下几类场景建议优先考虑 timer:任务需要"错过补跑"保证;任务之间有明确的先后依赖关系;任务对应的就是某个服务单元的定时启动;你希望任务的日志纳入 journald 统一管理。

比如某公司的数据同步任务,要求每天凌晨同步一次,但机器经常在凌晨关机。用 cron 的话,关机期间的任务直接就丢了;用 timer 加 Persistent=true,下次开机就能自动补跑。这种场景用 timer 的价值就非常明显,cron 无法可靠实现。

6. 常见问题与排查技巧实录

6.1 任务没执行,从哪一步开始查

我总结了一套排查定时任务的固定思路,按顺序走基本能定位大部分问题。

第一步,确认任务是否真的被加载了。crontab -l能看到用户级任务,把输出和时间字段再核对一遍。系统级的去看 /etc/crontab 和 /etc/cron.d/ 目录。

第二步,确认 crond 服务是运行状态。systemctl status crond看主进程是否 active,服务挂了任务必不可能执行。

第三步,看日志。cron 的日志在 /var/log/cron,里面记录每条任务的执行时间和执行的命令,比如:

Feb 16 02:00:01 hostname CROND[12345]: (root) CMD (/opt/scripts/backup.sh)

确认日志里有这一行,说明调度器确实触发了任务;如果连日志都没有,说明 crontab 配置根本没被识别。如果日志有但执行结果不对,问题就出在脚本或环境上,下一步检查脚本的手动执行结果和日志输出。

第四步,看任务输出日志。上一步如果确认脚本被执行但结果不对,就看脚本自己的日志文件,确认失败点是哪个命令。很多时候是脚本里某个程序依赖的路径没配置、依赖的数据库服务没启动,这些在 cron 的干净环境里特别容易暴露。

6.2 我踩过的几个经典坑位

第一类问题是权限。某同事写了一个需要读 root 私有文件的备份脚本,crontab 里用的用户是普通用户,手动 su 到该用户测试看着正常,一执行就 Permission denied。排查思路很简单,crontab 里配置的执行用户是谁,就用那个用户去手动执行,不要在 root 终端下测试。

第二类问题是时间格式。0 * * * *容易被误读成"每 0 分钟执行"也就是每小时执行一次,实际上它确实表示每小时整点执行,但很多人会把* * * * *当成每分钟执行,把0 0 * * *写成0 0 * * 0导致周日才执行,都是低级但真实的错误。

第三类问题是脚本里用了相对路径。比如脚本开头cd backups,在 cron 里默认工作目录是用户家目录,如果家目录下没有 backups 目录,整个脚本就崩了。所有路径改用绝对路径是解决这类问题最粗暴有效的办法。

第四类问题是时区。系统时区改了之后,cron 的任务时间会跟着变。如果服务器用的 UTC,而业务方要求北京时间执行,那么 cron 时间字段要按 UTC 算。排查时如果发现任务时间比预期差了几个小时,优先怀疑时区配置。

下面把这些坑整理成速查表,方便大家作为自查清单。

症状可能原因排查手段
任务完全不执行crond 未运行 / crontab 格式错systemctl status crond;crontab -l 核对
命令找不到PATH 精简,未配置脚本或 crontab 顶部写完整 PATH
手动执行正常、定时执行失败环境变量差异 / 相对路径用任务用户 su 过去手动执行复现
输出看不到cron 默认发邮件未配置 MTA命令追加 >> 日志 2>&1
执行时间不对时区配置 / 日、周字段用了"与"逻辑timedatectl 查看时区;核对 cron 语义
重复执行多次分钟位误写 *分钟位必须给具体值或用 */N

6.3 调试技巧:一个随时可用的测试方法论

调试定时任务时,有一个很实用的方法:先用一个安全的测试命令替换掉真实脚本,比如把真实任务临时改成0 * * * * /bin/echo test $(date) >> /tmp/test_cron.log,等一分钟看 /tmp/test_cron.log 里有没有内容。这一步能快速确认"调度链路是否通畅",排除脚本本身的问题。

确认链路通畅之后,再逐步把真实脚本加回去。如果这个时候出问题,就按 6.1 的第四步,去看脚本自己的日志。这一套"先测调度、再测脚本"的思路,能帮你快速缩小问题范围,而不是抓瞎式地在多个可能性里瞎试。

另外我还习惯给每个任务的脚本末尾加一行执行标记,比如:

echo "$(date '+%F %T') script executed" >> /var/log/script_exec.log

这样即使脚本中途失败,至少能确认"确实到这里了",把失败范围进一步缩小。这种加埋点排查的习惯,在大型脚本里尤其好用。

写在最后的一点个人心得

定时任务做久了,我最大的体会是:这类小功能平时不起眼,但是真正出问题的时候往往都是大问题。备份丢了、监控断了、证书过期了,这些事故的根源十有八九就是定时任务没跑起来或者跑错了。所以我现在对待定时任务的态度是"每一条都要有日志、每一个脚本都要可排查",绝不允许出现一个运行结果不可见的"黑盒任务"。

另一个体会是,不要迷恋某个工具。cron 简洁、普及,systemd timer 强大、现代,at 轻量、专一。真正的能力是能在面对需求时快速判断该用哪个,并且能解释清楚为什么。RHCE 考试里那些看似琐碎的知识点,其实都是在训练这种判断力。希望这篇内容能把你在定时任务这条路上的坑提前填平一些,剩下的,就靠你在真机上一遍遍地试了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进