说说分库分表的一个最佳实践

1,074 阅读20分钟
原文链接: mp.weixin.qq.com

概述

分布式数据库已经流行好多年,产品非常众多,其中分布式数据库中间件使用场景最广。本文主要是总结如何基于分布式数据库中间件做数据库架构设计,以充分发挥它的分布式能力。各个中间件产品功能核心原理相同,细节上有些区别。这里仅以阿里云的DRDS为例分析,在产品架构、功能、成熟度和市场占有率上,它都比同行产品有优势。

首先为了避免被误解为:「手里有把锤子,看什么都是钉子!」,说明一下不是什么业务都适合分布式数据库,更不是用了分布式数据库性能就一定能得到扩展。这个我在另外一篇文章《分布式数据库的拆分设计实践》已经有过分析。这里以 DRDS的使用为例更深阐述这个观点。

本文有关「线性扩展」的分析是来自于DRDS 产品团队梦实 的分享。有关如何发挥分布式资源能力是个人观点,仅供参考。DRDS 的功能相对很完备,如全局Sequence 、异构索引表、分库分表策略、分布式Join 、小表广播、分布式事务(柔性事务和强一致事务)、读写分离和只读实例、HTAP查询 等。这些在官网上都有详尽的介绍。这里总结的是官网上没有明示的或者重点突出的业务数据库架构设计经验。

好的数据库性能首先是应用设计出来的(还会有其他方面因素),如果你不认同这个观点,那本文并不适合你。

分布式数据库(中间件)架构

文章《一些关系数据库的架构总结》列举过很多分布式数据库的架构图,它们都有一个共同的特点就是主体功能都是在 MySQL数据库前面部署了一个中间件。这个中间件接管并响应应用的SQL 请求。所以它的基础必备能力就是解析SQL ,做分库分表路由,到底层MySQL 数据库里取数据并可能做一些计算(排序聚合等)然后返回给应用。这个中间件对应用屏蔽了表被拆分的细节。

运维视角下的DRDS

如上是运维视角下的DRDS的架构图。( 注:这里划分和后面各个概念都是个人理解。运维人员是能看到DRDS 的各个组成模块。)

DRDS 数据库整体上是分为两层。一个是中间件层,或者也叫服务层(也叫DRDS Server ),负责响应SQL请求,承担部分计算( SQL)功能。Server由多个特定相同资源规格( CPU和内存)的进程组成,运算数据会在内存里,但不持久化,所以Server层简单理解没有存储功能。也可以理解为 Server层是无状态的(内存数据可以丢失)。数据是存储在下面的数据库层(或者叫存储层)。这个数据库通常就是一组MySQL 实例(在云上是RDS MySQL 实例)。数据库层除了存储数据也承担了部分SQL计算功能,不过这里的 SQL 通常不会太复杂。所以也有一个不完全正确的观点:在分布式MySQL 数据库集群里,都是把MySQL 当存储用的。

有几个简单概念突出一下:

  • Server:由一组 DRDS Server(后简称Server )组成,Server 节点是部署在ECS 上的一个Java 进程,ECS 的资源规格(CPU 和内存)就是Server 节点的主要能力。用户购买的DRDS 实例实际上就是购买一组Server 节点。每个DRDS 实例至少会包含2个Server 节点(因为要高可用,其次是负载均衡),规格很大的实例,会有4或8个Server 节点组成(负载均衡是主要目的)。DRDS实例的规格决定了它的主要计算能力。

  • 物理实例:由一组MySQL 实例组成,不同的实例其包含的数据都是全部数据的子集(广播表的数据会在多个实例内部冗余这个例外)。每个实例有自己的Slave 实例,不是重点后面都忽略它。每个实例代表了一定的资源能力(CPU、内存和空间)。

  • 物理分库:在MySQL 里就是数据库,分库说的是这个数据库是总体数据的子集,一个MySQL 实例会包含多个分库,在RDS里默认是8个(外部实例默认不让改,这个设定导致了DRDS 的拆分设计思路在内部业务和外部业务上呈现不同的特点,其中有一个比较难理解)。

  • 物理分表:MySQL 每个数据库下的普通表(非分区表),分表说的是它的数据是总数据的子集,并且在所有实例里有很多结构相同的表(只是可能表名后面的编号不同)。每个物理分库下可以有1个或多个分表,不同产品特点不一。

  • 物理QPS:所有 MySQL实例的QPS 总和,衡量数据库层压力的一个指标。TPS 同理。

  • 逻辑QPS:所有 Server节点的QPS 总和,衡量DRDS 实例压力的一个指标。TPS 同理。

