跳至主要內容

KunlunBase弹性扩展和数据分布能力

Klustron大约 21 分钟

KunlunBase弹性扩展和数据分布能力

数据库系统为什么需要弹性伸缩能力

使用大量普通服务器组成集群,并且在其上部署具备优秀的扩展能力的分布式数据库,才能确保应用系统的吞吐率、性能和数据量可以持续增长的同时,提供流畅顺滑的用户体验。弹性伸缩能力是分布式数据库扩展能力的基础,而数据分片(data partitioning)是弹性伸缩的前提条件。KunlunBase满足所有这些要求和条件,能够为用户提供上不封顶的性能,吞吐率和数据读写负载和事务处理能力。

云计算与存储资源池

为什么弹性伸缩能力对于数据库系统来说是一个必须的能力呢?在当今云计算技术已经普及的情况下,存储空间不足已经不是数据库系统需要弹性伸缩的原因了,因为在公有云环境或者私有云环境里面,都可以借助分布式文件系统,技术上可以做到存储空间无限量供应,只要有足够的资源预算。分布式存储技术,把位于大量计算机服务器上的存储资源变为资源池,能够虚拟出一个具有无限存储空间的'磁盘',这样,磁盘空间无限增长对于数据库系统来说是透明的,这就非常完美的解决了外存空间不足的问题。

但问题是,集群计算资源不仅仅包括外存,还包括CPU和内存,而一台服务器的CPU和内存空间有限。并且,我们简单分析一下就会发现,内存是无法在不显著增大读写延时的前提下,做到像磁盘那样虚拟为资源池的。

内存无法池化

我们先想一想,一个服务器的CPU全硬件连接读写另一台服务器的内存的延时有多大?答案是大于10us。计算方法如下:一个数据中心(IDC)内两台服务器平均连线长度250米,光纤以接近光速传输(2.5*10^8m/s)、再加上光信号中继延迟、接收方RDMA中断处理延迟、cache淘汰等操作带来的延时,特别是网卡、网络设备(交换机)包转发的软硬件延时等。这个延时是本机内存访问耗时(通常在100ns以内)的100倍以上,实际情况下能达到数百倍甚至上千倍。 如此巨大的内存读写延迟,就导致假如读写同一个IDC内的另一个服务器的内存,实际上与内存读写的性能预期偏差巨大,其操作已经不能作为内存读写来看待了。也就是说,内存资源无法做成一个大量服务器组成的资源池,实现无线扩展内存容量的理想状况。那样做导致的性能损耗过于巨大而失去了实用意义。由于内存无法无限扩展,自然也就无法跨节点透明地分配CPU指令流,所以CPU也是无法做成资源池的。

公有云RDS的扩容能力受限于单台服务器的计算资源

性能可以接受的方法只能是增加计算机服务器,并且实现分布式数据库系统的水平弹性扩容。这些是单机数据库(MySQL,PostgreSQL,Oracle等)无法做到的,在公有云上用户会发现可以对这些单机数据库的RDS做扩容,比如从 2核心4GB升级为8核心32GB。但是这样的扩容的前提是虚拟机所在的物理机有更多的CPU核心和内存空间可以扩容,也就是说单机数据库RDS的扩容能力受限于所在的物理机的最大CPU核数和内存空间,无法对数据库系统实现计算资源的池化和自动弹性扩容。如果依靠操作系统的虚拟内存能力,在工作集如果无法完全装入内存的情况下借助虚拟内存的换页操作(swap)来辗转腾挪,则频繁换页操作会严重损耗性能,导致数据库系统及其所在的操作系统性能极低。

组建服务器集群才能持续增加计算资源

当数据库系统负载很重需要上千个CPU核心和数十TB内存的时候,用户会发现无论他们有多少预算,都无法买到一台这样的服务器。如果用64台相同配置的服务器集群的资源总量达到相近的水平,例如每台32个CPU物理核心,512GB内存,16TB SSD存储,则这些服务器的总价格可以控制在600万人民币左右,这对于有如此之高的业务负载的公司来说,是完全可以承担的价格。

