网易首页 > 网易号 > 正文 申请入驻

数据库迁移藏了1万个定时炸弹,他们花3周拆完了

0
分享至


运营一个几百个服务的大型平台,数据库迁移从来不是"把数据搬过去"这么简单。它会像推倒多米诺骨牌一样,波及各个团队、代码库,以及那些你以为早就没人用的祖传代码——尤其是序列(Sequence)这个角落里的老古董。

序列是数据库里的隐形管家:一个默默计数的对象,每次你要插入新数据、需要主键时,它就递过来下一个数字,不重复、不冲突。工程师们通常只有在被迫换掉它的时候,才惊觉自己有多依赖这个"自动发号机"。

Coupang 从关系型数据库迁往 NoSQL 时,就在序列这个环节撞上了一堵墙。

一百多个团队依赖数据库原生序列生成主键。有人拿它排序,有人拿它满足下游系统对"数字必须越来越大"的执念。这些序列本身不复杂,但数量惊人:整个组织里散落着近万个计数器。

以 DynamoDB 为代表的 NoSQL 不提供原生序列。UUID 能解决问题,但会破坏原有排序逻辑,得改一堆服务。雪花算法(Snowflake)又带来不想承担的运维负担。团队想要的是一个"即插即用"的替代方案——各团队不用重写应用,就能平滑迁出关系型数据库。

作为全面淘汰遗留数据库、转向云原生基础设施的一部分,这个新系统得兼容源数据库序列的所有能力:起始值、自定义步长、升序降序,还要保证完全向后兼容,让各团队按自己的节奏迁移,不影响现有系统运行。

订单团队最终做到了:零停机,三周,十二个服务,代码修改不到五十行。

分布式序列生成听起来像是个需要复杂协调的难题。共识协议、向量时钟、分布式锁——论文里满是白板推导时很优雅的方案。

但复杂的系统会以复杂的方式失效。每多一层协调,就多一分延迟、多一种故障场景、多一层凌晨三点被告警叫醒时的运维负担。而对于序列服务,实际需求其实挺朴素:

唯一性:不同调用方永远不会拿到相同的值

热路径零网络调用:本地完成生成,不跨网络

注意这个清单里没有的东西:消费者之间的严格全局有序、无间断序列、实时一致性。大多数团队并不需要这些。而那些声称需要的,经过几次坦诚沟通后,往往也会发现其实可以舍弃。

"零网络调用"这个约束至关重要。传统数据库序列每生成一个值就要一次网络往返,高吞吐场景下这就是延迟瓶颈。团队想要的性能表现,是"像递增一个本地变量"——而对绝大多数请求来说,实际运行逻辑也正是如此。

评估现有方案时,没有一个能满足全部约束。

UUID 最直接,但已有几十个服务用 BIGINT 主键。改列类型会连锁影响表结构、API、报表系统——相当于在迁移之上再叠一层迁移。UUID 还会让 B-tree 索引的插入变得离散,降低高吞吐表的写入性能。更何况有团队依赖 ID 有序性做分页,UUID 完全无法满足。

Snowflake ID 解决了排序问题,也适配 BIGINT,但管理工作节点 ID 在自动扩缩容环境里本身就是个分布式协调问题;它还依赖同步时钟,时钟偏移会导致序号乱序甚至冲突。更麻烦的是无法即插即用:原本 1001、1002 的序列会变成 1578323451234567890 这种数值。

单一协调器的数据库序列理论上最简单,但会产生想规避的瓶颈:单点故障、逐个生成值的延迟、随规模扩大的锁竞争。

基于时间戳的方案问题更多:同一毫秒内请求冲突、分布式节点时钟偏移导致排序不可靠、无法支持自定义起始值与步长。

没有现成方案能满足全部约束:与原数据库序列完全对等、无需改表结构、亚毫秒级延迟、运维简单。于是团队自研了一个专用方案——它的简洁是刻意设计的结果。

