
搞Linux运维这么些年要说最常用的功能定时任务绝对排得上号。不管是半夜跑备份、每分钟轮询一次接口状态、还是每周定期清理日志文件都离不开它。我见过不少新手刚开始直接用shell脚本while true加sleep死循环去凑合结果脚本一挂没人知道第二天一看服务早挂了。今天就把Linux里定时任务这块讲透从最经典的cron到现在厂商越来越推荐的systemd timer都掰开揉碎说清楚附上我平时踩过的坑和排查套路。这个话题适合所有人——刚入门的运维新手、自己玩服务器写脚本的开发者、还有准备面试Linux岗位的求职者定时任务都是躲不开的基础技能。读完你不仅能立刻上手配置自己的任务更重要的是遇到任务不跑、重复跑、时间对不上这类问题时不慌能自己定位解决。1. 定时任务的核心选项cron还是systemd timer先回答新人问得最多的一个问题Linux里做定时任务到底用什么你随手一搜几乎全是cron的内容crontab -e仿佛成了唯一答案。这没毛病cron确实老牌、够用、大多数发行版默认就装好。但如果你用的是CentOS 7以上、Ubuntu 16.04以上的系统其实主流厂商尤其Red Hat系已经默认把目光转向了systemd timer。这里头是有原因的我会在第5节详细拆解。我的建议是两种都得会但日常简单脚本用cron足够追求日志统一管理、怕漏跑任务、对任务依赖有要求就上systemd timer。怎么说呢cron像一台老式闹钟——准时、皮实但你只能设置提醒看不到它到底响没响systemd timer更像智能家居——它不光是定时唤醒还能告诉你上次任务跑了没、跑了多久、失败原因是什么跟系统日志完全打通。看场景取舍吧。2. cron基础五分钟上手的配置语法2.1 crontab基本操作命令cron的核心操作就几条命令先记熟crontab -e # 编辑当前用户的定时任务 crontab -l # 查看当前用户的定时任务 crontab -r # 删除当前用户的全部定时任务 crontab -u user -e # 编辑指定用户的定时任务正常情况下你登录服务器后执行crontab -e会进入一个编辑界面跟vim操作一模一样。每一行是一条任务格式如下分钟 小时 日 月 星期 要执行的命令这里必须强调一个经典误区新手以为cron配置是“几点几分执行”实际它的第一字段是分钟第二字段才是小时。两个字段分开写代表一个精确的时间点。2.2 最常用时间表达式一览我在实际操作中总结了一张等价表比看一堆抽象文档管用得多需求写法含义每天凌晨2点30分执行30 2 * * *分30时2每5分钟执行一次*/5 * * * *分钟位每隔5每小时的第10分钟10 * * * *每小时跑一次每天零点整0 0 * * *日志切割常用每周一早上8点0 8 * * 1星期1周一每月1号凌晨3点0 3 1 * *月任务每天8点到18点每2小时0 8-18/2 * * *工作时段常用每天三个指定时间点0 6,12,18 * * *注意逗号用法注意一个反直觉的小细节星期和日期同时指定时是「或」关系而不是「与」关系。比如你写0 0 1 * 1意为“每月1号”或者“每周一”都会触发而不是“每月1号且是周一”才触发。这个坑我见不少人踩过尤其在做月初周报任务时容易重复跑。2.3 一个最常用的实战配置示例比如我想在每天凌晨3点备份数据库并在备份完成后把日志写到指定文件0 3 * * * /home/zhao/scripts/backup_db.sh /home/zhao/logs/backup.log 21这里还涉及一个常被忽略的细节cron执行时的环境变量很少它不加载你的.bashrc。如果你在脚本里用了pg_dump这类不带全路径的命令很可能出现“脚本手动执行正常cron里执行报command not found”。等下第三节我会专门讲这个。3. 定时任务脚本的编写与踩坑细节3.1 先跑通脚本再交给cron不管任务多简单一定要遵守“先手动跑再加定时”的顺序。我有一个习惯先把要执行的命令写成一个独立的shell脚本加上set -x调试模式手动执行一遍确认无报错再写进crontab。因为在cron环境下手动调试非常痛苦日志不全报错也不像终端那样直观。举个例子一个清理系统垃圾的脚本新手写法可能是# 不建议直接这么写进crontab find /tmp -type f -mtime 7 -exec rm -rf {} \;且不说rm -rf的写法有多危险就find加-exec的组合如果遇到同名文件、符号链接、路径带空格的情况都可能出问题。我通常建议先把这类命令写进脚本文件然后一行行拆解验证#!/bin/bash find /tmp -type f -mtime 7 -print先不删看看输出文件列表是否符合预期。确认无误后再加-delete或-exec参数。不要嫌麻烦这个习惯能帮你躲掉大量“定时任务误删数据”的事故。3.2 环境变量为什么脚本手动执行OK但cron不执行这是新手提问区出现频率最高的问题。手动执行正常放进crontab里就是各种幺蛾子——找不到命令、脚本内部的相对路径失效、python找不到模块。原因很简单cron执行环境继承了极少的PATH通常只有/usr/bin:/bin。你手动在终端执行时~/.bashrc、~/.bash_profile会把类似/usr/local/bin、/opt/xxx/bin加进PATH脚本里的python3、docker-compose能正常找到但cron环境里没有。解决办法也很干脆。直接在脚本开头强行塞入环境变量#!/bin/bash source /etc/profile source ~/.bash_profile export PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH然后在crontab里写30 2 * * * /bin/bash /home/zhao/scripts/my_task.sh /home/zhao/logs/my_task.log 21这里还有一个细节脚本路径和日志路径最好都用绝对路径。你用相对路径cron的当前工作目录默认是你登录用户的$HOME一旦路径没对上脚本会因为找不到文件而直接报错而且这种报错特别隐蔽。3.3 配置脚本执行权限与调试模式很多新手写完脚本忘了chmod x然后crontab里直接写脚本路径结果任务完全不执行。我先说一个我在生产环境里最稳妥的做法chmod x /home/zhao/scripts/my_task.sh然后crontab里用绝对路径调用。更保险的是我都建议你测试阶段先在脚本第三行加一个输出当前时间和环境变量的语句比如echo $(date %F %T) task start /home/zhao/logs/debug.log env /home/zhao/logs/debug.log跑几次之后查看debug日志你就能迅速确认任务有没有被触发、系统的PATH到底是什么、当前目录在哪。定位完问题记得把env那行删掉毕竟环境信息打太多到日志里也不好看。4. cron运行机制与常见任务时间场景4.1 cron的服务与日志机制cron本身是一个系统服务。老一点的系统叫crondCentOS上现在也是这个名字Ubuntu上叫cron。管理命令分两种systemctl status crond # CentOS/RHEL系列 systemctl status cron # Debian/Ubuntu系列如果任务没有执行第一件事就是确认服务是否活着。我遇到过不止一次因为系统资源紧张或者被人误停了服务crontab -l能看到配置但任务就是一句话都不跑。查一下服务状态瞬间就破案了。cron日志的位置也有讲究。CentOS上一般在/var/log/cronDebian系在/var/log/syslog里过滤关键词CRON。看日志有什么好处你能直观看到系统有没有触发你的任务还能看到它实际上用什么用户名执行的排除了很多基本问题。4.2 只跑一次的定时任务at命令先澄清一个常见混淆cron是循环反复执行但如果你只想让系统在某个时刻执行一次命令比如晚上11点重启一个服务那该用at命令而不是cron。echo systemctl restart nginx | at 23:00 at -l # 查看等待中的任务 at -d 编号 # 删除指定任务at命令需要单独安装安装方法也很简单yum install at -y # CentOS apt install at -y # Ubuntu systemctl start atd systemctl enable atd为什么提这个因为我发现很多新手把一次性任务硬塞进crontab设个明年才触发的时间然后忘了删一年后突然跑一个早没意义的脚本直接把服务搞崩。定时任务定期清理也是运维的基本素养。4.3 秒级任务怎么实现cron的最低粒度是分钟这是它的硬伤。如果你需要每10秒轮询一次某个端口或者检测某个进程我推荐两个方案。方案一是写shell死循环加sleep但必须配合nohup和守护机制#!/bin/bash while true; do /home/zhao/scripts/check_status.sh sleep 10 done然后用nohup放到后台再配一个cron每5分钟检查这个死循环进程是否存活死了就重新拉起。说白了就是双保险结构。方案二是直接用第三方工具或者systemd timer加OnUnitActiveSec10s我后面会详细介绍。我更倾向于后者毕竟systemd已经内置了不用额外装supervisor之类的工具。5. systemd timer新一代定时任务玩法5.1 为什么大厂都在往systemd timer迁移你可能会好奇cron已经能完成大部分需求为什么还要用systemd timer我总结几个真实场景你感受一下cron如果漏跑了一次任务你不会有任何感知系统不会提醒“刚才有个任务没跑”而systemd timer可以设置错过时间后立即补跑还能通过Persistenttrue保存上次执行时间。cron想看上次执行结果得自己去翻日志文件拼接信息systemd timer直接一条systemctl status 某timer就告诉你上次运行时间、下次运行时间、运行结果成功/失败、耗时多少。cron没法控制任务之间的依赖关系systemd的unit天生支持After等依赖配置。有些任务需要严格串行cron会重复调起脚本导致并发冲突systemd的service默认不会重复启动同一个实例。这么说吧如果你是一个人在管理十几台服务器cron完全够用但如果到了几十上百的量级靠cron的日志去排查“到底哪些机器跑了、哪些没跑”干活效率太低了systemd timer能帮你省掉大量排查成本。5.2 timer单元两个文件的写法systemd timer由一对文件组成一个.timer文件定义触发时间一个.service文件定义具体动作。比如我要做一个每天凌晨3点的备份先创建service文件/etc/systemd/system/backup-task.service[Unit] DescriptionDaily backup task [Service] Typeoneshot ExecStart/home/zhao/scripts/backup_db.sh Userzhao注意Typeoneshot这种一次性任务类型的service在timer场景里最常见。如果你写默认的simple类型systemd会认为服务需要常驻而你的脚本执行完就退出系统会判定任务失败或状态混乱。再创建timer文件/etc/systemd/system/backup-task.timer[Unit] DescriptionRun backup task daily at 3am [Timer] OnCalendardaily Persistenttrue [Install] WantedBytimers.target然后两行命令启动并开机自启systemctl daemon-reload systemctl enable --now backup-task.timer systemctl status backup-task.timer执行systemctl list-timers能列出所有已经定义的timer以及它们的下次执行时间。这比crontab -l有可读性多了一眼扫过去就知道每台机器在跑什么周期任务。5.3 OnCalendar时间语法详解systemd timer的OnCalendar语法比cron稍微复杂一点但可读性更强尤其适合人类理解含义OnCalendar写法每天凌晨3点*-*-* 03:00:00每周一早上8点Mon *-*-* 08:00:00每月1号和15号*-*-01,15 00:00:00每5分钟*:0/5每小时的第10分钟*-*-* *:10:00工作日每天9点Mon..Fri *-*-* 09:00:00这里有一个很实用的功能Persistenttrue的意思是如果系统在计划时间点当时是关机的等下次开机后systemd会根据时间戳自动补跑你错过的任务。这对笔记本用户和经常停机的服务器相当友好cron就不行错过就错过追悔莫及。cron的anacron也能补跑但配置相对复杂且很多发行版默认没开。5.4 timer的调试与查看命令timer的调试其实比cron更舒服几条命令直接定位systemctl list-timers # 查看所有定时器及最近运行时间 systemctl start backup-task.timer # 手动启动定时器 systemctl stop backup-task.timer # 手动停止定时器 systemctl status backup-task.service # 查看上一次任务的执行结果 journalctl -u backup-task.service -e # 查看该任务最近的日志journalctl -u这条命令我特别推荐因为cron的日志散落在系统日志文件中而systemd统一用journald管理一条命令就能看到具体某次任务的标准输出、报错信息。遇到“任务到底跑没跑、跑成功没有”的问题直接查journal效率拉满。6. 定时任务的高级技巧与生产经验集锦6.1 日志切割与保留策略日志是定时任务最容易忽视的问题。我见过一台服务器因为一个定时任务每5分钟往日志文件里写一条结果半年没清理磁盘直接被撑爆数据库跟着挂了。建议写脚本时把日志切割逻辑内置进去比如MAX_SIZE100M LOG_FILE/var/log/my_task.log if [ -f $LOG_FILE ] [ $(stat -c%s $LOG_FILE) -gt $(echo $MAX_SIZE | tr -d M)000000 ]; then mv $LOG_FILE ${LOG_FILE}.$(date %Y%m%d) gzip ${LOG_FILE}.$(date %Y%m%d) fi虽然这是简化版但核心思路就是这个限制单文件大小超出就滚动配合系统日志轮转工具一起用更稳。生产环境其实我推荐直接用logrotate那又是另一个话题了但起码你要有意识写脚本时不要无脑输出日志。6.2 防止任务重复执行的锁机制这是运维事故重灾区。设想你有一个备份任务设置每小时跑一次结果上个小时的任务因为数据量大、迟迟没跑完下一个小时的又触发了两个进程同时写同一个备份目录大概率备份文件损坏。我给每个可能执行时间较长的脚本加锁用flock或者mkdir锁都行。最简单的方式是#!/bin/bash LOCK_FILE/tmp/my_task.lock exec 9$LOCK_FILE if ! flock -n 9; then echo 另一个实例还在运行退出 exit 1 fi # 实际任务内容flock -n是非阻塞模式拿不到锁就直接退出。这相当于给脚本加了一层互斥不会因为并发调用导致数据错乱。凡是超过10分钟的任务我都建议默认加这个保护这个习惯能救你很多次。6.3 任务结果通知定时任务跑完没通知失败了也没通知等于裸奔。我常用的最低成本的方案是把任务结果发到群里或者邮箱。比如脚本里加上if [ $? -eq 0 ]; then curl -s -X POST https://接口地址 --data-urlencode msg任务执行成功 else curl -s -X POST https://接口地址 --data-urlencode msg任务执行失败请检查 fi这是最简单的Webhook方式比配邮件网关省事太多。日常重保任务一定要有关联的通知渠道不然定时任务就是个黑盒跑没跑、成没成全靠猜。6.4 cron与systemd timer的选型决策说了这么多最后给个选型建议你可以对着自己的场景直接套维度cronsystemd timer上手难度低几行搞定中需要写两个unit秒级任务不支持原生支持崩溃补跑需要anacron支持Persistent日志管理分散难查journald统一依赖控制少量支持强大依赖关系系统兼容所有发行版systemd系发行版适合场景简单重复任务复杂周期任务、服务编排我的个人倾向是涉及数据库备份、服务重启、日志切割这类低频但重要的任务优先用systemd timer因为你能快速确认执行状态像每分钟轮询这种高频轻量级动作用cron就够反正日志不复杂两次任务覆盖一次也没啥问题。7. 定时任务常见故障案例与排查思路值得反复看7.1 案例一任务不执行但crontab配置看着没问题这是最常见的故障表现。我的排查顺序基本固定systemctl status crond # 1.服务是否活着 cat /var/log/cron | tail -100 # 2.看系统日志有没有CRON触发记录 crontab -l # 3.看当前用户配置在不在 ls -l /etc/crontab # 4.检查系统级配置有一次我排查了半天最后发现是crontab -e时的文件格式出了问题行的末尾被编辑器自动加了一个看不见的Windows换行符\r导致cron解析失败。因为\r会粘在命令后面cron尝试执行一个带奇怪字符的命令直接报错。解决方式是用dos2unix转一下或者在vim里:set ffunix后再保存。7.2 案例二脚本里环境变量找不到命令not found这个我在前面讲过一次但出镜率实在太高再补充一个细节。解决方法有两个层次第一层是脚本开头手动加载/etc/profile第二层是干脆在crontab里直接写全路径30 2 * * * /usr/local/bin/python3 /home/zhao/scripts/clean.py /tmp/clean.log 21比如容器场景下很多工具在/usr/local/bin这个路径并不在cron的默认PATH里。写全路径一了百了比什么都省事。7.3 案例三任务重复执行导致数据冲突之前有个同事写了一个每5分钟同步数据的cron任务任务正常但是数据偶尔有重复排查后发现是数据量大、上一次同步还没结束下一次已经开启了。当时用的就是我前面说的锁处理方式加上flock之后问题当场解决。值得提醒的是cron不像systemd service有防止重复启动的机制所以只要任务执行时间可能超过任务间隔就必须自行加锁或者数据库层面的幂等处理这点绝对不要省。7.4 排查命令速查表最后整理一份我每次排障必看的命令清单直接收藏即可排查目的命令查看cron服务状态systemctl status crond或systemctl status cron查看cron最近触发日志tail -100 /var/log/cron查看当前用户crontabcrontab -l手动立即触发一次任务run-parts /etc/cron.d或直接执行脚本查看systemd timer列表systemctl list-timers查看timer详细状态systemctl status 名字.timer查看任务执行日志journalctl -u 名字.service -e要特别说一句run-parts只能用于系统级cron目录比如/etc/cron.hourly、/etc/cron.daily它不会去执行用户crontab里的任务。别拿它来测试用户任务会带来误导。8. 定时任务管理心态与习惯最后聊点实操之外的。定时任务看着简单但真正出事故往往不是因为配置本身而是因为“没人记得它存在”。你离职三年后某个凌晨3点一个无人知晓的cron任务突然触发把已经升级过的新系统目录给清空了这种故事在运维圈一点都不新鲜。我有两个习惯值得大家借鉴第一每个定时任务脚本头部的注释里写清楚创建人、创建日期、目的、影响范围以及上游依赖。第二每季度主动检查一次服务器上所有crontab和systemd timer把已经不再需要的任务当场删掉。写定时任务不牛能保证五年后有人看得懂、敢删掉你的定时任务那才叫专业。我个人在实际操作里还有一个体会凡是跟数据删除、备份覆盖有关的任务测试期一定要先在临时目录里验证跑两周确认目标路径、文件匹配规则都没问题再换到正式路径。别怕麻烦定时任务这玩意儿跑错一次损失的不是时间而是数据。