3. 断点续传机制

断点续传,说白了就是让同步任务具备「记性」。

系统崩了、网络断了、磁盘满了——这些破事在生产环境天天发生。如果没有断点续传,每次故障恢复都得从头全量同步。TB 级的数据,重来一次?你想想看,谁能受得了。

我早年在一个金融客户现场就吃过这个亏。当时他们做跨机房数据库同步,没做断点,一次网络抖动导致 3 小时的同步回退,业务对账全乱套。嗯,从那以后,我对断点续传的敬畏心就刻进骨子里了。

3.1 断点的本质:你到底在「续」什么?

很多人以为断点就是一个「位置」。其实不对。

断点要回答三个问题:

  • 从哪续——数据源的消费位置(比如 Kafka 的 offset、MySQL binlog 的 GTID)
  • 续什么——当前同步任务的上下文(比如正在处理的表、批次号)
  • 续完怎么确认——下游已经成功应用了哪些数据

我个人习惯把断点拆成两层:

层级 内容 示例
物理断点 数据源的精确位置 binlog 文件名 + 偏移量,Kafka 分区 + offset
逻辑断点 任务处理状态 已提交事务 ID、去重窗口、批次序号

只有物理断点,恢复后可能重复消费;只有逻辑断点,你都不知道从哪开始拉数据。两者缺一不可。

核心原则:断点必须同时包含「位置」和「状态」,且两者要能原子化写入。

3.2 断点记录:写在哪里?怎么保证不丢?

断点记录的位置,我见过三种方案:

  1. 本地文件——简单,但节点挂了就丢了。适合单机测试,生产环境慎用。
  2. 数据库表——用事务保证原子性。我建议用独立的小表,别跟业务数据混在一起。
  3. 分布式协调服务——ZooKeeper、etcd。适合集群部署,天然支持选主和一致性。

我在项目中遇到过最坑的事:断点写本地文件,结果容器被调度到另一台机器,断点没了。恢复后重复同步了 20 分钟数据,下游 Kafka 消息直接翻倍。

所以我现在坚持:断点必须跟数据源解耦,存到独立的高可用存储里。

我的习惯:用 etcd 存断点,key 的格式是 /sync/{task_id}/checkpoint,value 是 JSON 序列化的断点对象。每次更新带上版本号,防止并发覆盖。

3.3 偏移量管理:从「记位置」到「管位置」

偏移量管理,说白了就是回答「我现在读到哪了」。

但这里有个坑:偏移量不是单调递增的。比如 MySQL binlog 的 GTID,它是全局唯一的,但不同事务的 GTID 可能乱序。Kafka 的 offset 虽然递增,但分区之间独立,你得记录每个分区的 offset。

我一般这样设计偏移量结构:

{
  "source_type": "mysql_binlog",
  "gtid_set": "3e11fa47-61ca-11ed-9b6a-0242ac120002:1-289",
  "binlog_file": "mysql-bin.000045",
  "binlog_pos": 892347,
  "timestamp": 1698123456,
  "last_committed_txid": "abc123-def456"
}

注意几个关键点:

  • 冗余设计:同时记录 GTID 和 binlog 位置。万一 GTID 解析出问题,还能用位置兜底。
  • 时间戳:方便人工排查,比如「昨天下午 3 点断的,位置大概在哪」。
  • 事务边界:记录最后提交的事务 ID,用于幂等判断。

避坑指南:我曾经遇到过一个 case——binlog 轮转后,旧的 binlog 文件被清理了。恢复时断点指向一个已经不存在的文件,任务直接卡死。后来我加了「断点有效性校验」,每次恢复前先检查目标文件是否存在,不存在就自动回退到上一个可用断点。

3.4 幂等性设计:重复执行不可怕

断点续传必然带来重复。网络超时、节点重启、断点回退——这些场景下,同一条数据可能被同步两次甚至多次。

怎么办?让下游具备幂等性。

幂等性设计有三个层次:

层次 做法 适用场景
天然幂等 操作本身可重复(如 SET 操作、覆盖写) Redis、KV 存储
去重表 用唯一键 + 事务去重 关系型数据库
版本号 每条数据带版本号,下游按版本更新 分布式系统、消息队列

我个人最推荐「版本号」方案。为什么?因为它不依赖下游的存储能力,只要数据本身带一个单调递增的版本字段就行。

举个例子:

-- 下游执行更新时,带上版本号判断
UPDATE target_table
SET value = 'new_value', version = 5
WHERE id = 'row_001' AND version < 5;

这样重复执行多少次都没问题——只有版本号更大的更新才会生效。

关键认知:幂等性不是「不重复」,而是「重复了也没事」。设计时别想着避免重复,那不可能。你要做的是让重复变得无害。

3.5 断点续传的完整流程

说了这么多,咱们把整个流程串起来看看。

断点续传完整流程 启动同步任务 读取断点记录 校验断点有效性 从断点位置拉取数据 幂等写入下游 原子更新断点 循环 关键:断点更新必须在数据成功写入下游之后,且两者要在一个事务内或通过分布式事务保证原子性

这个流程里,最容易被忽略的是「校验断点有效性」这一步。很多人直接从断点位置拉数据,结果源端数据已经变了,拉出来的东西对不上。

我建议加一个「快照一致性校验」:恢复时先对比源端当前状态和断点记录的时间戳,如果差距超过阈值,就触发全量校验。

3.6 故障恢复场景实战

最后,咱们看几个真实场景,你就知道断点续传到底怎么用了。

故障场景 断点恢复行为 注意事项
网络闪断(< 30秒) 重试连接,从原断点续传 设置重试次数上限,防止死循环
进程崩溃重启 读取最新断点,校验后续传 检查断点是否在有效窗口内
数据源主从切换 根据 GTID 或 LSN 重新定位 确保新主库的断点位置可解析
下游存储故障 回退到上一个确认断点 幂等写入保证数据不重复

一个小技巧:我习惯在断点里加一个「心跳时间戳」。每次同步任务正常运行时,每隔 5 秒更新一次心跳。恢复时如果发现心跳超过 30 秒没更新,就认为任务异常,触发告警并自动回退到上一个安全断点。

嗯,断点续传这块,说到底就是三个字:记清楚、管明白、扛得住。记清楚位置和状态,管明白偏移量的生命周期,扛得住重复执行的冲击。做到这三点,你的同步系统才算有了真正的「记忆」。