半夜不是没人用,是所有人都在抢资源
半夜不是没人用,是所有人都在抢资源
把任务排在 0 0 * * * 是最省事也最容易出错的选择。00:00 这一刻,日志切割(logrotate)、数据库全量备份、对象存储同步、报表跑批、证书续期,再加上其他团队几十个 0 0 * * *,都会在同一秒启动。CPU 被占满、连接池被占满、备份占满磁盘 IO,你的任务超时退出;白天手动跑一次又恢复正常——这就是“只在半夜失败”的典型现场。
做法很简单:把任务分散到不同时间。sleep $((RANDOM % 300)) 放在脚本第一行,或者直接用 17 2 * * * 这种带偏移的表达式。跑批任务按依赖关系串起来,不要都堆在 00:00。
容器里的 0 点,不是你以为的 0 点
cron 表达式本身不带时区,由系统时区解释。而绝大多数基础镜像默认 TZ=UTC,所以你写的 0 0 * * * 在 UTC 执行,对应北京时间早上 8 点——白天看着正常,一到月初对账就发现数据日期整体偏了一天。
- cronie 支持在 crontab 顶部写
CRON_TZ=Asia/Shanghai,注意它只影响其后的行。 - systemd timer 用
OnCalendar=*-*-* 00:10:00 Asia/Shanghai,比改全局时区安全。 - K8s CronJob 从 1.27 起用
spec.timeZone指定,别依赖容器镜像的 TZ。
业务代码里也别用 LocalDateTime.now(),统一显式传入时区,这样容器时区改动不会悄悄改变语义。
跨天任务:日期在跑到一半时变了
23:58 启动的任务,跑到 00:03 才写结果,中间两次调用 date 取到的是两天。分区表写错、增量窗口错位、幂等键算错,结果全靠运气。
约定一条:时间只在入口取一次。
RUN_DATE=$(date -d 'yesterday' +%F),之后所有 SQL、文件名、下游参数都传这个变量,脚本内部禁止再出现 now() 或 CURRENT_DATE。SQL 里如果必须用当前时间,改成显式参数 WHERE dt = '${RUN_DATE}'。这条规则能消除一大半“月初或年末才复现”的异常问题。
退出码 0 不代表成功
三种最常见的假成功:curl 拿到 200,但响应体是 {"code":500};cmd | tee app.log 的退出码来自 tee,前面的命令崩了也算成功;set -e 在 if、||、命令替换里会被绕过。
- 脚本头写
set -euo pipefail,管道中任一环节失败都算失败。 - HTTP 任务同时校验状态码和业务码,失败时执行
exit 1。 - 加
flock -n /var/lock/report.lock防止重复执行,加timeout 3600防止卡死。 - 加心跳:任务成功时请求
curl -fsS https://hc.example.com/ping/xxx,超过预期周期没收到就告警。只告警“失败”是不够的,任务被调度器删掉、机器下线,都不会产生失败事件。
被忽略的半夜杀手:日志与磁盘
logrotate 通常也在 00:00 运行。如果配置是 create,而进程不重新打开文件句柄,它就会继续往已 unlink 的旧文件写入——du 看不到,df 一路涨到 100%,然后所有定时任务在同一晚集体失败。排查时先看 lsof +L1。
加固清单:监控 df 的 inode 与使用率并在 85% 告警;日志用 copytruncate,或让应用自己按大小轮转;关键任务的成功、失败、耗时都打结构化日志,并带上 run_id 和 RUN_DATE,第二天出问题能一眼定位是哪一轮、哪个参数。
- 点赞
- 收藏
- 关注作者
评论(0)