Serverless 定时任务实战:Vercel Cron、Cloudflare Cron Triggers 与调度编排

系统拆解 Serverless 平台的定时任务:Vercel Cron 与 Cloudflare Cron Triggers 的配置、调度窗口与精度、幂等与防重入、定时任务触发的数据管道、与 GitHub Actions / 传统 cron 的对比,给出「定时任务 + 事件驱动」的可靠调度模式。

一、引言

定时任务是 Serverless 应用里最容易「随便写」又最容易「偷偷坏掉」的一环:日报忘了发、缓存忘了清、数据管道忘了跑。平台几乎都提供了托管 Cron(Vercel Cron、Cloudflare Cron Triggers),但它们的精度、窗口、超时各有约束,直接照搬传统服务器的 crontab 会踩坑。

本文拆解 Serverless 定时任务的五个核心问题:平台 Cron 怎么配、调度精度与约束、幂等与防重入、定时任务怎么驱动数据管道、和 GitHub Actions 及传统 cron 怎么分工。


二、平台 Cron 配置

2.1 Vercel Cron

// vercel.json
{
  "crons": [
    { "path": "/api/cron/report", "schedule": "0 8 * * *" }
  ]
}
// app/api/cron/report/route.ts
export const dynamic = 'force-dynamic'
export const maxDuration = 60   // Cron 任务超时上限
export async function GET() {
  await generateDailyReport()
  return Response.json({ ok: true })
}
项Vercel Cron 约束
计划数Hobby 免费 2 个,Pro 更多
最小粒度每分钟
超时受函数 maxDuration 限制
触发方式HTTP 调你的 API 路由

2.2 Cloudflare Cron Triggers

# wrangler.toml
[triggers]
crons = ["*/5 * * * *", "0 0 * * *"]
export default {
  async scheduled(event, env, ctx) {
    switch (event.cron) {
      case '*/5 * * * *': await refreshCache(env); break
      case '0 0 * * *':   await dailyAggregation(env); break
    }
  }
}
项Cloudflare Cron 约束
计划数免费 3 个,更多需付费
最小粒度每分钟
超时每 5 分钟窗口内多次调用累积上限
触发方式scheduled 事件直接执行(无 HTTP 开销)

2.3 平台对比

维度Vercel CronCloudflare Cron传统 crontab
托管是是否
精度分钟级分钟级秒级
超时函数上限5min 窗口累积无
触发HTTP事件进程
适用Next.js 全栈边缘 Worker服务器

心法:Serverless Cron 是「发起调度」,真正干重活的逻辑往往还是要另起一个异步任务。Cron 负责到点扣扳机,重活在队列/流里跑。


三、调度精度与约束

3.1 分钟级精度够吗

大多数业务(日报、缓存刷新、数据聚合)分钟级足够。但要注意:

「每小时的第 5 分钟」 vs 「精确 60 分钟」
平台 Cron 有调度偏差(队列拥塞、冷启动)→ 不要假设「正好 8:00:00」

应对:任务内先取当前时间,再判断该做什么,而不是假设「这次调用就是 8 点那次」。

export async function scheduled() {
  const now = new Date()
  // 每次调用都跑,由数据/时间判断该不该做
  const dayKey = now.toISOString().slice(0, 10)
  const last = await kv.get('last-report-day')
  if (last === dayKey) return   // 今天已生成过 → 幂等跳过
  await generateReport()
  await kv.put('last-report-day', dayKey)
}

3.2 冷启动叠加

定时任务往往「到点集中跑」,冷启动会叠加。平台会尽量调度到已有实例,但:

避免所有 Cron 同一分钟触发 → 错峰
  例:缓存刷新 0 5 * * *,日报 0 8 * * *,对账 0 13 * * *

铁律:Cron 任务必须幂等(见四)。平台可能重发、可能延迟,把「到点必跑且只跑一次」交给幂等键保证,而不是交给调度器。


四、幂等与防重入

4.1 为什么要防重入

定时任务跑的是「有副作用的操作」(发邮件、写库、调第三方)。若上一次还没跑完、下一次又开始,或平台重发,就可能重复执行。

4.2 防重入三招

1. 锁:以「任务名+周期键」做锁(Redis/KV setnx)
2. 幂等键:业务记录带唯一键,重复插入失败
3. 状态检查:先查「是否已做」,再做
// KV 锁:setnx 语义,防止同任务并发重入
const lockKey = `cron-lock:${task}:${periodKey}`
const acquired = await kv.get(lockKey)
if (acquired) return   // 已在跑,跳过本次
await kv.put(lockKey, '1', { expirationTtl: 600 })
try {
  await runTask()      // 真正执行
} finally {
  await kv.delete(lockKey)  // 释放
}

