Cron 时区陷阱与 DST 漂移

· 约 3 分钟 Cron

Cron 表达式本身没错,跑错点的锅常常在时区。尤其是”跨国部署”或”容器默认 UTC”的场景,同一条 Cron 能在不同实例上撞到完全不同的时刻。

三种最常见的时区翻车

1. 开发机和生产机时区不同

开发机是 Asia/Shanghai,部署容器默认 UTC。0 9 * * * 本意是北京 9 点,实际在北京下午 5 点(UTC 9 点)才跑。

排查:

cat /etc/timezone        # Debian / Ubuntu
timedatectl              # systemd
date                     # 终极确认
ls -l /etc/localtime     # 链到哪个 zoneinfo

修复两条路:

  • Dockerfile 显式设:ENV TZ=Asia/Shanghai + RUN ln -sf /usr/share/zoneinfo/$TZ /etc/localtime
  • 或把 Cron 时刻按 UTC 算回去:北京 9 点 = UTC 1 点 → 0 1 * * *

推荐前者,减少心算错误和新人踩坑。

2. 多机房 / 多实例时区不一致

集群 10 台机器,有的设了 Asia/Shanghai 有的忘了。同一条 Cron 在不同机器错开 8 小时——每台都按”本地 9 点”跑,实际看起来”每天有好几个时刻都在跑”。

修复:

  • 基础设施层统一时区(Ansible、镜像模板里固化)
  • 更稳的做法是把调度搬到 app 层(Quartz / XXL-Job / Elastic-Job / Airflow),集中读一个时区配置,不依赖机器本地

3. K8s CronJob 1.27 之前没有 timeZone

1.27 之前 K8s CronJob 没有 timeZone 字段,走的是 kube-controller-manager 所在节点的时区(多数镜像默认 UTC)。“凌晨 1 点结算”一不小心变成”上午 9 点结算”。

1.27+ 加了原生支持:

spec:
  timeZone: "Asia/Shanghai"
  schedule: "0 1 * * *"

但要确认 controller 的 CronJobTimeZone feature gate 打开(1.27 默认开)。1.25 以下版本只能让 Pod 内部时区对齐(镜像设 TZ)或按 UTC 换算。

DST 夏令时漂移

部署在 America/New_YorkEurope/LondonAustralia/Sydney 等地区才遇得到,但遇上就很疼。DST 春天把时钟拨快 1 小时(通常凌晨 2:00 → 3:00),秋天拨慢 1 小时(2:00 → 1:00)。

对 Cron 的影响:

表达式春季(跳过 2-3 点)秋季(2-3 点重复)
0 2 * * *(固定 2 点)不执行或顺延到 3:00执行两次
0 * * * *(每小时整点)缺一次多一次
*/5 * * * *(每 5 分钟)缺 12 次多 12 次

具体行为看实现——Vixie cron(Linux 默认)文档里明确定义了跳过/重复规则,Quartz、Spring、K8s 各自略有差异。

中国大陆自 1992 年起取消 DSTAsia/Shanghai 全年不会发生。但上了 AWS us-east / eu-west 或选了 EST / CET / PST 时区的机器就逃不掉。

关键任务避开 02:00–03:00 这个窗口,放 04:00 之后最安全——全球所有 DST 切换都在这之前完成。

Cron 写不出的几种时间

需求LinuxQuartz替代方案
每月最后一天L0 0 L * ?脚本判断 [ $(date -d tomorrow +%d) = 01 ] 再执行
每月第 N 个周 X#0 0 ? * 2#1=每月第一个周一)app 层判断 weekday == MON && day <= 7
工作日(跳过节假日)查节假日表 / 借助 chinese-calendar 类库
秒级6 字段systemd timer + OnUnitActiveSec=10s,或 app 内 setInterval
每台机器错峰(避免雷同打)脚本开头 sleep $((RANDOM % 60))

Linux Cron 本身很”纯粹”,复杂调度交给 Quartz、应用层调度或外部调度系统是正确方向。

部署检查清单

  1. 明确时区:Dockerfile / K8s 里 TZ,或 Cron 用 UTC 换算,二选一讲清楚
  2. 避开 DST 窗口:美欧部署不要把任务放 02:00–03:00,改 04:00 之后
  3. 看未来执行时间:拿 Cron 解析器预览 5 次,带时区一起看
  4. 关键任务加看门狗:任务应在某时刻前上报心跳,否则告警——防止调度器默默不跑
  5. 幂等化任务:即使 DST 重复执行两次,业务层靠幂等 key / 分布式锁去重

一句话

Cron 表达式描述”什么时刻”,但”在哪个时区”和”在哪台机器上”才决定它真正跑在何时。写完 Cron 对着解析器填上时区再看一眼——一分钟省未来一整个排查上午。

❓ 常见问题

Linux crontab 用的是哪个时区?

默认用系统时区(`/etc/localtime`)。容器镜像没显式设置时,多数是 UTC,而不是你开发机的 `Asia/Shanghai`。这就是为什么同一条 `0 9 * * *` 本地是早 9 点、部署到服务器变成下午 5 点。修复方法是 Dockerfile 里 `ENV TZ=Asia/Shanghai` 或把 Cron 时刻按 UTC 算回去。

Spring / Quartz / K8s 能指定时区吗?

能。Spring 的 `@Scheduled(cron = "...", zone = "Asia/Shanghai")`、Quartz 的 `CronTrigger.setTimeZone()` 都支持。没设时默认走 JVM 时区(`user.timezone`)。K8s CronJob 从 v1.27 起支持 `spec.timeZone` 字段(1.25 beta,1.27 GA);之前版本必须靠 POD 内部时区。

DST 切换那天 2:30 的任务会跑吗?

看实现和季节。美国春季 DST 把 2:00 直接跳到 3:00,2:30 这个时刻不存在——多数 Cron 实现选择不执行或顺延到 3:00。秋季 DST 2:00 回跳到 1:00,2:30 出现两次——默认可能执行两次。中国大陆 1992 年起取消了 DST 所以国内机器无此问题,但部署在 AWS us-east-1、eu-west-1 就要考虑。

怎么知道一条 Cron 在部署环境下几点会跑?

三步:1) 在 Cron 解析器里填上时区,看未来 5 次执行时刻;2) 登录服务器 `date` 确认系统时区;3) 关键任务在 app 层再加看门狗——"如果某时刻后还没上报就告警",避免调度器默默失效。

打开 Cron 解析 · 可视化生成 · 示例库

📖 同一工具的其他教程