概述
水平拆分的概念随着分布式数据库的推广已为大部分人熟知。分库分表、异构索引、小表广播、这些功能几乎是产品功能需求标配。然而有些客户使用分布式数据库后的体验不尽如意。
本文尝试从数据的角度总结分布式数据的复制(replication)和分区(partition)技术原理和方案,其中分区也有称为分片(sharding),希望能引起读者一些思考,在分布式数据库选型中能注意这些细节的区别,选择适合业务的数据水平拆分方案。
分布式数据库架构
分布式数据库以集群形式存在,有多个节点。集群架构有共享磁盘架构(shared-disk)和无共享架构(shared-nothing)。后者有时也称为水平扩展(horizontal scale)或向外扩展(scale out),本文主要总结无共享架构方案。
无共享架构的各个节点之间的通信都是软件层面使用网络实现,不同产品在架构不同导致这个细节也不同。有些架构是计算与存储分离。计算节点特点是无状态(即数据不要求持久化),通过集群方式管理,可以水平扩展;存储节点有数据,使用复制和分区技术,节点间任务集中调度或者独立交互。了解这个架构细节都可用性分析会更加具体。具体分布式数据库架构有哪些请参考《一些关系数据库的架构总结》。
这里节点的实际体现形式可以是一个机器,也可以是机器上的一个实例。比如说有些数据库支持单机安装多个实例,如MySQL。每个节点具备一定的资源和能力。资源指的是CPU、内存和磁盘,能力是提供数据读写和存储能力。分布式数据库需要把多个节点的能力聚集到一起集中管理,只是不同分布式数据库产品对资源的管理能力各有特点。
在分布式数据库里,数据随处可见,这是最容易让人混淆的地方。因为数据经过复制和分区后会有两种存在形式:副本(replica)和分区(partition)。
数据的复制(replication)
复制(replication)指在几个不同的节点上保存数据的相同副本(replica)。复制提供了冗余的能力。其作用一是提供高可用能力:如果一个节点不可用,剩余的节点可以快速提供数据服务。作用二是提供读写分离能力。常见的有两副本和三副本架构。
多个副本内容相同,角色会有区分。常见的是一个副本是Leader角色(有的也称主副本),默认提供读写服务;其他副本是Follower角色(有的也称备副本),默认不提供服务。这种架构也称为基于单Leader的(Single Leader-based)。还有其他架构是多Leader的,每个Leader都有数据要复制到其他Leader或Follower,这种架构会有个明显的问题就是数据冲突处理。如果产品层面不处理,用户直接使用风险会很高。
后面讨论的是前者:基于单Leader副本架构。
多副本之间数据同步不是依赖业务多写,而是采用副本间复制事务日志(Redo)技术。复制的方式有同步复制和异步复制。使用同步复制方式,备副本要收到Redo并落盘主副本才能提交,也叫强同步;使用异步复制方式,Follower副本相对Leader副本内容会有延时,具体延时多少取决于Leader副本上事务量、网络传输速度、Follower副本所在节点的负载和能力。强同步的缺点时主副本写性能会下降,同时如果备副本不可用主副本也不能提供服务(变相的解决方案是复制方式降级为异步复制)。
传统关系型数据库还有一种用法一主两备架构,使用同步复制,只要任何一个备副本收到Redo,主副本的事务就可以提交。这个方案优点是保障了数据在多个副本中存在,高可用时有候选副本,也不用担心挂掉一个备副本会影响主副本。它的缺点是不能自动知道哪个候选副本拥有主副本最新最全的数据,也不强制要求两个备副本都要拥有全部数据。
还有一类三副本架构在复制时使用的是Paxos协议,三副本会就Redo落盘事件进行投票,有两个副本成功了Leader副本的事务即可提交。这个表面上跟上面传统一主两备的三副本效果一样,实际上还是有区别的。区别一是使用Paxos协议时,如果Leader副本自身投票慢了,两个Follower副本投票成功,Leader副本的事务也是能提交的;区别二是第三个副本最终也必须写Redo成功,否则其状态就是异常,产品自身可以发现并自动修复(如重新创建一个副本);区别三是使用Paxos协议时,在Leader副本不可用时还可以自动选出新的Leader副本并且拥有老Leader副本的最新数据。这里其实说的是高可用机制。同样,这里对用户而言也不知道哪个Follower副本拥有最新最全的数据,如果访问Follower副本(读写分离),也可能发现数据有延时。
大部分数据库做副本复制使用的是Redo,也称为物理同步。在应用Redo的时候直接是数据块变更。使用物理同步机制的备副本是不提供写服务,不能修改。还有一类复制使用的是Binlog,也称为逻辑同步。Binlog里只包含已提交的事务,并且在应用的时候是通过执行SQL。使用逻辑同步的备副本通常也可能是主副本,可以修改(如MySQL的双向复制架构Master-Master)。如果目标端数据不对,应用SQL会失败,这个复制就会中断需要人介入处理。这也进一步加深了主备副本不一致的概率。
关于副本角色的粒度,有多种实现方案。
传统关系数据库主备架构,主副本或备副本的粒度就是实例。对于主实例(Primary)而言,里面所有数据库(或SCHEMA)的所有表的角色都是主;备实例(Standby)里数据则都是备副本。如果发生高可用切换,业务会中断几十秒或几分钟然后恢复(需要人工处理或自动化脚本处理)。
还有一种粒度是到表。即一个节点内有些表是Leader副本,有些表是Follower副本,这样这个节点就不能简单的说是主节点(实例)或备节点(实例)。这个副本角色细节业务也是可以获取的,如果发生高可用切换,业务会中断十几秒然后恢复。
还有一种粒度是存储级别的定长块。即一个节点的存储里,部分数据块是Leader副本,部分数据块是Follower副本。这种对业务就完全透明,业务基本不感知高可用切换。
数据的分区(partition)
上面总结的是数据的复制(冗余,多副本),对于非常大的数据集(表)或者非常高的访问量(QPS),仅仅靠复制是不够的,还需要对数据进行分区(partition),也称为分片(sharding)。
分区粒度
首先这里的分区(partition)是一种抽象概念,在不同数据库产品里这个体现是不一样的。如在MongoDB, Elasticsearch中体现为分片(shard),在HBase中体现为区域块(Region),Bigtable中体现为表块(tablet),ORACLE中体现为分区(partition),Couchbase中体现为虚拟桶(vBucket)。可见不同的数据库产品数据分区的粒度不同。在分布式关系数据库中间件中,分片的粒度是分表(物理表);在真正的分布式关系数据库里,分片的粒度有分区(partition,同ORACLE)或者区域块(Region)。
分区粒度对业务研发的使用体验影响很大。
比如说中间件常用分库分表方案,使用时对开发和运维会有一些要求。如建很多同构的表并后期维护、要求SQL带上拆分键,还有一些功能限制(如跨库JOIN问题)、底层存储节点用的数据库自身高可用和多副本的数据一致问题等等。不同的中间件产品能力上也有区别,互联网大厂的产品由于内部场景培育很久,做的相对成熟一些。
体验最好的分区粒度就是存储级别的Region,业务研发完全不用关心分片细节,也无法干预分片细节。当有些场景追求性能需要干预数据分布特点时就不好处理。
介入这两种策略之间的就是分区。物理上业务只要创建一个分区表,根据业务特点指定分区策略(包含分区列、拆分算法、分区数目等)。
数据复制是为了冗余和高可用,数据分区主要是为了可扩展性。不管使用哪种分区方案,业务的每条数据(记录)属于且仅属于一个分区(或分片sharding),同一个分区(分片)只会存在于一个节点。前面说了每个节点代表了一定的资源和能力。当复制和分区(分片)一起使用的时候,注意区分你看到的数据。
分区策略
分区的目标是将大量数据和访问请求均匀分布在多个节点上。如果每个节点均匀承担数据和请求,那么理论上10个节点就应该能承担10倍于单节点的数据量和访问量。这个理论是忽略了复制产生的Follower副本的存在。Follower副本的空间和内存是不可能跟其他Leader副本共享的,但是计算能力(CPU)是可以的。当所有节点都提供服务的时候(多活),是计算资源最大利用。
然而如果分区是不均匀的,一些分区的数据量或者请求量会相对比较高,出现数据偏斜(skew),这个可能导致节点资源利用率和负载也不均衡。偏斜集中的数据我们又称为热点数据。避免热点数据的直接方法就是数据存储时随机分配(没有规则)给节点,缺点是读取的时候不知道去哪个分区找该记录,只有扫描所有分区了,所以这个方法意义不大。实际常用的分区策略都是有一定的规则。
这个规则可以是业务规则,也可以不是。
业务规则的分区首先是选取一个或一组列作为分区键,然后选取拆分方法。比如说根据键的范围(Range)分区,分区数量和边界时确定的(后期还可以新增分区)。好处时针对分区键的范围扫描性能会比较好。分布式数据库中间件的分库分表、分区表的分区都支持RANGE 拆分函数。各个产品拆分细节上面会有一些创新。Range分区的缺点是某些特定的访问模式会导致热点。比如说根据时间列做RANGE分区,业务写入和读写数据集中在最近的时间,就可能导致各个分区负载不均衡。这只是一个缺点,业务层面还要考虑这样做的好处。比如说删除历史分区比较快。
还有种拆分方法是散列(HASH)分区,分区数量和边界是确定的(后期可以做分区分裂)。这时各个数据的分布是否均衡就取决于各个产品实现机制。大部分做法是使用一个散列(HASH)函数对Key计算一个值,然后针分段存储。
有的产品会使用这个HASH值对分区数取模,这个方法可能引起分区数据分布不均匀(若MySQL的Key分区)。此外如果要调整分区数,则需要移动所有数据。ORACLE的HASH分区时会先选取最接近分区数的一个2的幂值,对于分区数大于这个值的分区,会从前面分区里调过来。所以ORACLE 建议HASH分区数为2的幂。M有SQL建议Key分区数为奇数时数据分布最均匀。
此外在现有分区下还可以再做一次分区,分区键和分区方法都可以不一样。通常称为两级分区。比如说分库分表时,分库和分表策略不一样就是两级分区;分区表也支持两级分区。
有业务规则的分区方案的特点就是使用上。SQL如果要性能好建议带上分区键,这样分布式数据库才可以直接定位到所访问数据所在的分片;否则,数据库就要扫描所有分区去查询数据。通常分区键只能选取一个或一组业务字段,代表的是一个业务维度,那么另外一种业务维度的SQL请求性能就会不好。个别分布式数据库产品在HASH 方法上支持两种维度的分区列,其前提是在业务构造数据时让这两个列有着内部一致的分区逻辑。
详情可以参考《说说分库分表的一个最佳实践》。
另外一种分区策略就是无业务规则的,在存储级别按块的大小切分为多个定长块(Region)。这个分区对业务而言就是透明的,所以使用体验上会相对好一些。
不过,分布式数据库里的数据分区除了存储数据还要提供读写服务。业务读写数据的SQL本身是带业务逻辑的,如果一次SQL请求访问的数据分散到多个分区,而这些分区又散落在不同的节点上,不可避免的会发生跨节点的请求。如果是多表连接,这种情形更容易出现。如果这个业务请求有事务,那这就产生了分布式事务。分布式事务解决方案有两种,强一致的两阶段提交(XA)方案和最终一致的TCC方案。详情请参考《说说数据库事务和开发(下)—— 分布式事务》。
这里主要提示跨节点的请求带来的性能衰减。当然,硬件方面万兆网卡加RDMA技术下网络延时已经缩小很多,但是当分布式数据库的请求量(QPS)非常高时,或者分布式数据库是多机房部署(比如说两地三中心)时,跨机房的网络延时还是不可忽视,跨节点的请求带来的性能衰减也会很明显。所以有业务规则的分区策略可以提供策略给业务控制自己的数据分区分布特点,非常适合做异地多活和单元化类业务。此外还有个常用的规避跨节点请求读的方法就是小表广播,即将个别没有分区的表的数据复制到其他分区所在的节点,这样相关业务数据分区的JOIN就是在本地节点内部完成。这里就看复制使用的是物理同步还是逻辑同步,以及同步的延时是否满足业务需求。
分区数量
关于分区数量也需要评估。如果是无规则的分区策略,由于每个分区(分片)是定长块,那么分区数量就由总数据大小除以定长块大小,对业务也是透明的。这里总结的是有业务规则的分区的数量。
使用分区的目的是为了扩展性,具体就是能将不同分区分散多多个节点上,发挥多个节点的资源和能力。所以分区数一定要大于可用的资源节点数,为了考虑到将来分布式数据库可能会扩容,分区数应该是数倍于当前规划的节点数。这是一个总的指导思想。由于不同的分布式数据库其节点的表示方法不一样,实施的时候会略有不同。
比如说在分布式数据库中间件架构里,数据存储的节点是实例,数据分区的粒度是分表(物理表),中间还有一层分库的维度。分布式数据库实例:总物理实例数:总物理分库数:总物理分表数=1:M:N:X 。X是分区的数量,N 是总分库数。X 是固定的,如果要调整分区数,成本非常高,所以一般都是提前规划好。N 是总分库数,是2的幂。 M 是实例的数量,也建议是2的幂,决定了最大能用多少节点的资源。 N/M 的结果决定了未来能扩容的倍数。分布式数据库中间件由于数据分区落在具体的节点后就不能自由移动,其扩容方式多是对每个实例一分为二,最好的途径就是利用数据库(MySQL)自身的主从复制搭建新的备实例扩容节点数。
此外分区数还要考虑到单个分区的容量和请求量是否满足需求。即分区是否到位。这个也是需要业务评估的。在使用分区表的分区方案的分布式数据库里,分区数也是结合上面两点考虑的。
当然分区数太大了,可能会增加分布数据库内部管理成本。分区数量跟分区粒度恰好是相反关系,二者都需要取一个合适的值。
分区数量一旦确定后,调整的成本非常高,通常会引起数据重分布。有些产品可以针对特定类型的分区做分区分裂。如RANGE分区可以分裂为两个RANGE, HASH分区也可以一分为二。只要这个分区分裂的逻辑是数据库内部逻辑实现,保证数据不丢,且对业务透明的,那么风险就很低值得考虑。
分区负载均衡
随着时间的推移,数据库一直在发生各种变化。如QPS增加,数据集更大,或者新增/替换机器等。无论哪种都需要将部分数据分区和相应的请求从一个节点移动到另外一个节点,这个过程称为分区的再平衡(rebalance)。业务对再平衡的要求就是平衡过程中对业务当前读写影响要可控,数据读写服务不能中断。还有一点就是为了再平衡应尽可能少的迁移数据。
前面两个要求都不难满足,最后一个要求就考验各个分区方案的灵活度了。当分区粒度是存储级别的Region时,分区迁移的粒度就是Region,这个对业务也是透明的;分区粒度是分区时,这个取决于各个产品对节点资源管理的设计。比如说有的设计可以做到只需要迁移分区就可以调整各个节点的资源利用率和负载;如果分区方案是分库分表,此时分区粒度是分表。但是数据迁移的单位通常还是实例,利用数据库原生复制能力搭建新的级联备实例,然后新老实例分别删除一半分库数据。这里就迁移了不必要的很多数据分区。
分区访问路由
现在数据分区方案已经确定,业务数据分布在多个节点上。业务应用访问数据库如何连接呢?再分区负载均衡发生后部分分区节点发生变化,业务应用是否要修改连接?这个就是分区访问路由问题,是分布式数据库的基本能力。理论上分区访问路由有三种方案。一是每个节点都可以进行路由转发(如果请求的数据不在该节点上,该节点可以转发应用请求到正确的节点上);二是设置一个中心模块负责接受请求并转发到正确的节点上;三是应用自己获取分布式数据库所有分区的节点信息,直接连接对应的节点,不需要其他组件提供路由功能。
大部分分布式数据库架构,选择了第二种方案,有一个负责分区路由访问的模块。有些产品同时支持这三种方案。 针对分区路由问题情况还可能更复杂。如一个事务有多条SQL时该路由到哪个节点。此外就是如果负责路由的节点故障,或者分区所在节点故障,这个路由不可用或者失效时会如何恢复路由服务。
SQL线性扩展能力
当数据分区方案确定、分区路由问题也解决了后,运维和业务架构为业务的搭建了一个好的分布式数据库环境。很多业务误以为用上分布式数据库后,就一定会很好,或者扩容后业务的性能也能相应的提升。实际使用经验并不一定如此。还是前面那句话使用分区方案主要是获得扩展性,其关键就是分区分布在更多的节点上,能利用上更多节点的能力。
但这个并不是指让单个SQL利用更多节点的能力。举个例子在OLAP业务里,一条SQL 如果能让很多节点同时提供服务,其性能当然是最好的。不过这样的SQL的并发不能太多,否则很容易让所有节点都很忙。即使分布式数据库扩容了节点将分区进一步打散,由于业务的访问压力和数据量也会增加很多,很可能依然是每个SQL同时让所有节点为其服务,这个SQL的吞吐量并不会随着这个节点数量的扩容而得到相应的提升。
分布式数据库的优势在于对于空间问题和请求访问问题分而治之。针对每个分区的访问,由该分区所在的节点响应即可。即使该SQL 并发很高,由于访问的是不同的分区,分别由不同的节点提供服务。每个节点自身也有一定能力满足一定的QPS,所有节点集中在一起就能提供更大的QPS。这个时候如果扩容节点数量,该SQL总的QPS也能获得相应的提升。这是分布式数据库里最好的情形。
第二个例子根据PK 访问表,并且PK还是主键等。通常我们都建议分库分表或者分区时,业务SQL尽量带上拆分键就是这个道理。但是如果业务场景确实无法带上拆分键,除了强制扫描所有分区外,还有个解决方案就是全局索引表。全局索引是独立于数据分区存储的,全局索引可以避免扫描不必要的分区,负面作用就是业务分区的写操作很可能带来分布式事务。
以上两个例子就是分布式数据库里SQL的先行扩展能力的两个极端。前一个场景SQL没有扩展能力,后一个SQL的扩展能力几乎是百分百。大部分SQL的先行扩展能力就界于两者之间。比如说SQL里是分区列的IN条件。这个SQL的先行扩展能力取决于这个INLIST的数据特点。如果恰好每次都是命中同一个分区,那跟分区列等值访问效果一样好;如果INLIST的数据命中绝大部分分区,那就接近OLAP 场景的那个SQL。有些业务增长后,这个INLIST的长度基本不变。比如说人口业务,虽然总人口的激增,但每个家庭的子女数量大部分在1-2。这是一类特点,访问这个子女数据的SQL的先行扩展能力会很好。另外一个例子就是买家订单查询业务。10年前每个买家一段时间的订单数量可能就几个,如今每个买家一段时间的平均订单数量可能在几十或几百。
比INLIST 更复杂的逻辑就是表连接。 表连接时的条件是否是分区列,每个具体的连接值会相应命中多少个分区,是否有分布式执行计划等等。都会影响这个SQL的线性扩展能力。
对于无业务规则的分区方案,虽然分区对业务是透明的,但不可否认的是数据分区是分布在不同的节点上,只要业务读写这些数据,数据分布特点就会影响到SQL的性能。对于业务而言,该如何选择?如果业务通过分区策略控制数据分区分布特点,能够获得更高的性能,业务是否愿意选择会影响分布式数据库的选型。而不同分区方案在运维方面的特点也不一样,是影响选型的另外一个因素,这里就不细说。
蚂蚁的分布式数据库最佳实践
蚂蚁金服的业务规模非常大,业务模块划分非常细。以网商银行非常核心的交易、账务和支付模块举例,每个业务模块的数据经分布式数据库中间件(SOFA的DBP)拆分为多个OceanBase租户(实例)下百库百表,每个表同时变更为OceanBase自身的分区表,分为100个分区。总共有多个OceanBase集群,每个集群横跨杭州上海和深圳五机房,并同时提供服务。这里的数据总共分为10000个分区,不同分库下的数据分区的Leader副本分别位于不同的机房。不同分表之间可以分别进行结构变更(灰度发布能力),不同OceanBase租户甚至集群之间是物理隔离的,这是金融核心业务拆分有使用分库分表的第一个原因。
业务层面数据是按用户维度拆分的,不同的用户访问不同的机房的应用和数据。业务层面的流量分配规则和数据分区Leader副本分配规则保持一致并联动,实现了任意时刻的在线业务流量机房间比例调整。这是拆分使用分库分表的第二个原因。
OceanBase集群在蚂蚁金服业务里的核心作用是在数据库层面解决数据副本三地分布的强一致和高可用切换问题,并且提供了在线分区迁移和租户弹性伸缩能力。
后记
本文首先针对分布式数据库种的数据存在的两种形式副本(复制产生的)和分区(分区产生的)进行区分。然后总结了分区方案需要考虑的几个点:分区粒度、分区策略、分区迁移和负载均衡、分区数量和分区路由问题等。即使这些都考虑好了,也只是分布数据库这个初局做好了。后面业务能否发挥分布式数据库的优势就取决于业务SQL的写法是否有很好的线性扩展能力。最后简单总结了蚂蚁金服支付宝和网上银行在分布式数据库架构方面的最佳实践。
推荐阅读
- 分布式数据库的拆分设计实践
- 说说数据库事务和开发(下)—— 分布式事务
- 如何基于OceanBase构建应用和数据库的异地多活
- 揭秘OceanBase的弹性伸缩和负载均衡原理
- 网商银行 × OceanBase:首家云上银行的分布式数据库应用实践
- [Designing Data-Intensive Applications(文中配图大部分来自本书)]()
更多后续分享敬请关注公众号:obpilot