做运维这几年,我越来越觉得,crontab 定时任务就像一栋大楼里的消防通道——平时根本想不起它,可一旦真出了事,你发现它早就堵死了,那种绝望感足够让人在凌晨三点对着服务器骂娘。
很多团队对定时任务的态度是“配完就算完事”,日志不查、执行状态不看,直到业务方跑来问“为什么昨天的报表没生成”“为什么凌晨的清算没跑”,才手忙脚乱地登录服务器手动补跑。更坑的是,有些任务失败是静默的:进程起了,数据错了,退出码还是 0。这种故障你连报警都收不到。
所以就有了今天这篇东西:用 Shell 写一个轻量级的 crontab 监控脚本,不依赖额外的监控系统,不需要部署 agent,只要目标机器上有 Shell 环境和基础命令,就能搭一套“任务执行状态哨兵”。这篇文章会完整拆解我的设计思路、核心代码、实测案例,以及我在这个过程中踩过的坑。标题里的“第61章”是我自己的系列笔记编号,内容上完全独立,你可以直接照着用。
1. 先想明白:crontab 任务到底为什么会“不执行”
写监控脚本之前,先得搞清楚我们要防的是什么。很多新手第一反应是“crontab 不执行 = cron 服务挂了”,但实际上我在生产环境里遇到的失败原因,远比这个复杂。
1.1 定时任务失效的第一现场:三种典型现象
我在实践中把定时任务“失效”归纳成三种现象,监控脚本的设计就是围绕这三种现象展开的:
- 任务完全没跑:cron 服务没起来、
crontab -e里语法写错、用户被锁定、服务器宕机后 cron 没自动恢复,都会导致这种结果。最隐蔽的情况是服务器时间被 NTP 强制校准,任务原定凌晨 2 点跑,结果时间跳变导致 cron 直接跳过。 - 任务跑了但中途失败:脚本依赖的数据库连不上、磁盘空间打满导致日志写不进去、上游数据文件还没生成,这些都会让脚本在执行中段异常退出。如果脚本没有写错误处理,crontab 的日志里通常只有一句
exit status 1。 - 任务“成功”了但结果是错的:这点最恶心。比如脚本里有个变量没初始化,Python 代码里
None被当成 0 参与运算,或者 Shell 脚本里$?取错了上一条命令的退出码。最终任务退出码是 0,但产生的数据是脏的。
1.2 那些“看起来正常”却实际失效的隐蔽场景
除了上面三种常规失败,还有几个我实际踩过、常规监控方案根本发现不了的场景,专门列出来提醒你:
- 同一分钟任务堆积:有个任务处理数据平均要 3 分钟,但 cron 设定的是每分钟执行一次。上一个进程还没跑完,下一个又起来了,两个进程同时操作同一个目录,产生数据竞争。表面看任务每次都执行了,实际上业务数据已经乱了。
- Cron 环境变量与交互 Shell 不一致:cron 默认只加载极少的 PATH 环境变量(通常是
/usr/bin:/bin),你手动source /etc/profile后能跑通的脚本,放到 crontab 里可能直接command not found。这种失败不会产生任何业务日志,只有去翻/var/log/cron才能看到只言片语。 - 执行用户不一致:root 的 crontab 和普通用户的 crontab 完全是两套。我在排查问题时经常发现,某台机器上的任务是配在 root 下的,但某天 someone 用普通用户登录后,顺手执行
crontab -e把任务加了进去,两者互不干扰,排查半天没头绪。
1.3 监控脚本的三大目标与一条红线
基于上面这些失败场景,我的监控脚本需要满足三个目标:
- 判活:能准确判断任务是否在预期时间窗口内执行过,而不是只盯着“当前有没有相关进程在跑”。
- 定位:任务失败时能给出初步的定位信息(日志路径、退出码、最近错误),帮人快速判断是环境问题、代码问题还是资源问题。
- 闭环:能自动重试一定次数,并在重试仍失败时通过多种渠道通知到人。
但我给自己定了一条红线:监控脚本绝不直接修改 crontab 配置、绝不擅自重启 cron 服务。原因很现实——自动恢复机制如果逻辑不严谨,容易引发“抖动叠加”:比如监控脚本误判任务失败后执行了恢复操作,结果把正在正常执行的任务杀掉,造成二次故障。监控的职责是发现问题、暴露问题、辅助定位,最终决策必须由人来下,这是自动化脚本的边界感。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 监控脚本的整体设计:日志兜底、判活中心与时间戳比对
明确了要防什么,接下来就是怎么搭这套哨兵系统。我没有用复杂的分布式监控框架,而是老老实实基于 Linux 自带的组件设计了一个轻量方案。
2.1 为什么选择“日志兜底 + 时间戳比对”而不是守护进程
市面上有很多进程守护工具,比如 systemd 的 timer、supervisor,甚至有人会在任务脚本里写死循环保证进程常驻。但我的场景是监控 crontab 这种周期型任务,它本身就不要求常驻。如果用守护进程去“拉活”,反而会改变任务原本的运行模型。
我的核心思路是“日志兜底 + 时间戳比对”:
- 每个被监控的任务,在执行时强制追加一个“结束标记”到统一的监控日志文件里。这个标记里包含任务名、PID、退出码、耗时。
- 监控脚本定期扫描这些日志文件,检查每个任务最近一次标记是否落在“预期执行时间窗口”内。
- 如果某个任务预期应该执行,但超过时间窗口还没有新标记,就认为任务异常。
这个方案的优点是:不侵入业务逻辑,业务脚本只需要在结尾多一行写入监控日志即可;不依赖常驻进程,监控脚本本身也放进 crontab,每 5 分钟跑一次,完全符合周期型任务的管理习惯。
2.2 监控脚本的目录布局与配置文件设计
先看一下我的监控脚本整体装在哪个位置,以及它的目录结构:
bash复制/opt/cron_monitor/
├── etc/
│ └── tasks.conf # 被监控任务的配置文件
├── log/
│ ├── monitor.log # 监控脚本自身的运行日志
│ └── heartbeat/ # 任务执行后写入的心跳标记文件
│ └── data_backup.mark
├── run/
│ └── monitor.sh # 主监控脚本
└── lib/
└── functions.sh # 公共函数库
为什么单独把配置文件和脚本拆开放?因为我希望以后新增被监控任务时,只改配置文件,不动脚本本体。这样即使脚本有更新逻辑,也不会影响已有任务的监控配置。
配置文件 tasks.conf 我用的是“空格分隔 + # 注释”的简单格式:
bash复制# 任
