---
title: "Worker 挂了，同步没有"
description: "在批次中途杀掉 worker，跟踪一位联系人：保存的是哪个游标，重放的写入为什么不会落地两次，重启期间由谁掌管同步。然后是两起事故，它们说明恢复只是工作的一半。"
url: https://tajo.io/zh-cn/blog/worker-died-sync-didnt/
markdown: https://tajo.io/zh-cn/blog/worker-died-sync-didnt.md
lang: zh-CN
author: "Set Noa"
author_url: https://tajo.io/zh-cn/set-noa/
published: 2026-11-05
updated: 2026-11-05
alternates: {en-US: /blog/worker-died-sync-didnt.md, bg-BG: /bg/blog/worker-died-sync-didnt.md, cs-CZ: /cz/blog/worker-died-sync-didnt.md, de-DE: /de/blog/worker-died-sync-didnt.md, da-DK: /dk/blog/worker-died-sync-didnt.md, es-ES: /es/blog/worker-died-sync-didnt.md, fr-FR: /fr/blog/worker-died-sync-didnt.md, el-GR: /gr/blog/worker-died-sync-didnt.md, hi-IN: /hi/blog/worker-died-sync-didnt.md, id-ID: /in/blog/worker-died-sync-didnt.md, hr-HR: /hr/blog/worker-died-sync-didnt.md, hu-HU: /hu/blog/worker-died-sync-didnt.md, it-IT: /it/blog/worker-died-sync-didnt.md, ja-JP: /jp/blog/worker-died-sync-didnt.md, ko-KR: /kr/blog/worker-died-sync-didnt.md, ar-SA: /ar/blog/worker-died-sync-didnt.md, nl-NL: /nl/blog/worker-died-sync-didnt.md, no-NO: /no/blog/worker-died-sync-didnt.md, pl-PL: /pl/blog/worker-died-sync-didnt.md, pt-BR: /pt/blog/worker-died-sync-didnt.md, ro-RO: /ro/blog/worker-died-sync-didnt.md, ru-RU: /ru/blog/worker-died-sync-didnt.md, sv-SE: /se/blog/worker-died-sync-didnt.md, sl-SI: /sl/blog/worker-died-sync-didnt.md, sr-Latn-RS: /sr/blog/worker-died-sync-didnt.md, th-TH: /th/blog/worker-died-sync-didnt.md, tr-TR: /tr/blog/worker-died-sync-didnt.md, ur-PK: /ur/blog/worker-died-sync-didnt.md, vi-VN: /vi/blog/worker-died-sync-didnt.md, sk-SK: /sk/blog/worker-died-sync-didnt.md}
---

