午夜不是闲时,是所有批处理撞车的时刻
午夜不是闲时,是所有批处理撞车的时刻
很多人把定时任务默认排在 0 0 * * *,理由都是“凌晨没人用”。但真实情况是:数据库全量备份、日志切割(logrotate)、数仓 ETL、云厂商账单结算、监控日聚合报表,全都盯着这个时刻。你的任务并不孤单,只是要和其他任务在最忙的时段抢 CPU 和 IO。
我们线上一个导出任务从 00:00 改到 03:20 后,平均耗时从 47 分钟降到 9 分钟,失败率从 6% 降到 0。改动只是把它和备份窗口错开,代码一行没动。
时区:你的 0 点不一定是机器的 0 点
容器基础镜像默认 TZ=UTC,宿主机是 Asia/Shanghai,cron 写在镜像里按 UTC 解释。结果你以为的“每天零点跑一次”变成“每天早上 8 点跑一次”。反过来更常见:业务按北京时间 0 点切日,任务在 UTC 0 点(北京时间 8 点)执行,读到的是已经切过日的表。
- 显式设置容器环境变量
TZ=Asia/Shanghai,并在启动日志里打印一次date的结果; - 在 cron 表达式旁写注释,注明它按哪个时区解释;
- 业务逻辑里用
ZoneId.of("Asia/Shanghai")显式取时区,不要直接调用LocalDate.now()。
跨日边界是幂等性的检验点
午夜任务天然要处理“昨天”和“今天”。如果任务按天补数据,中途失败后重跑,而统计逻辑里用了 now() 而不是参数化日期,重跑就会把两天数据混在一起,出现重复计数。这类 bug 平时不显形,只在跨零点前后几分钟被触发。
实操:把“业务日期”作为唯一入参传进任务,写成 run --date=2024-06-01,任务内部禁止读取当前时间;落库用唯一索引 (biz_date, metric) 兜底,重跑时覆盖而不是追加。这样即使半夜重试三次,结果也不会翻倍。
任务不是失败了,是被杀掉了
多数“半夜失败”其实是进程被杀:K8s 节点凌晨做镜像 GC 或滚动升级,Pod 收到 SIGTERM,默认 30 秒宽限期一到就 SIGKILL;任务已经跑了 3 小时,进度全丢,日志里只剩一行 exit code 137。翻应用日志看不到异常,查事件才看到 Evicted。
- 把长任务拆成可续跑的批次,每处理 1000 条提交一次游标;
- 给这类 Pod 配
terminationGracePeriodSeconds: 300,并实现 SIGTERM 优雅退出; - 用
startingDeadlineSeconds限制补跑,避免关机一夜后堆积出几十个并发实例。
让失败在第二天早上被看见
定时任务最大的问题不是失败,是失败后没人知道:它静默少跑一天数据,两周后报表对不上才被发现。三个低成本动作:
- 任务结束上报一条心跳(Prometheus
pushgateway或一张job_heartbeat表),超过预期时间没心跳就告警; - 加结果行数校验,
count == 0直接判失败,空跑比报错更危险; - 重试要有上限和退避(3 次,间隔 1/5/15 分钟),无限重试会把下游打挂。
做完这三件事,你的任务仍然可能失败,但至少不会再“总在半夜失败,而且没人知道”。
- 点赞
- 收藏
- 关注作者
评论(0)