半夜失败不是玄学,是时区在作祟
半夜失败不是玄学,是时区在作祟
最常见的“半夜失败”场景:白天手动跑得好好的任务,一到凌晨两点就报错或漏跑。第一嫌疑人永远是 cron 的时区。服务器、容器、数据库可能各用一套时区,而 cron 只认系统时区。
先确认三处时区是否一致:
- 宿主机:
timedatectl或date +"%Z %z",看是否为CST +0800 - 容器:
docker exec 容器名 date,很多基础镜像默认 UTC,比北京时间慢 8 小时 - 数据库:
SELECT @@global.time_zone, @@session.time_zone;,MySQL 常见的SYSTEM会跟随宿主
真实踩坑:任务写在 0 2 * * *,容器是 UTC,实际在北京时间 10:00 执行,恰好和上午的备份窗口撞车,锁表超时。修复方式不是改 cron 表达式猜时区,而是在容器里显式设置 TZ=Asia/Shanghai 并挂载 /etc/localtime,然后重启 cron 守护进程。若用 crond,改完时区必须 systemctl restart crond,否则旧进程仍持有启动时的时区缓存。
夏令时与跨天边界:少跑一次或多跑一次
如果你的业务涉及海外节点,夏令时切换当天会出现“2:30 这个时间不存在”或“2:30 出现两次”。cron 的默认行为是:不存在的时刻跳过,重复的时刻只执行一次——但不同实现(cronie、busybox crond、k8s CronJob)行为并不统一。
工程上的稳妥做法是避开 0:00–3:00 这个危险区间,把任务放到 3:30 之后,或者改用带时区语义的调度器。例如 k8s 从 1.27 起支持:
spec.timeZone: "Asia/Shanghai",让 CronJob 的表达式按指定时区解释- 老版本只能靠
startingDeadlineSeconds兜底,避免节点重启后堆积的任务一次性全跑
还有个易忽略点:@daily 在部分实现里等价于 0 0 * * *,跨天时如果前一次运行耗时超过 24 小时,flock 没加就会叠加执行。给所有长任务套一层文件锁是成本最低的保险:flock -n /var/lock/backup.lock /opt/scripts/backup.sh。
资源竞争:半夜正是别人跑批的黄金时段
凌晨不是“没人用”,而是全公司的批处理都挤在这时候。你以为是自己的任务挂了,其实是隔壁的 ETL 把磁盘 IO 或数据库连接池吃满,你的查询等锁超时。
- 数据库侧:
SHOW PROCESSLIST抓当时的长事务;确认是否被lock_wait_timeout或innodb_lock_wait_timeout打断 - 系统侧:用
sar -d 1 60或iostat -x 1回看凌晨的%util,超过 90% 基本就是 IO 瓶颈 - 日志侧:给任务统一加开始/结束时间戳和
set -x,失败时能立刻定位卡在哪一步
实操建议:把重任务错峰到 1:15、2:45、4:00 分散开,并在脚本开头加一个轻量的预检——查询当前活跃连接数或磁盘剩余空间,不达标就 exit 75(EX_TEMPFAIL),交由重试机制处理,而不是硬跑失败。
重试与告警:别让失败静默到第二天
半夜失败最致命的不是失败本身,是没人知道。默认 cron 会把输出邮件投递给本地 mail,而多数服务器根本没配 MTA,输出直接进黑洞。
最低配的方案:在 crontab 顶部声明 MAILTO 之外,把每条任务的 stdout/stderr 重定向到带日期的日志,再用退出码驱动告警:
0 3 * * * /opt/run.sh >> /var/log/job/$(date +\%F).log 2>&1 || /opt/alert.sh "job failed"- 注意
%在 crontab 里必须转义成\%,否则会被当成换行 - 更稳的是用
systemd timer替代 cron:OnFailure=alert@%n.service原生支持失败回调,还有RandomizedDelaySec天然错峰
重试要区分错误类型:网络抖动、临时锁冲突值得重试(指数退避 3 次);数据校验失败、权限不足重试只是浪费时间,应当直接告警。最后,给任务加一个“心跳”——每次成功运行写一行到监控系统或健康检查端点,超过预期周期没更新就报警。这样即使 cron 本身没触发,你也能在第二天上班前发现问题,而不是等业务方打电话来问“昨天的报表怎么没出”。
- 点赞
- 收藏
- 关注作者
评论(0)