一、引言
定时任务是 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 Cron | Cloudflare 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 Actions | CI/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 定时任务的关键是把「调度」和「执行」解耦:
- Cron 只扣扳机:到点发起调度,重活交给队列/流。
- 幂等是底线:周期键 + 幂等键,扛住重发与延迟。
- 错峰调度:别让所有任务同一分钟挤冷启动。
- 管它是拉是推:周期数据用定时,实时事件用事件驱动。
- 心跳监控:静默失败的 Cron 最可怕,写心跳 + 对账。
把 Cron 当「可靠的定时触发器」来设计,配合 Webhook 集成 的事件消费与 可观测性 的心跳监控,定时任务就能从「偶尔坏没人知道」变成「每次都跑、跑没跑都看得见」。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。