关于 task-scheduling
用于规划任务何时运行及由哪个 Worker 执行的架构技能。涵盖定时与延迟触发、租约、优先级、公平调度、重试和任务级幂等,适合构建可靠的后台处理系统。
主要功能
- 01Cron、延迟与周期性触发
- 02基于拉取的 Worker 租约
- 03优先级与公平调度
- 04租约、心跳与去重指引
- 05积压与故障监控
适用场景
- 01安排提醒或周期性任务
- 02将队列任务分配给多个 Worker
- 03防止 Worker 崩溃后重复产生副作用
- 04管理任务优先级与租户公平性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ proyecto26/ system-design-skills --skill task-scheduling安装后在 Claude Code 中输入/task-scheduling确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/deep-dive.mdreferences/providers/aws.mdazure.mdgcp.mdgeneric.mdtemporal.md详细介绍
任务调度
决定工作何时运行,以及由哪个工作进程运行:按计划触发作业
(cron/延迟/周期性),通过租约将每个作业分配给且仅分配给一个工作进程,并
确保即使发生崩溃和重试,作业也只完成一次。这构建在
messaging-streaming 队列之上——队列负责传输;此技能增加了调度、租约、优先级和任务级幂等性。设计不当会导致作业永远不运行、运行两次(重复扣款、重复发邮件),或不断堆积,直到工作进程集群永久落后。
适用场景
工作必须稍后运行(24 小时后发送提醒)、按计划运行(每晚汇总、每小时执行 cron),或重复运行(每 5 分钟轮询);某个耗时操作已经移出请求处理路径(→ messaging-streaming),现在需要可靠地分配给一组工作进程;作业需要优先级(付费用户先于免费用户)或公平性(不能让某个租户饿死其他租户);或者即使持有作业的工作进程在执行过程中崩溃,作业也必须恰好完成一次。
不适用场景
调用方需要内联返回结果——这是同步调用,而不是定时作业。某个一次性的即发即忘异步步骤不需要调度、优先级或恰好一次保证——使用普通队列和幂等消费者(messaging-streaming)更简单;不要再叠加调度器。单台机器上的一个周期性作业——使用操作系统的 cron 即可,直到你需要多个调度器,或需要保留历史记录和重试。需要回滚的长时间运行、多步骤 Saga——应使用持久化工作流引擎,而不是手动在多个作业间维护状态。不要因为“以后可能会有批处理作业”(YAGNI)就部署 Airflow/Celery:这是需要运维和监控的有状态控制平面。
先明确需求
- 触发类型——定时(cron/指定时间)、延迟(N 秒后运行)、周期性(每 N 次/每隔 N),还是事件驱动(队列收到消息)?这决定了是否需要调度器。
- 恰好一次还是至少一次——重复运行是否有害(资金、邮件),还是无关紧要(幂等重算)?这决定了租约和去重设计。
- 延迟预算与吞吐量——延迟作业是否必须在预定时间后的几秒内触发,还是“几分钟内”也可以?严格的时间要求成本更高。
- 优先级/公平性——某些作业是否可以插队?是否必须防止某个租户或作业类别饿死其他任务?(→
back-of-the-envelope,用于估算到达率与处理速率。) - 作业时长与波动——几秒还是几小时?这决定可见性超时/租约时长,以及长时间运行的作业是否需要心跳。
- 幂等键——重试时,如何识别两个任务是同一个任务?(键的契约由
api-design负责。)
选项
调度触发方式
- 操作系统 cron/单一调度器 — 一个进程根据 crontab 触发作业。适用于 单节点、少量作业且不要求高可用的场景。
- 分布式调度器(高可用 cron) — 通过选举产生的 Leader 调度器将到期作业加入队列;Follower 处于待命状态。适用于 调度必须能够承受节点故障且不能重复触发的场景。
- 延迟队列/定时器 — 作业带有“不得早于”时间;队列会将其保留至到期(通过投递延迟、有序集合评分或时间轮实现)。适用于 每个作业的延迟各不相同,且不想使用 cron 定时触发的场景。
- 工作流/编排 DAG — 声明任务依赖关系,并支持回填和历史记录(Airflow 风格)。适用于 批处理流水线存在依赖关系,且需要运行历史记录和重新运行的场景。
工作节点分配
- 拉取(工作节点租约) — 工作节点轮询队列,为作业申请一个可见性超时的租约,并在成功后确认/删除作业。适用于 希望免费获得背压机制,并使用弹性、自平衡工作节点的场景。这是默认方式。
- 推送(调度器分配) — 协调器将作业分配给特定工作节点。适用于 亲和性/局部性很重要(作业必须在数据所在位置运行)的场景。
优先级与公平性
- 优先级队列 — 将高优先级和低优先级作业放入不同队列,并按顺序处理。适用于 某些类别必须优先运行的场景。
- 加权/公平调度 — 在各租户队列之间轮询或按权重抽取。适用于 某个租户的突发任务不能让其他租户挨饿的场景。
权衡
| 选项 | 解决的问题 | 带来的问题 | 何时更换 |
|---|---|---|---|
| 操作系统 cron/单一调度器 | 简单;无需基础设施 | 单点故障(SPOF)——节点宕机后调度停止;没有重试/历史记录 | 需要高可用或恢复错过的运行 → 分布式调度器 |
| 分布式调度器(高可用 cron) | 可承受节点故障;不会重复触发(通过选举产生 Leader) | 需要 Leader 选举(→ consistency-coordination);组件更多 | 一台机器和一个作业就够用 → 操作系统 cron |
| 延迟队列/定时器 | 无需 cron 定时触发即可设置各作业的延迟;时间较为精确 | 远期作业会一直留在队列中;定时精度受轮询间隔限制 | 延迟时间统一/周期性 → cron;存在依赖关系 → DAG |
| 工作流 DAG(Airflow 风格) | 依赖关系、回填、运行历史记录、重新运行 | 控制平面较重;调度延迟;对于单个作业来说过于复杂 | 作业彼此独立且只运行一次 → 普通队列 + 调度器 |
| 拉取(工作节点租约) | 自平衡、弹性、自然实现背压 | 至少一次:慢作业的租约到期后会重新运行(需要幂等性) | 作业必须在特定节点上运行(数据局部性) → 推送 |
| 推送(调度器分配) | 亲和性/局部性;集中控制 | 调度器会成为瓶颈/单点故障;必须跟踪工作节点的健康状况 | 不需要局部性 → 拉取更简单 |
| 优先级队列 | 重要工作优先运行 | 持续高负载下低优先级任务可能挨饿 | 需要保障租户之间的公平性 → 加权/公平调度 |
| 加权/公平调度 | 不会有租户让其他租户挨饿 | 更复杂;需要按租户进行记账 | 只有一种工作负载类别 → 单一队列 |
高压下的行为
调度器可能会悄悄落后,也可能通过重新分派难以处理积压任务的集群所无法完成的工作,放大故障。
- 积压增长:到达速率超过工作进程的处理能力;到期任务排队,原本“计划在 09:00 执行”的任务拖到 09:40 才运行。端到端延迟不断上升,而 CPU 使用率看起来正常。*缓解措施:*监控最早到期任务的等待时间和队列深度,而不只是速率;扩容工作进程;丢弃或延后低优先级任务。
- 租约过期/重新运行风暴(典型问题):任务运行时间超过可见性超时,租约过期,队列将任务重新投递给第二个工作进程,于是两个工作进程同时运行同一任务,浪费容量;如果没有幂等性,还会重复产生副作用。*缓解措施:*将超时设为高于任务时长的 p99,长任务通过心跳续租,并确保任务具备幂等性/去重能力。
- **毒性任务:**某个任务总是失败,导致无限重试,耗尽工作进程资源,并加重故障下游服务的负担。*缓解措施:*设置重试上限,并采用退避和抖动;之后将任务路由到死信队列(重试/退避/DLQ 由
resilience-failure负责)。 - **调度时刻的惊群效应:**数千个 cron 任务都安排在整点触发,同时启动并冲击下游服务。*缓解措施:*为计划添加抖动、分散触发时间,或对任务分派进行速率限制。
- **调度器脑裂:**两个调度器都认为自己是领导者,因而为每个重复任务重复入队。*缓解措施:*通过领导者选举和 fencing 确保只有一个领导者(→
consistency-coordination);使用(job, scheduled_time)作为键,实现幂等入队。 - **饥饿:**大量高优先级任务,或某个吵闹租户的任务洪流,让其他所有任务都得不到处理。*缓解措施:*采用公平/加权调度,并限制每个租户的并发数。
**监控:**最早到期任务的等待时间(最能反映延迟的信号)、各优先级的队列深度、租约过期/重新投递速率、重试和 DLQ 速率、工作进程利用率,以及各租户所占份额。
如何应用
- **明确输入条件。**确定触发类型、恰好一次还是至少一次、延迟预算、优先级/公平性以及任务时长(参见先明确这些事项)。如果工作只是一个没有计划或优先级的异步步骤,就到此为止——普通队列 + 幂等消费者(
messaging-streaming)已经足够。 - **根据权衡表做出选择。**选择触发方式(cron → 分布式调度器 → 延迟队列 → DAG,选满足需求的最简单方案)、分配模型(默认采用拉取式租约;仅在需要本地性时采用推送式),并且仅在存在多个类别时选择优先级/公平性模型。
- **设置关键参数。**将可见性超时设为高于任务时长的 p99;在进入 DLQ 前设置重试上限和退避策略;为每个任务设置去重/幂等键;为长任务设置租约心跳间隔;设置每个租户的并发限制。
- **对方案进行压力测试。**逐一检查积压增长、租约过期后的重新运行、毒性任务、整点惊群、调度器脑裂和饥饿。确认工作负载可能触发的每种情况都有相应的缓解措施。
- **用数字确定规模。**所需工作进程数 = 到达速率 × 平均任务秒数 / 目标并发数(Little 定律);确认持续吞吐量足以清空峰值到达量,并确认存储空间足以容纳遥远未来才执行的延迟任务(→ 重要指标)。
- **选择提供商。**默认采用通用方案;只有用户指定了云服务时才打开相应的提供商文件(参见选择提供商)。
- 在回答中展示权衡——提供一张
Decision | Solves | Worsens | Change it when…表格,为此处做出的每项选择各列一行(例如,所选方案、其存储方式、其故障策略);最后一列须给出具体触发条件(某个数值或可观察信号)。然后说明最先会出什么问题。
应做与不应做
应做
- 默认采用拉取式工作线程租约,并设置可见性超时;这样可以自动平衡负载。
- 将可见性超时设为高于作业时长的 p99,并通过心跳为长时间运行的作业延长租约。
- 让每个任务都具备幂等性(按任务键去重),确保重新运行不会造成影响。
- 为调度器选出唯一的主节点,并使用 (job, time) 为周期性入队操作设置键。
- 设置带退避和抖动的重试上限,之后移入死信队列;对最早到期作业的等待时长设置告警。
不应做
- 不要假设租用的作业只运行过一次——至少一次投递意味着必须针对重复执行进行设计。
- 不要把每个 cron 作业都安排在 :00——加入抖动,避免定时触发时造成请求风暴。
- 不要在没有主节点选举的情况下运行两个调度器(脑裂会导致重复触发)。
- 不要无限重试有毒任务;应限制重试次数,并将其移入死信队列。
- 在调度、优先级或高可用需求尚未明确前,不要急着采用 Airflow/Celery(YAGNI)。
重要数值
使用 Little 定律来配置工作线程池:在途作业数 = 到达率 × 平均作业时长,因此工作线程数 ≈ 峰值到达率 × 每个作业的平均秒数 / 每个工作线程的并发数。持续处理速率必须高于峰值到达率,否则积压永远无法清空。将可见性超时设为高于作业时长的 p99(超时时间过短是导致重复运行的头号原因);将远期延迟存储容量设为:延迟作业速率 × 最大延迟 × 作业大小。不要重复列出延迟/QPS 表格——从 back-of-the-envelope 获取速率和时长。
接口草图
计划任务是一种契约。定义它,而不是定义“一个作业”:
- **任务信封:**稳定的
task_id/ 幂等键(重试时去重——键契约由api-design负责),task_type/版本、payload、priority、scheduled_for(不早于该时间运行)、attempt和trace_id。 - **调度规范:**对于周期性作业,使用 cron/间隔表达式,并且设置
dedup_key = (job_name, scheduled_time),确保重复入队时不会产生影响。 - **租约契约:**出队时,工作线程会获得任务的租约,在
visibility_timeout内任务不可见;成功后必须确认/删除任务,或通过心跳延长租约。租约到期 → 自动重新投递。达到重试上限后仍失败 → 将任务移入 DLQ,并附上尝试次数和最后一次错误。
选择提供商
默认采用上述通用方案。如果用户指定了云服务,请阅读
references/providers/<provider>.md,了解托管服务映射、
配额/限制和特定于提供商的权衡。如果该提供商没有对应文件,则采用通用方案。
图示
要可视化调度器 → 队列 → 已租用工作线程的流程,或租约到期后的重新投递循环,请使用插件内的 architecture-diagram 技能。快速内联草图:[scheduler] → [delay/priority queue] → workers (lease) ─expire→ requeue ─fail×N→ [DLQ];主路径使用实线,超时/DLQ 分支使用虚线。
相关构建模块
messaging-streaming— 依赖于它:队列是本技能用于调度和租用任务的传输层;它负责投递保证、顺序和 DLQ,本技能不会重复实现这些功能。resilience-failure— 与它配合使用以制定重试策略:退避、抖动和作为隔离机制的 DLQ 都在那里进行调优;本技能会提及这些策略。api-design— 依赖于其幂等键契约,这是确保租约到期后重新运行安全的机制。consistency-coordination— 依赖于它来实现主节点选举(和 fencing),确保只有一个调度器处于活动状态,周期性作业不会重复触发。back-of-the-envelope— 为容量估算提供依据:到达率和作业时长决定工作线程数量和积压清空速度。system-design— 为编排器的推理循环提供依据;当工作必须稍后运行、按计划运行或可靠地分配给工作线程时,它会将请求路由至此。
参考资料
references/deep-dive.md—— 租约/可见性超时机制与心跳、分布式 cron + 领导者选举、延迟队列实现(有序集合、时间轮)、优先级/公平性算法、通过去重实现恰好一次效果,以及 Celery/Sidekiq/Airflow 的内部机制。设计调度器的细节时请阅读。references/providers/{generic,aws,azure,gcp,temporal}.md—— 各环境的服务映射、会影响决策的限制,以及注意事项。