数据的垂直切分,也可以称之为纵向切分。将数据库想象成为由很多个一大块一大块的“数据块”(表)组成,我们垂直的将这些数据块切开,然后将他们分散到多台数据库主机上面,这样的切分方法就是一个垂直(纵向)的数据切分。
系统功能可以基本分为以下四个功能模块:用户、群组消息、相册以及事件,分别对应为如下这些表:
1.用户模块表user、user_profile、user_group、user_photo_album
2.群组讨论表groups、group_message、group_message_content、top_message
4.事件信息表event
模块之间的关系:
1.群组讨论模块和用户模块之间主要存在通过用户或者是群组关系来进行关联。一般关联的时候都会是通过用户id或者nick_name以及group的id来进行关联,,通过模块之间的接口实现不会带来太多麻烦。
2.相册模块仅仅与用户模块存在通过用户的关联。这两个模块之间的关联基本就有通过用户id关联的内容,简单清晰,接口明确。
通过这样的垂直切分之后,之前只能通过一个数据库来提供的服务,就被分拆成四个数据库来提供服务,服务能力自然是增加几倍了。
垂直切分的优点
u数据库的拆分简单明了,拆分规则明确
u应用程序模块清晰明确,整合容易
u数据维护方便易行,容易定位
垂直切分的缺点
u部分表关联无法在数据库级别完成,需要在程序中完成
u对于访问极其频繁且数据量超大的表仍然存在性能平静,不一定能满足要求
u事务处理相对更为复杂
u切分达到一定程度之后,扩展性会遇到限制
u过度切分可能会带来系统过渡复杂而难以维护
数据的水平切分,一般来说,简单的水平切分主要是将某个访问极其平凡的表再按照某个字段的某种规则来分散到多个表之中,每个表中包含一部分数据。简单来说,我们可以将数据的水平切分理解为是按照数据行的切分,就是将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中。
水平切分的优点
u表关联基本能够在数据库端全部完成
u不会存在某些超大型数据量和高负载的表遇到瓶颈的问题
u事务处理相对简单
u只要切分规则能够定义好,基本上较难遇到扩展性限制
水平切分的缺点
u切分规则相对更为复杂,很难抽象出一个能够满足整个数据库的切分规则
u后期数据的维护难度有所增加,人为手工定位数据更困难
u应用系统各模块耦合度较高,可能会对后面数据的迁移拆分造成一定的困难
一般来说,我们数据库中的所有表很难通过某一个(或少数几个)字段全部关联起来,所以很难简单的仅仅通过数据的水平切分来解决所有问题。而垂直切分也只能解决部分问题,对于那些负载非常高的系统,即使仅仅只是单个表都无法通过单台数据库主机来承担其负载,我们必须结合“水平”和“垂直”两种切分方式同时使用,充分利用两者的优点,避开其缺点。
每一个应用系统的负载都是一步一步增长上来的,在开始遇到性能瓶颈的时候,大多数架构师和DBA都会选择先进行数据的垂直拆分,然而,随着业务的不断扩张,系统负载的持续增长,在系统稳定一段时期之后,经过了垂直拆分之后的数据库集群可能又再一次不堪重负,遇到了性能瓶颈。
这时候我们就必须要通过数据的水平切分的优势,来解决这里所遇到的问题。对于我们的示例数据库,假设在最开始,我们进行了数据的垂直切分,然而随着业务的不断增长,数据库系统遇到了瓶颈,我们选择重构数据库集群的架构。如何重构?考虑到之前已经做好了数据的垂直切分,而且模块结构清晰明确。而业务增长的势头越来越猛,即使现在进一步再次拆分模块,也坚持不了太久。我们选择了在垂直切分的基础上再进行水平拆分。
下图展示了切分后的整个架构:
在应对不同的应用场景的时候,也需要充分考虑到这两种切分方法各自的局限,以及各自的优势,在不同的时期(负载压力)使用不同的结合方式。
联合切分的优点
u可以充分利用垂直切分和水平切分各自的优势而避免各自的缺陷
u让系统扩展性得到最大化提升
联合切分的缺点
u数据库系统架构比较复杂,维护难度更大
u应用程序架构也相对更复杂
第一部分:实施策略
1.准备阶段
对数据库进行分库分表(Sharding化)前,需要充分了解系统业务逻辑和数据库schema.绘制一张数据库ER图,以图为基础划分shard,直观易行,可以确保清醒思路。
2.分析阶段
1.垂直切分
垂直切分的依据原则是:将业务紧密,表间关联密切的表划分在一起,例如同一模块的表。结合已经准备好的数据库ER图或领域模型图,仿照活动图中的泳道概念,一个泳道代表一个shard,把所有表格划分到不同的泳道中。下面的分析示例会展示这种做法。这种方式多个数据库之间的表结构不同。
2.水平切分
垂直切分后,需要对shard内表格的数据量和增速进一步分析,以确定是否需要进行水平切分。这些数据库中的表结构完全相同。
2.1若划分到一起的表格数据增长缓慢,在产品上线后可遇见的足够长的时期内均可以由单一数据库承载,则不需要进行水平切分,所有表格驻留同一shard,所有表间关联关系会得到最大限度的保留,同时保证了书写SQL的自由度,不易受join、groupby、orderby等子句限制。
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、groupby、orderby都将被禁止,需要在应用程序层面协调解决这些问题。
3.实施阶段
如果项目在开发伊始就决定进行分库分表,则严格按照分析设计方案推进即可。如果是在中期架构演进中实施,除搭建实现sharding逻辑的基础设施外,还需要对原有SQL逐一过滤分析,修改那些因为sharding而受到影响的sql。
第二部分:示例演示
以下使用jpetstore(宠物店的电子商务系统)来演示如何进行分库分表(sharding)在分析阶段的工作。jpetstore来自原ibatis官方的一个Demo版本,SVN地址为:
由于系统较简单,我们很容易从模型上看出,其主要由三个模块组成:用户,产品和订单。那么垂直切分的方案也就出来了。接下来看水平切分,如果我们从一个实际的宠物店出发考虑,可能出现数据激增的单表应该是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)NOTNULL存根字段,并非表名,因此,一般来说,一个Sequence表只有一条纪录,可以同时为多张表生成ID,如果需要表的ID是有连续的,需要为该表单独建立Sequence表。
4.使用REPLACEINTO插入数据,这是很讨巧的作法,主要是希望利用mysql自身的机制生成ID,不仅是因为这样简单,更是因为我们需要ID按照我们设定的方式(初值和步长)来生成。
5.SELECTLAST_INSERT_ID()必须要于REPLACEINTO语句在同一个数据库连接下才能得到刚刚插入的新ID,否则返回的值总是0
实现该方案,应用程序同样需要做一些处理,主要是两方面的工作:
1.自动均衡数据库ID生成服务器的访问
2.确保在某个数据库ID生成服务器失效的情况下,能将请求转发到其他服务器上执行。
通过前面的章节,我们已经很清楚了通过数据库的数据切分可以极大的提高系统的扩展性。但是,数据库中的数据在经过垂直和(或)水平切分被存放在不同的数据库主机之后,应用系统面临的最大问题就是如何来让这些数据源得到较好的整合。
在应用服务器与数据库之间通过代理实现
AmoebaForMySQL主要是专门针对MySQL数据库的解决方案,前端应用程序请求的协议以及后端连接的数据源数据库都必须是MySQL。对于客户端的任何应用程序来说,amoebaForMySQL和一个MySQL没有什么区别,任何使用MySQL协议的客户端请求,都可以被AmoebaForMySQL解析并进行相应的处理,下图可以告诉我们AmoebaForMySQL的架构信息:
Amoeba使用指南:
其他的一些实现层可以参考:
Sharding扩容——系统维护不能承受之重
一般来说,“理想”的扩容方案应该努力满足以下几个要求:
1.最好不迁移数据(无论如何,数据迁移都是一个让团队压力山大的问题)
2.允许根据硬件资源自由规划扩容规模和节点存储负载
3.能均匀的分布数据读写,避免“热点”问题
4.保证对已经达到存储上限的节点不再写入数据
目前,能够避免数据迁移的优秀方案并不多,相对可行的有两种,一种是维护一张记录数据ID和目标Shard对应关系的映射表,写入时,数据都写入新扩容的Shard,同时将ID和目标节点写入映射表,读取时,先查映射表,找到目标Shard后再执行查询。该方案简单有效,但是读写数据都需要访问两次数据库,且映射表本身也极易成为性能瓶颈。为此系统不得不引入分布式缓存来缓存映射表数据,但是这样也无法避免在写入时访问两次数据库,同时大量映射数据对缓存资源的消耗以及专门为此而引入分布式缓存的代价都是需要权衡的问题。另一种方案来自淘宝综合业务平台团队,它利用对2的倍数取余具有向前兼容的特性(如对4取余得1的数对2取余也是1)来分配数据,避免了行级别的数据迁移,但是依然需要进行表级别的迁移,同时对扩容规模和分表数量都有限制。总得来说,这些方案都不是十分的理想,多多少少都存在一些缺点,这也从一个侧面反映出了Sharding扩容的难度。
取长补短,兼容并包——一种理想的Sharding扩容方案
原理
在这个模型中,有几个细节需要注意:ShardGroup的writable属性用于标识该ShardGroup是否可以写入数据,一个Partition在任何时候只能有一个ShardGroup是可写的,这个ShardGroup往往是最近一次扩容引入的;startId和endId属性用于标识该ShardGroup的ID增量区间;Shard的hashValue属性用于标识该Shard节点接受哪些散列值的数据;FragmentTable的startId和endId是用于标识该分段表储存数据的ID区间。
让我们通过示例来了解这套方案是如何工作的。
阶段一:初始上线
假设某系统初始上线,规划为某表提供4000W条记录的存储能力,若单表存储上限为1000W条,单库存储上限为2000W条,共需2个Shard,每个Shard包含两个分段表,ShardGroup增量区间为0-4000W,按2取余分散到2个Shard上,具体规划方案如下:
(上面说单表的存储上线为1000W条,但是为什么图中Table_0的范围是0-2000W?)
与之相适应,Sharding拓扑结构的元数据如下:
阶段二:系统扩容
相应拓扑结构表数据下:
从这个扩容案例中我们可以看出该方案允许根据硬件情况进行灵活规划,对扩容规模和节点数量没有硬性规定,是一种非常自由的扩容方案。
让我们还是通过一个示例来了解升级后的方案是如何工作的。
阶段三:不扩容,重复利用再生存储空间
图7.重复利用2000W再生存储空间的规划方案
相应拓扑结构的元数据如下:
这套方案综合利用了增量区间和散列两种路由方式的优势,避免了数据迁移和“热点”问题,同时,它对Sharding拓扑结构建模,使用了一致的路由算法,从而避免了扩容时修改路由代码,是一种理想的Sharding扩容方案。