定时任务总在半夜失败:时区、锁与重试的三个坑

举报
yd_257405204 发表于 2026/10/28 09:09:46 2026/10/28
【摘要】 先把“半夜”定义清楚 排查夜间任务失败时,第一个容易踩的坑不是代码,而是时间口径。应用日志打的是本地时间,宿主机 cron 按 UTC 调度,数据库里又存着不带时区的时间戳。三方一对,就会得出“任务在凌晨 3 点无故消失

先把“半夜”定义清楚

排查夜间任务失败时,第一个容易踩的坑不是代码,而是时间口径。应用日志打的是本地时间,宿主机 cron 按 UTC 调度,数据库里又存着不带时区的时间戳。三方一对,就会得出“任务在凌晨 3 点无故消失”的结论。更稳妥的做法是:日志统一输出带偏移的 ISO8601 时间,例如 2026-10-28T02:00:00+08:00;调度表达式旁边写明时区;数据库连接串显式设置 timezone,不要依赖服务器默认值。

我们曾经遇到一个每日对账任务,在测试环境一直正常,上线后每天少跑一次。最后发现容器镜像是 UTC,而 date 命令的输出被当成了业务日期。把“业务日期”和“机器时间”分开之后,问题才真正收敛。

坑一:cron 的时区与夏令时

0 2 * * * 看起来是每天凌晨两点,但它依赖守护进程所在时区。容器化之后,基础镜像通常没有安装 tzdata,TZ=Asia/Shanghai 也可能被忽略。可以在启动脚本里加一句 date +%Z%z 作为自检,输出不是 CST+0800 就直接失败退出,避免任务在错误的时间静默执行。

另一个细节是夏令时。即使业务只在国内,海外节点也可能受 DST 影响,出现某天 2 点不存在、任务被跳过的情况。对账、结算这类不能漏跑的任务,建议不要卡在 2 点到 3 点之间,或者改成“每小时检查一次水位,缺了就补”的幂等模型。

坑二:锁的粒度比锁本身更重要

很多团队给定时任务加了一把全局锁,结果一个慢任务把整条流水线堵死。锁应该按“资源”而不是按“进程”来设计:同一张表、同一个租户、同一个文件目录各一把锁,键里带上业务日期。这样昨天的补偿任务和今天的正常任务可以并行,又不会重复写同一批数据。

锁还要有超时和持有者信息。只写一个 locked=1 的标记,进程被杀之后没人知道该不该清理。更实用的做法是把 pid、开始时间、任务名写进锁文件,等待方超时后打印持有者再退出,而不是无限重试。我们现在的约定是:拿锁最多等 10 分钟,拿到后先写心跳,超过 30 分钟没有心跳视为僵尸锁,由下一次调度接管。

坑三:重试没有上限等于放大故障

夜间失败往往伴随下游抖动。如果每个任务都重试三次,而任务本身又会调用下游三次,故障期间的压力会被放大九倍。重试必须满足三个条件:错误可重试、次数有上限、间隔有退避。像参数错误、鉴权失败这类确定性错误,重试只会浪费窗口,应该直接失败并告警。

还要区分“提交类”和“查询类”。查询可以放心重试;提交、支付、发布这类有副作用的操作,必须带幂等键,否则重试就是重复提交。一个简单的判断方法是:这次调用失败后,先回读一次状态,确认对方到底有没有成功,再决定要不要重发。

一份可以照抄的检查清单

  • 调度表达式旁标注时区,启动时打印 date +%Z%z;
  • 日志、数据库、接口三处时间口径一致,禁止裸时间戳;
  • 锁按资源拆分,记录持有者与心跳,等待有上限;
  • 重试只覆盖可重试错误,指数退避,提交类先回读再决定;
  • 每次执行落一份带字节数的产物,失败时能直接定位到阶段。

夜间任务的问题很少是“某一个 bug”,更多是时间、并发和失败处理三件事没有说清楚。把这三件事写成可检查的约束,比事后盯着日志猜要省力得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。