[转]数据库分库分表
一、 基本思想
Sharding的基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。对于海量数据的数据库,如果是因为表多而数据多,这时候适合使用垂直切分,即把关系紧密(比如同一模块)的表切分出来放在一个服务器上。如果表并不多,但每张表的数据非常多,这时候适合水平切分,即把表的数据按某种规则(比如按ID散列)切分到多个数据库(server)上。根据实际情况做出选择,也可能会综合使用垂直与水平切分。
1、 垂直切分
数据的垂直切分,也可以称之为纵向切分。将数据库想象成为由很多个一大块一大块的“数据块”(表)组成,我们垂直的将这些数据块切开,然后将他们分散到多台数据库主机上面,这样的切分方法就是一个垂直(纵向)的数据切分。
系统功能可以基本分为以下四个功能模块:用户、群组消息、相册以及事件,分别对应为如下这些表:
1. 用户模块表 user、user_profile、user_group、user_photo_album
2. 群组讨论表 groups、group_message、group_message_content、top_message
3. 相册相关表 photo、photo_album、photo_album_relation、photo_comment
4. 事件信息表 event
模块之间的关系:
1. 群组讨论模块和用户模块之间主要存在通过用户或者是群组关系来进行关联。一般关联的时候都会是通过用户id或者nick_name以及group的 id来进行关联,,通过模块之间的接口实现不会带来太多麻烦。
2. 相册模块仅仅与用户模块存在通过用户的关联。这两个模块之间的关联基本就有通过用户id关联的内容,简单清晰,接口明确。
3. 事件模块与各个模块可能都有关联,但是都只关注其各个模块中对象的信息ID,同样可以做到很容易分拆。
所以,我们第一步可以将数据库按照功能模块相关的表进行一次垂直拆分,每个模块 涉及的表单独到一个数据库中,模块与模块之间的表关联都在应用系统端通过接口来处理。 如下图所示:
通过这样的垂直切分之后,之前只能通过一个数据库来提供的服务,就被分拆成四个数据库来提供服务,服务能力自然是增加几倍了。
垂直切分的优点
u 数据库的拆分简单明了,拆分规则明确
u 应用程序模块清晰明确,整合容易
u 数据维护方便易行,容易定位
垂直切分的缺点
u 部分表关联无法在数据库级别完成,需要在程序中完成
u 对于访问极其频繁且数据量超大的表仍然存在性能平静,不一定能满足要求
u 事务处理相对更为复杂
u 切分达到一定程度之后,扩展性会遇到限制
u 过度切分可能会带来系统过渡复杂而难以维护
2、 水平切分
数据的水平切分,一般来说,简单的水平切分主要是将某个访问极其平凡的表再按照某个字段的某种规则来分散到多个表之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中。
对于我们的示例数据库来说,大部分的表都可以根据用户ID来进行水平的切分,不同用户相关的数据进行切分之后存放在不同的数据库中。如将所有用户ID通过2取模,然后分别存放于两个不同的数据库中,每个和用户ID关联上的表都可以这样切分。这样,基本上每个用户相关的数据,都在同一个数据库中,即使是需要关联,也可以非常简单的关联上。
我们可以通过下图来更为直观的展示水平切分相关信息:
水平切分的优点
u 表关联基本能够在数据库端全部完成
u 不会存在某些超大型数据量和高负载的表遇到瓶颈的问题
u 应用程序端整体架构改动相对较少
u 事务处理相对简单
u 只要切分规则能够定义好,基本上较难遇到扩展性限制
水平切分的缺点
u 切分规则相对更为复杂,很难抽象出一个能够满足整个数据库的切分规则
u 后期数据的维护难度有所增加,人为手工定位数据更困难
u 应用系统各模块耦合度较高,可能会对后面数据的迁移拆分造成一定的困难
3、 垂直与水平联合切分
一般来说,我们数据库中的所有表很难通过某一个(或少数几个)字段全部关联起来,所以很难简单的仅仅通过数据的水平切分来解决所有问题。而垂直切分也只能解决部分问题,对于那些负载非常高的系统,即使仅仅只是单个表都无法通过单台数据库主机来承担其负载,我们必须结合“水平” 和 “垂直”两种切分方式同时使用,充分利用两者的优点,避开其缺点。
每一个应用系统的负载都是一步一步增长上来的,在开始遇到性能瓶颈的时候,大多数架构师和DBA都会选择先进行数据的垂直拆分,然而,随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时期之后,经过了垂直拆分之后的数据库集群可能又再一次不堪重负,遇到了性能瓶颈。
这时候我们就必须要通过数据的水平切分的优势,来解决这里所遇到的问题。对于我们的示例数据库,假设在最开始,我们进行了数据的垂直切分,然而随着业务的不断增长,数据库系统遇到了瓶颈,我们选择重构数据库集群的架构。如何重构?考虑到之前已经做好了数据的垂直切分,而且模块结构清晰明确。而业务增长的势头越来越猛,即使现在进一步再次拆分模块,也坚持不了太久。我们选择了在垂直切分的基础上再进行水平拆分。
在经历过垂直拆分后的各个数据库集群中的每一个都只有一个功能模块,而每个功能模切中的所有表基本上都会与某个字段进行关联。如用户模块全部都可以通过用户ID进行切分,群组讨论模块则都通过群组ID 来切分,相册模块则根据相册 ID 分,最后的事件通知信息表考虑到数据的时限性(仅仅只会访问最近某个事件段的信息),则考虑按时间来切分。
下图展示了切分后的整个架构:
在应对不同的应用场景的时候,也需要充分考虑到这两种切分方法各自的局限,以及各自的优势,在不同的时期(负载压力)使用不同的结合方式。
联合切分的优点
u 可以充分利用垂直切分和水平切分各自的优势而避免各自的缺陷
u 让系统扩展性得到最大化提升
联合切分的缺点
u 数据库系统架构比较复杂,维护难度更大
u 应用程序架构也相对更复杂
二、 拆分实施策略和示例演示
第一部分:实施策略
1. 准备阶段
对数据库进行分库分表(Sharding化)前,需要充分了解系统业务逻辑和数据库schema.绘制一张数据库ER图,以图为基础划分shard,直观易行,可以确保清醒思路。
2. 分析阶段
1. 垂直切分
垂直切分的依据原则是:将业务紧密,表间关联密切的表划分在一起,例如同一模块的表。结合已经准备好的数据库ER图或领域模型图,仿照活动图中的泳道概念,一个泳道代表一个shard,把所有表格划分到不同的泳道中。下面的分析示例会展示这种做法。这种方式多个数据库之间的表结构不同。
2. 水平切分
垂直切分后,需要对shard内表格的数据量和增速进一步分析,以确定是否需要进行水平切分。这些数据库中的表结构完全相同。
2.1 若划分到一起的表格数据增长缓慢,在产品上线后可遇见的足够长的时期内均可以由单一数据库承载,则不需要进行水平切分,所有表格驻留同一shard,所有表间关联关系会得到最大限度的保留,同时保证了书写SQL的自由度,不易受join、group by、order by等子句限制。
2.2 若划分到一起的表格数据量巨大,增速迅猛,需要进一步进行水平分割。进一步的水平分割就这样进行:
2.2.1结合业务逻辑和表间关系,将当前shard划分成多个更小的shard,通常情况下,这些更小的shard每一个都只包含一个主表(将以该表ID进行散列的表)和多个与其关联或间接关联的次表。这种一个shard一张主表多张次表的状况是水平切分的必然结果。这样切分下来,shard数量就会迅速增多。如果每一个shard代表一个独立的数据库,那么管理和维护数据库将会非常麻烦,而且这些小shard往往只有两三张表,为此而建立一个新库,利用率并不高,因此,在水平切分完成后可再进行一次“反向的Merge”,即:将业务上相近,并且具有相近数据增长速率(主表数据量在同一数量级上)的两个或多个shard放到同一个数据库上,在逻辑上它们依然是独立的shard,有各自的主表,并依据各自主表的ID进行散列,不同的只是它们的散列取模(即节点数量)必需是一致的。这样,每个数据库结点上的表格数量就相对平均了。
2.2.2 所有表格均划分到合适的shard之后,所有跨越shard的表间关联都必须打断,在书写sql时,跨shard的join、group by、order by都将被禁止,需要在应用程序层面协调解决这些问题。
3. 实施阶段
如果项目在开发伊始就决定进行分库分表,则严格按照分析设计方案推进即可。如果是在中期架构演进中实施,除搭建实现sharding逻辑的基础设施外,还需要对原有SQL逐一过滤分析,修改那些因为sharding而受到影响的sql。
第二部分:示例演示
以下使用jpetstore(宠物店的电子商务系统)来演示如何进行分库分表(sharding)在分析阶段的工作。jpetstore来自原ibatis官方的一个Demo版本,SVN地址为:
http://mybatis.googlecode.com/svn/tags/java_release_2.3.4-726/jpetstore-5
由于系统较简单,我们很容易从模型上看出,其主要由三个模块组成:用户,产品和订单。那么垂直切分的方案也就出来了。接下来看水平切分,如果我们从一个实际的宠物店出发考虑,可能出现数据激增的单表应该是Account和Order,因此这两张表需要进行水平切分。对于Product模块来说,如果是一个实际的系统,Product和Item的数量都不会很大,因此只做垂直切分就足够了,也就是(Product,Category,Item,Iventory,Supplier)五张表在一个数据库结点上(没有水平切分,不会存在两个以上的数据库结点)。但是作为一个演示,我们假设产品模块也有大量的数据需要我们做水平切分,那么分析来看,这个模块要拆分出两个shard:一个是(Product(主),Category),另一个是(Item(主),Iventory,Supplier),同时,我们认为:这两个shard在数据增速上应该是相近的,且在业务上也很紧密,那么我们可以把这两个shard放在同一个数据库节点上,Item和Product数据在散列时取一样的模。根据前文介绍的图纸绘制方法,我们得到下面这张sharding示意图:
对于这张图再说明几点:
1. 使用泳道表示物理shard(一个数据库结点)
2. 若垂直切分出的shard进行了进一步的水平切分,但公用一个物理shard的话,则用虚线框住,表示其在逻辑上是一个独立的shard。
3. 深色实体表示主表
4. X表示需要打断的表间关联
三、 全局主键生成策略
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。
flickr开发团队在2010年撰文介绍了flickr使用的一种主键生成测策略,同时表示该方案在flickr上的实际运行效果也非常令人满意,它与一般Sequence表方案有些类似,但却很好地解决了性能瓶颈和单点问题,是一种非常可靠而高效的全局主键生成方案。
flickr这一方案的整体思想是:建立两台以上的数据库ID生成服务器,每个服务器都有一张记录各表当前ID的Sequence表,但是Sequence中ID增长的步长是服务器的数量,起始值依次错开,这样相当于把ID的生成散列到了每个服务器节点上。例如:如果我们设置两台数据库ID生成服务器,那么就让一台的Sequence表的ID起始值为1,每次增长步长为2,另一台的Sequence表的ID起始值为2,每次增长步长也为2,那么结果就是奇数的ID都将从第一台服务器上生成,偶数的ID都从第二台服务器上生成,这样就将生成ID的压力均匀分散到两台服务器上,同时配合应用程序的控制,当一个服务器失效后,系统能自动切换到另一个服务器上获取ID,从而保证了系统的容错。
关于这个方案,有几点细节这里再说明一下:
1. flickr的数据库ID生成服务器是专用服务器,服务器上只有一个数据库,数据库中表都是用于生成Sequence的,这也是因为auto-increment-offset和auto-increment-increment这两个数据库变量是数据库实例级别的变量。
2. flickr的方案中表格中的stub字段只是一个char(1) NOT NULL存根字段,并非表名,因此,一般来说,一个Sequence表只有一条纪录,可以同时为多张表生成ID,如果需要表的ID是有连续的,需要为该表单独建立Sequence表。
3. 方案使用了MySQL的LAST_INSERT_ID()函数,这也决定了Sequence表只能有一条记录。
4. 使用REPLACE INTO插入数据,这是很讨巧的作法,主要是希望利用MySQL自身的机制生成ID,不仅是因为这样简单,更是因为我们需要ID按照我们设定的方式(初值和步长)来生成。
5. SELECT LAST_INSERT_ID()必须要于REPLACE INTO语句在同一个数据库连接下才能得到刚刚插入的新ID,否则返回的值总是0
6. 该方案中Sequence表使用的是MyISAM引擎,以获取更高的性能,注意:MyISAM引擎使用的是表级别的锁,MyISAM对表的读写是串行的,因此不必担心在并发时两次读取会得到同一个ID(另外,应该程序也不需要同步,每个请求的线程都会得到一个新的connection,不存在需要同步的共享资源)。经过实际对比测试,使用一样的Sequence表进行ID生成,MyISAM引擎要比InnoDB表现高出很多!
7. 可使用纯JDBC实现对Sequence表的操作,以便获得更高的效率,实验表明,即使只使用spring JDBC性能也不及纯JDBC来得快!
实现该方案,应用程序同样需要做一些处理,主要是两方面的工作:
1. 自动均衡数据库ID生成服务器的访问
2. 确保在某个数据库ID生成服务器失效的情况下,能将请求转发到其他服务器上执行。
四、 sharding实现层面
通过前面的章节,我们已经很清楚了通过数据库的数据切分可以极大的提高系统的扩展性。但是,数据库中的数据在经过垂直和(或)水平切分被存放在不同的数据库主机之后,应用系统面临的最大问题就是如何来让这些数据源得到较好的整合。
在应用服务器与数据库之间通过代理实现
在应用服务器与数据库之间加入一个代理,应用程序向数据发出的数据请求会先通过代理,代理会根据配置的路由规则,对SQL进行解析后路由到目标shard,因为这种方案对应用程序完全透明,通用性好,所以成为了很多sharding产品的选择。在这方面较为知名的产品是mysql官方的代理工具:Mysql Proxy和一款国人开发的产品:amoeba。mysql proxy本身并没有实现任何sharding逻辑,它只是作为一种面向mysql数据库的代理,给开发人员提供了一个嵌入sharding逻辑的场所,它使用lua作为编程语言,这对很多团队来说是需要考虑的一个问题。amoeba则是专门实现读写分离与sharding的代理产品,它使用非常简单,不使用任何编程语言,只需要通过xml进行配置。不过amoeba不支持事务(从应用程序发出的包含事务信息的请求到达amoeba时,事务信息会被抹去,因此,即使是单点数据访问也不会有事务存在)一直是个硬伤。当然,这要看产品的定位和设计理念,我们只能说对于那些对事务要求非常高的系统,amoeba是不适合的。
Amoeba For MySQL主要是专门针对MySQL数据库的解决方案,前端应用程序请求的协议以及后端连接的数据源数据库都必须是MySQL。对于客户端的任何应用程序来说, amoeba For MySQL和一个MySQL没有什么区别,任何使用MySQL协议的客户端请求, 都可以被 Amoeba For MySQL解析并进行相应的处理,下图可以告诉我们Amoeba For MySQL的架构信息:
Amoeba使用指南:
http://docs.hexnova.com/amoeba/index.html
其他的一些实现层可以参考:
http://blog.csdn.NET/bluishglc/article/details/7766508
五、 多数据源的事务处理
六、 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案
Sharding扩容——系统维护不能承受之重
任何Sharding系统,在上线运行一段时间后,数据就会积累到当前节点规模所能承载的上限,此时就需要对数据库进行扩容了,也就是增加新的物理结点来分摊数据。如果系统使用的是基于ID进行散列的路由方式,那么团队需要根据新的节点规模重新计算所有数据应处的目标Shard,并将其迁移过去,这对团队来说无疑是一个巨大的维护负担;而如果系统是按增量区间进行路由(如每1千万条数据或是每一个月的数据存放在一个节点上 ),虽然可以避免数据的迁移,却有可能带来“热点”问题,也就是近期系统的读写都集中在最新创建的节点上(很多系统都有此类特点:新生数据的读写频率明显高于旧有数据),从而影响了系统性能。面对这种两难的处境,Sharding扩容显得异常困难。
一般来说,“理想”的扩容方案应该努力满足以下几个要求:
1. 最好不迁移数据 (无论如何,数据迁移都是一个让团队压力山大的问题)
2. 允许根据硬件资源自由规划扩容规模和节点存储负载
3. 能均匀的分布数据读写,避免“热点”问题
4. 保证对已经达到存储上限的节点不再写入数据
目前,能够避免数据迁移的优秀方案并不多,相对可行的有两种,一种是维护一张记录数据ID和目标Shard对应关系的映射表,写入时,数据都写入新扩容的Shard,同时将ID和目标节点写入映射表,读取时,先查映射表,找到目标Shard后再执行查询。该方案简单有效,但是读写数据都需要访问两次数据库,且映射表本身也极易成为性能瓶颈。为此系统不得不引入分布式缓存来缓存映射表数据,但是这样也无法避免在写入时访问两次数据库,同时大量映射数据对缓存资源的消耗以及专门为此而引入分布式缓存的代价都是需要权衡的问题。另一种方案来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。
如前文所述,Sharding扩容与系统采用的路由规则密切相关:基于散列的路由能均匀地分布数据,但却需要数据迁移,同时也无法避免对达到上限的节点不再写入新数据;基于增量区间的路由天然不存在数据迁移和向某一节点无上限写入数据的问题,但却存在“热点”困扰。我们设计方案的初衷就是希望能结合两种路由规则的优势,摒弃各自的劣势,创造出一种接近“理想”状态的扩容方式,而这种方式简单概括起来就是:全局按增量区间分布数据,使用增量扩容,无数据迁移,局部使用散列方式分散数据读写,解决“热点”问题,同时对Sharding拓扑结构进行建模,使用一致的路由算法,扩容时只需追加节点数据,不再修改散列逻辑代码。
首先,作为方案的基石,为了能使系统感知到Shard并基于Shard的分布进行路由计算,我们需要建立一个可以描述Sharding拓扑结构的编程模型。按照一般的切分原则,一个单一的数据库会首先进行垂直切分,垂直切分只是将关系密切的表划分在一起,我们把这样分出的一组表称为一个Partition。 接下来,如果Partition里的表数据量很大且增速迅猛,就再进行水平切分,水平切分会将一张表的数据按增量区间或散列方式分散到多个Shard上存储。在我们的方案里,我们使用增量区间与散列相结合的方式,全局上,数据按增量区间分布,但是每个增量区间并不是按照某个Shard的存储规模划分的,而是根据一组Shard的存储总量来确定的,我们把这样的一组Shard称为一个ShardGroup,局部上,也就是一个ShardGroup内,记录会再按散列方式均匀分布到组内各Shard上。这样,一条数据的路由会先根据其ID所处的区间确定ShardGroup,然后再通过散列命中ShardGroup内的某个目标Shard。在每次扩容时,我们会引入一组新的Shard,组成一个新的ShardGroup,为其分配增量区间并标记为“可写入”,同时将原有ShardGroup标记为“不可写入”,于是新生数据就会写入新的ShardGroup,旧有数据不需要迁移。同时,在ShardGroup内部各Shard之间使用散列方式分布数据读写,进而又避免了“热点”问题。最后,在Shard内部,当单表数据达到一定上限时,表的读写性能就开始大幅下滑,但是整个数据库并没有达到存储和负载的上限,为了充分发挥服务器的性能,我们通常会新建多张结构一样的表,并在新表上继续写入数据,我们把这样的表称为“分段表”(Fragment Table)。不过,引入分段表后所有的SQL在执行前都需要根据ID将其中的表名替换成真正的分段表名,这无疑增加了实现Sharding的难度,如果系统再使用了某种ORM框架,那么替换起来可能会更加困难。目前很多数据库提供一种与分段表类似的“分区”机制,但没有分段表的副作用,团队可以根据系统的实现情况在分段表和分区机制中灵活选择。总之,基于上述切分原理,我们将得到如下Sharding拓扑结构的领域模型:
在这个模型中,有几个细节需要注意:ShardGroup的writable属性用于标识该ShardGroup是否可以写入数据,一个Partition在任何时候只能有一个ShardGroup是可写的,这个ShardGroup往往是最近一次扩容引入的;startId和endId属性用于标识该ShardGroup的ID增量区间;Shard的hashValue属性用于标识该Shard节点接受哪些散列值的数据;FragmentTable的startId和endId是用于标识该分段表储存数据的ID区间。
确立上述模型后,我们需要通过配置文件或是在数据库中建立与之对应的表来存储节点元数据,这样,整个存储系统的拓扑结构就可以被持久化起来,系统启动时就能从配置文件或数据库中加载出当前的Sharding拓扑结构进行路由计算了(如果结点规模并不大可以使用配置文件,如果节点规模非常大,需要建立相关表结构存储这些结点元数据。从最新的Oracle发布的《面向大规模可伸缩网站基础设施的MySQL参考架构》白皮书一文的“超大型系统架构参考”章节给出的架构图中我们可以看到一种名为:Shard Catalog的专用服务器,这个其实是保存结点配置信息的数据库),扩容时只需要向对应的文件或表中加入相关的节点信息重启系统即可,不需要修改任何路由逻辑代码。
示例
让我们通过示例来了解这套方案是如何工作的。
阶段一:初始上线
假设某系统初始上线,规划为某表提供4000W条记录的存储能力,若单表存储上限为1000W条,单库存储上限为2000W条,共需2个Shard,每个Shard包含两个分段表,ShardGroup增量区间为0-4000W,按2取余分散到2个Shard上,具体规划方案如下:
(上面说单表的存储上线为1000W条,但是为什么图中Table_0的范围是0-2000W?)
与之相适应,Sharding拓扑结构的元数据如下:
阶段二:系统扩容
经过一段时间的运行,当原表总数据逼近4000W条上限时,系统就需要扩容了。为了演示方案的灵活性,我们假设现在有三台服务器Shard2、Shard3、Shard4,其性能和存储能力表现依次为Shard2<Shard3<Shard4,我们安排Shard2储存1000W条记录,Shard3储存2000W条记录,Shard4储存3000W条记录,这样,该表的总存储能力将由扩容前的4000W条提升到10000W条,以下是详细的规划方案:
相应拓扑结构表数据下:
从这个扩容案例中我们可以看出该方案允许根据硬件情况进行灵活规划,对扩容规模和节点数量没有硬性规定,是一种非常自由的扩容方案。
增强
接下来让我们讨论一个高级话题:对“再生”存储空间的利用。对于大多数系统来说,历史数据较为稳定,被更新或是删除的概率并不高,反映到数据库上就是历史Shard的数据量基本保持恒定,但也不排除某些系统其数据有同等的删除概率,甚至是越老的数据被删除的可能性越大,这样反映到数据库上就是历史Shard随着时间的推移,数据量会持续下降,在经历了一段时间后,节点就会腾出很大一部分存储空间,我们把这样的存储空间叫“再生”存储空间,如何有效利用再生存储空间是这些系统在设计扩容方案时需要特别考虑的。回到我们的方案,实际上我们只需要在现有基础上进行一个简单的升级就可以实现对再生存储空间的利用,升级的关键就是将过去ShardGroup和FragmentTable的单一的ID区间提升为多重ID区间。为此我们把ShardGroup和FragmentTable的ID区间属性抽离出来,分别用ShardGroupInterval和FragmentTableIdInterval表示,并和它们保持一对多关系。
让我们还是通过一个示例来了解升级后的方案是如何工作的。
阶段三:不扩容,重复利用再生存储空间
假设系统又经过一段时间的运行之后,二次扩容的6000W条存储空间即将耗尽,但是由于系统自身的特点,早期的很多数据被删除,Shard0和Shard1又各自腾出了一半的存储空间,于是ShardGroup0总计有2000W条的存储空间可以重新利用。为此,我们重新将ShardGroup0标记为writable=true,并给它追加一段ID区间:10000W-12000W,进而得到如下规划方案:
图7. 重复利用2000W再生存储空间的规划方案
相应拓扑结构的元数据如下:
小结
这套方案综合利用了增量区间和散列两种路由方式的优势,避免了数据迁移和“热点”问题,同时,它对Sharding拓扑结构建模,使用了一致的路由算法,从而避免了扩容时修改路由代码,是一种理想的Sharding扩容方案。
转载:
http://blog.csdn.Net/bluishglc/article/details/6161475
转载:《MySQL性能调优与架构设计》简朝阳
http://www.2cto.com/database/201410/341071.html
一、 基本思想
Sharding的基本思想就要把一个数据库切分成多个部分放到不同的数据库(server)上,从而缓解单一数据库的性能问题。对于海量数据的数据库,如果是因为表多而数据多,这时候适合使用垂直切分,即把关系紧密(比如同一模块)的表切分出来放在一个服务器上。如果表并不多,但每张表的数据非常多,这时候适合水平切分,即把表的数据按某种规则(比如按ID散列)切分到多个数据库(server)上。根据实际情况做出选择,也可能会综合使用垂直与水平切分。
1、 垂直切分
数据的垂直切分,也可以称之为纵向切分。将数据库想象成为由很多个一大块一大块的“数据块”(表)组成,我们垂直的将这些数据块切开,然后将他们分散到多台数据库主机上面,这样的切分方法就是一个垂直(纵向)的数据切分。
系统功能可以基本分为以下四个功能模块:用户、群组消息、相册以及事件,分别对应为如下这些表:
1. 用户模块表 user、user_profile、user_group、user_photo_album
2. 群组讨论表 groups、group_message、group_message_content、top_message
3. 相册相关表 photo、photo_album、photo_album_relation、photo_comment
4. 事件信息表 event
模块之间的关系:
1. 群组讨论模块和用户模块之间主要存在通过用户或者是群组关系来进行关联。一般关联的时候都会是通过用户id或者nick_name以及group的 id来进行关联,,通过模块之间的接口实现不会带来太多麻烦。
2. 相册模块仅仅与用户模块存在通过用户的关联。这两个模块之间的关联基本就有通过用户id关联的内容,简单清晰,接口明确。
3. 事件模块与各个模块可能都有关联,但是都只关注其各个模块中对象的信息ID,同样可以做到很容易分拆。
所以,我们第一步可以将数据库按照功能模块相关的表进行一次垂直拆分,每个模块 涉及的表单独到一个数据库中,模块与模块之间的表关联都在应用系统端通过接口来处理。 如下图所示:
通过这样的垂直切分之后,之前只能通过一个数据库来提供的服务,就被分拆成四个数据库来提供服务,服务能力自然是增加几倍了。
垂直切分的优点
u 数据库的拆分简单明了,拆分规则明确
u 应用程序模块清晰明确,整合容易
u 数据维护方便易行,容易定位
垂直切分的缺点
u 部分表关联无法在数据库级别完成,需要在程序中完成
u 对于访问极其频繁且数据量超大的表仍然存在性能平静,不一定能满足要求
u 事务处理相对更为复杂
u 切分达到一定程度之后,扩展性会遇到限制
u 过度切分可能会带来系统过渡复杂而难以维护
2、 水平切分
数据的水平切分,一般来说,简单的水平切分主要是将某个访问极其平凡的表再按照某个字段的某种规则来分散到多个表之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中。
对于我们的示例数据库来说,大部分的表都可以根据用户ID来进行水平的切分,不同用户相关的数据进行切分之后存放在不同的数据库中。如将所有用户ID通过2取模,然后分别存放于两个不同的数据库中,每个和用户ID关联上的表都可以这样切分。这样,基本上每个用户相关的数据,都在同一个数据库中,即使是需要关联,也可以非常简单的关联上。
我们可以通过下图来更为直观的展示水平切分相关信息:
水平切分的优点
u 表关联基本能够在数据库端全部完成
u 不会存在某些超大型数据量和高负载的表遇到瓶颈的问题
u 应用程序端整体架构改动相对较少
u 事务处理相对简单
u 只要切分规则能够定义好,基本上较难遇到扩展性限制
水平切分的缺点
u 切分规则相对更为复杂,很难抽象出一个能够满足整个数据库的切分规则
u 后期数据的维护难度有所增加,人为手工定位数据更困难
u 应用系统各模块耦合度较高,可能会对后面数据的迁移拆分造成一定的困难
3、 垂直与水平联合切分
一般来说,我们数据库中的所有表很难通过某一个(或少数几个)字段全部关联起来,所以很难简单的仅仅通过数据的水平切分来解决所有问题。而垂直切分也只能解决部分问题,对于那些负载非常高的系统,即使仅仅只是单个表都无法通过单台数据库主机来承担其负载,我们必须结合“水平” 和 “垂直”两种切分方式同时使用,充分利用两者的优点,避开其缺点。
每一个应用系统的负载都是一步一步增长上来的,在开始遇到性能瓶颈的时候,大多数架构师和DBA都会选择先进行数据的垂直拆分,然而,随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时期之后,经过了垂直拆分之后的数据库集群可能又再一次不堪重负,遇到了性能瓶颈。
这时候我们就必须要通过数据的水平切分的优势,来解决这里所遇到的问题。对于我们的示例数据库,假设在最开始,我们进行了数据的垂直切分,然而随着业务的不断增长,数据库系统遇到了瓶颈,我们选择重构数据库集群的架构。如何重构?考虑到之前已经做好了数据的垂直切分,而且模块结构清晰明确。而业务增长的势头越来越猛,即使现在进一步再次拆分模块,也坚持不了太久。我们选择了在垂直切分的基础上再进行水平拆分。
在经历过垂直拆分后的各个数据库集群中的每一个都只有一个功能模块,而每个功能模切中的所有表基本上都会与某个字段进行关联。如用户模块全部都可以通过用户ID进行切分,群组讨论模块则都通过群组ID 来切分,相册模块则根据相册 ID 分,最后的事件通知信息表考虑到数据的时限性(仅仅只会访问最近某个事件段的信息),则考虑按时间来切分。
下图展示了切分后的整个架构:
在应对不同的应用场景的时候,也需要充分考虑到这两种切分方法各自的局限,以及各自的优势,在不同的时期(负载压力)使用不同的结合方式。
联合切分的优点
u 可以充分利用垂直切分和水平切分各自的优势而避免各自的缺陷
u 让系统扩展性得到最大化提升
联合切分的缺点
u 数据库系统架构比较复杂,维护难度更大
u 应用程序架构也相对更复杂
二、 拆分实施策略和示例演示
第一部分:实施策略
1. 准备阶段
对数据库进行分库分表(Sharding化)前,需要充分了解系统业务逻辑和数据库schema.绘制一张数据库ER图,以图为基础划分shard,直观易行,可以确保清醒思路。
2. 分析阶段
1. 垂直切分
垂直切分的依据原则是:将业务紧密,表间关联密切的表划分在一起,例如同一模块的表。结合已经准备好的数据库ER图或领域模型图,仿照活动图中的泳道概念,一个泳道代表一个shard,把所有表格划分到不同的泳道中。下面的分析示例会展示这种做法。这种方式多个数据库之间的表结构不同。
2. 水平切分
垂直切分后,需要对shard内表格的数据量和增速进一步分析,以确定是否需要进行水平切分。这些数据库中的表结构完全相同。
2.1 若划分到一起的表格数据增长缓慢,在产品上线后可遇见的足够长的时期内均可以由单一数据库承载,则不需要进行水平切分,所有表格驻留同一shard,所有表间关联关系会得到最大限度的保留,同时保证了书写SQL的自由度,不易受join、group by、order by等子句限制。
2.2 若划分到一起的表格数据量巨大,增速迅猛,需要进一步进行水平分割。进一步的水平分割就这样进行:
2.2.1结合业务逻辑和表间关系,将当前shard划分成多个更小的shard,通常情况下,这些更小的shard每一个都只包含一个主表(将以该表ID进行散列的表)和多个与其关联或间接关联的次表。这种一个shard一张主表多张次表的状况是水平切分的必然结果。这样切分下来,shard数量就会迅速增多。如果每一个shard代表一个独立的数据库,那么管理和维护数据库将会非常麻烦,而且这些小shard往往只有两三张表,为此而建立一个新库,利用率并不高,因此,在水平切分完成后可再进行一次“反向的Merge”,即:将业务上相近,并且具有相近数据增长速率(主表数据量在同一数量级上)的两个或多个shard放到同一个数据库上,在逻辑上它们依然是独立的shard,有各自的主表,并依据各自主表的ID进行散列,不同的只是它们的散列取模(即节点数量)必需是一致的。这样,每个数据库结点上的表格数量就相对平均了。
2.2.2 所有表格均划分到合适的shard之后,所有跨越shard的表间关联都必须打断,在书写sql时,跨shard的join、group by、order by都将被禁止,需要在应用程序层面协调解决这些问题。
3. 实施阶段
如果项目在开发伊始就决定进行分库分表,则严格按照分析设计方案推进即可。如果是在中期架构演进中实施,除搭建实现sharding逻辑的基础设施外,还需要对原有SQL逐一过滤分析,修改那些因为sharding而受到影响的sql。
第二部分:示例演示
以下使用jpetstore(宠物店的电子商务系统)来演示如何进行分库分表(sharding)在分析阶段的工作。jpetstore来自原ibatis官方的一个Demo版本,SVN地址为:
http://mybatis.googlecode.com/svn/tags/java_release_2.3.4-726/jpetstore-5
由于系统较简单,我们很容易从模型上看出,其主要由三个模块组成:用户,产品和订单。那么垂直切分的方案也就出来了。接下来看水平切分,如果我们从一个实际的宠物店出发考虑,可能出现数据激增的单表应该是Account和Order,因此这两张表需要进行水平切分。对于Product模块来说,如果是一个实际的系统,Product和Item的数量都不会很大,因此只做垂直切分就足够了,也就是(Product,Category,Item,Iventory,Supplier)五张表在一个数据库结点上(没有水平切分,不会存在两个以上的数据库结点)。但是作为一个演示,我们假设产品模块也有大量的数据需要我们做水平切分,那么分析来看,这个模块要拆分出两个shard:一个是(Product(主),Category),另一个是(Item(主),Iventory,Supplier),同时,我们认为:这两个shard在数据增速上应该是相近的,且在业务上也很紧密,那么我们可以把这两个shard放在同一个数据库节点上,Item和Product数据在散列时取一样的模。根据前文介绍的图纸绘制方法,我们得到下面这张sharding示意图:
对于这张图再说明几点:
1. 使用泳道表示物理shard(一个数据库结点)
2. 若垂直切分出的shard进行了进一步的水平切分,但公用一个物理shard的话,则用虚线框住,表示其在逻辑上是一个独立的shard。
3. 深色实体表示主表
4. X表示需要打断的表间关联
三、 全局主键生成策略
一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。
flickr开发团队在2010年撰文介绍了flickr使用的一种主键生成测策略,同时表示该方案在flickr上的实际运行效果也非常令人满意,它与一般Sequence表方案有些类似,但却很好地解决了性能瓶颈和单点问题,是一种非常可靠而高效的全局主键生成方案。
flickr这一方案的整体思想是:建立两台以上的数据库ID生成服务器,每个服务器都有一张记录各表当前ID的Sequence表,但是Sequence中ID增长的步长是服务器的数量,起始值依次错开,这样相当于把ID的生成散列到了每个服务器节点上。例如:如果我们设置两台数据库ID生成服务器,那么就让一台的Sequence表的ID起始值为1,每次增长步长为2,另一台的Sequence表的ID起始值为2,每次增长步长也为2,那么结果就是奇数的ID都将从第一台服务器上生成,偶数的ID都从第二台服务器上生成,这样就将生成ID的压力均匀分散到两台服务器上,同时配合应用程序的控制,当一个服务器失效后,系统能自动切换到另一个服务器上获取ID,从而保证了系统的容错。
关于这个方案,有几点细节这里再说明一下:
1. flickr的数据库ID生成服务器是专用服务器,服务器上只有一个数据库,数据库中表都是用于生成Sequence的,这也是因为auto-increment-offset和auto-increment-increment这两个数据库变量是数据库实例级别的变量。
2. flickr的方案中表格中的stub字段只是一个char(1) NOT NULL存根字段,并非表名,因此,一般来说,一个Sequence表只有一条纪录,可以同时为多张表生成ID,如果需要表的ID是有连续的,需要为该表单独建立Sequence表。
3. 方案使用了MySQL的LAST_INSERT_ID()函数,这也决定了Sequence表只能有一条记录。
4. 使用REPLACE INTO插入数据,这是很讨巧的作法,主要是希望利用mysql自身的机制生成ID,不仅是因为这样简单,更是因为我们需要ID按照我们设定的方式(初值和步长)来生成。
5. SELECT LAST_INSERT_ID()必须要于REPLACE INTO语句在同一个数据库连接下才能得到刚刚插入的新ID,否则返回的值总是0
6. 该方案中Sequence表使用的是MyISAM引擎,以获取更高的性能,注意:MyISAM引擎使用的是表级别的锁,MyISAM对表的读写是串行的,因此不必担心在并发时两次读取会得到同一个ID(另外,应该程序也不需要同步,每个请求的线程都会得到一个新的connection,不存在需要同步的共享资源)。经过实际对比测试,使用一样的Sequence表进行ID生成,MyISAM引擎要比InnoDB表现高出很多!
7. 可使用纯JDBC实现对Sequence表的操作,以便获得更高的效率,实验表明,即使只使用spring JDBC性能也不及纯JDBC来得快!
实现该方案,应用程序同样需要做一些处理,主要是两方面的工作:
1. 自动均衡数据库ID生成服务器的访问
2. 确保在某个数据库ID生成服务器失效的情况下,能将请求转发到其他服务器上执行。
四、 sharding实现层面
通过前面的章节,我们已经很清楚了通过数据库的数据切分可以极大的提高系统的扩展性。但是,数据库中的数据在经过垂直和(或)水平切分被存放在不同的数据库主机之后,应用系统面临的最大问题就是如何来让这些数据源得到较好的整合。
在应用服务器与数据库之间通过代理实现
在应用服务器与数据库之间加入一个代理,应用程序向数据发出的数据请求会先通过代理,代理会根据配置的路由规则,对SQL进行解析后路由到目标shard,因为这种方案对应用程序完全透明,通用性好,所以成为了很多sharding产品的选择。在这方面较为知名的产品是mysql官方的代理工具:Mysql Proxy和一款国人开发的产品:amoeba。mysql proxy本身并没有实现任何sharding逻辑,它只是作为一种面向mysql数据库的代理,给开发人员提供了一个嵌入sharding逻辑的场所,它使用lua作为编程语言,这对很多团队来说是需要考虑的一个问题。amoeba则是专门实现读写分离与sharding的代理产品,它使用非常简单,不使用任何编程语言,只需要通过xml进行配置。不过amoeba不支持事务(从应用程序发出的包含事务信息的请求到达amoeba时,事务信息会被抹去,因此,即使是单点数据访问也不会有事务存在)一直是个硬伤。当然,这要看产品的定位和设计理念,我们只能说对于那些对事务要求非常高的系统,amoeba是不适合的。
Amoeba For MySQL主要是专门针对MySQL数据库的解决方案,前端应用程序请求的协议以及后端连接的数据源数据库都必须是MySQL。对于客户端的任何应用程序来说, amoeba For MySQL和一个MySQL没有什么区别,任何使用MySQL协议的客户端请求, 都可以被 Amoeba For MySQL解析并进行相应的处理,下图可以告诉我们Amoeba For MySQL的架构信息:
Amoeba使用指南:
http://docs.hexnova.com/amoeba/index.html
其他的一些实现层可以参考:
http://blog.csdn.NET/bluishglc/article/details/7766508
五、 多数据源的事务处理
六、 一种支持自由规划无须数据迁移和修改路由代码的Sharding扩容方案
Sharding扩容——系统维护不能承受之重
任何Sharding系统,在上线运行一段时间后,数据就会积累到当前节点规模所能承载的上限,此时就需要对数据库进行扩容了,也就是增加新的物理结点来分摊数据。如果系统使用的是基于ID进行散列的路由方式,那么团队需要根据新的节点规模重新计算所有数据应处的目标Shard,并将其迁移过去,这对团队来说无疑是一个巨大的维护负担;而如果系统是按增量区间进行路由(如每1千万条数据或是每一个月的数据存放在一个节点上 ),虽然可以避免数据的迁移,却有可能带来“热点”问题,也就是近期系统的读写都集中在最新创建的节点上(很多系统都有此类特点:新生数据的读写频率明显高于旧有数据),从而影响了系统性能。面对这种两难的处境,Sharding扩容显得异常困难。
一般来说,“理想”的扩容方案应该努力满足以下几个要求:
1. 最好不迁移数据 (无论如何,数据迁移都是一个让团队压力山大的问题)
2. 允许根据硬件资源自由规划扩容规模和节点存储负载
3. 能均匀的分布数据读写,避免“热点”问题
4. 保证对已经达到存储上限的节点不再写入数据
目前,能够避免数据迁移的优秀方案并不多,相对可行的有两种,一种是维护一张记录数据ID和目标Shard对应关系的映射表,写入时,数据都写入新扩容的Shard,同时将ID和目标节点写入映射表,读取时,先查映射表,找到目标Shard后再执行查询。该方案简单有效,但是读写数据都需要访问两次数据库,且映射表本身也极易成为性能瓶颈。为此系统不得不引入分布式缓存来缓存映射表数据,但是这样也无法避免在写入时访问两次数据库,同时大量映射数据对缓存资源的消耗以及专门为此而引入分布式缓存的代价都是需要权衡的问题。另一种方案来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。
如前文所述,Sharding扩容与系统采用的路由规则密切相关:基于散列的路由能均匀地分布数据,但却需要数据迁移,同时也无法避免对达到上限的节点不再写入新数据;基于增量区间的路由天然不存在数据迁移和向某一节点无上限写入数据的问题,但却存在“热点”困扰。我们设计方案的初衷就是希望能结合两种路由规则的优势,摒弃各自的劣势,创造出一种接近“理想”状态的扩容方式,而这种方式简单概括起来就是:全局按增量区间分布数据,使用增量扩容,无数据迁移,局部使用散列方式分散数据读写,解决“热点”问题,同时对Sharding拓扑结构进行建模,使用一致的路由算法,扩容时只需追加节点数据,不再修改散列逻辑代码。
首先,作为方案的基石,为了能使系统感知到Shard并基于Shard的分布进行路由计算,我们需要建立一个可以描述Sharding拓扑结构的编程模型。按照一般的切分原则,一个单一的数据库会首先进行垂直切分,垂直切分只是将关系密切的表划分在一起,我们把这样分出的一组表称为一个Partition。 接下来,如果Partition里的表数据量很大且增速迅猛,就再进行水平切分,水平切分会将一张表的数据按增量区间或散列方式分散到多个Shard上存储。在我们的方案里,我们使用增量区间与散列相结合的方式,全局上,数据按增量区间分布,但是每个增量区间并不是按照某个Shard的存储规模划分的,而是根据一组Shard的存储总量来确定的,我们把这样的一组Shard称为一个ShardGroup,局部上,也就是一个ShardGroup内,记录会再按散列方式均匀分布到组内各Shard上。这样,一条数据的路由会先根据其ID所处的区间确定ShardGroup,然后再通过散列命中ShardGroup内的某个目标Shard。在每次扩容时,我们会引入一组新的Shard,组成一个新的ShardGroup,为其分配增量区间并标记为“可写入”,同时将原有ShardGroup标记为“不可写入”,于是新生数据就会写入新的ShardGroup,旧有数据不需要迁移。同时,在ShardGroup内部各Shard之间使用散列方式分布数据读写,进而又避免了“热点”问题。最后,在Shard内部,当单表数据达到一定上限时,表的读写性能就开始大幅下滑,但是整个数据库并没有达到存储和负载的上限,为了充分发挥服务器的性能,我们通常会新建多张结构一样的表,并在新表上继续写入数据,我们把这样的表称为“分段表”(Fragment Table)。不过,引入分段表后所有的SQL在执行前都需要根据ID将其中的表名替换成真正的分段表名,这无疑增加了实现Sharding的难度,如果系统再使用了某种ORM框架,那么替换起来可能会更加困难。目前很多数据库提供一种与分段表类似的“分区”机制,但没有分段表的副作用,团队可以根据系统的实现情况在分段表和分区机制中灵活选择。总之,基于上述切分原理,我们将得到如下Sharding拓扑结构的领域模型:
在这个模型中,有几个细节需要注意:ShardGroup的writable属性用于标识该ShardGroup是否可以写入数据,一个Partition在任何时候只能有一个ShardGroup是可写的,这个ShardGroup往往是最近一次扩容引入的;startId和endId属性用于标识该ShardGroup的ID增量区间;Shard的hashValue属性用于标识该Shard节点接受哪些散列值的数据;FragmentTable的startId和endId是用于标识该分段表储存数据的ID区间。
确立上述模型后,我们需要通过配置文件或是在数据库中建立与之对应的表来存储节点元数据,这样,整个存储系统的拓扑结构就可以被持久化起来,系统启动时就能从配置文件或数据库中加载出当前的Sharding拓扑结构进行路由计算了(如果结点规模并不大可以使用配置文件,如果节点规模非常大,需要建立相关表结构存储这些结点元数据。从最新的Oracle发布的《面向大规模可伸缩网站基础设施的MySQL参考架构》白皮书一文的“超大型系统架构参考”章节给出的架构图中我们可以看到一种名为:Shard Catalog的专用服务器,这个其实是保存结点配置信息的数据库),扩容时只需要向对应的文件或表中加入相关的节点信息重启系统即可,不需要修改任何路由逻辑代码。
示例
让我们通过示例来了解这套方案是如何工作的。
阶段一:初始上线
假设某系统初始上线,规划为某表提供4000W条记录的存储能力,若单表存储上限为1000W条,单库存储上限为2000W条,共需2个Shard,每个Shard包含两个分段表,ShardGroup增量区间为0-4000W,按2取余分散到2个Shard上,具体规划方案如下:
(上面说单表的存储上线为1000W条,但是为什么图中Table_0的范围是0-2000W?)
与之相适应,Sharding拓扑结构的元数据如下:
阶段二:系统扩容
经过一段时间的运行,当原表总数据逼近4000W条上限时,系统就需要扩容了。为了演示方案的灵活性,我们假设现在有三台服务器Shard2、Shard3、Shard4,其性能和存储能力表现依次为Shard2<Shard3<Shard4,我们安排Shard2储存1000W条记录,Shard3储存2000W条记录,Shard4储存3000W条记录,这样,该表的总存储能力将由扩容前的4000W条提升到10000W条,以下是详细的规划方案:
相应拓扑结构表数据下:
从这个扩容案例中我们可以看出该方案允许根据硬件情况进行灵活规划,对扩容规模和节点数量没有硬性规定,是一种非常自由的扩容方案。
增强
接下来让我们讨论一个高级话题:对“再生”存储空间的利用。对于大多数系统来说,历史数据较为稳定,被更新或是删除的概率并不高,反映到数据库上就是历史Shard的数据量基本保持恒定,但也不排除某些系统其数据有同等的删除概率,甚至是越老的数据被删除的可能性越大,这样反映到数据库上就是历史Shard随着时间的推移,数据量会持续下降,在经历了一段时间后,节点就会腾出很大一部分存储空间,我们把这样的存储空间叫“再生”存储空间,如何有效利用再生存储空间是这些系统在设计扩容方案时需要特别考虑的。回到我们的方案,实际上我们只需要在现有基础上进行一个简单的升级就可以实现对再生存储空间的利用,升级的关键就是将过去ShardGroup和FragmentTable的单一的ID区间提升为多重ID区间。为此我们把ShardGroup和FragmentTable的ID区间属性抽离出来,分别用ShardGroupInterval和FragmentTableIdInterval表示,并和它们保持一对多关系。
让我们还是通过一个示例来了解升级后的方案是如何工作的。
阶段三:不扩容,重复利用再生存储空间
假设系统又经过一段时间的运行之后,二次扩容的6000W条存储空间即将耗尽,但是由于系统自身的特点,早期的很多数据被删除,Shard0和Shard1又各自腾出了一半的存储空间,于是ShardGroup0总计有2000W条的存储空间可以重新利用。为此,我们重新将ShardGroup0标记为writable=true,并给它追加一段ID区间:10000W-12000W,进而得到如下规划方案:
图7. 重复利用2000W再生存储空间的规划方案
相应拓扑结构的元数据如下:
小结
这套方案综合利用了增量区间和散列两种路由方式的优势,避免了数据迁移和“热点”问题,同时,它对Sharding拓扑结构建模,使用了一致的路由算法,从而避免了扩容时修改路由代码,是一种理想的Sharding扩容方案。