题目重点:多篇 Phone/Onsite 面经都提到 Job Scheduler 设计;本文围绕可检验的调度契约,推导任务状态、租约、幂等与多租户公平。
题意与假设
这道练习题保留了面经中值得训练的核心约束,并将需要澄清的条件单独列出。现场拿到变体时,先和面试官确认这些条件。
- 训练版支持一次性和 cron job、每个 job 的可重试 execution、取消和查询状态;执行可能持续秒到小时,worker 可以崩溃。
- 设 100 万个 active job、每分钟 10 万次触发、p99 触发延迟低于 5 秒;这些数字只是容量练习。
- 平台承诺至少一次派发,业务副作用依赖 idempotency key 或外部事务去重;不声称跨数据库和外部系统的真正 exactly-once。
解题路线
- 持久化 JobDefinition(schedule,payload,tenant,version) 与 Execution(runId,scheduledAt,state,attempt,leaseUntil);runId 可由 jobId+scheduledAt+definitionVersion 稳定产生,避免 leader 重扫制造重复实例。
- 时间分片索引扫描即将到期的 schedule;调度器以条件写把 due job 变成 execution,再投递队列。控制面 leader 换届后从 durable schedule/执行表重放,不依赖本地 timer。
- Worker 原子 claim execution 并拿有限租约,周期性续租;成功写 completed,超时由 recovery scanner 重新放回队列。状态转换按版本 compare-and-swap,过期 worker 的 late completion 被拒绝。
- 重试使用指数退避、最大次数和 dead-letter;任务执行入口传稳定 runId 作为幂等键。取消同样是条件状态转换,正在运行的 worker 还需合作式取消。
- 多租户队列按配额、优先级和权重做公平调度,区分交互任务与重批量任务;热点 cron 分钟边界加 jitter,但不得悄悄改变严格定时 SLA。
- 观测不要只看队列长度:分别量测 schedule lag、claim lag、执行时长、租约过期、重试放大、租户饥饿和死信率。压测时注入 worker kill、调度器 failover 和 DB 部分超时。
手动走一遍
job J 每小时触发,10:00 的 runId 是 J:10:00:v3。调度器重启后再次扫描 10:00,条件插入不会重复建 execution。Worker A claim 后运行,租约过期前崩溃;扫描器让 Worker B 重试同一个 runId。若 A 的旧请求稍后完成,版本与租约 token 阻止它覆盖 B 的结果;外部写也要按 runId 幂等。
规模与取舍
以每分钟 10 万次触发估算平均约 1,667 次/秒,分钟边界可能远高于平均;到期扫描取 B 个触发约 O(log J+B)(J 为索引规模),队列与 execution 存储随保留期线性增长。网络与数据库写延迟决定 p99,必须按突发峰值和恢复重扫验证。
易错点与追问
容易失分的边界
- 只用内存定时器,调度器重启就丢 job。
- 把 queue 的 at-least-once 投递说成业务副作用 exactly-once。
- worker 超时重试时没有 fencing token,旧 worker 晚到结果覆盖新结果。
- cron 触发时间只由当前机器本地时间计算,DST/时区导致重复或遗漏。
- 忽略多租户公平,单个批量客户占满所有 worker。
继续深挖
- 任务有 DAG 依赖时,如何只在上游完成后使下游可运行?
- 跨地区容灾时如何避免双活调度同一 runId?
- 极长任务的租约续期与失联判定怎样设计?
- 客户要求严格一次外部扣款时,调度器能承诺什么,不能承诺什么?
