全球新年 - 横跨26小时的倒计时浪潮
新年以一波26小时的浪潮降临全球,从基里巴斯的莱恩群岛开始,到贝克岛结束。本文追踪这一浪潮经过各大城市的足迹,介绍仍在庆祝的非公历新年,并探讨新年对运行在UTC上的IT系统意味着什么。
Linux 的 cron 守护进程默认使用系统时区 (/etc/localtime 或 TZ 环境变量) 来解释调度表达式。写下“0 2 * * *”意味着系统本地时间的凌晨 2:00。这在单台服务器上直观易懂,但在多台服务器运行不同时区或云实例默认为 UTC 的环境中,任务会在非预期的时间运行。
AWS EC2 实例默认为 UTC。要在日本时间凌晨 2:00 运行批处理任务,必须写“0 17 * * *”(UTC 17:00 = JST 次日凌晨 2:00)。手动转换容易出错,而夏令时地区还会增加一层复杂性,进一步混淆计算。
当 cron 以本地时间运行在有夏令时的地区时,每年会出现两次问题。春季切换时 (如凌晨 2:00 跳到 3:00),计划在 2:00-2:59 执行的任务会被完全跳过。秋季切换时 (如凌晨 2:00 回拨到 1:00),计划在 1:00-1:59 执行的任务会运行两次。
数据库备份运行两次可能不会造成太大实际危害,但计费或汇总批处理运行两次则是严重问题。反过来,一个关键的每日任务被跳过可能直到第二天才被发现。根本的解决方案是让 cron 以 UTC 运行,从根本上规避这个问题。
将系统时区设为 UTC 并以 UTC 编写 cron 调度表达式是最安全的运维模式。UTC 没有夏令时,因此原则上既不会重复执行也不会跳过。要在“日本时间凌晨 2:00”运行某任务,只需写一次“0 17 * * *”即可; 该任务全年在相同的 UTC 时刻运行。
UTC 运行也有注意事项。一个原本应在“每月 1 日午夜 (JST)”运行的任务,在 UTC 中变成了“上月最后一天的 15:00”。由于上月最后一天在 28 到 31 之间变化,没有固定的 cron 表达式能完美表达这一需求。这类任务需要在固定的 UTC 时间启动,并在内部逻辑中验证 JST 月份是否已经切换。
Kubernetes CronJob 自 v1.27 (GA) 起支持 timeZone 字段。将 spec.timeZone 设为 IANA 名称 (如 Asia/Tokyo) 会使调度按该时区解释,消除手动 UTC 转换并提高可读性。工程师可以按照自己的思维方式编写调度表达式。
即使使用了 timeZone,夏令时行为仍需关注。Kubernetes 控制器能感知夏令时切换,但其对跳过或重复时间的处理方式可能因版本而异。对于关键任务,以 UTC 定义调度并将 timeZone 保留给可读性最重要的场景,是更安全的折中方案。
AWS EventBridge Scheduler、Google Cloud Scheduler 和 Azure Logic Apps 都原生支持时区指定。AWS EventBridge 在 ScheduleExpression 中接受时区并自动处理夏令时切换。然而,各服务对“春季前拨时不存在的时间”的处理方式各不相同,因此请查阅所使用服务的具体文档。
无论使用哪种调度器,都要在注释中记录每个调度的意图。仅凭“0 17 * * *”本身,未来的维护者无法判断这是 UTC 17:00 还是用 UTC 表示的 JST 2:00。即使是基础设施即代码也应该像记录应用程序业务逻辑一样,以显式注释记录预期的时区。
这篇文章对您有帮助吗?
新年以一波26小时的浪潮降临全球,从基里巴斯的莱恩群岛开始,到贝克岛结束。本文追踪这一浪潮经过各大城市的足迹,介绍仍在庆祝的非公历新年,并探讨新年对运行在UTC上的IT系统意味着什么。
了解时区如何将世界划分为不同本地时间的区域、UTC 偏移量的工作原理,以及为什么某些时区使用半小时增量。
软件中时间戳处理的首要法则是“以 UTC 存储,以本地时间显示”。本文介绍这一原则的理由、ISO 8601 格式、JavaScript 和 Python 中的实践模式、最常见的 bug,以及能在上线前捕获夏令时和日期线问题的测试策略。