`DRDS`在内部叫`TDDL`,没有`Server层`,而是跟应用代码部署在一起(应用引用`TDDL`的一个`Jar包`)。拆分规则、数据库拓扑和连接信息等配置会由其他模块(集中式部署)推送(`PUSH`)到各个应用客户端(可能客户端也有自己`PULL`逻辑)。

业务视角下的DRDS

如上是业务视角下的DRDS架构图。对业务而言看到的就是一个数据库实例,实例下有库有表。这几个概念很简单:

  • 逻辑实例:就是DRDS 实例,会有个链接地址,通常是域名或者某个负载均衡产品上的VIP

  • 逻辑库:就是DRDS 实例下的数据库,后端是由一组物理分库组成。

  • 逻辑表:就是业务表,后端是由一组物理分表组成。逻辑表理论上跟传统数据库一样,不同分布式数据库产品支持的类型可能有细微差别。如某些分布式数据库产品可能不支持某些特殊类型的列,或者不支持外键,或者不支持全局索引等。不能简单的按传统数据库的用法去用。

理论上业务只要申请到DRDS实例然后建库建表即可。 稍有不同的时候需要设计物理分库的数量和物理分表的数量。后面重点首先是介绍这个分库分表的设计,然后是业务SQL 如何写最佳。

分库分表设计

分库分表设计首先要根据业务选择合适的拆分维度以及拆分策略。这个在前文《分布式数据库的拆分设计实践》已经有过分析。这里重点说的分多少个库和分多少个表的选择考虑。

为什么要拆分?

技术上DRDS 也支持不做分库分表拆分这种用法,不过这个中间层就显得多余了。当业务要做水平拆分决定的时候,通常是因为业务碰到下面几个难题:

  • 集中式数据库的连接数瓶颈 :应用是无状态的,可以水平扩容,但数据库是集中的。数据库的实际连接数越来越高,逐步消耗数据库性能以及达到连接数设置上限 。

  • 存储容量瓶颈:业务数据量越来越大,单机硬盘扩容达到上限,或者存储扩容成本越来越高 。

  • QPS瓶颈:集中式数据库的业务QPS上不去,数据库主机资源利用率到达瓶颈( CPU瓶颈或者IO 瓶颈等)。

具体是哪个瓶颈会影响分库分表数量的选择。

分多少个表合适?

首先常见的一类问题是“表数据量到多少就要拆分?或者表大小到多少就要拆分?”。业务方总希望能用一个万能的公式就直接给出答案。如果产品给出了一个公式,那多半是被逼的;如果业务方简单去套用公式,那多半就是不清楚数据库细节懒于分析。

换个问题“拆分为多少个分表比较合适?”。总分表数也是数据的分片数。总数据分表数目一旦确定后,后期调整数量就非常不方便(那意味着全量数据重分布)。这个就是选择这个分表数的第一个考虑点。

分表是存在于分库中,分库在分实例里,多个实例组成了全部的业务数据。关于分表数这里倒是有个简单万能的公式:

总分表数(N ) = 总物理实例数(X )* 每个实例下的分库数(Y )* 每个分库下的分表数(Z )

所以,当你定一个总的分表数N时,这个 N要能够拆分为三个数(XYZ)的乘积。这个是选择分表数的第二个考虑点。而这个 XY的选择都有讲究。

最后分表数目也不能太大,否则元数据管理成本会比较高,对稳定性和性能都有影响。这点不同产品能力不一样。DRDS的内部案例里分表数最多到 4096

所以分多少个表此时还没有结论。先往后看。

分多少个实例合适?

每个物理实例占用了一定的主机资源(CPU 、内存和空间),提供一定的计算和存储能力,重点是计算能力,具体指标就是QPS (也包括TPS )。

我们先假设每个物理实例都独占一台主机资源,那么一个物理实例的能力上限就是那个主机的能力上限。如果一个DRDS 实例后端包括一个物理实例(默认Slave 实例不提供服务,这里不考虑读写分离场景),那理论上它数据层最大的能力就是一台主机的能力;如果后端包括两个物理实例,那理论上它数据层最大的能力就是两台主机的能力。

所以,一个DRDS实例的计算能力有个简单公式就是:

DRDS 实例数据层计算能力(物理QPS ) = ∑ 物理实例iQPS  ,  i  ∊ (1, X )

所以,如果当初主要是解决集中式数据库的计算能力瓶颈的时候,拆分为多少个实例就决定了DRDS实例的理论上的上限。大规模业务分布式数据库集群能力评估的时候,这个公式是主要考量点。评估的结果如果少了一个数量级,则很可能导致业务有性能风险。

还有个别业务一个是基于存储能力考虑的,这个同理。

