搞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 2>&1这里还涉及一个常被忽略的细节: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 2>&1这里还有一个细节:脚本路径和日志路径最好都用绝对路径。你用相对路径,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本身是一个系统服务。老一点的系统叫crond,CentOS上现在也是这个名字,Ubuntu上叫cron。管理命令分两种:
systemctl status crond # CentOS/RHEL系列 systemctl status cron # Debian/Ubuntu系列如果任务没有执行,第一件事就是确认服务是否活着。我遇到过不止一次,因为系统资源紧张或者被人误停了服务,crontab -l能看到配置,但任务就是一句话都不跑。查一下服务状态,瞬间就破案了。
cron日志的位置也有讲究。CentOS上一般在/var/log/cron,Debian系在/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加OnUnitActiveSec=10s(我后面会详细介绍)。我更倾向于后者,毕竟systemd已经内置了,不用额外装supervisor之类的工具。
5. systemd timer:新一代定时任务玩法
5.1 为什么大厂都在往systemd timer迁移
你可能会好奇,cron已经能完成大部分需求,为什么还要用systemd timer?我总结几个真实场景你感受一下:
- cron如果漏跑了一次任务,你不会有任何感知,系统不会提醒“刚才有个任务没跑”;而systemd timer可以设置错过时间后立即补跑,还能通过
Persistent=true保存上次执行时间。 - 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] Description=Daily backup task [Service] Type=oneshot ExecStart=/home/zhao/scripts/backup_db.sh User=zhao注意Type=oneshot,这种一次性任务类型的service,在timer场景里最常见。如果你写默认的simple类型,systemd会认为服务需要常驻,而你的脚本执行完就退出,系统会判定任务失败或状态混乱。
再创建timer文件/etc/systemd/system/backup-task.timer:
[Unit] Description=Run backup task daily at 3am [Timer] OnCalendar=daily Persistent=true [Install] WantedBy=timers.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 |
这里有一个很实用的功能:Persistent=true的意思是,如果系统在计划时间点当时是关机的,等下次开机后,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_SIZE=100M 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的选型决策
说了这么多,最后给个选型建议,你可以对着自己的场景直接套:
| 维度 | cron | systemd 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 ff=unix后再保存。
7.2 案例二:脚本里环境变量找不到,命令not found
这个我在前面讲过一次,但出镜率实在太高,再补充一个细节。解决方法有两个层次,第一层是脚本开头手动加载/etc/profile,第二层是干脆在crontab里直接写全路径:
30 2 * * * /usr/local/bin/python3 /home/zhao/scripts/clean.py >> /tmp/clean.log 2>&1比如容器场景下很多工具在/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 |
| 查看当前用户crontab | crontab -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,把已经不再需要的任务当场删掉。写定时任务不牛,能保证五年后有人看得懂、敢删掉你的定时任务,那才叫专业。
我个人在实际操作里还有一个体会:凡是跟数据删除、备份覆盖有关的任务,测试期一定要先在临时目录里验证跑两周,确认目标路径、文件匹配规则都没问题再换到正式路径。别怕麻烦,定时任务这玩意儿跑错一次,损失的不是时间,而是数据。