4.3 周期键的设计

日任务 → periodKey = 日期(2026-09-28)
时任务 → periodKey = 小时(2026-09-28T14)
周任务 → periodKey = ISO 周(2026-W39)
月任务 → periodKey = 年月(2026-09)
好处:同一周期内重跑自动幂等,跨周期自然允许新跑

心法:「周期键 + 幂等键」是 Cron 可靠性的核心。有了它们,平台重发、延迟、冷启动叠加都无伤大雅。


五、定时任务驱动的数据管道

5.1 定时管道 vs 事件驱动

定时管道(拉模式):到点跑 → 拉数据 → 处理 → 写结果
事件驱动(推模式):事件到 → 触发处理 → 写结果
适用:
  定时 → 周期性聚合、报表、缓存预热(无事件源)
  事件 → 实时响应、增量处理(有事件源)

5.2 定时管道模板

// 每天凌晨聚合昨日数据
export async function scheduled(env) {
  const yesterday = new Date(Date.now() - 86400_000).toISOString().slice(0, 10)
  const key = `agg:${yesterday}`
  if (await kv.get(key)) return     // 幂等
  const rows = await db.query(
    `SELECT product_id, sum(amount) FROM orders
     WHERE created_at >= $1 AND created_at < $2 GROUP BY 1`,
    [yesterday + 'T00:00', yesterday + 'T24:00']
  )
  await db.bulkUpsert('daily_stats', rows, { onConflict: 'product_id, day' })
  await kv.put(key, '1')
}

5.3 长任务拆解

单次 Cron 跑不完的聚合 → 拆阶段:
  阶段 1(Cron):扫描主键区间 → 生成「待处理分片」进队列
  阶段 2(队列):消费分片并行处理
  阶段 3(Cron 或事件):汇总结果
好处:Cron 只负责「切分 + 编排」,重活交给可并行的队列

心法:Cron 的 60s/5min 限制不是让你把任务做完,而是让你把任务「切好交给别人做」。


六、与 GitHub Actions / 传统 cron 的分工

6.1 三者定位

工具定位典型场景
Serverless Cron应用内定时业务报表、缓存、聚合
GitHub ActionsCI/CD + 仓库级定时构建、发布、依赖更新
传统 cron服务器运维日志轮转、系统任务

6.2 何时用哪个

业务数据 → Serverless Cron(与应用共享密钥/DB)
构建发布 → GitHub Actions(源码仓库绑定)
系统运维 → 传统 cron(服务器上跑)
跨项目对账 → Serverless Cron + 可调用多个外部 API

边界:不要用 GitHub Actions 跑业务数据任务(它在仓库环境、密钥/DB 访问绕远路);也不要为业务任务自建服务器 cron(Serverless Cron 够用且免运维)。


七、监控与排查

Cron 任务的「没跑」最难发现(静默失败)
必备监控:
  - 每次执行写心跳(KV key = cron-heartbeat)
  - 结果对账:预期该跑 vs 实际是否跑了
  - 失败告警:任务失败推送到可观测平台
  - 执行时长埋点(超时预警)
排查三板斧:
  1. 看执行日志(平台提供 Cron 调用记录)
  2. 验幂等键状态(是「没跑」还是「跑了但跳过」)
  3. 手动触发一次(平台都支持手动跑 Cron)

八、总结

Serverless 定时任务的关键是把「调度」和「执行」解耦:

  1. Cron 只扣扳机:到点发起调度,重活交给队列/流。
  2. 幂等是底线:周期键 + 幂等键,扛住重发与延迟。
  3. 错峰调度:别让所有任务同一分钟挤冷启动。
  4. 管它是拉是推:周期数据用定时,实时事件用事件驱动。
  5. 心跳监控:静默失败的 Cron 最可怕,写心跳 + 对账。

把 Cron 当「可靠的定时触发器」来设计,配合 Webhook 集成 的事件消费与 可观测性 的心跳监控,定时任务就能从「偶尔坏没人知道」变成「每次都跑、跑没跑都看得见」。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「tools」更多文章

  1. 域名与 DNS 接入实战:NS/解析记录、SSL 签发、CDN 接管与多级域名策略
  2. 源站与缓存策略:回源优化、缓存穿透防护、Origin Shield 与动态内容缓存
  3. API 网关与 BFF 层:边缘聚合、统一鉴权与接口编排实战