注意:

  • 这里的计算能力是理论上的,实际能不能发挥还取决于SQL 写法。

  • 理论上总物理实例数这个是可以很方便调整的,就像细胞分裂和合并一样。1个实例可以分裂(扩容)为2个实例,8个实例也可以合并(缩容)为4个或2个实例。所以,总实例数建议是2的幂,方便扩容和缩容。但这要求不是必须的,不同产品实现方式不一样。

  • 实际情况一个物理实例不一定是独占一台主机资源的。在阿里云上, RDS MySQL实例都是有具体的规格的(如多少 CPU多少内存多少空间等),资源之间有一定的资源隔离策略。所以存在拆分为2个物理实例,但是这2个实例依然在一台主机上。由于 MySQL的功能特点,2个小实例的计算能力很可能也高于合并为1个大实例后的计算能力。具体以业务实际运行结果为准。

  • 同样,总实例数最大值不同产品能力不一样,DRDS 内部最多128 个实例。

1个实例是否能方便扩容为2个实例,取决于这个实例里有多少个分库。

分多少个分库合适?

如果1个实例的分库数是2或2的倍数,那拆分为两个实例还是比较方便的,简单说把分库对半分。具体就是搭建一个Slave 实例,数据同步追上后断开同步,分别去掉一半分库。如果1个实例只有1个分库,那这个方法就行不通。还要继续看有多少个分表,然后对分表集合进行对半分。但总体操作上没有对分库集合对半分要方便。

这种对半拆的方案是最简单的,但并不是唯一的选择。DRDS 有能力通过精卫对数据全量进行重分布,从而突破不可继续对半分 的限制,只是要消耗更多资源和更多时间。通常运维会选择对半拆。所以分库数量决定了实例可以分拆(扩容)的次数。

注意:

  • 在阿里云RDS里,每个实例默认8个分库是固定的。所以总分库数 = 总实例数 * 8. 这个也决定了,通过对半拆分库的方式最多能扩容3次。

  • 在电商内部业务里,每个实例下分库数可以是(4,8,16,32,64,128)等,不同业务要求不一样。

  • 每个分库名在全局范围内不重名(尾部编号不一样)

再看分表数

由上面分析知,分多少个实例(X )主要决定了整个DRDS 实例后端计算能力的上限(部分业务还要考虑存储能力因素,但越是规模大的业务,对计算能力要求弹性远高于存储能力)。每个实例下多少个分库( Y)决定了未来的弹性扩容的能力(倍数)。

所以也可以粗略的说XY的乘积决定了这个DRDS实例后端未来最大的计算能力。 在业务初步上线的时候,可以X 很小,Y 稍微大一些;在业务规模上去后,再调大X 调小Y 。这就是分布式数据库容量评估的艺术。可以说考虑未来若干年内的需求时,这个XY的乘积基本确定了,然后总的分表数(N)就看每个分库下的分表数( Z)了。

关于这个Z的指定, DRDS在内外业务上的处理方法稍有不同。DRDS建表语句支持指定分表数,指定的就是这个 Z的值(并不是指定总的分表数)。DRDS 对这个Z 值并没有建议要求。可以是奇数、质数、合数都没关系。每个分表名只是在分库内部不重名,不同分库的分表名是一样的。

总分表数会通过公式 N =X *Y *Z 来计算。这个计算结果值不宜超过目前实践最大值(4096 )。

在内部业务设计里,分表总数是在建表的时候指定,一般是2的幂,16起步,最大4096.然后你才会看到每个分库下有多少个分表。每个分表的命名在全局不重名(尾部编号不同)。

这是两种设计习惯,当先熟悉了一种后再用另外一种会有一点不适应,至少我是这么感觉的。

分表数的选择有可能要考虑业务数据分布特点。当有很多热点数据的时候,选择分区策略后要尽可能让每个分表数据尽可能分布均衡,并且访问量也尽可能的均衡。但是绝对均衡也很难,只是说如果业务上访问有热点数据,总分表数尽量大一些,以便热点数据能够分散的开一些。这只是个建议,对于特别热点的访问( 比如说秒杀IPhoneX)这个效果也不明显( 还需要其他手段并用,热点行应对策略以后有时间再分享)。

线性扩展能力

前面分库分表的设计是把分布式数据库集群的最大能力尽可能的提升,但并不意味着业务SQL就一定能发挥出数据库的分布式能力。所以 DRDS提出一个线性扩展的概念。

这里线性扩展DRDS 用来描述SQL 的一种能力的。我不确认在分布式领域或者其他什么领域是否也有这个概念。如果有先忽略它在别的地方的含义。

线性扩展指的是随着业务规模成倍的增长,对分布式数据库后端实例进行弹性扩容(增加实例数)后,业务SQL的响应时间(RT)能维持不变或者小范围的变慢,以及吞吐量能相应倍数的增长。

