资讯动态

Linux 定时任务调度实战:cron、at 与 systemd 配置排错

发布时间:2026/10/26 3:59:53 来源:尧图企业网站定制
凌晨三点服务器上的批处理任务挂了早上到工位第一件事就是翻日志这种经历做过运维或写过自动化脚本的人大概都不陌生。cron、at、systemd 这三样东西几乎是每台 Linux 机器上都会碰到的任务调度基础设施cron 负责周期性的重复工作at 处理一次性延后执行systemd 则把服务管理和调度合到了一起。很多人对它们只停留在“会写个 crontab”的程度真到出问题的时候——任务没跑、跑了但报错、跑了两遍、换了机器就不生效——就抓瞎了。这篇东西想做的事很具体把 cron 和 at 的语法、配置位置、环境差异讲透再手把手带出一个能上生产的简单 systemd 服务最后把常见的排查路径整理成一张能直接对着查的表。读者对象是已经能登录服务器、会写基本 shell 脚本但对调度这块只有零散经验的人如果你平时只是照抄别人给的 crontab 行那这篇正好能帮你把那些说不清的地方补上。1. 先捋清楚三种调度工具的分工到底在哪动手写配置之前值得花几分钟想明白这三者的边界。我在实际项目里见过太多“用 cron 硬扛一切”的写法最后演变成一堆互相依赖的定时任务谁先跑谁后跑全靠猜。工具本身没有高下之分关键是场景匹配。1.1 重复、一次性、常驻三种需求对应三种工具先把需求归个类。如果你的任务是“每隔一段时间就要执行一次而且长期存在”比如每分钟采集一次指标、每天凌晨打包数据、每周日清理临时文件这是典型的周期任务cron 和 systemd timer 都能干。如果任务是“从此刻起过一段时间执行一次执行完就结束”比如半小时后重启某个进程、明早八点触发一次数据回补这是一次性延后任务at 是最顺手的。如果任务本身是“需要常驻、要能被监督、挂了要自动拉起来”那它就不是调度问题而是服务管理问题应该交给 systemd 的 service。这三者的区别不只是功能还有一层是心态上的。cron 的定位是“到点了扔一条命令出去”它不关心命令执行得怎么样、跑了多久、有没有并发systemd 的定位是“管理一个受控的进程”它会记录退出码、限制资源、处理依赖关系、挂了按策略重启。理解这一点后面很多设计取舍就顺理成章了。下面这张表是我自己总结的对照放在手边查比较方便维度cronatsystemd service timer主要用途周期任务一次性延后任务常驻服务 / 精确周期任务精度分钟级分钟级-t可到秒秒级错过执行的处理直接丢弃机器关机则丢失除非补跑timer 可配Persistenttrue补跑日志默认邮件需手动重定向默认邮件自动进 journal并发控制无需 flock 等外部手段天然一次性有Type与ExecStart语义约束依赖与顺序无无有After/Wants/Requires资源限制无无有 cgroup 级别的 CPU/内存/IO 限制上手成本极低极低中等1.2 选型时我实际会问自己的四个问题光看表格还不够真到了要选的时候我一般会按顺序问自己四件事基本能落到唯一答案上。第一个问题这个任务需要跑多久会不会和下一次重叠如果一个任务执行时间可能超过周期间隔cron 会毫不犹豫地再起一个实例两边争抢同一份文件或者同一个数据库连接问题很难查。这种情况下我更倾向用 systemd timer因为Typeoneshot配合 timer 默认不会重叠启动如果坚持用 cron必须自己加flock。第二个问题机器重启后错过的任务要不要补cron 不补昨天的日报今天不会自动生成。systemd timer 的Persistenttrue会记住上次触发时间开机后补跑一次。数据类、对账类的任务这个特性非常关键。第三个问题任务里的逻辑复杂不复杂如果只是find ... -delete这种一行命令直接写进 crontab 完全没问题如果有十几步、要判断前置条件、要处理异常那就老老实实写成一个独立脚本cron 只负责调用脚本。把复杂逻辑塞进 crontab 的一行里是后期维护灾难的起点。第四个问题出了问题我要怎么知道cron 默认把输出通过邮件发给任务所属用户可现在大多数服务器根本不配邮件服务输出直接进黑洞。所以无论用哪个工具日志落地这件事必须在设计阶段就定下来而不是等出事再补。1.3 时间基准最容易被忽略的第一道坑所有调度工具的时间判断都依赖系统时钟而系统时钟有两层时区和是否准确。这两个东西任何一层出问题表现都是“任务跑了但跑的时间不对”而且往往过很久才被发现。先说时区。cron 的表达式是按系统本地时区解释的容器环境里基础镜像的时区经常是 UTC你按北京时间写了0 2 * * *实际会在北京时间上午十点执行。排查方法很简单timedatectl看一眼Time zone那一行或者直接date对比一下。systemd timer 也一样OnCalendar默认按本地时区解析但可以在表达式里显式带时区后缀比如OnCalendar*-*-* 02:00:00 Asia/Shanghai这在跨区域部署时很有用因为它把时区固化在了配置里不依赖机器设置。再说时钟准确性。虚拟机的时钟漂移是常态尤其是长期不重启、负载又高的机器一天漂个几十秒不稀奇。半年下来累积偏差可能到几分钟对于“必须在整点前后触发”的业务就有实际影响。所以时间同步服务基本上是必备项chronyc tracking或者timedatectl status里能看到当前偏差量超过几十毫秒就该查一下同步源了。提示写 cron 表达式时先在命令行用date -d next ...之类的方式验证一下你的理解比直接上线试错成本低得多。特别是“日”和“星期”两个字段同时非*的情况它们的逻辑是“或”而不是“与”。2. cron 实战从语法到能上生产的配置cron 大概是 Linux 上被用得最多、也被误解得最多的工具。语法看起来五分钟就能学会但真正让它稳定工作需要处理的细节比想象中多。2.1 五个时间字段到底怎么读标准 crontab 一行是五个时间字段加一条命令字段之间用空格或制表符分隔位置含义取值范围常见写法示例1分钟0-590、30、*/5、10-202小时0-233、*/6、1,133日1-311、*/7、154月1-12*、1-6、3,95星期0-70 和 7 都表示周日1-5、6,0几个容易读错的点我一条条说。*/5表示“步进”从字段的起始值开始每 5 个单位取一个。分钟字段*/5是 0、5、10……55这个没问题但“日”字段写*/7就有点微妙了它是从 1 号开始每 7 天落到 1、8、15、22、29 号跨月时并不均匀。想做“每 7 天一次”更稳的做法是配合脚本内部记录上次执行时间而不是指望字段步进。“日”和“星期”同时被限定的时候二者是或关系。0 3 1 * 1的含义是“每月 1 号的 3 点”或者“每周一的 3 点”而不是“每月 1 号且恰好是周一的 3 点”。这个设计在当年可能是为了灵活性但在实际使用里几乎只会造成困惑。想表达“且”只能靠脚本里判断。另外 cron 还提供了一批以开头的快捷写法reboot开机后执行一次、hourly、daily、midnight、weekly、monthly、yearly。它们在可读性上确实更好但要注意reboot的语义是“cron 守护进程启动后执行”不是“系统完全就绪后执行”如果任务依赖网络或某个后端服务很可能跑得太早而失败。2.2 用户 crontab 和 /etc/cron.d 的差别cron 的配置有三个落点用途不同混用是常见错误来源。用户 crontab用crontab -e编辑存到/var/spool/cron/下面每行没有用户字段因为任务天然以当前用户身份执行。crontab -l查看crontab -u 用户名 -l看别人的需要权限。/etc/crontab系统级配置文件格式比用户 crontab 多一个用户字段位置在第五个时间字段之后、命令之前# 分 时 日 月 周 用户 命令 30 2 * * * root /opt/scripts/rotate.sh很多人从网上抄配置贴进/etc/crontab结果不生效就是因为抄来的那行少了这个用户字段cron 把用户名字段当成了命令的一部分。/etc/cron.d/目录里面每个文件都是独立配置片段格式和/etc/crontab一样带用户字段。这个目录适合打包分发一个应用带一个文件卸载时删掉即可。但它有几个硬性约束文件属主必须是 root权限不能是组可写或其他人可写文件名只能包含字母、数字、连字符和下划线文件最后一行必须以换行符结尾。最后这条坑过不少人——编辑器里不显示末尾换行缺了它最后一行的任务会被静默忽略。还有/etc/cron.daily、/etc/cron.hourly这类目录由run-parts机制驱动文件名同样有字符限制带点的文件名会被跳过比如backup.sh放在里面就不会被执行得叫backup才行。2.3 PATH 与环境变量任务不执行的头号元凶我自己统计过cron 相关的问题里环境差异占了一半以上。命令在终端里手敲没问题写进 crontab 就不动了原因基本是cron 执行任务时不加载任何登录 shell 的初始化文件。.bashrc、.bash_profile、/etc/profile这些都不会被读取cron 给你的环境极简——通常是SHELL/bin/sh、PATH/usr/bin:/bin、HOME设成用户家目录、LOGNAME设成用户名。你在.bashrc里加的export PATH$PATH:/usr/local/bin在 cron 里完全不存在。对应的处理办法有三层按推荐程度排序第一层命令和脚本里全部用绝对路径。调用/usr/local/bin/python3、/usr/bin/flock、/bin/rm不依赖 PATH 查找。这是最稳的一层因为它把不确定性彻底消除了。第二层在 crontab 顶部显式声明环境变量。crontab 支持这种写法SHELL/bin/bash PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANGC.UTF-8 MAILTO HOME/home/appuser */10 * * * * /opt/scripts/collect.sh /var/log/collect.log 21这样声明出来的变量会注入到所有任务的执行环境里。注意MAILTO是显式关闭邮件不然默认会把输出发给本机用户一旦本机没配邮件传输代理输出就到处乱飘。第三层在脚本内部自己准备好环境。如果脚本需要复杂的运行时环境比如 Python 虚拟环境、特定版本的 JDK更规范的做法是脚本第一行就source一个 env 文件或者在启动时设置好PATH#!/bin/bash set -euo pipefail export PATH/opt/app/venv/bin:/usr/local/bin:/usr/bin:/bin export LANGC.UTF-8 cd /opt/app || exit 1 exec python3 /opt/app/jobs/collect.pyset -euo pipefail这行值得单独说-e让命令失败立即退出-u让引用未定义变量时报错-o pipefail让管道中任一环节失败都算失败。加不加这行脚本的“静默失败”概率差别巨大。注意cd这一步经常被漏掉。cron 执行任务时的工作目录是用户家目录HOME不是脚本所在目录。如果你的脚本里有相对路径行为就会和手工执行时完全不同。养成脚本里显式cd的习惯能省掉很多莫名其妙的 file not found。2.4 输出重定向、日志与执行时长监控cron 默认把任务的标准输出和标准错误收集起来通过邮件发给任务所属用户。服务器上邮件基本是废的所以这些输出要么进黑洞要么攒在本地邮件队列里越堆越大。正确做法是显式重定向*/5 * * * * /opt/scripts/ping_check.sh /var/log/ping_check.log 21是追加21把标准错误也并到标准输出两者一起落到同一个文件。如果用覆盖那每次执行都会把上次的日志冲掉出问题时就只剩最后一条记录基本没法查。日志文件本身也需要管理。一天跑 288 次的任务日志增长很快不加轮转迟早把分区塞满。可以交给 logrotate写一个配置文件/var/log/ping_check.log { daily rotate 14 compress missingok notifempty copytruncate }copytruncate这一步很关键——它复制完日志后原地清空而不是重命名文件让程序重开。因为 cron 任务每次启动都是新进程追加写入的模式下用copytruncate最省事不需要通知程序重新打开文件句柄。如果某个任务只关心“有没有失败”不需要全部输出可以只在失败时留痕*/5 * * * * /opt/scripts/ping_check.sh /dev/null 2 /var/log/ping_check.err这样正常输出丢弃错误信息才落盘。日志量能降一个数量级。至于“任务跑了但不知道跑了多久”这是 cron 天生的短板。我一般会在脚本首尾加时间戳打印或者干脆包一层time*/30 * * * * /usr/bin/time -f elapsed %E, maxrss %MKB /opt/scripts/collect.sh /var/log/collect.log 21%E是总耗时%M是峰值内存KB。有了这两项性能退化能提前发现。2.5 用 flock 解决任务叠加cron 没有任何并发保护。一个每 5 分钟跑一次、但偶尔要跑 8 分钟的任务会稳定地出现两个实例同时在跑的情况。如果它们都在处理同一批文件轻则重复处理重则数据损坏。flock是最轻量的解决办法*/5 * * * * /usr/bin/flock -n /var/lock/collect.lock /opt/scripts/collect.sh /var/log/collect.log 21-n表示非阻塞拿不到锁就立刻退出不会排队堆积。锁文件路径放在/var/lock或者/tmp都行但要注意如果多个任务共用同一个锁文件它们会互相排斥一个任务一个锁文件不要图省事复用。还有一个更细的用法-w指定等待秒数*/5 * * * * /usr/bin/flock -w 60 /var/lock/report.lock /opt/scripts/report.sh拿不到锁就最多等 60 秒等到了就执行等不到就放弃。适合那种“允许稍微延迟但绝对不能重复”的场景。flock的锁是文件描述符级别的进程退出无论正常还是崩溃锁自动释放不需要手动清理。这一点比自己在脚本里写 pid 文件判断可靠得多——pid 文件的经典问题是进程被 kill -9 后留下死文件后续任务全部被挡住。2.6 一个完整的 crontab 样板把上面这些拼起来我给新项目用的 crontab 模板大致是这样SHELL/bin/bash PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin LANGC.UTF-8 MAILTO # 每 5 分钟采集一次禁止重叠 */5 * * * * /usr/bin/flock -n /var/lock/collect.lock /opt/app/bin/collect.sh /var/log/app/collect.log 21 # 每小时第 7 分钟做一次聚合避开整点高峰 7 * * * * /opt/app/bin/aggregate.sh /var/log/app/aggregate.log 21 # 每天 03:20 全量打包记录耗时 20 3 * * * /usr/bin/time -f elapsed %E /opt/app/bin/pack.sh full /var/log/app/pack.log 21 # 每周一 04:00 清理删 30 天前的产物 0 4 * * 1 /opt/app/bin/cleanup.sh --days 30 /var/log/app/cleanup.log 21几个细节值得留意聚合任务故意错开整点是因为整点往往是其他系统批量任务的高峰IO 和 CPU 都在抢打包任务带了个full参数方便同一个脚本支持增量模式清理任务放在周一凌晨因为周末积累的产物最多周一早上看到的空间报告最真实。3. at一次性延后执行的正确用法相比 cronat 的存在感低很多但有些场景它就是最合适的工具。比如“给某个用户发一条通知两小时后执行”或者“把这个变更在今晚十点自动回滚”用 cron 做会很别扭用 at 一行就完事。3.1 atd 守护进程与安装at 依赖atd守护进程很多精简系统默认没装也没启动。用之前先确认systemctl status atd如果提示不存在装一下包apt 系是atrpm 系也是at然后systemctl enable --now atd。有的系统上叫atd.service都是同一个东西。还要检查一个容易忽略的地方/etc/at.allow和/etc/at.deny。如果at.allow存在只有里面列出的用户才能用 at如果只有at.deny里面列出的用户被禁止两个文件都不存在的话行为取决于发行版默认策略。普通用户执行at报 “You do not have permission to use at” 多半就是这个原因。3.2 时间表达式的几种写法与坑at 的时间解析由at命令自己实现支持的自然语言写法相当宽松echo /opt/scripts/rollback.sh | at now 2 hours echo /opt/scripts/notify.sh | at 22:30 at -f /opt/scripts/rollback.sh 2025-06-01 03:00 at -f /opt/scripts/rollback.sh now 45 minutes常用的相对时间单位有minutes、hours、days、weeks可以组合比如now 1 hour 30 minutes。绝对时间支持HH:MM、HH:MM AM/PM、midnight、noon、teatime16:00也支持指定日期。坑主要在两处。第一处是“过去时间”。如果你写at 03:00而当前已经过了 3 点at 会顺延到明天3 点而不是报错。这个行为本身合理但容易让人误判——你以为设的是今天实际执行在明天。想精确控制就用at -t格式at -t 202506010300.00 -f /opt/scripts/rollback.sh-t的参数格式是[[CC]YY]MMDDhhmm[.ss]精确到秒也不会做任何“智能顺延”写错了就直接拒绝。生产环境我优先用这个因为它不含歧义。第二处是秒级精度。默认情况下 at 只解析到分钟now 30 seconds这种写法会被向上取整到下一分钟。如果确实需要秒级-t是唯一选择。3.3 队列查看、删除与环境继承任务提交后用atq查看待执行队列3 Mon Jun 1 03:00:00 2025 a root 5 Mon Jun 1 22:30:00 2025 a appuser第一列是任务编号后面是计划时间、队列字母、提交用户。删除用atrm 3。查看任务具体内容用at -c 3它会把这行要执行的完整脚本打出来包括 at 自动注入的环境变量部分——这个功能在排查“为什么 at 任务里找不到命令”时非常有用。关于环境继承有个和 cron 明显不同的地方值得说at会继承提交时的当前环境变量包括你export的那些。这意味着你在终端里export PATH...之后提交的 at 任务执行时能看到这个 PATH。听起来很方便但也带来一个问题——不同人、不同时刻提交的任务环境可能不一致同一份脚本表现不同。我的做法是不依赖继承的环境在提交时就写清楚at -f /opt/scripts/rollback.sh now 2 hours EOF PATH/usr/local/bin:/usr/bin:/bin LANGC.UTF-8 EOF这样任务内容里就显式带了变量at -c查看时一目了然也不会受提交终端影响。还有一个细节at 任务的输出默认通过邮件发给提交者同样因为没有邮件服务而丢失。所以要么在命令里重定向要么用-M参数显式禁止发送邮件at -M -f /opt/scripts/rollback.sh now 2 hours3.4 at 适合干什么、不适合干什么用了几年下来我的判断是at 适合短期的、一次性的、人工触发的动作不适合承载业务逻辑。适合的典型场景变更窗口结束后自动回滚某个开关给某个临时账号设置定时禁用批量任务的“延时重试”——第一次失败后两小时再试一次。这类场景的特点是数量少、生命周期短、失效了影响可控。不适合的场景任何需要幂等保证、需要执行结果反馈、需要在机器重启后依然有效的任务。at 的队列存在/var/spool/atd/下面机器重启后 atd 会重新加载未执行的任务但如果任务本身依赖的内存状态没了起来也是白跑。而且 at 没有任何重试机制执行失败就是失败你不主动去看根本不会知道。4. 手写一个 Systemd 服务从 unit 文件到开机自启到这一步我们处理的是比“定时跑一行命令”更重的东西一个需要被系统当作服务来管理的任务。用一个具体例子走完整个流程——一个每晚执行的数据打包任务。4.1 unit 文件放哪里、怎么命名systemd 的单元文件按优先级从几个目录加载常用的有三个路径用途是否建议手改/usr/lib/systemd/system/发行版和软件包自带的单元不建议升级会被覆盖/etc/systemd/system/管理员自定义的单元推荐~/.config/systemd/user/用户级单元不需要 root适合个人任务自己写的放/etc/systemd/system/文件名就是单元名必须以.service结尾。命名上我习惯用「项目-功能」的形式比如app-pack.service避免和系统自带的重名。改完或新建后必须执行systemctl daemon-reload这一步不做systemd 根本不知道有新文件。改配置之后忘记 reload然后反复start发现行为没变是最常见的自我怀疑时刻。4.2 三段结构逐条拆解一个 service 文件通常分三段[Unit]、[Service]、[Install]。很多人抄模板只抄不改实际上每一行都有它的作用。[Unit]段管描述和依赖关系[Unit] DescriptionNightly data pack job Documentationfile:/opt/app/README.md Afternetwork-online.target Wantsnetwork-online.target ConditionPathExists/opt/app/bin/pack.shAfter和Wants经常一起出现但含义不同After只保证启动顺序对方的成败不影响自己Wants表示“希望对方也启动”但对方失败了自己照跑如果要强依赖用Requires。对于一个需要访问网络的任务Afternetwork-online.targetWantsnetwork-online.target是标准组合注意是network-online而不是network——后者只表示网络子系统初始化完成不代表已经拿到地址。ConditionPathExists是个很实用但少人用的指令条件不满足时单元直接跳过不报失败。适合那种“脚本可能不存在”的部署场景。[Service]段是核心[Service] Typeoneshot Userappuser Groupappuser WorkingDirectory/opt/app EnvironmentLANGC.UTF-8 EnvironmentFile-/etc/app/pack.env ExecStartPre/usr/bin/test -d /data/out ExecStart/opt/app/bin/pack.sh --mode full ExecStartPost/usr/bin/logger -t app-pack pack finished TimeoutStartSec7200 Nice10 IOSchedulingClassbest-effort IOSchedulingPriority6 PrivateTmptrue NoNewPrivilegestrue ProtectSystemfull StandardOutputjournal StandardErrorjournal逐条说几个关键的。Typeoneshot表示这个任务跑完就退出不常驻最适合批处理。WorkingDirectory显式指定工作目录避免脚本里相对路径出问题——这是 cron 里要靠cd手动做的事systemd 直接给了配置项。EnvironmentFile-/etc/app/pack.env前面的减号表示“文件不存在也不算错”里面可以放KEYVALUE形式的环境变量敏感配置和单元文件分离便于权限控制。ExecStartPre用来做前置检查失败就直接中止不会执行主命令。ExecStartPost做收尾比如打日志。注意这两个指令失败也会导致整个单元被判定为失败所以ExecStartPost里别放太脆弱的逻辑。TimeoutStartSec7200把启动超时放宽到两小时。Typeoneshot的默认超时不算长跑得久一点的任务如果不设这个会被 systemd 直接判超时然后杀进程。Nice和IOSchedulingClass是资源礼让。夜间批处理虽然跑在低峰但如果和早上的业务有重叠压一下优先级能减少互相干扰。最后那几个PrivateTmp、NoNewPrivileges、ProtectSystem是安全加固项属于“顺手加上不吃亏”的类型PrivateTmp给服务一个独立的/tmpNoNewPrivileges禁止提权ProtectSystemfull把/usr、/boot、/etc挂成只读。这些在容器化之后更常见但在宿主机上同样值得加。[Install]段决定怎么开机自启[Install] WantedBymulti-user.targetWantedBymulti-user.target意味着systemctl enable时会在multi-user.target.wants目录下创建一个软链接从而在系统进入多用户模式时被拉起。对于定时任务类型的服务这一行其实可以省略因为真正需要 enable 的是 timer 而不是 service。4.3 Type 的取值怎么选Type选错会导致 systemd 误判服务状态进而出现“明明跑完了却一直显示 activating”或者“进程还在但 systemd 认为已退出”的问题。Type适用场景判定启动成功的时机simple默认值前台常驻进程进程 fork 出来即认为启动成功exec同上但更严格ExecStart的二进制真正 exec 成功oneshot跑完就退出的批处理进程退出且退出码为 0forking自己 daemonize 的老式程序父进程退出后认为启动完成notify支持 sd_notify 的程序程序主动发通知idle极少用其他任务都完成后才启动批处理任务一律用oneshot这是最没有歧义的选择。如果任务跑完后还有RemainAfterExityes的需求比如“执行一次配置后一直标记为 active”可以配合使用。4.4 完整跑通的步骤把上面的单元文件存成/etc/systemd/system/app-pack.service属主 root、权限 644然后按顺序操作# 1. 重新加载单元定义 systemctl daemon-reload # 2. 手工跑一次看是否正常 systemctl start app-pack.service # 3. 看状态和退出码 systemctl status app-pack.service # 4. 看日志 journalctl -u app-pack.service -n 100 --no-pager # 5. 跟随日志实时观察 journalctl -u app-pack.service -fsystemctl status的输出里最值得看的是Active:那一行的结果码和Process:那一行记录的ExecStart退出状态。如果显示status203/EXEC基本可以断定是路径写错或者文件没有执行权限status1/FAILURE则是脚本自己返回了非零。脚本本身的权限和格式也要检查。chmod x是必须的另外如果脚本是在 Windows 上编辑后传上去的行尾可能是 CRLFbash 会报bad interpreter: /bin/bash^M用file命令或者cat -A能看出来dos2unix处理一下即可。4.5 用 timer 替代 cron有了 service再加一个.timer就能实现周期调度。文件名要和 service 对应比如app-pack.timer搭配app-pack.service[Unit] DescriptionRun app pack job nightly [Timer] OnCalendar*-*-* 03:20:00 RandomizedDelaySec300 Persistenttrue Unitapp-pack.service [Install] WantedBytimers.targetOnCalendar的格式是星期 年-月-日 时:分:秒支持通配和范围。几个常用写法表达式含义*-*-* 03:20:00每天 03:20Mon-Fri *-*-* 09:00:00周一到周五 09:00*-*-01 04:00:00每月 1 号 04:00*:0/15每 15 分钟整刻起hourly每小时RandomizedDelaySec300会在计划时间上叠加一个 0 到 300 秒的随机延迟。单机看不出意义但如果几十台机器都配了同一个定时任务不加这个就会在同一秒集体打向数据库这种“惊群”是真实会压垮服务的。Persistenttrue是 timer 相对 cron 最大的优势之一它会记录上次触发时间机器关机期间错过的触发开机后会补跑一次注意只补一次不是把错过的每次全补一遍。启用方式是 enable timer 而不是 servicesystemctl daemon-reload systemctl enable --now app-pack.timer systemctl list-timers --alllist-timers会显示下一次触发时间、上次触发时间、以及对应的单元这个视图比翻 crontab 直观得多。手工触发一次用systemctl start app-pack.service要注意 timer 的Persistent状态不会因为手工触发而更新这是设计如此。5. 任务没跑起来按这张表排查前面都是“怎么配”这一节讲“配了不生效怎么办”。我把这些年遇到的问题归了归绝大多数都能落到下面这张表里。5.1 常见原因对照表现象最可能的原因验证方式cron 任务完全没执行crond 没运行systemctl status crond/crond某个任务单独不生效配置文件末尾缺换行、权限不对tail -c 20 文件 | xxd、ls -l手工执行正常cron 报 command not foundPATH 不含该命令目录脚本里全用绝对路径脚本报 No such file or directory工作目录不是脚本目录脚本开头cd到固定目录脚本报 bad interpreter行尾是 CRLFfile 脚本、cat -A任务跑了两次任务叠加无并发控制加flock -n时间对不上时区是 UTC 不是本地时区timedatectltimer 不触发enable 的是 service 不是 timersystemctl list-timers --allservice 显示 activating 不结束Type选错或TimeoutStartSec未设看systemctl status详情service 报 203/EXEC路径错误或无执行权限ls -l、file日志里什么都没有输出没重定向进了邮件队列检查与21换了机器就不生效配置放错位置或未 daemon-reloadsystemctl cat 单元名5.2 几个典型场景的复盘场景一日志文件一行都没写。这是最常见的“看起来没跑”。第一反应别急着改配置先确认任务到底有没有被触发。cron 的触发记录在系统日志里journalctl -u crond -n 50或者翻/var/log/cron能看到每次调用的时间戳和用户。如果日志里连调用记录都没有问题在配置层如果有记录但业务日志为空问题在执行层去看输出跑哪儿了。场景二脚本里mkdir成功了但第二天文件不见了。十有八九是相对路径。cron 的工作目录是HOME你mkdir output创建在了家目录而检查脚本找的是/opt/app/output。这类问题在加了WorkingDirectory的 systemd 服务里不会出现这也是我逐渐把重要任务迁到 systemd 的原因之一。场景三任务偶尔漏跑没有规律。先看有没有并发控制再看执行时长。一个跑 8 分钟的任务配 5 分钟周期日志里应该能看到重叠的痕迹——开始时间戳密集、结束时间戳交错。另一个可能是有外部锁多个任务共用同一个flock文件或者都在抢同一个数据库行锁导致某些实例拿不到资源直接退出。用flock -n的话拿不到锁就是静默退出日志里啥都没有很容易被误认为“没跑”。场景四systemd 服务启动后立刻变成 inactive。这在Typesimple配了一个跑完就退出的脚本时特别常见。脚本执行完正常退出systemd 认为服务结束了状态变成inactive (dead)退出码 0 所以不算失败。解决办法就是改成Typeoneshot语义上就对了。5.3 日志到底去哪儿找新手最容易卡住的一步就是不知道去哪看。不同工具的日志位置差别很大cron任务自身的输出在你重定向的地方cron 守护进程的调度记录在系统日志里journalctl -u crond或-u cron取决于发行版。at任务的执行输出默认邮件atd的日志同样在系统日志journalctl -u atd。systemd service全部进 journaljournalctl -u 单元名。默认可能只保留最近一段需要长期留存的话得配置SystemMaxUse或者转发到外部日志系统。journalctl有几个参数值得记住-n 100看最后 100 行-f跟随--since 1 hour ago按时间过滤-p err只看错误级别以上-o cat去掉元数据只留消息体。排查时用-o cat --since组合输出最清爽。6. 上线前我会固定过的几项检查写了这么多配置最后还是想落回到一些更“手感”的东西上。这些东西文档里通常不写但真能省事。6.1 一份可以直接抄的检查清单新任务上线前我固定会过一遍下面这些脚本里所有外部命令是否都是绝对路径或者脚本顶部显式设了PATH。脚本开头有没有set -euo pipefail失败会不会被吞掉。有没有显式cd到工作目录或者用WorkingDirectory指定。输出是否重定向到了具体文件日志有没有配轮转。有没有并发保护任务执行时长和周期间隔的关系是否安全。单独跑一次日志内容是否符合预期退出码是不是 0。把系统时间临时往后调几分钟验证触发时间点是否符合预期。重启一次服务或机器验证开机自启和补跑逻辑。故意制造一次失败比如临时改错路径确认告警或日志能反映出来。最后一条是我觉得最有价值的只有你亲手验证过“失败了能被发现”才算这套调度真的可靠。没验证过的告警约等于没有告警。6.2 几个容易被忽略的细节第一个是时间同步。所有调度都建立在系统时钟准确的前提上而虚拟机的时钟漂移是常态。定时任务的机器上时间同步服务应该是标配timedatectl status里System clock synchronized: yes这一行是必须的。第二个是任务执行的幂等性。不管用哪个工具都可能出现重复触发——手动补跑、timer 的Persistent补跑、failover 切换。脚本本身要能承受“同样的输入跑两次”的结果要么用状态标记要么用唯一键约束别指望调度层保证只跑一次。第三个是不要把所有东西都塞进 crontab。一个 crontab 里堆了二十行互相依赖的任务谁先谁后没有任何保证出问题基本靠猜。超过五六个任务就值得把相关逻辑收敛到一个入口脚本里让脚本内部按顺序执行并做好错误处理cron 只留一行调用。这样调度层保持极简复杂度收敛到一个可测试的地方。第四个是留一条“手动触发”的路。不管是systemctl start还是直接跑脚本同一个任务必须有办法在不改配置的情况下手工执行一次。生产上最常见的一类求助就是“数据没出来能不能现在补一下”如果补跑要改 crontab 再改回来风险就高了一个量级。我现在的习惯是所有 systemd service 都写成可以安全手工触发的形式脚本里带--mode参数区分全量和增量补数据的时候直接systemctl start xxx -- --modefull之类的实际用Environment或者额外单元更稳妥比临时改配置安全得多。最后再分享一个小技巧如果你不确定一个OnCalendar表达式到底会在什么时候触发用systemd-analyze calendar *-*-* 03:20:00让它自己算出来包括下一次触发时间和间隔。写复杂表达式的时候先跑一遍比上线后等结果靠谱。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