跳到主要内容

定时任务、分片与故障恢复

定时调度只能决定任务何时应该被创建,不能保证某个时间点恰好执行一次。调度器重启、网络分区和执行器崩溃都可能造成重复、漏调和中途失败,因此任务本身必须可识别、可重试和可恢复。

1. 调度时间不等于执行时间

调度器在计划时间生成一次任务实例,执行器取得实例后才真正开始工作。队列积压、资源不足或故障转移会让实际开始时间晚于计划时间。

Kubernetes CronJob 等调度系统也明确把调度视为近似过程:某次计划可能生成两个 Job,也可能因控制器不可用或超过截止时间而没有生成。因此,任务不能把“调度器只触发一次”作为正确性前提。

2. 每次计划需要稳定实例标识

任务实例可以由任务名称和计划时间共同标识,例如:

daily-settlement:2026-08-30T00:00:00+08:00

执行开始前在数据库中用该标识创建唯一记录。重复触发时读取已有状态,而不是再次创建一份独立业务结果。

实例记录至少包含计划时间、实际开始时间、状态、尝试次数、当前所有者、租约、进度和错误。它既用于幂等,也用于恢复与审计。

3. 并发策略要说明重叠任务如何处理

当前一次任务尚未结束,下一计划时间已经到达时,可以选择:

  • Allow:允许多个实例并行,任务必须能处理重叠数据。
  • Forbid:跳过或延后新实例,避免并发执行。
  • Replace:停止旧实例并启动新实例,需要安全取消与恢复协议。

Kubernetes CronJob 的 concurrencyPolicy 只约束同一个 CronJob 创建的 Job,不会协调其他调度器或手工启动的同类任务。关键互斥仍应落实在共享状态或业务约束上。

4. 分片把大任务拆成独立工作单元

按租户、日期、主键区间或稳定哈希把数据划分成 shard,每个 shard 保存独立状态。执行器可以并行领取不同 shard,失败时只重做未完成部分。

一个好的分片键需要:

  • 覆盖全部数据且不重叠。
  • 可以稳定重建和核对。
  • 各分片工作量尽量均衡。
  • 单个分片能在合理时间内完成或继续细分。
  • 不破坏必须串行的业务键顺序。

仅按记录数平均切分,可能因为单条处理成本差异产生严重倾斜。应根据实际耗时调整分片或支持动态拆分。

5. 领取任务需要租约和所有者校验

执行器领取 shard 时,在共享存储中原子写入 owner 和 lease deadline。租约到期后其他执行器可以接管,避免原执行器永久失联。

旧执行器可能在暂停后恢复,因此提交进度时要校验 owner 或递增 fencing version。发现租约失效就停止写入,防止新旧执行器同时覆盖进度。

续租间隔应显著短于租约,并设置最大任务时长。网络异常时不能无限续租或继续假设所有权有效。

6. 检查点缩小失败重做范围

长任务应周期性保存稳定检查点,例如最后处理的主键、页游标或已完成分片。更新业务结果和推进检查点最好位于同一个本地事务,或者让每条业务写入本身可幂等。

外部 API、文件和消息副作用无法与检查点原子提交时,需要稳定请求 ID和结果核对。进程可能在外部调用成功后、检查点保存前崩溃,恢复时会再次调用。

检查点只保存“已经可靠完成”的位置,不能在异步工作仍在进行时提前推进。

7. 重试、补跑和追赶要有明确规则

调度器停机两小时后,恢复时要决定是补跑每个错过的实例、只执行最新一次,还是合并为一个时间区间。这个选择取决于任务语义:账单结算可能每期都要执行,状态快照可能只需要最新结果。

使用 startingDeadlineSeconds 等配置可以限制错过多久的计划仍允许启动,但业务仍应监控漏调,并提供带实例标识的手工补跑入口。

重试应区分瞬时故障与永久数据错误,设置退避、上限和死信或人工处理状态。恢复后还要限速,避免补跑流量与在线业务争抢资源。

8. 监控任务的时效和完整性

需要记录:

  • 计划实例是否按时创建。
  • schedule delay、queue delay 和执行耗时。
  • 成功、失败、重试、跳过和取消数量。
  • 每个 shard 的进度、吞吐和最老未完成时间。
  • 租约接管、重复触发和幂等命中次数。
  • 业务完成量与源数据量的核对结果。

“最近一次执行成功”无法说明中间没有漏掉某个计划实例。监控应以预期实例集合和业务结果为基准。

9. 常见问题

9.1 使用分布式锁能否保证任务只执行一次

不能。锁可能过期,执行器也可能在完成业务后、释放或记录结果前崩溃。稳定实例 ID、唯一约束、幂等写入和检查点才是恢复重复执行的基础,锁只用于减少并发。

9.2 Cron 表达式使用哪个时区

应显式配置并记录时区,避免依赖容器或主机默认值。还要定义夏令时切换时重复或不存在的本地时间如何处理;涉及账期时,实例 ID 应包含清晰的时区或直接使用 UTC 时间点。

10. 面试题

10.1 如何设计可分片、可重试且不会重复产生业务结果的定时任务

出现公司:字节跳动、Zoom

考察重点

  • 调度触发与业务执行的边界。
  • 稳定实例 ID、租约、分片与检查点。
  • 重复、漏调、补跑和幂等副作用。

相关内容:第 1 节“调度时间不等于执行时间”至第 8 节“监控任务的时效和完整性”。

参考回答

调度器只负责按计划创建任务,不能保证恰好触发一次。应使用“任务名 + 计划时间”作为稳定实例 ID,在共享数据库中以唯一键记录状态;重复触发读取同一实例。大任务按稳定键分片,每个 shard 用 owner、租约和版本领取,并保存只包含已完成工作的检查点。

业务写入要幂等,或者和检查点放在同一本地事务;外部副作用使用稳定请求 ID 并可核对。失败按错误类型有限重试,租约到期可由其他执行器接管,调度恢复后根据任务语义补跑或合并错过的实例。监控不仅看最近一次成功,还要检查所有预期实例、最老未完成 shard 和业务结果完整性。