[数据同步](https://tajo.io/zh-cn/blog/?category=data-sync)

# Worker 挂了，同步没有

在批次中途杀掉 worker，跟踪一位联系人：保存的是哪个游标，重放的写入为什么不会落地两次，重启期间由谁掌管同步。然后是两起事故，它们说明恢复只是工作的一半。

数据同步故障恢复

Worker 在一个批次中途挂掉时，正在运行的同步会怎样？

游标只在一个干净批次结束时移动，所以在批次中途被杀掉的 worker 会把游标留在该批次的起点，下一次运行读取同一页。每条记录的投递键由页游标及其位置推导而来，一个保存已完成写入、保留七天的账本防止重放落地两次。每个同步只有一个租约（lease），两次运行不会同时工作。这是容易的那一半。有一个同步在有东西把它暂停之前，一次运行接着一次运行地失败；有一个工作区只有错误、没有一次成功，在有东西察觉之前一直静默。所以现在有一个熔断器，在连续五次失败后暂停同步，还有一个每小时运行的告警，在二十四小时没有成功时触发。

[了解更多](https://tajo.io/zh-cn/blog/worker-died-sync-didnt/#%E8%B7%9F%E8%B8%AA%E4%B8%80%E4%BD%8D%E8%81%94%E7%B3%BB%E4%BA%BA)

在 [AI Engineer 于 2026年10月3日发布的一场演讲](https://www.youtube.com/watch?v=bRGyYaE0lxI&t=701s)中，一位 Crusoe 的开发者布道师在两台 A100 节点上启动了一个 PyTorch 训练任务。随后，演讲者告诉演示自带的监控系统，有一块 GPU 报出了 XID 79 错误。该节点被标记为下线，任务被取消。进程收到 SIGTERM，并有“[最多两分钟来处理这个 SIGTERM，用来保存检查点或刷出日志](https://www.youtube.com/watch?v=bRGyYaE0lxI&t=633s)”。一台备用节点接替上来，“[slurm 任务会重启。它会加载检查点，从中断的地方继续训练](https://www.youtube.com/watch?v=bRGyYaE0lxI&t=769s)”。从头到尾略少于十五分钟。没有人被叫起来处理。

同步比训练任务小得多。问题却是一样的：运行需要具备什么，才能让杀掉 worker 只损失几分钟，而且不产生重复？

下面是 [Tajo 自己的同步运行器](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/)给出的答案，读自代码，而不是宣传册。[反向 ETL](https://tajo.io/zh-cn/blog/what-is-reverse-etl/) 描述了这项工作的形态。本文讲的是这项工作挂掉时会发生什么。

## 跟踪一位联系人

运行从一个已保存的游标开始，一次读取数据源的一页。把这一页的游标记作 `c3`，我们的联系人是这一页上的第 212 条记录。在写入任何东西之前，关于这位联系人有三件事成立。

第一，它有一个投递键。这个键是同步 id、数据源、页游标、页内位置和记录本身的哈希。游标和记录只进入哈希计算，从不嵌入键中，所以键里[不含任何邮箱地址](https://tajo.io/zh-cn/resources/help/getting-started/understanding-data-sync/)。

第二，写入要经过[账本](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#ledger)（ledger），而不是直接发往目标系统。调用目标系统之前，这个键会先被认领为待处理。写入成功后，这一行被标记为已完成，并保留七天。

第三，游标还没有移动。它只在批次结束时移动，只在该批次的每条记录都已落地时移动，并且只在运行证明自己仍然掌管该同步之后移动。

现在，在写入和检查点之间杀掉 worker。

收到 SIGTERM 时，每个队列 worker 都在一个共享的十五秒期限内排空任务，无论还有什么在运行，进程都会在二十五秒后退出；systemd 在强制杀掉之前给它四十五秒。没能在这段时间内完成的运行仍被标记为进行中，下一个 worker 会通过队列的卡住任务恢复（stalled-job recovery）机制接手。如果是强制杀掉（hard kill），就完全没有排空。无论哪种情况，同样的两个事实都成立：目标系统里已经有了我们的联系人；我们的[游标](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#cursor)仍然是 `c3`。

## 重放

下一次运行从 `c3` 开始，读取同一页，为第 212 条记录推导出同一个键，然后询问账本。账本回答：已完成。目标系统不会被调用；已存储的结果被重放，运行继续往下走。在批量路径上，Brevo 的导入端点一次调用接收整页数据，同样的检查会在构建请求之前逐个键执行。如果一页中的每个键都已完成，这一页什么都不会发送。

这就是全部的窍门，值得说清楚它为什么有效。键是从批次开始时的页游标推导出来的，而不是从碰巧正在执行的那次运行推导出来的。一次崩溃、六十秒后的一次重试和明天的一次定时运行，都会为同一条记录[推导出同一个键](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#ledger)。账本并不在乎是其中哪一个在询问。

再说代价。[账本七天后就会遗忘](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#ledger)。代码自己的注释说，七天足以覆盖一次延迟的重试和一次短暂的中断，它声称的也仅此而已。从比这更旧的游标重放，会再次写入。而且被强制中断的运行可能要等待。已死运行的租约（lease）最长可保持十五分钟，在定时同步中，租约已空闲的运行要[在开始超过一小时后才会被清扫并重新调度](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#stuck-runs)。Crusoe 的演示[在十五分钟内恢复](https://www.youtube.com/watch?v=bRGyYaE0lxI&t=753s)。我们的并不总是如此。

## 重启期间由谁掌管同步

同一时间只有一次运行掌管一个同步。运行会在 Redis 中用自己的令牌获取一个[租约](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#lease)，有效期十五分钟，每五分钟续期一次。每次写检查点之前，它都会确认自己仍然持有这个租约。如果不再持有，它就停下来。这次运行会被记录为[一句用户将会读到的直白的话](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#failures)：“The run lost its slot part-way through and stopped to avoid writing twice.”（运行中途失去了位置，为避免重复写入已停止。）与一个仍然存活的持有者发生冲突的第二次运行不会重试。它以 “Another run of this sync was already in progress”（此同步的另一次运行已在进行中）[结束](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#failures)，下一次定时运行照常开始。

修复由谁负责也写得清清楚楚。每个[失败代码](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#failures)都对应一句说明发生了什么的话、一句说明下一步该做什么的话，以及一个负责人：你或者 Tajo。因为映射错误而导致目标系统拒绝所有记录，归你负责。[租约丢失，归我们负责](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#failures)。运行页面会显示下一步；[同步恢复指南](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#failures)列出了是哪一方。

到这里是容易的那一半。一次崩溃的代价是一次重放，没有任何东西落地两次。另一半是两起事故教会我们的。

## 一次又一次的失败

有一个同步一次又一次地失败。不是在紧密的循环中，而是每个调度周期一次，一次运行接着一次运行，每次都是同一条消息：自定义 id 不能包含冒号。这次运行从未到达目标系统。它在入队过程中就挂了，任何运行器都还没看到它，所以挂在运行器上的告警根本没有可挂靠的东西。

没有任何东西在盯着这串连续失败。这就是全部的发现。

修复方案是一个熔断器。每记录一次失败，它就读取该同步自上次修改以来最近五次已结束的运行。如果五次全是错误，它就暂停同步，写一行审计记录，原因为 `circuit_breaker_tripped`，并发出一条严重告警。连续失败是从已存储的运行推导出来的，而不是来自 worker 中的计数器。通过任何路径记录的失败都会被计入，包括引发这起事故的入队失败。代码注释说，它本可以在第五次运行时制止那起事故。一次成功会重置连续失败计数。取消暂停会让熔断器重新布防，而不是根据旧记录再次跳闸。[Shopify 连接器指南](https://tajo.io/zh-cn/docs/ecommerce-platforms/shopify/)用一句话说了同样的事：连续五次运行失败后，同步会自行暂停，其页面会显示最后一个错误。

熔断器不做的事情是修复任何东西。Crusoe 不经询问就更换节点并重启任务。Tajo 会停下来询问：一个已暂停的同步会一直保持暂停，直到有人修复原因并[重新发布它](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#pause)。这个谨慎的默认值是有意为之：自动重试一个写入客户记录的同步，本身就是一次写入。

## 无人察觉的静默

有一个合理的反对意见，大意如下：“一个连续失败五次就暂停的熔断器，本来也能抓住第二起事故。为什么还要再建一个监视器？”因为第二起事故看起来不像失败。在很长一段时间里，一个工作区有一个活动的同步，错误记录越积越多，却从来没有一次成功，而且没有任何告警。现有的每个信号都挂在活动上：一次运行失败了，一个 Webhook 到达了，一项预算被超出了。零次成功不是一个事件。它是事件的缺席，而熔断器只有在记录到失败时才会运行。一个已经死掉的调度器、一个卡死的队列、一个从未启动的 worker：这些都不会记录任何东西，熔断器也永远不会醒来。

机车上的死人开关正好反过来工作。司机必须一直按住它，一旦静默，列车就会停下。同步版本[每小时运行一次](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#hourly-check)，在每小时的第十三分钟。对于每个带有活动同步的工作区，它只问一个问题：过去二十四小时内是否有任何一次运行成功？如果一个工作区中最早的活动同步启用不到一天，就不去打扰它；它还没有一天的时间来成功。更早启用且处于静默的工作区会收到一条严重告警。告警会说明是哪一种静默：有运行记录但没有一次成功，或者根本没有运行记录。每起事故只告警一次。Redis 中的一个标记会让它保持关闭，直到第一次成功运行，然后清除，这样下一起事故还会再次告警。

两个监视器是在事后通过同一个提交加入的。熔断器的注释说，它本可以在第五次运行时制止第一起事故。死人开关的注释说，一个很久以前就已启用、却一次运行都没有的同步必须告警。需要它们的时候，两者都不存在。我们是事后才写出它们的，并把两起事故都记录在文件头部，好让没有人忘记原因。

## 已经构建的，和尚未构建的

截至撰写本文时，已经构建：

- 每个干净批次结束时保存游标，崩溃时再保存一次。
- 每条记录的投递键由页游标及其位置推导而来，并在每次调用目标系统之前与一个[七天的账本](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#ledger)核对。
- 每个同步一个[租约](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#lease)，有效期十五分钟。
- 每次运行最多[尝试五次](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#retries)，从六十秒开始退避。
- 一个[熔断器](https://tajo.io/zh-cn/resources/help/getting-started/understanding-data-sync/)，在连续五次失败后暂停同步并记录原因。
- 一个[每小时运行的告警](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#hourly-check)，在二十四小时没有成功时触发。

尚未构建，在依赖它之前值得了解：

- 死人开关监视的是工作区，而不是同步。一个健康的同步会掩盖同一工作区中另一个静默的同步。
- 它的告警[发送给 Tajo 的运维人员](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#hourly-check)：总会写一行日志，在已配置的情况下还会发送到 Sentry、Webhook 或邮件。它不会出现在同步的页面上。熔断器的暂停会出现。
- 没有任何东西会重启一个已暂停的同步。由人来重启。
- 强制杀掉之后的恢复并不快。租约可能保持十五分钟，[清扫要等一小时](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#stuck-runs)。
- 两个阈值，五次失败和二十四小时，都是[部署级设置](https://tajo.io/zh-cn/resources/help/troubleshooting/recover-a-stuck-or-failed-sync/#limits)。你不能为单个同步分别设置。

[由近乎完美的步骤组成的长链仍会失败](https://tajo.io/zh-cn/blog/almost-perfect-steps-unreliable-chain/)；出路是定义最终状态并检查它。本文是这件事在运维层面的另一半：定义一次挂掉的运行会留下什么，并检查静默留下的那片空白。

Crusoe 的结束语是“[失败不可避免](https://www.youtube.com/watch?v=bRGyYaE0lxI&t=955s)”。我们的结论再往前一步。崩溃是容易的那一半，因为它会留下记录。静默是我们必须主动去寻找的那一半。

## 常见问题

**Worker 在一个批次中途挂掉时，正在运行的同步会怎样？**

游标仍然指向被中断批次的起点，因为 Tajo 只在一个批次干净落地之后才移动游标。下一次运行会重新读取同一页。每条记录都带有一个由页游标及其在页内位置推导出的投递键，账本（ledger）会把每一次已完成的写入保留七天，因此已经写入的记录会从账本中重放，而不会第二次发送到目标系统。

**为什么重放的同步不会把同一位联系人写入两次？**

每次调用目标系统之前，运行都会拿记录的投递键去核对账本。这个键是同步、数据源、页游标、页内位置和记录本身的哈希，因此一次崩溃、一分钟后的重试和第二天的定时运行，都会为同一条记录推导出同一个键。在七天窗口内已标记为完成的键不会再次发送。

**同步的死人开关告警是什么？**

一种在“没有发生”时触发、而不是在失败时触发的检查。Tajo 每小时查看一次所有带有活动同步的工作区，如果过去二十四小时内没有任何一次运行成功就发出告警，包括根本没有记录到任何运行的情况。每起事故只告警一次，在第一次成功运行后重新布防。它之所以存在，是因为失败告警只能看到被记录下来的失败。

## 相关文章

### [无人值守时也能运行的同步：六条规则](https://tajo.io/zh-cn/blog/six-rules-unattended-sync/)

2026年11月6日 · 7 分钟阅读

7 个章节

### [方框之间才是真正的工作流](https://tajo.io/zh-cn/blog/the-workflow-between-the-boxes/)

2026年10月8日 · 3 分钟阅读

商店记录 → 人工修正 → 邮件系统

### [Brevo 集成：选择能够验证的连接方式](https://tajo.io/zh-cn/blog/brevo-connector-guide/)

2026年8月19日 · 4 分钟阅读

6 个章节
