Linux定时任务实战:cron与at从入门到故障排查
很多朋友在把脚本部署上线之后最担心的不是脚本本身逻辑有多复杂而是它到底有没有按预期跑起来。日志里啥记录都没有数据库该更新的没更新文件该备份的没备份一问就是“我明明加了定时任务啊”。这种问题十有八九出在 cron 和 at 的使用细节上。cron 负责周期性任务at 负责一次性任务这两个是 Linux 下最基础的定时任务工具几乎每台服务器都自带。这篇文章我会把 cron 的完整用法、常见的坑、at 的实操套路以及生产环境下的排查经验一次讲清楚适合刚接触定时任务的运维新手也适合写过不少 crontab 但一直被各种小问题折磨的开发同学。1. 定时任务到底解决什么问题为什么 cron 和 at 是首选1.1 需求场景拆解周期性任务和一次性任务定时任务的本质是让系统代替人工去按计划执行命令。听起来很简单但实际需求可以分成两类处理方式完全不同。一类是周期性任务。比如每天凌晨备份数据库、每5分钟拉取一次数据、每周日凌晨清理日志。这类任务的特点是节奏固定、重复发生需要系统长期、稳定地按时间表执行。另一类是一次性任务。比如现在是下午3点你想在3点半执行一个临时脚本或者今晚12点要重启某个服务。这类任务只需要执行一次用完就结束不需要留下一个长期存在的配置。cron 擅长处理第一类at 擅长处理第二类。两者的定位非常清晰cron 是“闹钟”每天固定时间响at 是“便利贴”写个时间到时候提醒一次。很多新手一上来就把所有任务都塞进 crontab包括只跑一次的任务这其实是不合适的。只跑一次的任务用 at 更干净因为它不会残留配置也没有误触发第二次的风险。还有一类场景容易被忽略任务需要在低峰期执行但具体时间又不确定。比如某个数据同步任务希望“某个时间点之后尽快跑”但不想添加到 crontab 里长期存在。这时候用 at 配合判断条件比如先判断文件是否生成完毕再 at 一个几分钟后的任务比硬编码在 crontab 里灵活得多。1.2 为什么我推荐先从 cron 和 at 入手Linux 下能实现定时任务的方案不少。新一点的系统有 systemd timer容器环境里有 Kubernetes CronJob这些方案功能更强但从学习成本和通用性来看cron 和 at 依然是性价比最高的选择。cron 的优势在于它是 POSIX 标准的一部分几乎所有 Linux 发行版都内置了 cron 服务和 crontab 命令。你在一台 CentOS 上写的 crontab 语法换到 Ubuntu、Debian、甚至 macOS 上基本不用改。对于大多数脚本语言和工具cron 的任务定义就是一行文本非常容易版本化管理。systemd timer 虽然更现代但它依赖 systemd 环境而且语法上要写好几个单元文件不适合轻量场景。at 的优势则是简单直接。一条命令、一个时间去执行不需要考虑循环和周期。而且 at 的队列管理与 cron 完全独立不会因为 crontab 写错了某个语法就影响到 at 任务。在纯运维场景里at 经常被用来做“延迟执行”和“临时任务”特别实用。所以我在实际项目中只要是周期任务就先想 cron只要是一次性任务就先想 at。这不是说其它方案不好而是这套组合足够解决绝大多数问题而且心智负担最低。接下来我把两个工具的细节挨个拆开讲。2. cron 配置核心时间语法、配置文件与运行环境2.1 五段时间域的完整拆解含容易混淆的日/星期关系crontab 的每一行代表一个任务基本格式是五个时间字段加一个命令顺序是分 时 日 月 星期 命令注意顺序是“分 时 日 月 周”不是“时分秒”。我第一次学的时候总是习惯性想写秒结果发现 cron 最小粒度就是分钟不支持秒。如果你确实需要每30秒执行一次只能借助循环加 sleep 的方式变通实现。字段的取值范围如下字段取值范围说明分0-59第几分钟时0-23第几小时日1-31几号月1-12几月星期0-70或7都代表周日1-6代表周一至周六每个字段都支持星号、逗号列表1,2,3、范围1-5和步进/10。这些语法单独看很容易理解但组合在一起就有讲究了。最容易被误解的一个点日字段和星期字段同时设置时是 OR 关系不是 AND 关系。也就是说如果这样写0 2 15 * 1 /opt/scripts/backup.sh这个任务的意思是“每月15号 或 每周一 的凌晨2点执行”而不是“每月15号且正好是周一的凌晨2点执行”。如果需求真的是“每个月15号且如果是周一才执行”必须要写两行或者加判断条件cron 本身做不到这个组合逻辑。这个细节我踩过坑某次想限制“每月1号执行”结果因为同时还写了星期几导致任务每周都跑了一遍。另外逗号和范围的混用也很常见比如0 8,20 1-5 * * /opt/scripts/sync.sh这表示每天早上8点和晚上8点且只在每月1号到5号执行。写法没问题但要注意逻辑上是否符合预期。cron 的星号只代表“该字段的合法取值范围内全部值”不代表“任意”这一点新手容易理解偏。2.2 crontab 的三种配置位置怎么选cron 任务配置的存放位置有几种分别对应不同的使用场景用错了会带来不必要的困扰。第一种是用户级 crontab。用crontab -e编辑crontab -l查看crontab -r删除。这种方式最简单命令执行身份就是当前用户每个用户都有自己独立的任务列表。绝大多数场景用这个就够了。第二种是/etc/crontab。这是系统级 crontab区别在于它多了一个用户字段格式是分 时 日 月 星期 用户 命令因为需要指定用户所以适合需要以特定身份执行任务的情况比如用 www 用户跑 Web 相关的任务。但系统级 crontab 的内容在升级维护时可能被覆盖如果不是有特殊需求不建议所有任务都往里塞。第三种是/etc/cron.d/目录。目录下的每个文件都会作为 crontab 格式被加载而且支持用户字段。这种方式适合软件包安装时自带的定时任务因为每个任务一个文件互不干扰卸载时直接删文件就行。这也是我写部署脚本时比较推荐的方案因为可以随代码库一起分发方便统一管理。还有几个标准目录比如/etc/cron.hourly/、/etc/cron.daily/、/etc/cron.weekly/、/etc/cron.monthly/。这些目录配合 anacron 使用适合“每天/每周/每月跑一次”的简单任务。你只需要把一个可执行脚本放进对应目录系统会在周期内自动执行不需要写 crontab。这种方式有个好处即使服务器在预定时间没开机anacron 会在开机后补跑这是普通 crontab 做不到的。不过补跑机制和最小执行粒度比不上 crontab 精细两者互相配合才是正解。2.3 环境变量与执行用户定时任务“玄学”问题的根源cron 任务执行时环境变量和你手动在终端里执行脚本时完全不一样。这是定时任务领域最典型的“玄学”问题来源。手动跑脚本一切正常一进 crontab 就报错或者找不到命令、找不到库基本都是环境变量导致的。手动登录终端时shell 会加载/etc/profile、~/.bash_profile、~/.bashrc等文件把 PATH、JAVA_HOME、LD_LIBRARY_PATH 这些东西都设置好。但 cron 执行任务时使用的是非交互、非登录的 shell它不会加载这些文件PATH 通常只是默认的最小路径/usr/bin:/bin。如果你的脚本里用到了某个安装在其他目录的命令比如/usr/local/bin/python3或者某个自定义工具那么 cron 环境下直接执行可能就找不到。解决办法有两个。一个是在 crontab 里显式指定 PATH另一个是在脚本开头设置环境变量。比较稳妥的做法是在 crontab 顶部加上一行PATH/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin这样所有任务都能继承这个 PATH。如果某个任务依赖 JAVA_HOME 之类的变量建议在脚本内部显式 export而不是依赖 crontab 的全局配置。因为不同任务的环境需求不一样全局配置容易互相影响。另外还要注意执行用户。用户级 crontab 里的命令默认使用当前用户的身份执行但你在系统级 crontab 配置时如果不指定用户可能会以 root 身份跑。以 root 跑普通业务脚本一旦脚本里有 rm 之类的危险命令出问题就是大事故。我的习惯是能用普通用户跑的任务绝不用 root只有备份、系统日志轮转这类真正需要 root 权限的任务才用 root。2.4 实战建议配置示例与常见的错误写法说了这么多直接给一个标准的示例。假设有一个脚本/opt/scripts/backup.sh需要在每天凌晨3点15分执行并且日志输出到/var/log/backup.log15 3 * * * /opt/scripts/backup.sh /var/log/backup.log 21这行里的表示追加输出21把标准错误也重定向到一个文件不然 cron 会尝试把输出通过邮件发给你很多系统没配邮件服务导致输出直接丢失。常见错误写法之一是15 3 * * * cd /opt/scripts ./backup.sh /var/log/backup.log 21这个写法没问题但很多人以为cd只会影响当前命令实际上没问题后面的命令会在 cd 后的目录下执行。比较隐蔽的错误是下面这种15 3 * * * /opt/scripts/backup.sh /dev/null 21虽然把输出丢掉了但这样一旦脚本报错你连一点线索都没有。我建议至少保留标准错误输出到一个日志文件方便排查。还有%号问题crontab 里%会被解释成换行符如果你命令里需要传date %Y%m%d这种参数必须转义15 3 * * * /opt/scripts/backup.sh $(date \%Y\%m\%d)如果不转义cron 会把%后面的内容当作标准输入传给命令结果完全不是你想要的。这个细节非常隐蔽遇到“脚本没跑但是 crontab 语法看起来对”的怪事先检查有没有裸写的%。最后加注释是好习惯。crontab 里用#注释任务多了以后能省不少回忆成本。我的规范是每行任务至少写一行注释说明是谁加的任务、用来干什么、大概什么时候加的后面接手的人会非常感激。3. at 一次性任务从使用场景到完整配置3.1 at 命令的基本用法与时间写法at 的使用比 cron 简单得多。一条典型的 at 命令是at 15:30输入之后进入交互式 shell逐行输入要执行的命令最后用CtrlD结束。不过这种交互式方式不太适合脚本化更常用的是通过管道一次性传入echo /opt/scripts/temp_task.sh | at 15:30或者用-f指定脚本文件at 15:30 -f /opt/scripts/temp_task.shat 支持的时间写法非常灵活。绝对时间可以直接写15:30、10:45、23:59带日期可以写15:30 2025-06-01也可以写17:30 tomorrow。我更常用的是相对时间at now 5 minutes at now 2 hours at now 1 day这种写法不需要关心当前具体时间脚本里算好“多久以后执行”就行特别适合那种迁移任务。比如某个数据文件正在生成预计10分钟后完成那就echo /opt/scripts/process.sh | at now 10 minutes要查看当前有哪些待执行的任务用atqatq输出会显示任务编号和执行时间。要取消任务用atrm 任务编号。比如atrm 5这个流程和 cron 的增删查改相比真的是轻量很多。3.2 at 队列权限控制与日志at 也有自己的权限控制机制。系统里如果有/etc/at.allow文件那么只有文件中列出的用户允许使用 at如果没有 at.allow但有/etc/at.deny文件那么 at.deny 中列出的用户被禁止使用 at。如果两个文件都不存在只有 root 可以使用 at。这个机制的核心思路是“默认拒绝”和“默认允许”的策略差异。大多数发行版默认只有一个空的 at.deny也就是说所有用户默认都可以使用 at。如果出于安全考虑要限制某些用户就把他们写进/etc/at.deny。反过来如果是只允许指定用户用 at 的严格策略就创建/etc/at.allow只写入允许的用户名。我在某公司服务器上就遇到过这种情况多个开发者共用一个部署机为了不让普通用户随意提交 at 任务运维直接创建了 only root 的 at.allow瞬间安静了很多。at 执行完毕后输出默认会通过邮件发给提交者。没有邮件服务的话建议在提交任务时显式重定向echo /opt/scripts/temp_task.sh /var/log/temp_task.log 21 | at now 5 minutesat 的日志一般在/var/log/at.log不同发行版路径可能不同有的在/var/log/atd.log。任务真的出问题时这里是第一手的线索。3.3 cron 与 at 的联动场景cron 和 at 虽然各自定位不同但配合起来能解决一些比较麻烦的场景。第一个场景是条件触发。cron 固定每天凌晨1点检查数据文件是否已经到位如果到位就执行后续任务如果没到位就再 at 一个15分钟后的任务重新检查。这样既保证了固定节奏的尝试又避免了用 while 循环在 crontab 里空转占用资源。第二个场景是错峰执行。有些任务本身是周期性的但执行时间取决于某个上游任务的完成情况。cron 里可以先跑一个“探针脚本”探针脚本检查条件满足后用 at 把真正的耗时任务安排在低峰期执行。这样避免了长任务阻塞下一个周期。第三个场景是临时延期。生产环境里有时候需要临时延后某个任务的执行时间但不想改 crontab 里的固定配置。直接 at 一个任务去“锁住”或跳过下一次周期执行等延后时间到了再恢复正常。这个思路在做发布窗口控制时很实用。我个人的习惯是所有需要“基于状态动态安排”的一次性动作优先考虑 at所有“固定节奏无论如何都要触发”的动作优先考虑 cron。两者搭配使用基本上能把绝大多数定时需求覆盖住。4. 完整案例定时备份脚本的落地全过程4.1 需求与脚本设计用一个具体案例把前面的内容串起来。假设现在要为一个 Web 应用增加日志备份任务需求是每天凌晨2点30分将前一天的日志文件压缩归档到备份目录压缩后的日志文件名包含日期同时清理7天前超过2GB的临时文件避免磁盘告警所有执行过程记录到日志文件方便追溯周末不执行周一至周五执行。先写备份脚本/opt/scripts/log_backup.sh。脚本开头先设置环境变量和日志目录#!/bin/bash export PATH/usr/local/bin:/usr/bin:/bin YESTERDAY$(date -d yesterday %Y%m%d) LOG_DIR/var/log/myapp BACKUP_DIR/data/backup LOG_FILE/var/log/log_backup.log echo $(date %F %T) start backup $LOG_FILE cd $LOG_DIR || exit 1 tar czf $BACKUP_DIR/myapp-$YESTERDAY.tar.gz myapp.log.$YESTERDAY $LOG_FILE 21 if [ $? -eq 0 ]; then echo tar ok: myapp-$YESTERDAY.tar.gz $LOG_FILE rm -f $LOG_DIR/myapp.log.$YESTERDAY else echo tar failed, keep original file $LOG_FILE fi find $BACKUP_DIR -type f -name *.tar.gz -mtime 7 -size 2G -exec rm -f {} \; echo $(date %F %T) end backup $LOG_FILE脚本最后一行用 find 清理7天前大于2GB的备份包。这里不加-exec rm之前先-size 2G是因为即使删错也只影响超过2GB且超过7天的包而这正是需求里要求的。脚本里的$?判断很关键如果 tar 失败了就不删原始日志防止数据丢失。这些细节在实际生产环境非常重要。4.2 部署 cron 定时任务并验证脚本写完先手动跑一次确认逻辑没问题。然后通过crontab -e添加任务30 2 * * 1-5 /opt/scripts/log_backup.sh注意这里没有把输出重定向到文件因为脚本内部已经把日志写到了/var/log/log_backup.logcron 就不需要再处理标准输出了。如果脚本内部没有重定向就需要在 crontab 里加 /var/log/log_backup.log 21二选一即可不要重复重定向否则日志文件看起来会有两遍。添加之后用crontab -l确认crontab -l这时能看到刚加的那行。接下来验证实际执行情况关键的排查方法是看两个地方一是脚本自己写的/var/log/log_backup.log二是 cron 的执行日志。如果是 CentOS 系列日志在/var/log/cronUbuntu 系列一般在/var/log/syslog里可以 grep 到CRON。如果任务是普通用户添加的执行身份就是普通用户。上面这个脚本里归档目录写的是/data/backup如果普通用户没有写权限就会失败。所以要么给这个目录设置好 ACL 权限要么用系统级 crontab 指定用户。我的建议是保持普通用户身份调整目录权限避免一切用 root 跑业务脚本的风险。4.3 系统级 cron.d 配置与 anacron 补跑如果这个备份脚本是从软件包或某个部署工具分发的我不太建议每台机器都手动crontab -e因为任务多了以后会变成一团乱麻。更推荐在/etc/cron.d/下放一个独立文件比如log_backup内容如下30 2 * * 1-5 root /opt/scripts/log_backup.sh注意/etc/cron.d/下的文件要指定执行用户这里写root是因为 /data/backup 的权限允许 root 读写。如果想让脚本用普通用户身份执行可以改成对应的用户名。另外如果这台服务器的关机时间不规律比如是某台实验室的机器经常晚上关机那么固定凌晨2点30分执行的任务经常错过。这时候可以考虑用 anacron 来补跑。anacron 的配置在/etc/anacrontab里它不需要写精确到分钟的时间而是按天判断“发现上次执行时间距离现在超过指定天数就补跑”。把备份脚本放进/etc/cron.daily/anacron 会每天检查并补跑。不过/etc/cron.daily/这种方式无法控制到“2点30分”这个粒度它只会保证“每天的某个固定时间点后执行”所以真正对时间精度有要求的任务还是得靠 crontab。把时间精确要求和补跑需求结合的正确姿势是crontab 里写精确时间同时脚本内部自己写一个时间戳文件检查“上次成功执行时间”如果超过24小时说明漏跑了就在脚本里补一次。这个思路兼顾了 cron 的精确性和 anacron 的容错性。5. 常见故障排查与避坑实录5.1 任务没跑按这个顺序排查定时任务最常见的故障就是“明明到点了什么都没有发生”。很多同学第一步就是去翻 crontab 语法但真正的问题往往不在语法上。我总结了排查顺序照着走能省很多时间。第一步确认 cron 服务在运行。不同发行版的系统服务名字不同CentOS 上一般是 crondUbuntu 上是 cron。用systemctl status crond或service cron status查看。很多临时环境里服务根本没启动任务自然不可能跑。第二步检查时间。确认系统当前时间和预期执行时间注意时区问题。如果服务器时区是 UTC而你预期的是北京时间任务会在“看起来早了8小时”的时候执行。生产环境里我见过太多因为时区误解导致的定时任务故障了。第三步确认任务是否真的已保存。crontab -l能列出当前用户任务。如果你用crontab -e编辑后没保存或者编辑了别的用户的任务自然不生效。第四步看日志。CentOS 查/var/log/cronUbuntu 查/var/log/syslog。日志里会看到类似May 20 02:30:01 host CROND[12345]: (user) CMD (/opt/scripts/log_backup.sh)这个记录说明 cron 服务已经执行了这个任务那问题就出在脚本本身而不是 cron。这时候手动跑一遍脚本对比输出就知道了。第五步检查脚本权限。脚本文件要有可执行权限chmod x不要忘。如果是用sh script.sh方式写的 crontab权限要求可以放宽但直接写脚本路径的方式必须保证可执行。5.2 环境变量与转义问题实录我在实际处理过的定时任务故障里环境变量问题占了很大的比例。有一次排查某个数据同步任务发现脚本手动执行没有问题放在 crontab 里却报“command not found: mysql”。原因就是 cron 环境的 PATH 不包含 mysql 的安装路径/usr/local/mysql/bin。解决方案是在脚本开头显式加上export PATH/usr/local/mysql/bin:$PATH export LD_LIBRARY_PATH/usr/local/mysql/lib:$LD_LIBRARY_PATH而不是在 crontab 里盲目地把一堆全局 PATH 写死。原因很简单脚本是共享的它自身的依赖环境在脚本里声明最合适换了部署环境也方便维护。还有一个非常经典的坑是通配符和特殊字符。某次定时任务里写了0 4 * * * /opt/scripts/clean.sh $(date \%Y\%m\%d)因为 crontab 里%必须转义当时忘了加反斜杠结果 cron 把date 后面的内容当成标准输入吞掉命令执行结果完全乱掉。排查了很久才定位到是%的锅。从那以后我在 crontab 里需要用到动态日期时总是先转义或者干脆不在 crontab 里写命令参数而是在脚本内部用date生成文件名这样 crontab 行就保持干干净净。这是一个非常值得借鉴的实践。5.3 实践中的注意事项清单综合考虑自己和多位同事踩过的坑整理一个定时任务注意事项清单新手照着做可以少走很多弯路最小粒度是分钟没有秒级任务。需要秒级的话用循环加 sleep 自己实现。分和时从0开始计数日和月从1开始。别把“分钟字段填1”当成1点这个低级错误其实很容易犯。日字段与星期字段同时设置时是 OR 关系不是 AND。要表达 AND 只能自己加条件判断。crontab 中的%要转义成\%建议需要动态参数时在脚本内部处理不要在 crontab 行里传参。cron 环境不加载登录 shell 的环境变量PATH 和自定义变量要在脚本内显式设置。命令输出必须重定向到文件或/dev/null否则 cron 会尝试发邮件没邮件服务时直接丢日志。任务执行用户身份默认是 crontab 所有者系统级 crontab 需要显式指定。不要滥用 root。脚本里涉及删除操作前先判断错误码确保上游命令成功了再删避免删掉还没处理完的数据。长时间运行的脚本要加超时控制比如用timeout 3600 /opt/scripts/xxx.sh防止异常挂住持续占用资源。定时任务脚本要做幂等设计。日志清理、数据同步这类任务多次执行的结果应当一致这样即使漏跑补跑也不会出问题。每次修改 crontab 后用crontab -l确认结果避免编辑器保存失败或者语法错误导致的静默失败。重要任务建议在脚本里写心跳或结果标记文件便于外部监控系统检查“上次执行是否正常”。这些清单里的每一条几乎都是从故障现场里总结出来的。建议第一次接手定时任务时把脚本先手动执行一遍确认完整状态再放进去 cron 或 at最后再手动“模拟端到端”验证一遍。实测下来这样能过滤掉绝大部分低级问题。我个人在实际操作中的体会是定时任务不怕复杂怕的是不可见。只要每次执行都有日志、有状态、有迹可循再大的问题都能快速定位。cron 和 at 都不难难的是在细节上养成好习惯。如果你现在维护着一堆 crontab 任务可以先花10分钟把当前所有任务列出来把注释补齐、把危险写法修正、把输出重定向补上一次修完以后会稳定很多。后面遇到需要临时跑一次的场景也别忘了一起用上 at 这个顺手的小工具。