我们再来看一下应用软件的弹性扩容 --- 当今依靠k8s和容器化等技术,可以对各类应用实现自动化的高可用和弹性伸缩,无需应用系统做任何事情。但是这里有一个很容易被忽略的假设 --- 之所以应用可以借助k8s做到自动弹性扩容,是因为其本身无持久状态。对于大多数应用系统来说,其持久状态都存储在数据库系统中。因此,数据库系统的弹性扩容能力是大多数应用系统整体弹性伸缩能力的基础和根本保障;数据库系统如果不能弹性扩展(scale out),那么使用数据库系统的应用系统的整体性能和吞吐率就有一个无法逾越的硬性瓶颈和天花板。即使那些应用系统处理能力扩容而增大了,其数据库系统仍然成为其性能瓶颈,导致这样的应用系统整体性能被其数据库系统的瓶颈限制住了。

更有一个难点是,数据库系统的可扩展能力(scalability),即如何把一台性能强劲的,拥有数百个CPU核心的服务器的性能完全利用起来。这需要在软件层面做前瞻性的架构设计和大量细致严谨的工程设计和实现。如果没有优秀的可扩展能力,那么即使有一台性能强劲的服务器,也无法充分发挥其性能。这对于单机数据库尤其困难;当然,分布式数据库系统也要很好地系统设计和工程实现才能做到优秀的可扩展能力。

使用KunlunBase提供上不封顶的水平扩展能力

我们坚信,分布式数据库系统是未来通过大量商用服务器硬件(commodity servers)提供上无止境的系统性能的关键,而且这种硬件技术方案的成本可控,技术路线清晰而且可落地执行,没有技术风险、成本风险和厂商锁定的风险,是适应所有行业所有类型应用的通用的最优技术路线。

在这个路线中,具有优秀的扩展能力的分布式数据库管理系统(Distributed Database Management System, DDBMS)软件的作用至关重要,KunlunBase就是一个优秀的DDBMS。

首先,DDBMS需要能够利用大量服务器的硬件资源,把数据大致均匀地存储到这些服务器上面,并且随着数据量持续增加以及数据读写负载持续增加,DBA只需要增加服务器给DDBMS,DDBMS就可以自动把一部分数据搬迁到这些新增的服务器上,同时把计算和存储负载扩展到这些新增的服务器上,从而重新达到均衡并且提升集群整体的负载能力。这就是DDBMS的弹性伸缩能力open in new window

同样重要的是,DDBMS要有优秀的扩展能力,也就是说,如果10台服务器组成的集群可以提供10万TPS的话,那么20台服务器组成的集群要能够提供20万TPS,这就是完美的“线性扩展”的情况。

同时,DDBMS必须有效解决服务器软硬件和网络故障,确保这些故障不会导致数据损坏、丢失或者服务停顿,这就是DDBMS的故障恢复和容灾能力。除了最关键的这两点之外,DDBMS还需要有便捷的数据库集群管理控制监控排障工具API等,以便提升DBA的工作效率。

KunlunBase具备所有这些能力,本文主要介绍KunlunBase在数据分布和弹性伸缩方面的能力。

使用分布式数据库时的数据库设计

在经典的单机数据库时代,数据库设计主要就是根据应用系统的架构和功能设计,来设计和定义数据表,包括每个数据表有哪些列,每个列的数据类型和约束、表的主键、(唯一)索引、表级约束、每个表的触发器等,以及定义存储过程、视图、访问控制规则等。

使用分布式数据库的情况下,还需要设计数据表的分区规则和分布策略。KunlunBase 数据表分布方法包括分区规则和分布策略两部分。分区规则是指如何拆分一个表,用哪些列做什么样的计算来作为拆分依据;分布策略是指一个单表或者表分区应该放置到哪个shard中。KunlunBase基于分布策略把表分区分配到存储集群。

分区规则和分布策略对应用透明的,用户永远只需要读写根表,不需要知道这个表如何分区以及其分区如何分布在哪些shard上面等细节,计算节点负责计算得知用户的查询语句对应的数据在哪些shard的哪些表分片中,并且发送适当的数据读写语句到对应的shard。

