分布式数据库扩容缩容从来不是简单的加机器减机器,真正让人头疼的是数据怎么在节点间重新分配。你加了三台新节点,如果数据不挪过去,新节点就是摆设;你下线两台老机器,如果数据不提前迁走,业务立刻中断。这个数据迁移和重新分配的过程,就是数据重平衡。
数据重平衡的核心矛盾在于:既要保证数据均匀分布,又要在迁移过程中不影响正常读写。传统的单机数据库扩容基本靠主从切换和手动导数据,停机窗口动辄几小时。分布式数据库必须在线完成这件事,难度直接上了一个量级。
哈希分片下的重平衡机制绝大多数分布式数据库采用哈希分片,比如用一致性哈希或者范围哈希把数据打散到各个节点。当节点数量变化时,哈希环或者分片映射关系必然改变,数据就得跟着动。
一致性哈希的做法是把整个哈希空间切成很多个虚拟槽,每个节点负责一部分槽。扩容时新节点从老节点手里接管一部分槽,缩容时老节点把槽交出去。这个过程中只有被接管的槽对应的数据需要迁移,其他数据原地不动。比如原来3个节点平分10000个槽,现在加到4个节点,每个节点负责2500个槽,那么每个老节点要吐出大约833个槽给新节点,实际迁移的数据量大约是总数据量的25%。
范围分片的做法是按主键区间切分,比如节点A负责ID 1到100万,节点B负责100万到200万。扩容时通常采用分裂分片的方式,把某个热点区间一分为二,一半留在老节点,一半迁到新节点。这种方式的好处是迁移粒度可控,缺点是容易产生热点,需要配合负载监控动态调整。
在线迁移的三种主流策略数据迁移期间业务不能停,这就要求迁移过程必须在线进行。目前业界主要有三种实现方式。
第一种是双写方案。迁移前先在元数据里标记目标数据正在迁移,所有对这个数据的写操作同时发到源节点和目标节点,读操作先从源节点读,等数据同步完成后切换读流量到目标节点,最后删除源节点数据。这种方案实现简单,但双写期间性能有损耗,而且需要处理双写失败的一致性问题。TiDB在早期版本就采用了类似思路,通过raft协议保证副本一致性,迁移时直接添加新副本,等副本数据追平后再移除老副本。
第二种是逻辑日志回放。源节点把需要迁移的数据转换成逻辑日志,比如binlog或者redo log,目标节点像从库一样回放这些日志。等追到接近实时时短暂阻塞写入,完成最终切换。这种方案对业务侵入小,但需要数据库本身支持逻辑日志输出,而且切换瞬间有短暂不可用。OceanBase在扩缩容时就是通过Paxos组变更来完成,新节点加入Paxos组后自动同步日志,追上进度后发起成员变更提案,整个过程对业务完全透明。
第三种是物理文件拷贝加增量同步。先全量拷贝数据文件到目标节点,同时记录拷贝期间产生的增量变更,全量完成后回放增量,最后切换。这种方式适合数据量特别大的场景,全量阶段走物理通道速度快,但需要处理文件一致性问题。HBase的region迁移就类似这个思路,先把region下线,拷贝HFile到新regionserver,再上线region并回放WAL。
实际执行中的关键细节说起来原理都不复杂,但真正落地时有一堆坑要填。
迁移粒度问题。一次迁移多少数据合适?粒度太小,迁移频繁,元数据开销大;粒度太大,单次迁移时间长,失败回滚代价高。通常的做法是把数据切成固定大小的chunk,比如每100MB一个chunk,逐个迁移。每个chunk迁移完成后立即更新路由表,这样即使中途失败,也只需要重试当前chunk。MongoDB的chunk迁移默认就是按64MB的块来做的,迁移前先检查目标节点磁盘容量和负载,条件满足才执行。
热点数据保护。如果正在迁移的chunk恰好是写入热点,双写期间目标节点可能被打爆。好的做法是在迁移前先分析chunk的访问频率,对热点chunk要么延后迁移,要么先做冷热分离。CockroachDB在rebalance时会计算每个range的QPS,优先迁移冷range,避免影响在线业务。
限速和流控。数据迁移会消耗网络带宽和磁盘IO,如果不加限制,可能拖垮整个集群。必须支持按时间窗口限速,比如白天业务高峰期降低迁移速度,晚上再放开。同时要监控节点负载,当节点CPU或IO超过阈值时自动暂停迁移。这个功能看起来不起眼,但实际运维中能救命。
失败重试和回滚。迁移过程中任何一步都可能失败,网络抖动、磁盘满、节点重启,必须有完善的回滚机制。每个chunk迁移前记录快照,迁移失败就回滚到快照状态,同时重试次数要有上限,超过上限就告警人工介入。这里有个容易忽略的点,回滚时不仅要恢复数据,还要恢复路由信息,否则会出现数据在目标节点但路由指向源节点的脏状态。
一致性保证的工程实践数据迁移期间的一致性是最难保证的。假设一个chunk正在从节点A迁到节点B,此时客户端发来一个写请求,这个写请求到底发给谁?
业界通用的做法是引入epoch或者版本号机制。每个chunk或者分片都有一个单调递增的版本号,路由表里记录当前版本的chunk在哪个节点。迁移开始时版本号加1,标记为迁移中状态,此时写请求必须同时发给新旧两个节点,读请求根据配置可以读旧节点也可以读新节点。迁移完成后版本号再加1,路由指向新节点,旧节点清理数据。
这里有个边界情况,如果客户端拿到的路由表是旧的,它可能把请求发到已经迁移完成的旧节点。旧节点收到请求后发现本地chunk版本号已经落后,会返回一个重定向错误,客户端重新获取路由表再发到新节点。这个机制要求客户端必须实现重定向逻辑,对业务有轻微侵入,但换来的是迁移期间零数据丢失。
TiKV的做法更彻底,它把数据分片叫做region,每个region是一个raft group。扩缩容时通过raft的成员变更机制添加或移除副本,raft协议本身就保证了副本间的强一致性,不需要额外的双写或重定向逻辑。代价是每个region的副本数通常只有3到5个,节点数多了以后raft心跳开销会增大。
缩容的特殊挑战缩容比扩容更危险,因为你要下线一个存着真实数据的节点。必须在节点下线前把数据全部迁走,而且要确保迁完后集群的副本数满足配置要求。
安全的缩容流程应该是:先标记节点为下线中状态,调度器开始把该节点上的所有分片迁走,同时不再往这个节点分配新分片。等所有分片迁移完成,节点数据为空,再正式从集群中移除。这个过程可能很漫长,如果节点上有TB级数据,迁移可能要几个小时。期间如果节点故障,迁移就会中断,所以缩容前最好先确认节点健康状态良好。
有个容易被忽视的问题是,缩容后集群的容灾能力是否下降。比如原来5个节点,每个分片3副本,任意2个节点故障不影响可用性。缩到3个节点后还是3副本,但任意1个节点故障就有分片只剩2副本,风险明显上升。所以缩容不只是数据迁移的问题,还要重新评估副本放置策略,确保副本分散在不同的故障域。
自动化与人工干预的平衡理想情况下数据重平衡应该全自动,节点加入或退出后系统自动感知、自动迁移、自动恢复平衡。但现实是,自动化做得太激进容易出事故,太保守又达不到效果。
好的实践是系统默认开启自动重平衡,但提供丰富的控制参数。比如可以设置迁移的时间窗口、限速阈值、并发迁移数、是否允许迁移leader分片等。同时提供手动迁移命令,当自动调度不符合预期时,运维人员可以手动指定迁移哪些分片到哪些节点。
监控和告警也必不可少。迁移进度、迁移速度、待迁移数据量、迁移失败次数这些指标都要可视化展示。一旦迁移出现异常,比如某个chunk反复迁移失败,或者迁移速度骤降,要及时告警。很多事故都是因为迁移卡住了没人发现,等业务报错时已经晚了。
不同数据库的实现差异不同分布式数据库在重平衡上的实现差异很大,选型时需要重点关注。
MongoDB的分片集群使用config server存储元数据,mongos做路由,chunk迁移由balancer负责。balancer会根据各分片的chunk数量差异决定是否迁移,默认阈值是迁移后chunk数差距不超过2个。迁移过程是先把chunk数据复制到目标分片,复制期间源分片的写入会记录到内存,复制完成后应用增量,最后更新config server的元数据并清理源分片。整个过程对客户端透明,但迁移期间chunk所在的源分片写入性能会下降。
Cassandra采用一致性哈希,每个节点负责一段token区间。扩容时新节点加入后,通过nodetool move或者bootstrap自动计算新的token分配,然后从相邻节点流式拉取数据。Cassandra的迁移是纯点对点的,没有中心调度器,每个节点自己决定要迁出哪些数据。这种去中心化设计的好处是没有单点瓶颈,坏处是迁移过程比较难控制,容易出现某个节点负载过高。
Redis Cluster使用16384个哈希槽,每个节点负责一部分槽。扩容时用redis-cli的reshard命令,指定要迁移的槽数量和目标节点,迁移过程是一个个槽进行的。每个槽迁移时源节点把槽内的所有key用migrate命令原子地迁到目标节点,迁移期间该槽处于只读状态。槽的粒度比较小,单个槽迁移很快,对业务影响可控,但槽数量固定16384个,节点数特别多时每个节点分到的槽很少,迁移管理的开销会上升。
TiDB的扩缩容通过PD调度器完成。PD收集各TiKV节点的region分布和负载信息,计算出最优的调度计划,然后下发调度指令给TiKV。TiKV通过raft的AddPeer和RemovePeer来增减副本,region迁移就是先加新副本再删旧副本的过程。整个过程完全在线,对业务无感知,而且PD会考虑跨机架、跨机房的高可用策略,确保副本分布合理。
性能优化经验数据重平衡期间最怕的就是性能抖动。几个优化点值得注意。
迁移并发度控制。同时迁移太多chunk会把网络和磁盘打满,太少又导致迁移时间过长。一般建议按节点磁盘带宽的30%到50%来设定迁移速率上限,同时限制单节点同时进行的迁移任务数,比如不超过3个。
小文件合并。如果数据模型是小key-value,迁移时会产生大量小IO,效率极低。可以在迁移前先把小数据打包成批量文件,到目标节点再解包写入,减少网络往返和磁盘寻道。
压缩传输。跨机房迁移时带宽通常紧张,开启传输压缩可以显著减少数据量。但压缩会消耗CPU,需要在CPU和带宽之间做取舍。一般内网迁移不压缩,跨机房迁移开启lz4或snappy这类轻量压缩。
预加热。数据迁移到目标节点后,如果立即承接大量读请求,缓存未命中会导致响应时间飙升。可以在迁移完成后先对数据做一轮预读,把热点数据加载到缓存,再切换读流量。
数据重平衡是分布式数据库的核心能力之一,直接决定了集群的弹性和运维复杂度。一个设计良好的重平衡机制,应该做到在线迁移、自动调度、可观测、可控制。选型时不要只看宣传的弹性伸缩能力,要实际测试一下在数据量较大、并发较高的情况下,扩缩容对业务的影响到底有多大。很多系统在空载时扩缩容很丝滑,一旦上了生产压力,各种问题就暴露出来了。