线性扩展能力是衡量单个SQL 的扩展性能力,跟SQL 写法有关。不同的SQL表现可能不一致,我们只考虑核心业务 SQL或者对性能影响很大的SQL

拆分键的影响

当业务表拆分为N个分表后,分布在 X个实例里。DRDS建议业务 SQL尽可能的带上具体的拆分条件。这样Server节点可以直接将 SQL路由到后端具体的MySQL实例中。 每个请求落在具体的实例节点上,不同请求可能落在不同的实例节点上,所有实例同时提供服务,DRDS 实例整体吞吐量最大。如下图,当SQL 都带拆分键(等值条件)后,1个逻辑QPS 就对应1个物理QPS

此时如果后端数据层扩容增加了1个MySQL 实例,则物理QPS 提升50% ,逻辑QPS 也提升50% 。当然这里前提是Server 节点层不是瓶颈(Server 节点的QPS 能力高于MySQLQPS 能力。这类SQL 的线性扩展能力就很好。

如果SQL 没有带上拆分键,则Server 节点会将SQL 请求路由到所有MySQL 实例。1个逻辑QPS 就对应3个物理QPS

此时如果后端数据层扩容了,虽然物理QPS提升了 50%时,但是Server节点的能力并没有增加。这类 SQL的线性扩展能力就不好。

这个对比很好理解。

拆分键IN查询业务的线性扩展能力分析

上面说的SQL带了拆分键并且是等值条件。也有一类 SQL带了拆分键,但是是拆分键IN查询。 Server节点在处理这个 SQL时,会将IN后的 LIST值一个个判断在后端哪个MySQL实例里。如果都是属于同一个 MySQL实例,那效果跟上面那种带了拆分键等值条件一样。这是最好的情形,取决于业务数据。如果 LIST里的值都是分属于不同的MySQL实例,则近似于上面不带拆分键查询情形。

如下面是买家订单查询业务举例。拆分键是订单ID,16个分实例。每个订单 IN 查询里的列表长度平均为2.假设分布均匀,每个逻辑 QPS 就对应2个物理QPS 。如果单个实例的QPS 能力是1000,则 DRDS 的逻辑 QPS 能力就是8000。

但是如果后期业务规模增长,平均一个买家订单数量扩了100倍( IN后面列表长度200),实例扩容了4倍。此时整个 DRDS的逻辑 QPS能力是1000。相比之前的业务能力,这个是下降了。 大家很容易看到 导致业务能力下降的关键是新业务下 SQL IN 列表长度比实例数要多很多。这是一类业务特点。

还有一类业务即使业务规模增长,其 IN后的列表长度不会大变。比如说人口业务。根据子女 ID  查询相关信息。虽然总人口增长了很多,单每个家庭子女数量平均在1-2个左右。这个场景下如果实例扩容了,业务是能收获相应倍数的 QPS提升。

分布式 Join

上面描述的拆分键 IN  查询还是很直观的,容易理解。稍微复杂一点的业务 SQL会使用表连接。 这个又分多种情形。 一是做根据参加表连接的表类型。分为非拆分表和拆分表的连接、拆分表和拆分表的连接。后者还分拆分维度是否一致。 二是根据连接条件是否是拆分键。有些连接条件是一方的拆分键是另外一方的非拆分键。

分析分布式 Join 的线性扩展能力关键首先了解连接的算法。通常是嵌套循环( Nested-Loop Join )。然后判断在应用目标表连接条件时该条件是否是拆分键。再参照前面拆分键 IN 查询的表现综合分析。

本节只是个引子,具体分析可以参考梦实的分享。

总结

分布式数据库(中间件)的特点就是分库分表,这个比较灵活和容易理解,使用场景最广。具体分多少个实例多少个库会影响分布式数据库性能理论上限。此外, SQL 能否将该分布式数据库的性能能力都发挥出来,取决于 SQL 的写法,一般跟拆分键的行为有关。不同分布式数据库产品的功能有细节上的差别,但是分库分表的逻辑基本相同,所以上面的分析同样适用于其他分布式数据库(中间件)产品。

OceanBase里的租户的能力取决于资源单元( Unit )的数量,OceanBase里的分区表设计思路等同于分库分表思路。所以上面的思路也可以在OceanBase的业务数据库设计里参考。

个人总结,如有表达不当之处欢迎指正。公众号文章还没有留言功能,可以给公众号留言。

参考

  • 阿里云, 分布式数据库DRDS 最佳实践,https://help.aliyun.com/document_detail/51308.html

  • 梦实, 分布式数据库——从线性扩展谈分布式JOIN,https://yq.aliyun.com/articles/156276

推荐阅读