在数据被拆分存储到多个shard后,经常会出现同一个事务中写入多个shard上的数据表分片,KunlunBase的分布式事务处理机制会确保这些事务可靠地提交,任何节点故障都不会导致数据丢失损坏或者不一致。同时用户也无需考虑一条SQL涉及的表如何分片存储到那些shard上,KunlunBase的分布式查询处理机制把所有这些细节封装在内部,自动执行任何合法的SQL。

下面简单介绍分区规则,后文详述分区规则的使用场景和SQL语法,以及KunlunBase的数据表分布策略。

KunlunBase的分区规则

分区表示意图

(分区表及其所在的shard 示意图)

KunlunBase支持Hash, Range, List三种分区规则,每一种规则用户都需要在创建分区表时根据这个表的预期的数据分布规律和常用查询方式选择适当的分区方式,并且选择一个或者多个列作为分区列;并且在创建该分区表的表分区时为它的每个表分区设定该表的分区方式所需的特定的参数。

然后,kunlun-server就会把分区表按照一系列分布策略安排存储到合适的shard上面,并且把这个信息记录在计算节点的元数据表中,并且通过DDL日志复制机制扩散告知其他计算节点。于是一个KunlunBase集群的每个计算节点都知道每个分区表的每一个分区在哪个shard中。

这样,KunlunBase的计算节点(Klustron-server)就可以通过INSERT 语句的数据行,用该表的分区方式和参数计算出每行应该插入到哪一个表分片中,从而发送给每个shard 适当的一组INSERT语句插入最终数据到相关的数据分片中;或者通过用户的UPDATE/DELETE/SELECT SQL语句设定的查询过滤条件指定的分区列得知目标行所在的分区,从而向那些分区发送特定的针对每个数据分片的SQL语句;如果过滤条件中没有指定分区列,那么计算节点就无法知道目标行所在的分区,这样它就会向该表的所有分区所在的shard发送经过KunlunBase的分布式查询优化之后,存储节点应该执行的查询语句,这些存储节点执行这些语句的实际结果,可能是过滤条件在有些shard中匹配到了一些行,另一些shard中并没有任何一行匹配过滤条件。

KunlunBase数据分布功能和用法

使用KunlunBase的表分区功能

下面简单介绍每种分区方式的适用场景以及语法。在KunlunBase中为一个表指定分区方式的方法就是创建这个表及其所有表分区。KunlunBase支持用多个列做分区列,因此我们称多个分区列的字段值组成了一行数据的分区字段向量(sharding key vector)。

Hash

Hash分区方式适用于如下场景:

A. 表的数据量大致可预测,数据量增长不快不突发。这条不是必须条件。如果一个hash方式分区的表的数据量在后期大幅变化导致分区数量不再合适,那么可以使用KunlunBase的重分区(repartition)功能调整分区数量。

B. 对某个或者某组序数值(cardinality) 很大(即 字段值的重复度很低)的列做等值查询是最频繁的查询方式之一

C. 每个分区字段向量的行数分布较均匀

这个表就适合做hash分区,这(几)个列就适合作为hash分区的分区列。如果高频查询所用的列的重复度较高,比如性别,那么最好再增加一两个列一起作为hash分区,否则会发生严重的数据偏斜(skew),即一些分区数据量很大,另一些分区数据量很少。

常见的适合做Hash分区的信息包括:名称,ID,邮箱,手机号。Hash分区的优缺点都非常明显。

优点:定义简单,不需要知道字段值的分布

缺点:分区字段值分布很不均匀时可能导致该表的数据分布和系统负载严重不均衡。

Hash方式需要选择列和分区个数,下面是创建hash分区表的语法示例:

CREATE TABLE t1(a serial primary key, b int, c varchar(32)) partition by hash(a);

CREATE TABLE t1_0 partition of t1 for values with (modulus 4, remainder 0);

CREATE TABLE t1_1 partition of t1 for values with (modulus 4, remainder 1);

CREATE TABLE t1_2 partition of t1 for values with (modulus 4, remainder 2);

