写一个 node 定时任务框架,保证能一直周期性的执行这些任务。这个框架不一定要用 web 框架暴露端口这些,要求写出来的框架简单易扩展,能够支持任务易扩展,所以得考虑好设计模式。任务的这些配置的话需要用快手的那些 kconf 的包,可以让用户从 kconf里面进行可视化配置,而不重启服务。这个框架肯定是要将任务的时间数据进行一个持久化的,好让整个框架服务断开的时候,进行一个断线重连,例如一个任务以一个小时为周期执行一次,中间这个服务断开了 30min 中,30min 过后再进行一次执行,如果这个任务要执行 2 个小时,要等 3 个小时再执行,因此一个周期=空闲周期时间+任务执行时间
需求澄清版 v1
需求定位
该需求不是为某几个固定业务任务写脚本,而是建设一个面向内部业务方复用的 Node 定时任务基础框架。框架需要提供清晰的任务注册协议、调度语义、状态持久化能力和运行历史记录,让业务方可以用较低成本接入周期性任务,并在服务重启后保持任务执行状态可恢复、可追踪。
核心目标
- 支持业务方通过代码注册任务,框架负责任务调度、状态记录、运行历史和异常日志。
- 周期性执行统一使用 Cron 表达式描述
- 任务配置通过 kconf 管理,第一版只支持热更新任务开关和 Cron 表达式,避免运行中修改复杂策略导致状态语义混乱。
- 框架需要持久化任务运行状态和运行历史,便于服务重启恢复、问题排查和执行结果复盘。
- 第一版按单实例部署设计,不考虑多实例竞争、分布式锁和任务分片。
任务注册方式
任务由业务方在代码中显式注册,任务逻辑以 handler 函数形式提供。kconf 不负责动态注册任务,只负责控制已注册任务是否启用以及 Cron 表达式。
示例形态:
scheduler.register({
name: "syncAdvertiserReport",
cron: "0 * * * *",
enabled: true,
overlapPolicy: "skip",
misfirePolicy: "once",
handler: async (ctx) => {
// business logic
},
})调度语义
Cron 表达式是唯一的周期性触发来源。每当 Cron 命中一个计划时间点,框架尝试触发对应任务。
如果任务没有正在执行的实例,则直接执行本次触发。
如果上一次执行尚未结束,则按照任务声明的 overlapPolicy 处理:
skip:跳过当前 Cron 触发点,等待下一次 Cron 时间点。适合缓存刷新、报表刷新、巡检同步等允许少跑一轮的任务。queue:将当前 Cron 触发点放入等待队列,等当前执行结束后继续执行。适合不希望漏触发的扫描、补偿类任务。
第一版不支持 parallel。并行策略会引入并发上限、幂等、锁、下游限流和运行实例追踪等复杂问题,暂不纳入范围。
Queue 策略约束
当任务使用 queue 策略时,需要支持最大队列长度配置,例如 maxQueueSize。如果队列已满,超出的触发点应被丢弃并记录日志/运行事件,避免任务执行速度慢于 Cron 触发速度时无限积压。
第一版需要明确:
- 队列长度按任务维度维护。
- 队列只表达“待补跑的触发点”,不扩展成完整任务队列系统。
- 队列溢出时需要记录可排查信息。
停机恢复语义
服务停机或框架暂停期间可能错过 Cron 触发点。第一版通过 misfirePolicy 控制恢复行为:
none:恢复后不补偿错过的触发点,只等待下一次 Cron 命中。once:恢复后如果发现存在错过的触发点,最多立即补偿执行一次,然后回到正常 Cron 调度。
第一版暂不支持 all,即不按错过次数逐个补偿,避免服务恢复时集中打满下游。
kconf 热更新范围
第一版只允许通过 kconf 热更新以下字段:
enabled:任务是否启用。cron:任务的 Cron 表达式。
暂不支持运行中热更新 overlapPolicy、misfirePolicy、maxQueueSize 或业务参数。原因是这些字段会影响任务状态解释和恢复语义,随意热更新容易导致运行状态不可预期。
持久化与观测
框架需要使用数据库进行状态持久化,不使用本地文件、Redis 或抽象 Storage Adapter 作为第一版主方案。
至少需要两类数据:
- 任务状态表:记录任务当前状态、最近一次开始/结束时间、最近一次成功/失败信息、下次计划执行时间、队列数量等。
- 任务运行历史表:记录每一次任务执行的触发来源、开始时间、结束时间、耗时、执行结果、错误信息等。
同时框架需要输出关键日志:
- 任务注册成功/失败。
- kconf 配置加载与更新。
- Cron 触发。
- 任务开始、成功、失败、跳过、入队、队列溢出。
- 服务启动恢复时的 misfire 判断结果。
非目标
第一版暂不做以下能力:
- 多实例部署与分布式锁。
- 任务分片执行。
- 并行执行同一个任务。
- 完整任务队列系统。
- Web 管理后台。
- 通过配置动态注册任意任务。
- 复杂告警平台集成。
待确认问题
- 数据库类型是否有指定,例如 MySQL、PostgreSQL 或公司内部统一存储。
- Cron 表达式第一版仅需支持到分钟级。
- 任务失败是否需要内置重试机制,还是只记录失败并等待下一次 Cron。
queue队列溢出后是静默丢弃、记录历史,还是需要触发告警钩子。- 任务执行超时是否由框架统一控制。
- kconf 配置异常时是沿用上一版有效配置,还是停用对应任务。
需求澄清版 v2:基于 Agenda 的内部封装方案
方案调整背景
v1 的思路偏向自研一个 Node 定时任务调度内核,需要自己处理 Cron 解析、任务持久化、服务重启恢复、任务锁、运行历史、队列积压和并发控制。进一步调研后发现,开源框架 Agenda 已经封装了大量底层调度能力,包括任务持久化、重复任务、Cron 调度、任务锁、并发控制、任务事件和优雅停机。
因此该需求不再优先定义为“从零自研 Node 定时任务框架”,而是调整为:
基于 Agenda 封装一层适配快手内部使用习惯的定时任务基础框架。底层复用 Agenda 的持久化调度、锁、并发控制和运行事件能力;上层提供统一的任务注册 API,并接入 kconf 实现任务启停和 Cron 表达式热更新。
核心设计理念
Agenda 的设计中,数据库是调度引擎的运行态事实来源。Agenda 会把任务定义、下次执行时间、锁状态、执行状态等信息持久化到数据库中,并基于数据库中的记录驱动任务执行。
但本需求希望业务方通过 kconf 可视化配置任务开关和 Cron 表达式,因此在业务配置层面,kconf 才是期望状态的事实来源。
所以整体设计应区分两类事实来源:
- kconf:业务期望态的 Single Source of Truth,负责描述某个任务是否启用、应该按照什么 Cron 表达式执行。
- Agenda 数据库:调度运行态的 Single Source of Truth,负责记录 Agenda 实际执行所需的 job、nextRunAt、lockedAt、lastRunAt、lastFinishedAt 等运行状态。
两者之间需要通过一个配置协调器进行同步:
kconf 配置变更
-> KconfWatcher / ConfigReconciler 感知变化
-> 校验任务是否已在代码中注册
-> 校验 Cron 表达式是否合法
-> 对比 kconf 期望态与 Agenda 数据库运行态
-> 调用 Agenda public API 同步 job
-> Agenda 基于数据库中的最新 job 状态执行任务主要模块
TaskRegistry
任务注册表,负责收集业务方通过代码注册的任务。
业务方只能在代码中注册任务 handler,不能只靠 kconf 动态创建任意任务。这样可以保证任务逻辑可测试、可审查、可类型约束,也避免配置错误导致执行不存在或未审核的任务。
示例形态:
taskScheduler.register({
name: "syncAdvertiserReport",
handler: async (ctx) => {
// business logic
},
})KconfWatcher
监听 kconf 配置变化,并在服务启动时执行一次全量配置加载。
KconfWatcher 不直接执行任务,只负责拿到最新配置快照并交给 ConfigReconciler。需要支持:
- 服务启动时全量读取配置。
- 运行中监听配置热更新。
- 配置异常时保留上一版有效配置,避免错误配置导致所有任务异常停摆。
ConfigReconciler
配置协调器,是该封装层的核心模块。
它负责把 kconf 中的期望状态同步到底层 Agenda job:
- 如果 kconf 中配置了一个任务,但代码注册表中不存在该任务,应拒绝同步并记录错误。
- 如果 Cron 表达式非法,应拒绝同步该任务,并保留 Agenda 中上一版有效配置。
- 如果 Agenda 中不存在对应 job,应通过 Agenda 创建重复任务。
- 如果 Cron 表达式发生变化,应通过就地更新修改已有 single job 的 repeat 配置并保存,不能先
cancel()再every()重建。 - 如果
enabled为false,应 disable 对应 job,而不是直接删除。 - 如果
enabled为true,应 enable 对应 job;如果 job 不存在,则先创建再启用。
推荐优先使用 Agenda public API,而不是依赖内部实现:
agenda.define(name, handler):注册任务处理函数。agenda.every(cron, name, data, options):创建或更新 single 类型的重复任务。agenda.queryJobs(query):查询已有 job。job.repeatEvery(cron, options)+job.save():就地更新已有 job 的 Cron 配置。agenda.enable(query)/agenda.disable(query):启用或禁用 job。agenda.cancel(query):仅用于任务确认下线的场景,不用于 Cron 热更新。
AgendaEngine
对 Agenda 实例进行统一初始化和生命周期管理:
- 配置 PostgreSQL 数据库连接。
- 注册所有任务 handler。
- 设置默认并发、锁超时、任务处理间隔等参数。
- 监听 Agenda 的 start/success/fail/complete 等事件。
- 在框架包装层中用 Watchdog/看门狗模式处理长任务续锁逻辑,业务 handler 不需要直接调用 Agenda 的
touch()。 - 在服务关闭时执行
stop()或drain(),尽量保证优雅停机。
kconf 配置范围
第一版 kconf 只管理任务开关和 Cron 表达式:
type KconfTaskConfig = {
name: string
enabled: boolean
cron: string
timezone?: string
}暂不允许通过 kconf 修改 handler、并发策略、锁超时、重试策略、任务业务参数等复杂配置。
Cron 表达式第一版仅需支持到分钟级,不需要支持秒级调度,避免高频任务带来额外锁续约、日志和下游压力。
原因:
- handler 必须来自代码注册,避免配置注入任意执行逻辑。
- 并发、锁、重试等策略会影响运行状态解释,第一版不做热更新。
- 业务参数可以由业务任务自己读取对应业务配置,不放进调度框架的通用配置层。
Agenda 同步策略
服务启动时需要先完成以下步骤:
- 初始化 Agenda 实例。
- 注册所有代码中的任务 handler。
- 读取 kconf 全量任务配置。
- 执行一次 full reconcile,将 kconf 期望态同步到 Agenda 数据库。
- 启动 Agenda worker。
- 开始监听 kconf 后续变更。
运行中当 kconf 发生变化时:
- KconfWatcher 获取最新配置快照。
- ConfigReconciler 对配置做合法性校验。
- 对每个任务执行增量 reconcile。
- 通过 Agenda public API 修改对应 job。
- 记录同步成功或失败日志。
采用 reconcile 思路,而不是只依赖 watch 事件。这样即使服务重启、watcher 漏事件、Agenda 数据库残留旧 job,也可以通过下一次全量 reconcile 重新收敛到 kconf 期望态。
调度与并发语义
第一版不在封装层自研 skip / queue / parallel 任务队列语义,而是优先复用 Agenda 的能力:
- 通过
agenda.define(name, { concurrency: 1 }, handler)或等价配置,默认保证同一个任务不并行执行。 - 通过 Agenda 的 lock 机制避免多 worker 抢占同一个 job。
- 通过
lockLifetime控制任务锁有效期。长任务可能超过锁时间时,由框架包装层启动 Watchdog 定时续锁,避免把 Agenda 的touch()暴露给业务层。 - 任务执行时间超过 Cron 间隔时,第一版接受 Agenda 默认行为,不额外实现自定义 overlap 策略。
如果后续确实需要更强的 skip / queue 语义,再在 Agenda 之上补充封装,不在第一版实现完整队列系统。
停机恢复语义
Agenda 会持久化 job 的运行状态和下次执行时间。服务重启后,Agenda 会基于数据库中的 job 状态继续调度。
第一版不自行实现 misfirePolicy: none | once | all,而是直接接受 Agenda 的默认恢复机制。服务停机期间错过 Cron 点、任务执行时间超过 Cron 间隔、disable 后再 enable 的 nextRunAt 计算,都交给 Agenda 的默认策略处理。
持久化与观测
Agenda 自身会将 job 状态持久化到数据库。封装层需要在此基础上补充业务可观测能力:
- 监听 Agenda 任务生命周期事件,如 start、success、fail、complete。
- 第一版优先使用 Agenda 自带日志能力记录任务名、触发时间、开始时间、结束时间、耗时、结果、错误信息。
- 封装层只补充必要的 kconf 同步日志和异常日志,不重复建设运行历史表。
第一版优先复用 Agenda 的持久化与日志能力,避免重复维护一套任务状态表或运行历史表。
已确认约束
- 数据库后端使用 PostgreSQL,公司内部可以稳定接入,且 Agenda PostgreSQL backend 的接入成本可接受。
- Cron 表达式第一版仅需支持到分钟级。
- 任务执行时间超过 Cron 间隔时,接受 Agenda 默认行为。
- 服务停机期间错过 Cron 点后的恢复行为,接受 Agenda 默认行为。
- 多次调用
agenda.every()同名任务时,Agenda 能稳定避免重复 job。 - Cron 表达式更新时采用就地更新:查询已有 single job,调用
job.repeatEvery(newCron, options)后job.save()。 disable()后重新enable()不需要框架手动修正nextRunAt,使用 Agenda 默认策略。- 长任务超过
lockLifetime时需要续锁,续锁逻辑使用 Watchdog/看门狗模式封装在框架层,不能要求业务 handler 直接处理。 - 任务执行日志优先使用 Agenda 自带日志能力,不额外建设独立运行历史表。
非目标
第一版不做以下能力:
- 从零自研 Cron 调度器。
- 自研任务锁和分布式协调。
- 自研完整任务队列。
- 通过 kconf 动态创建未在代码中注册的任务。
- Web 管理后台。
- 复杂告警平台集成。
- 任意任务参数热更新。
仍需调研与验证的问题
- Agenda PostgreSQL backend 在当前内部 Node 运行环境中的具体初始化方式和连接配置。
- 分钟级 Cron 表达式在 Agenda 当前版本中的具体格式,以及与 kconf 配置校验规则如何保持一致。
- Watchdog 自动续锁的时间间隔、停止时机和异常处理策略。
- Cron 就地更新时如何处理配置抖动、连续更新和
job.save()失败回滚。