序列服务成了 0 级核心服务(内部对关键路径基础设施的叫法,这类服务一故障,订单、支付等核心业务直接停摆)。多个一级服务依赖它生成主键。整体分三层:DynamoDB 作为可信数据源,上层是服务端缓存,再上层是带本地缓存的厚客户端。

请求在到达 DynamoDB 之前流经两个缓存层,DynamoDB 处理的序列需求不到 0.1%。

客户端请求序列时,有三种场景:客户端缓存命中,请求全程不离开应用进程;客户端缓存不足,序列服务后台补充;两层缓存均耗尽,才触发 DynamoDB 查询。

每个序列对应一个独立的 DynamoDB 项:计数器名称作键,当前数值作值。服务需要更多序列时,通过 DynamoDB 的条件更新实现原子自增。条件更新失败说明已有其他实例获取该段序列,服务会用新值重试——无需分布式锁就实现了安全唯一性。

每次只获取一个序列会导致频繁请求 DynamoDB。团队改为批量获取 500 到 1000 个,一次写入支撑数百次缓存级请求。这降低了 DynamoDB 成本、提升了延迟、增强了可用性——服务可承受 DynamoDB 短暂故障。

代价是序列间隙。服务器缓存中还有 400 个未使用序列时崩溃,这些数值就永久丢失了。但对业务场景而言,完全可以接受。

序列服务为每个计数器维护预获取序列的内存缓存。多实例同时运行,每个实例持有从 DynamoDB 分配的、互不重叠的序列段。跨实例生成的 ID 并非严格全局单调递增,但保证唯一;单个实例内始终单调递增。如果需要所有调用方之间严格全局有序,这个设计不适用。

特意选用内存缓存而非 Redis、Valkey 这类共享外部缓存——外部缓存会引入网络开销和新的故障依赖,而这正是想避免的。每个服务实例从 DynamoDB 原子分配专属序列段,实例之间无需共享缓存状态。代价是实例重启时未使用序列形成间隙,对场景而言可接受。

服务端也有可配置的填充阈值,控制每个计数器在缓存中保留的最大序列数量。客户端请求时,若缓存不足或为空,触发后台任务从 DynamoDB 重新填充。每个计数器可根据预期流量配置缓存参数。

缓存越大,对 DynamoDB 调用越少,但流量被高估或服务器重启时会浪费更多序列。团队根据实际流量模式和成本容忍度调整。对序列间隙敏感的业务方,在服务端内存与 DynamoDB 之间增加了可选的 Berkeley DB 层,提供本地持久化而无需网络往返。

运维中最关键的环节不是序列生成本身,而是判断何时重填缓存。时机错了,整个系统都可能出问题。重填过早,多余 DynamoDB 调用与容量浪费;重填过晚,缓存未命中、延迟增加、潜在故障。

团队用滑动窗口算法持续估算消费速率,在合适时机触发异步重填——不等缓存空了才动手。滑动窗口预测何时启动后台重填,确保新序列在现有缓存耗尽前就位。

滑动窗口运行在厚客户端内部(直接嵌入应用进程的库),不涉及独立进程或服务调用。客户端跟踪自身序列消费速率,在本地缓存耗尽前决定何时向序列服务请求重填。这让用户请求几乎不会被网络调用或 DynamoDB 写入阻塞。

针对每个计数器,服务在 60 秒滚动窗口内跟踪序列分配情况,计算当前消费速率。重填阈值动态变化:

refill_threshold = current_rate × buffer_seconds

缓存中的序列数量低于该阈值时,启动后台重填。

滑动窗口自动适配流量模式:流量上升,消费速率提升、阈值升高,服务提前获取新数据块维持缓冲区;流量下降,速度放缓、阈值降低,服务主动停止预取;突发流量时,短时突增使速率临时飙升、阈值提高并触发重填,若突发消退,阈值逐步恢复正常。

min_threshold 防止低流量计数器将阈值设得太低——每分钟仅一个请求的计数器,不应等到序列耗尽才重填。

服务器端缓存减轻 DynamoDB 负载,客户端缓存则让绝大多数请求不再需要网络调用——这是核心设计目标。

