index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html
![]()
本文作者分享了团队如何实现一项艰巨任务——将超过10亿条关键金融数据从旧数据库无缝迁移至新数据库,全程零停机。文章详细阐述了分阶段批量迁移冷数据、通过双写机制处理实时流量、利用影子读进行线上暗测,以及最终切换策略。作者强调了幂等性、重试队列、缓存预热、详尽的可观测性以及制定回滚计划的重要性。整个过程不仅是技术挑战,也充满了对系统设计原理和人性压力的深刻洞察,最终证明零停机迁移是设计而非运气的产物。
✅ **分块并行与幂等性批量迁移**:面对海量数据,直接导出易导致数据库崩溃。作者团队将数据按主键范围分块,禁用二级索引和约束,并行加载,并对每个数据块进行校验和比对,确保数据完整性。此方法强调了“分块+并行+幂等”是处理超大规模迁移的关键。
💡 **双写与重试队列实现低成本分布式事务**:为应对实时新增流量,应用层修改为双写,同时写入旧库和新库。新库写入失败的数据会进入Kafka重试队列,并通过唯一ID确保写入操作的幂等性。这种机制有效降低了数据同步的成本和风险,是处理部分失败的有效手段。
🧐 **影子读捕捉真实流量下的潜在问题**:在数据同步后,通过影子读(后台对新库执行查询并与旧库结果比对)来验证新库的可靠性。这种方法能发现测试环境难以暴露的时区差异、NULL值处理、排序规则不匹配等问题,为客户无感知地修复提供了宝贵时间。
🚀 **缓存预热与细致监控确保平稳切换**:切换数据库时,新库缓存为空是主要风险。团队通过运行合成查询预热缓存,并在流量最低谷时启用功能标志进行切换,同时保持双写开启。全程依赖Grafana仪表盘进行近乎偏执的监控,确保业务指标稳定,为成功切换奠定基础。
📊 **全面可观测性是迁移成功的生命线**:作者认为,迁移成功的关键在于可观测性。通过监控复制延迟、死锁、缓存命中率、影子读取不匹配计数以及业务关键绩效指标,团队能够及时发现并解决问题,将迁移视为一个监控问题而非单纯的数据问题。
原创 Himanshu Singour 2025-11-10 07:16 广东

