我有个批处理任务,20 个页面要抓,跑到第 13 个的时候进程被杀了——可能是断网,可能是容器重启,也可能就是我手贱关了终端。

重跑一遍,它从第 1 个开始。前 12 个白干。

这种事发生第二次之后,我给它加了一条 ledger(账本):干完一个活儿就往文件里写一行,下次启动先读账本,干过的直接跳过。三十行代码,从此再没重跑过。

核心就是这两件事

function finishedSet() {
  try {
    return new Set(fs.readFileSync(LEDGER, 'utf8').trim().split('\n').filter(Boolean).map(l => JSON.parse(l).id));
  } catch (e) { return new Set(); }
}
function append(rec) { fs.appendFileSync(LEDGER, JSON.stringify(rec) + '\n'); }

const done = finishedSet();
for (const t of tasks) {
  if (done.has(t.id)) { skipped++; continue; }   // 干过了,跳过
  // ...真的去干活...
  append({ id: t.id, url: t.url, bytes: ..., at: new Date().toISOString() });
}

没有依赖,没有状态机,就是一个集合加一行追加写。

真跑:先崩一次,再恢复

我在脚本里留了个 CRASH_AT 环境变量,用来模拟"处理到第 N 个时进程被杀"。第一次跑:

=== 第 1 次运行 | ledger 里已有 0 条完成记录 ===
💥 模拟崩溃:正要处理 job-13,进程被杀(本轮已完成 12 个)

什么都不改,直接再跑一次:

=== 第 2 次运行 | ledger 里已有 12 条完成记录 ===
✅ 本轮完成 8 个 | 跳过 12 个(ledger 里有记录)
ledger 现有 20 条 → 任务总数 20,剩余 0

第二次只干了没干完的那 8 个。任务总数、顺序、结果都对得上。这就是我想要的全部效果。

最容易翻车的地方:任务 id 必须稳定

我第一次写的时候,图省事用数组下标当 id:

tasks.forEach((t, i) => { /* 用 i 当 id */ });

这是错的。下标依赖顺序,哪天我在列表前面插一条新任务,所有 id 全错位,ledger 里记录的"job-3 已完成"指的就是另一个东西了。

id 必须来自任务本身:URL、文件绝对路径、数据库主键、内容哈希。我用 job-01 这种形式是故意的——它是从任务的稳定属性生成的,跟它在列表里排第几无关。

另外三条我给自己定的规矩

一、先落账,再干下一个。 顺序必须是"干完 → 写 ledger → 干下一个"。反过来就会丢一条:进程在"干完"和"写 ledger"之间被杀,这个活儿会被重做一次。重做一次不致命,但如果是"发邮件""扣款"这种,就致命了。

二、失败的不要写进 ledger。 只有真正成功的才记账。否则那条任务会被永久跳过,而且错得悄无声息。我现在除了成功账本,还单独记一个 failed.jsonl,方便事后挑出来重试。

三、ledger 要能重建。 我把 ledger 当缓存,不当数据源。真正确认"这个活儿干没干完",应该查产物(文件在不在、数据库里有没有记录)。ledger 文件误删了,就从产物重建;两者对不上时,以产物为准。

我的判断

断点续跑和重试(retry)是两件事,很多人混着说。重试解决的是单次操作的瞬时故障,断点续跑解决的是整批任务的进度丢失。前者一个 try/catch 加三次循环就够,后者必须把进度写到进程外面——因为进程说没就没了。

我还想说一句:这个思路跟 Agent 没什么关系,跟"跑批量任务"有关系。抓页面、迁数据、批量改文件名、跑一夜的回测,全都是同一个套路。只要一个任务的耗时超过你能忍受的重跑成本,就该上 ledger。

判断标准我给自己定的是:超过 5 分钟、或者失败一次就要人肉介入的批处理,一律写账本。 三十行代码换一个晚上的安心,太划算了。

脚本在 daily/job-ledger.js:

CRASH_AT=12 RUN=1 node job-ledger.js   # 跑到第 12 个时"崩溃"
RUN=2 node job-ledger.js               # 接着跑完剩下的

(写作与实测日期:2026-09-30,Node 22.22.2,本地真实运行输出。)

Last modification:September 30, 2026
如果觉得我的文章对你有用,请随意赞赏