厚客户端是直接嵌入应用的 SDK,维护自身本地序列缓存,仅在需要重新填充时才与序列服务通信,而通过合理调优滑动窗口,这种通信频率并不高。

每秒处理上万订单的服务无法承受为每个序列都进行一次网络往返,厚客户端彻底消除了这一瓶颈。

遗留数据库的序列其实也有缓存,团队不会为每个值都发起数据库调用——那种方案本身就不可行。但现有缓存实现方式在各团队间不统一,通常为自研,与连接池行为绑定,在缓存块大小、重填策略、故障处理上均有差异。新系统对缓存进行了标准化,采用优化的双层架构。

厚客户端能处理:本地缓存管理、后台预填充、速率计算与预测、服务不可用时降级、进程内单调性保障。客户端不负责配置逻辑——对序列增量、起始值、生成方向一无所知,仅根据序列名称申请序列块,按接收顺序分配序号。所有复杂逻辑都在服务器端。

服务器向客户端建议初始填充速率,客户端通过滑动窗口持续监控实际消耗并自动调整。部署后几分钟内,即便初始配置不佳,也能收敛到适配实际负载的最优填充速率。

客户端与服务器端参数的不对称是有意设计:服务端每次从 DynamoDB 获取 500 至 1000 个序列的块,客户端缓存上限则根据应用需求设为 50 至 500。客户端内存更受限,因崩溃造成的序列浪费也更常见(应用频繁重启)。服务端处理多客户端请求,使用更大块合理;较小客户端缓存在缩容或重新部署时减少浪费。

双层缓存不仅能提升性能,还能针对中断提供分层保护。

客户端缓存避免服务中断影响:序列服务不可用时,客户端可继续从本地缓存获取。缓存 500 个序列、每秒消费 10 个,就能承受 50 秒服务中断而不影响业务。

服务器缓存避免 DynamoDB 中断影响:DynamoDB 分区中断或限流时,序列服务可继续基于内存缓存提供服务。每个计数器缓存数千个序列,可继续处理数分钟流量。

双重保护成倍提升系统弹性。DynamoDB 中断不会立即影响客户端,服务器缓存承接了影响;序列服务中断不会立即影响应用,客户端缓存承接了影响。组合式缓存让系统有效可用性高于任意单个组件,任意一层短暂中断对终端用户无感知。

不会有两个调用方获取到相同序列值,这个保证全局成立。通过 DynamoDB 条件写入确保每个数据块仅被分配一次,即便在崩溃、重试、网络分区等故障场景下,唯一性依然得到保证,最坏情况只会出现序号间隙,而非重复。

单个客户端实例内部序号严格递增(递减序列则严格递减)。跨客户端时,序号通常随时间递增,但整体可能无序——客户端 A 的 1050 可能在客户端 B 的 1100 之后被使用。

间隙是设计中固然存在的。服务器或客户端崩溃导致缓存中有未使用序列,或数据块已分配但未产生实际流量时,就会出现间隙。对大多数使用场景而言,间隙无关紧要。主键无需连续,审计日志也不要求序号必须保持顺序。真正需要无间隙序列的只有那些对外部连续性有强依赖的系统,而这类系统远比工程师最初设想的要少。

双层缓存与滑动窗口速率计算带来显著效率提升。

峰值每秒生成五万个序列的系统中:约 49500 个直接从客户端内存即时提供,约 450 个需要调用服务端(仍可通过服务端缓存快速响应),仅约 50 个触发 DynamoDB 写入。

滑动窗口的异步重填至关重要。由于重填在缓存耗尽前触发,几乎没有用户请求需要等待网络调用。无论后台发生何种情况,用户都能感受到一致的亚毫秒级延迟。

所有计数器峰值吞吐量超过每秒五万个序列,常规每秒一万至两万。流量最高的计数器单实例峰值约每秒五千个。绝大部分吞吐量并未真正请求序列服务,实际服务流量仅约为序列总消耗量的百分之一。