CREATE TABLE t1_3 partition of t1 for values with (modulus 4, remainder 3);

Range

Range分区方式适用于以下场景:

A. 对某个或者某组序数值(cardinality) 很大(即 字段值的重复度很低)的列做范围查询是最频繁的查询方式之一

B. 或者 分区字段值的数据量分布很不均匀

常见的适合做Hash分区的信息包括:数量或者日期、时间、ID。

优点:可以对数据分片规则做精确定义

缺点:随着数据量的变化,分布规则可能不再适合。此时,就需要KunlunBase的重分区(repartition)功能调整分区边界和数量。

Range方式需要选择分区列和分区边界向量,比如:如下语句创建的表t1:

CREATE TABLE t1(a serial primary key, b int, c varchar(32)) partition by range(a);

CREATE TABLE t1_0 partition of t1 for values from (MINVALUE) to (100000);

CREATE TABLE t1_1 partition of t1 for values from (100000) to (1000000)

T1的分区列是a,则分区边界是100000, 1000000这样的标量;随着t1更多数据,可以按需创建更多的分区。用户也可以创建有多个分区列的range分区表:

CREATE TABLE t2(a serial primary key, b int, c varchar(32)) partition by range(c, a)

t2的分区列是(c,a),则分区边界形如('abc',1000), ('ufo',10000)等:

CREATE TABLE t2_0 partition of t1 for values from (MINVALUE, MINVALUE) to ('abc', 100000);

CREATE TABLE t2_1 partition of t1 for values from ('mvp', 100000) to ('ufo', 1000000);

List

List方式适合分区字段的不同值数量较少(比如:几十个),分区列每个分区字段向量对应的数据量差别不能太大,并且对分区列主要做等值查询的场景。

常见的适合做Hash分区的信息包括::国家名,省市区名称,数量很少(数百个以内)的事物的名称、种类等枚举值。适合搭配KunlunBase的枚举类型使用。

List分区方式的优缺点是:

优点:表定义简单直观

缺点:需要准确掌握数据表在分区列上的分布,否则数据在各个分区上很容易发生严重偏斜。不过如果偏斜严重的话,可以增加下一级分区来解决问题。

List分区需要设置分区列及其对应的一组分区字段向量,例如:

CREATE TABLE t1(a serial primary key, b int, province varchar(32), city varchar(32)) partition by list(c);

CREATE TABLE t1_0 partition of t1 for values in ('beijing','beijing');

CREATE TABLE t1_1 partition of t1 for values in ('guangdong','shenzhen');

多级分区和缺省分区

KunlunBase支持多级分区和缺省(default)分区,这主要是为了解决在实际生产环境中,在特定的分区规则下,不同表分区的数据量经常会出现显著的不同,数据偏斜比较严重,并且还必须按照此方式分区。通过多级分区,可以把一个表拆分为一个不等高的树,并且这棵树的所有叶子结点的数据量差别不大,从而解决单级分区数据分布严重不均匀的问题。

例如,很多大公司把业务团队、销售渠道、客户资源等按照省份做了划分,但是不同的省份的人口规模有显著的区别,所以按照省份做了一级分区之后,还需要对人口大省按照城市做二级分区,比如广东省这个一级分区下面要把深圳、广州、东莞等几个巨型城市做二级分区,而省内其他小城市则不做二级分区,而是全部放入'default' 分区。而西北各个省自治区则完全不需要做二级分区;而深圳和广州还需要做第三级分区,因为其每个区都有几百万人口,比很多城市的总人口都要多。

再比如,某直播电商平台按照网红ID做range 拆分订单表,他们面临的技术难题是超级网红的带货量千百倍于普通工作人员,此时可以对这几个超级网红每个人的ID作为一个分区,然后按照生产厂商ID对超级网红的分区做二级分区

而普通工作人员则按照其ID列,每100个人为一个分区。而对于销量很小的其余所有人,放入default分区即可。

表分布策略

分布策略就是要回答一个问题 --- 把一个单表或者表分片放到哪个shard上?