有些项目,你一辈子都忘不了……
有些项目,你一辈子都忘不了。对我来说,就是那个夜晚(其实连续了很多个夜晚),我们把超过10亿条记录从旧数据库迁移到新数据库……
全程没有一秒钟的停机。我们迁移的是关键金融数据——支付、订单、账本。一旦出错,客户会亏钱,仪表板会崩溃,信任会在一夜之间蒸发。我们通过研究数据库内部原理、艰难权衡以及每个决策背后的人性压力,以最痛苦的方式学会了系统设计。
我们为什么必须这么做 旧数据库一直兢兢业业地为我们服务着,但规模扩大改变了一切。以前只需几毫秒就能完成的查询,现在却要耗费几秒钟批处理任务(比如结算)有时候要跑好几个小时我们已经做了垂直扩展(更强的硬件),也做了水平扩展(只读副本),但还是不够模式僵化,每一个新功能都像做外科手术
数据库记录数量突破10亿,不堪重负。业务不断增长,停机时间是不可接受的。我们别无选择:迁移。但,这么大的数据量,怎么做到不停机迁移?
第一步:批量迁移旧数据我们从“冷数据”入手,也就是那些不再更新的旧交易数据。
数据库内部发生了什么?如果你尝试导出十亿行数据,数据库会占用巨大的缓冲区,可能会溢出到磁盘,然后崩溃每向新数据库中插入一行数据都会更新索引,这会减慢整个数据库的运行速度外键约束意味着每次插入数据都必须检查其他表
我们怎么做的:将表格拆分成按主键范围分块(例如,ID 1–5M、5M–10M)加载期间禁用二级索引和约束并行运行多个工作进程每个数据块处理完毕后,我们运行了校验和确保数据完全匹配
这办法不花哨,但管用。教训:超大迁移,别想着“一把梭”。要分块 + 并行 + 幂等。
第二步:双写,接住实时流量复制旧数据很容易,真正的挑战在于如何应对不断增长的新流量。
试想一下:在我们复制旧数据的同时,每秒仍有数千笔新的付款信息涌入。如果我们不记录这些信息,新的数据库就会始终滞后。我们怎么做的:修改了应用程序,使其能够进行双重写入:每次插入新数据都会同时写入旧数据库和新数据库。如果新的数据库写入失败,则该事件会被推送到Kafka 重试队列中。消费者反复尝试,直到成功为止。我们通过给写入操作添加唯一ID标签,使写入操作具有幂等性。(因此,即使重试两次,也不会重复写入同一行。
为什么可行:在PostgreSQL/MySQL这类关系库,每次插入先写WAL(预写日志)。我们利用了这一原理,确保至少在一个地方写入成功,并不断重试,直到两个数据库的结果一致为止。教训:双写 =低成本分布式事务。重试队列能救你于部分失败。
第三步:影子读(线上暗测)现在旧库新库同步了。但……我们能信新库吗?我们的秘密武器:影子读。客户仍然从旧数据库中读取数据但在后台,每个查询都会默默地针对新数据库重复执行我们比较了结果
我们的发现:时区表现不同(TIMESTAMP WITHOUT TZvs WITH TZ)NULL在新数据库中,部分值变成了默认值排序方式不同是因为排序规则(UTF-8 与 Latin1)不相同
这些问题测试环境永远抓不到,只有真实流量才能暴露,影子读给了我们几周时间修这些坑,客户毫无感知。教训:影子流量并非可有可无,它是赶在客户发现之前捕获查询规划器和编码不匹配的唯一方法。
第四步:切换(让人抓狂的那一夜)系统切换日就像在备战。风险:新数据库的缓冲池(缓存)是冷的,最初的几个查询可能会导致磁盘读写速度过快指数可能尚未完全升温后台任务(如自动清理/压缩)可能会导致 I/O 激增
我们的计划:通过运行合成查询来预热数据库,加载索引和缓存选凌晨4:30(流量最低)启用功能标志后,读取操作开始转移到新数据库为了安全起见,保持双写开启
前10分钟……我们盯着Grafana仪表盘,像在看病人的心电图。延迟?正常错误率?平的业务指标(支付、退款)?全绿
没人庆祝,我们怕得要死。24小时后,曲线依旧平静,我们才敢笑。教训:切换不是按个开关就完事,要缓存预热 + 回滚方案 + obsessive监控。
第五步:可观测性(我们的生命线)如果问我什么救了我们,不是酷炫SQL,而是可观测性。我们发现:复制延迟(比主服务器慢几秒)新数据库中出现死锁缓存命中率(必须保持在 95% 以上)影子读取导致的不匹配计数器业务关键绩效指标(每分钟订单量、收入流量)
没有这些仪表盘,我们如同盲人摸象。教训:迁移实际上是伪装成数据问题的监控问题。
我们面对的权衡1、大爆炸 vs 分阶段
ETL流程更简单,但无法处理实时流量双写操作难度更高,但更安全
→ 我们选择了双写操作3、迁移时是否建索引
加载期间构建索引会降低所有操作的速度先加载数据,后建立索引速度更快
→ 我们延迟了索引的创建人性的一面业务团队不停问:“你们就不能周末搞完?”DBA夜夜失眠,担心隐藏数据损坏开发盯着Grafana,像父母守着生病孩子的心跳
切换成功那一刻,我们没有欢呼,只是沉默地看绿色曲线。然后有人开了个玩笑:“要是明天炸了,谁来写复盘?”
我们笑了,笑得有点虚。最终教训(如果你也要设计这种系统)设计每个任务时都要确保其幂等性,因为你会重复执行某些操作批量加载期间禁用索引和约束,稍后重建使用双重写入和唯一 ID来避免重复写入运行影子读取以捕获规划器/编码怪异之处切换前务必预热缓存,否则你的延迟曲线图会像心脏病发作一样监控内部情况(WAL、缓存、死锁),而不仅仅是应用程序指标制定回滚计划,自信源于知道自己可以撤销操作
结束语我们不仅仅是迁移了十亿条记录,我们认识到,数据迁移不是数据库问题,而是系统设计问题。你不会一次性迁移十亿行数据。你会一次迁移一个安全批次,一次迁移一个 WAL 条目,一次迁移一个校验和。这就是秘诀。因为当你把迁移当作构建分布式系统来对待时,零停机时间就不是运气,而是设计的结果。
作者丨Himanshu Singour 编译丨Rio来源丨网址:https://medium.com/@himanshusingour7/how-we-migrated-db-1-to-db-2-1-billion-records-without-downtime-c034ce85d889dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn

阅读原文
跳转微信打开