一个每秒提供 50000 个序列的系统,正常负载下每秒仅产生 10 至 20 次 DynamoDB 写入。1000:1 的比例得益于批量获取与预测重填的协同。

正常情况下,超过 99% 的请求命中客户端缓存。服务器调用极少,DynamoDB 调用更少。

成本方面:配置了最小写入容量且存储占用极低的 DynamoDB 每月约 50 美元;用于小规模集群、CPU 使用率较低的序列服务计算成本约 500 美元;网络成本可忽略,厚客户端模式有效减少跨服务流量。总月成本低于 1000 美元,支撑每秒数万序列的服务能力。

构建系统只是工作的一部分,让上百个团队真正落地使用是更难的部分。订单团队在三周内完成十二个服务迁移,峰值每秒 8000 个序列,全程零停机。

客户端 API 比遗留数据库更简单:

// 遗留数据库 (之前)

long id = connection.getNextSequenceValue("orders_seq");

// 新系统 (之后)

long id = sequenceClient.next("orders_seq");

对大多数团队,迁移只需修改一行代码并更新依赖。无需额外配置、初始化或参数设置,传入序列名称即可。所有复杂逻辑(步长、生成方向、块大小)均在接入时在服务器端处理。

序列配置完全在服务器端,保存在动态配置存储中。团队接入时使用与源数据库兼容的参数注册序列:序列名称、起始值、增量大小、最小值、最大值、生成方向(升序或降序)、块大小。服务器处理所有复杂性,客户端保持轻量。

迁移期间,团队可同时运行新旧系统,用特性开关控制切换。这种渐进式方法让团队能验证行为、监控性能、在完全提交前回滚。

序列服务现已运行超过一年,服务数百个生产服务,生成数十亿个序列,零计划外停机。架构的简洁性——刻意为之的简洁——是可靠性的关键。

最复杂的方案很少是最好的。分布式序列生成的文献充满 elegant 的协调机制,而实际需要的只是一个带缓存的计数器、一个滑动窗口,以及对"足够好"的坦然接受。

订单团队迁移完成后,一位工程师在复盘文档里写了一句:"以前我们以为序列是数据库的事,现在发现它其实是缓存的艺术。"

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
埃尔多安罕见放狠话:巴基斯坦若不介入,土耳其或直接踏入以色列

埃尔多安罕见放狠话:巴基斯坦若不介入,土耳其或直接踏入以色列

精彩启程
2026-04-14 12:41:43
两分钟内连震两次,中国地震台网:四川内江市资中县发生3.9地震

两分钟内连震两次,中国地震台网:四川内江市资中县发生3.9地震

环球网资讯
2026-04-13 17:20:19
这游戏把"开高达"做成了真事,日本玩家众筹140%求它别死

这游戏把"开高达"做成了真事,日本玩家众筹140%求它别死

像素与芯片
2026-04-13 13:15:19
斯诺克世锦赛资格赛审判轮对阵出炉!9名中国球员全力冲击正赛

斯诺克世锦赛资格赛审判轮对阵出炉!9名中国球员全力冲击正赛

莼侃体育
2026-04-14 11:36:36
萝莉岛大雷出现!比想象中炸裂,牵扯多国总统,难怪爱泼斯坦必死

萝莉岛大雷出现!比想象中炸裂,牵扯多国总统,难怪爱泼斯坦必死

离离言几许
2026-02-02 21:16:35
女生主动起来有多黏人?网友:这些女的太开放了

女生主动起来有多黏人?网友:这些女的太开放了

带你感受人间冷暖
2026-01-27 00:20:06
6.8万紫貂被扯坏后续:女子丢工作后道歉,黑历史被扒,全网社死

6.8万紫貂被扯坏后续:女子丢工作后道歉,黑历史被扒,全网社死

小徐讲八卦
2026-04-12 15:43:13
没想到,张雪机车夺冠8天后,台湾馆长竟因一特殊举动口碑暴涨

没想到,张雪机车夺冠8天后,台湾馆长竟因一特殊举动口碑暴涨

梨花黛娱
2026-04-13 15:43:12
中国跳水队公布世界杯总决赛名单:陈芋汐、陈艺文在列

中国跳水队公布世界杯总决赛名单:陈芋汐、陈艺文在列

懂球帝
2026-04-13 22:48:15
芒果台踩雷!赵子琪被淘汰后直播开撕,她的过往连张朝阳都忌惮

芒果台踩雷!赵子琪被淘汰后直播开撕,她的过往连张朝阳都忌惮

小徐讲八卦
2026-04-12 06:23:01
水到渠成的好运!4月下旬赚钱速度越来越快,越来越轻松的3生肖

水到渠成的好运!4月下旬赚钱速度越来越快,越来越轻松的3生肖

毅谈生肖
2026-04-14 10:17:01
“新型啃老”已来了,比“传统啃老”更严重,很多父母还没认识到

“新型啃老”已来了,比“传统啃老”更严重,很多父母还没认识到

芭比衣橱
2026-04-14 11:43:20
勒温:利马拽我头发被罚下场?规则又不是我制定的,我不记仇

勒温:利马拽我头发被罚下场?规则又不是我制定的,我不记仇

懂球帝
2026-04-14 05:27:14
77年李德生探望叶帅,席间叶帅突然提点:你去看看你们的老政委

77年李德生探望叶帅,席间叶帅突然提点:你去看看你们的老政委

掠影后有感
2026-04-14 10:04:25
第三波来了!多架专机直飞中国,东南亚11国选边站,中方已致贺电

第三波来了!多架专机直飞中国,东南亚11国选边站,中方已致贺电

书纪文谭
2026-04-13 16:06:20
三天闪电访华!苏林急得直跺脚,东南亚集体掉头靠向中国

三天闪电访华!苏林急得直跺脚,东南亚集体掉头靠向中国

安珈使者啊
2026-04-14 09:30:18
粟裕那一枪,到底是打歪了,还是故意打歪的

粟裕那一枪,到底是打歪了,还是故意打歪的

鹤羽说个事
2026-03-30 22:03:41
伊朗军方:若伊朗港口受威胁 波斯湾任何港口都不安全

伊朗军方:若伊朗港口受威胁 波斯湾任何港口都不安全

新京报
2026-04-13 15:25:21
杨贵妃没有死,被安倍仲麻吕带到了日本,生下了安倍晴明(上)

杨贵妃没有死,被安倍仲麻吕带到了日本,生下了安倍晴明(上)

青山易观
2024-09-23 23:22:48
96年,军委给清贫度日的李敏副军级待遇,李:父母是父母,我是我

96年,军委给清贫度日的李敏副军级待遇,李:父母是父母,我是我

旧史新谭
2026-04-14 01:45:21
2026-04-14 14:55:00
赛博兰博
赛博兰博
专注捣鼓AI效率工具,试图在这个时代留下数字分身的探索者。
1408文章数 16关注度
往期回顾 全部

科技要闻

离职同事"炼化"成AI?这届公司不需要活人了

头条要闻

恒大集团、恒大地产及许家印案开庭 许家印认罪悔罪

头条要闻

恒大集团、恒大地产及许家印案开庭 许家印认罪悔罪

体育要闻

他做对了所有事,却被整个职业网坛放逐了八年

娱乐要闻

宋祖儿刘宇宁恋情大反转 正主火速辟谣

财经要闻

许家印受审当庭表示认罪悔罪

汽车要闻

长城欧拉5限定版纯电版上市 限量99台售价13.38万元

态度原创

游戏
手机
教育
数码
时尚

V社新Steam手柄要来了 首批大货已运抵美国

手机要闻

安卓最强Pro!小米18 Pro首发高通骁龙8E6系列:电池突破7000mAh

教育要闻

2026高考考生注意!7所高校全部启动

数码要闻

TCL T7M Pro体验:SQD-Mini LED技术加持,客厅换代首选

今年科切拉的风吹向了谁?

无障碍浏览 进入关怀版