分布策略的最高目标是最优化查询性能,为此可以对不同访问模式的表使用不同的分布策略。在计算节点元数据表pg_class有一个列relshardid,该列记录的就是每个表所在的shard 的id。然后计算节点可以使用pg_shard和pg_shard_node这两个元数据表的数据,得到每个shard当前的主节点的连接信息,从而获得表分片的数据。KunlunBase目前支持如下几种分布策略。

Least Burden

创建一个表分片时,把它分配到平均负载最低的shard。

Least Space

创建一个表分片时,把它分配到占用了最少存储空间的shard。

Random

创建一个表分片时,把它分配到随机选择的shard

Mirror(复制表,镜像表)

如果一个表不常更新(比如每天几万次,如果一个表高频更新的话,创建为mirror表对性能有较大的影响)并且数据量不大小表(比如1个GB以内),并且它和一些大表经常坐join查询,那么适合创建为mirror表,也就是在每个shard放一份。同一个mirror表在所有shard上的copy的数据完全相同。更新一个mirror表时,计算节点会更新所有shard上所有的副本。mirror表与其他表t1 join时,如果t1是单表,则kunlun-server把join下推到t1所在的分区执行本地join;如果t1是分区表:则kunlun-server把join下推到t1的所有叶子分区所在的shard做本地join。

Mirror表的好处是kunlun-server可以做join下推,特别适合OLAP 的雪花模型,也就是一个巨大的事实表(分片存储在集群所有shard上面)与多个维度表(每个都是一个mirror表)做join,此时kunlun-server能够把join下推到所有shard,让事实表在每个shard上面的表分片与本shard的维度表做join即可,然后kunlun-server 收回join结果即可。甚至join之上的aggregate节点也可以顺便下推,这样就可以用很多shard的计算资源执行同一个查询了。

KunlunBase可以确保在增加新shard时,复制所有mirror表到新shard。

并且化解创建mirror表与增加新shard 的冲突,确保正在创建的mirror表在新shard存在。

Table Grouping

在应用实践中,有些表关联紧密,经常做表连接,或者经常在同一个事务中更新,因此Klustron支持用户把这些表始终放在同一个shard,以便得到更好的查询性能和事务处理性能。

为此,Klustron实现了TABLEGROUP功能,让用户把这些紧密相关的表放到一个TABLEGROUP中,以便执行组内表连接时可以将表连接下推到存储分片中,同一个事务中更新这些表可以避免两阶段提交,从而获得更好的性能。

创建一个单表或者表分片时可以设定其属于一个table group,例如:CREATE TABLE t1(a int) WITH(TABLEGROUP=tg1); 

不设定的话它就不属于任何table group。同一个table group的所有表或者表分片总是保持在同一个shard。一个TABLEGROUP中的表不可以单独搬迁到另一个shard,KunlunBase会禁止此操作。如果要搬迁(比如扩容时)必须把tablegroup 的所有表全部整体搬迁到新的shard。在一个database中可以有多个TABLEGROUP,以及一些不在任何TABLEGROUP中的表。

手动分配

KunlunBase也支持让用户显式指定表分区所在的shard, e.g. create table t1(a int) with (shard=3)。我们并不推荐用户这么做,但是始终给用户尽可能大的自由度去定制KunlunBase的关键行为。另外,用户还可以在CREATE TABLE语句中设置若干其他选项

调整分区规则和表分片位置

如果需要调整一个表的分区规则,那么可以使用Klustron的repartition功能 自动调整;如果需要搬动一些数据分片到其他shard,比如新增的shard,则使用Klustron的 表搬迁功能 自动完成。

总结

使用具备优秀的扩展能力的分布式数据库才能确保应用系统的吞吐率和数据量可以持续增长。弹性伸缩能力是分布式数据库扩展能力的基础,而数据分片是弹性伸缩的前提条件。KunlunBase支持的多种的表分区规则和分布策略,让用户可以根据其数据特征和数据访问模式,做出最优性能的数据定义。并且KunlunBase支持Online DDL&Repartition功能,让用户可以随时按需修改表分区规则和分布策略。

本章目录: