在 OceanBase 中,通过将表进行分区,按照分区粒度将数据划分到集群的不同节点上,来实现负载均衡的目标。但这依赖用户能设计合理的分区规则才能更好地使用水平扩展能力,这不仅要求用户具备对各种分区方式有充分的理解,并且还要求用户对业务使用数据库的方式有深入的了解,甚至在有些场景中,采用手动分区很难达到各方面都比较优的效果。
自动分区分裂特性能自动地根据用户设定的分区阈值大小自动进行分区分裂,使得用户无须关注分区规划的前提下,也能用好 OceanBase 数据库的分布式能力。
本篇文章将首先介绍手动分区存在的痛点问题,接着描述自动分区是如何解决手动分区这些问题的,最后详细介绍自动分区的特点,使用场景,使用方式以及限制等。
欢迎大家关注 OceanBase 社区公众号 “老纪的技术唠嗑局”,在这个公众号中,会持续为大家更新与 #数据库、 #AI、 #OceanBase 相关的技术内容!
分布式数据库负载均衡的目标是让各个节点的资源使用尽量达到均衡,要达到这个目标,就需要设计出合理的分区,使得分区间的资源占用比较适中,分区过大可能存在负载不均衡,合并空间放大等问题,分区过小可能会导致元数据过多,性能不优等问题。在自动分区分裂特性发布之前,通常需要手动设计分区规则,这个过程通常是比较困难,以下以抽象出来的业务场景为例来说明手动分区的痛点问题。
假设有一个系统,里面有一张表,它的主键是(公司 ID,员工 ID),现在将这个系统部署在 OceanBase 上,它上面当前有一条 SQL 查询,按照公司 ID,员工 ID 进行点查,客户对数据库的期望。
为了满足点查的 SQL 性能比较优的需求,很自然地想到基于(公司 ID,员工 ID)进行 Key 分区,Key 分区基本上能保证数据量均衡,并且,也能够高效地按照公司 ID,员工 ID 进行查询。
目前,业务的需求都得到了满足,我们来看看如果业务发生一些变化后,会出现什么情况?
业务变化有两种,一种是业务的请求发生变化,另一种是业务的数据发生变化。
我们先来看业务的请求发生变化的场景。
假如业务系统新增一个功能需求,它希望对每个公司的员工,进行按批处理,对应的 SQL 请求是一个范围扫描。如果在原来的 Key 分区上做范围扫描,会导致每个分区都参与范围扫描查询,特别是按批处理的场景如果每个批次数据量不大时,会带来比较多的 IO 放大和网络放大,SQL 的性能是不优的。
为了解决这个问题,一个解决办法就是按照 range(公司 ID,员工 ID)进行分区,但比较难以设置划分的分区的边界,因为运维人员或者开发人员很难知道每个公司的员工数量,也就比较难以进行均衡的划分。
即使这个是老的业务,我们提前知道数据,我们可以手工统计目前数据的分布,划分出一些边界,但这也会有新的问题,例如,随着业务的发展,每个公司可能在员工信息上会有变化,可能导致重新不均衡。
假如某些公司因业务发展需要,招聘并入职了大量员工,如果不做任何人工干预,新入职的员工可能会分布在某个分区上,使得该分区的数据量远超其他分区,集群会重新变得不均衡。为了避免该问题,可能运维人员需要规划好新的机器,并提前创建好新的分区,以便新的分区能够负责新的数据,防止已有分区的数据量过大的问题,这给业务运维带来了复杂度。
总结下来,通过手动分区的方式,存在如下痛点:
在讨论自动分区如何解决问题之前,我们先来简单的了解下 OceanBase 的自动分区的大致流程,在 OceanBase 中,当一个分区的数据量达到数据量阈值后,会自动地将数据按照主键或者主键前缀的 Range 进行一分为二,这里面有两个好处:
如果使用自动分区,按照一定数据量阈值,进行自动拆分 range 分区,那么就能够解决上述需求。
总体来看自动分区存在三个业务价值:
自动分区能够很好地解决手动分区存在的痛点问题,在用户准备用 OceanBase 自动分区之前,通常会关注自动分区对业务的影响以及使用的便捷性等,我将通过对 OceanBase 自动分区的特点介绍,来解答用户关注的问题。
OceanBase 自动分区具备三个特点:
为了避免分裂对正常业务的影响,我们主要做了两方面的工作,一方面,当表上存在业务的普通 DML 和查询时,如果此时做自动分裂,会将查询和 DML 的流量自动转移到新分区继续处理,对正在执行的 DML 和查询的性能影响比较小;另一方面,我们做了一些优化使得分裂操作本身占用的资源比较少,包括重用大部分的数据、网络只同步逻辑操作等,充分节省了磁盘空间、带宽、网络带宽、CPU 和内存资源等。
我们在系统中只有一个分区的情况下,对这个分区进行分裂,模拟分裂带来影响最显著的场景,观察分裂对性能的影响。在面向 OLTP 场景的 sysbench 场景的测试,我们测试了点查,范围查和写入,性能影响在 4% ~ 8% 左右,参考下图。
实际生产环境中,分区数有很多个,同时在分裂的分区数只有一少部分,此时,分裂的影响会更小。
为了让用户方便的使用自动分区,我们对于一些场景,例如全局索引,HBase 等 KV 场景,默认放开了分裂,用户无须配置即可使用,对于其他场景,用户也只需要配置两个租户级的参数,即可使用上自动分裂。
当前 OceanBase 在行存表模式下支持了自动分区分裂,所以,它适用于的业务场景主要是行存表所适用的业务场景,包括 KV 场景,以及 OLTP 场景。
在尚未支持的自动分区的 OceanBase 版本中,默认推荐的是按照 Key 方式进行预分区,分区数量一般设置数百到上千,这种使用方式通常能较好地将数据量进行打散,支持点查的负载,但对于范围扫描的负载不够友好,需要扫描所有的分区,同时 Key 分区的方式把新老数据打散了,不方便做分区级的数据管理。
由于 OceanBase 自动分区一期支持的自动 Range / Range Columns 分区,它继承了 Range 分区的优势之一,比较适合范围扫描的场景,同时它还有普通 Range 分区所不具有的自动按照数据量分裂的能力,将数据量打散。为了避免写热点,在自动分区下,尽量避免按照主键追加写入。如果业务上在读、写没有明显的按照主键范围的热点,那么即使业务负载都是点查,也能够使用自动分区将业务的负载打散。
在尚未支持自动分区的 OceanBase 版本中,如果为了更好地利用分布式集群的能力,用户需要结合业务规则设计相关的相关的分区,以便将数据量,业务负载打散到各个节点上。而支持自动分区后,我们可以默认对所有的表都是自动分裂的,用户在业务开始阶段就创建普通的非分区表,当业务体量增长时,由数据库本身自动的进行分区分裂,达到自动的负载均衡。
为了更有助于帮助大家理解适用的场景,我们列举一部分例子介绍下自动分区的使用场景。
第一个场景是业务平滑迁移。目前业界已经有支持自动分裂的 KV 数据库和分布式关系型数据库,无论是自动可扩展的 KV 数据库,或者分布式关系型数据库,他们在源库的表结构通常是不带分区的。如果希望迁移到 OceanBase,享受到高压缩和高性能等优势,但又不想对一张张表做分区的修改,那么可以把表结构原封不动迁移过来,利用自动分区就能使用上分布式数据库的能力,使得业务迁移流程更加顺畅。
第二个场景就是尽可能利用上各个节点的资源。
这里可能有两种场景,第一个场景是多个 zone,每个 zone 虽然只有一台机器,但 3 个 zone 加起来就有 3 台机器了,为了充分的利用这 3 台机器资源,我们一般会用 random 部署模式,希望将负载打散到多个节点上,如果表只有单分区的话,是做不到打散的,因为一个分区只能有一个 Leader,这样就无法重复利用资源了。
如果对表开自动分区,当分裂出多个分区后,可以将所有分区的 Leader 均匀地分布在多个节点上,这样就能实现负载均衡。
同样地,如果一个 zone 本身就有多个节点了,那么即使是非 random 部署的形态下,它也有多个节点的资源可用。此时,也可以利用分裂的能力,生成多个分区,将分区迁移到不同的节点上,实现负载均衡。
我们碰到一些客户,他们可能有比较多的业务,可能他们自己也难预测哪些业务的规模未来会增长比较大。
为了应对这种可能出现增长的情况,那么在手动分区时代,我们通常建议客户配置比较多的分区数,以应对未来数据量或者业务流量暴涨的情况。比如每张表都配置 256 个分区,当集群的业务数据量较多时,分区数量会比较大,这个是不优的。如果采用自动分裂后,会自动根据表的分区的数据量进行自动的分裂。
OceanBase 支持为每张表独立地设置是否开启自动分区,也就是可以单独地为每张表选择使用自动分区还是手动分区,有可能有些场景我们需要为部分表选择手动分区管理。
例如,为了业务层面的分区管理以及更细致的性能调优等等,此时,我们可以针对这部分表单独使用创建指定的分区规则,其他表还是走自动分区的方式。
一般情况下,多数表建议走自动分区,只有少部分有特殊需求的表手动设置分区规则,这样整体上也能节省很多表的分区规则配置。
还有一部分业务场景中,可能之前就已经对主表已经建了分区。但有些查询不带分区键,这个时候就需要创建全局索引来避免对所有的分区进行扫描。
一般索引的查询多为范围扫描,所以比较适合用 range 分区,但全局索引的索引键的类型通常多变,存在无法确定分区上下界的问题。因此,这种情况也非常适合对全局索引单独开自动分区。特别值得注意的是,这种情况即使主表是列存表,但全局索引是行存的,我们也能对它进行自动分裂。
要使用自动分区功能,只需要关注两个配置项:
如果用户需要开启自动分区,并且内存资源足够,我们使用 ALTER SYSTEM SET enable_auto_split = true; 来打开自动分区。由于租户内存资源一般有限制,而我们支持的 tablet 数量跟租户内存大小有关,一般经验计算公式是 1GB 能分配 2W 个 tablet,所以,为了避免 tablet 数量过多,我们可以调整 auto_split_tablet_size 来避免因数据量太大而分配太多的 tablet。
通过租户级的配置使得用户能够很容易地将自动分区使用起来,但这种使用方式是将租户内所有的业务全部打开自动分区模式。
刚刚提到还有一些手动和自动分区一体化的场景,例如:
-- 创建自动分区的非分区表(使用默认配置的 128MB 为分裂阈值)
CREATE TABLE auto_pt2 (c1 int, c2 int, primary key(c1));
PARTITION BY RANGE ();
-- 创建自动分区的非分区表(分裂阈值为 1024MB,使用用户配置)
CREATE TABLE auto_pt3 (c1 int, c2 int, primary key(c1))
PARTITION BY RANGE () SIZE('1024MB');
-- 业务从老版本 OceanBase 升级来的表 t1,未开启自动分区
CREATE TABLE t1 (C1 INT, C2 INT, PRIMARY KEY(C1))
PARTITION BY RANGE(C1) SIZE('10GB')
(PARTITION p0 VALUES LESS THAN(100),
PARTITION p1 VALUES LESS THAN(200),
PARTITION p_max VALUES LESS THAN (MAXVALUE)
);
-- 修改表 t1 为自动分区表
ALTER TABLE t1 PARTITION BY RANGE() SIZE('128MB');
-- 打开全局索引自动分裂配置项
ALTER
SYSTEM
SET
global_index_auto_split_policy =
'ALL'
;
-- 添加分区表上的全局索引,将自动开启全局索引的自动分裂
ALTER
TABLE
t1
ADD
INDEX
(c1)
GLOBAL
;
以下这部分内容,属于 OceanBase 分区的前置知识点。
大家可以选择性地进行阅读。
在 OceanBase 社区论坛中,一个很常见的用户问题问题:“按日期分区是否能达到让查询变快的目的?”
个人理解,分区除了可以让一张超级大表的数据比较均衡地负载在不同的数据库节点上,另外一个目的就是加速查询。因为查询时会利用过滤条件里面的分区键进行分区裁剪。
例如下面这两个例子:
很多社区用户会问:“有一张订单流水表,数据很大,想考虑按年份对数据进行分区。现在只有 ID 列是主键。尝试了一下好像无法按日期进行分区。是必须要把日期做成和 ID 的联合主键才可以分区么?”
答案是对的,主键必须包含所有分区键。
因为主键的唯一性检查是在各个分区内部进行的,如果主键不包含全部分区键,这个检查就会失效。所以 MySQL 及其他数据库,一般情况下,也都会有这个要求。
-- 如果主键不包含全部分区键,建表就会失败报错,报错信息也挺明确的。
create table t1(c1 int,
c2 int,
c3 int,
primary key (c1))
partition by range (c2)
(partition p1 values less than(3),
partition p1 values less than(6));
ERROR 1503 (HY000): A PRIMARY KEY must include all columns in the table's partitioning function
说明:
对于这个限制,前一阵儿还有一位数据库界的 KOL 老师认为上面的写法过于绝对了,尝试举出反例,最后终于找到一个叫 pg_pathman 的第三方 pg 插件。
这类第三方插件为了提升设置分区的灵活性,不要求分区键是主键或者唯一键的子集,但往往也会导致数据库中的主键和唯一键都不再保证唯一性,进而可能引入比较严重的正确性问题。不过终究也算是打破了这个限制。
最后在上面那段话中,又加入了 “一般情况下” 这几个字。
下面举个例子解释下原因:
create table t1(c1 int,
c2 int,
c3 int,
primary key (c1, c2))
partition by range (c2)
(partition p0 values less than(3),
partition p1 values less than(6));
Query OK, 0 rows affected (0.146 sec)
obclient [test]> insert into t1 values(1, 2, 3);
Query OK, 1 row affected (0.032 sec)
obclient [test]> insert into t1 values(1, 5, 3);
Query OK, 1 row affected (0.032 sec)
obclient [test]> select * from t1;
+----+----+------+
| c1 | c2 | c3 |
+----+----+------+
| 1 | 2 | 3 |
| 1 | 5 | 3 |
+----+----+------+
2 rows in set (0.032 sec)
obclient [test]> select * from t1 PARTITION(p0);
+----+----+------+
| c1 | c2 | c3 |
+----+----+------+
| 1 | 2 | 3 |
+----+----+------+
1 row in set (0.033 sec)
obclient [test]> select * from t1 PARTITION(p1);
+----+----+------+
| c1 | c2 | c3 |
+----+----+------+
| 1 | 5 | 3 |
+----+----+------+
1 row in set (0.034 sec)
分布式数据库的优势在于对于空间问题和请求访问问题分而治之。针对每个分区的访问,由该分区所在的节点响应即可。 即使该 SQL 并发很高,由于访问的是不同的分区,分别由不同的节点提供服务。每个节点自身也有一定能力满足一定的 QPS,所有节点集中在一起就能提供更大的 QPS。这个时候如果扩容节点数量,该 SQL 总的 QPS 也能获得相应的提升,这是分布式数据库里最好的情形。
分区的目标是将大量数据和访问请求均匀分布在多个节点上,一是想充分利用资源进行并行计算,消除查询热点问题;二是想利用分区裁剪来提升查询效率。 如果每个节点均匀承担数据和请求,那么理论上 10 个节点就应该能承担 10 倍于单节点的数据量和访问量。然而如果分区是不均匀的,一些分区的数据量或者请求量会相对比较高,出现数据偏斜(skew),这个可能导致节点资源利用率和负载也不均衡。偏斜集中的数据我们又称为热点数据。避免热点数据的直接方法就是数据存储时随机分配(没有规则)给节点,缺点是读取的时候不知道去哪个分区找该记录,只有扫描所有分区了,所以这个方法意义不大。实际常用的分区策略都是有一定的规则。
用户必须在业务查询条件明确的情况下,根据真实业务场景进行分区规划,不要在场景不明确的情况下随意进行分区规则。 在规划分区时,建议尽量保证各个分区的数据量相对均衡。
最常用的三种分区方式如下:
为了更好地支持并行计算和分区裁剪,OceanBase 还支持二级分区。OceanBase 数据库 MySQL 模式目前支持
HASH
、
RANGE
、
LIST
、
KEY
、
RANGE COLUMNS
和
LIST COLUMNS
六种分区类型,二级分区为任意两种分区类型的组合。
例如在用户账单领域,数据库往往需要按照 user_id 做 HASH 一级分区,然后再在各个一级分区内部,继续按照账单创建时间做 RANGE 二级分区。
尽管 OceanBase 数据库在组合分区上支持 RANGE + HASH 和 HASH + RANGE 两种组合,但是对于 RANGE 分区的分区操作 add / drop,必须是 RANGE 分区做为一级分区的方式。所以针对例如数据量较大的流水表,为了维护方便(新增和删除分区),建议使用 RANGE + HASH 组合方式(时间列 range 一级分区 + 业务列 hash 二级分区)。
总结一下:
参考自这个 OceanBase 社区公众号中的一篇文章 《OceanBase PoC 经验总结(二)—— AP 业务》
OceanBase 数据库支持在分区表中手动进行分区分裂(REORGANIZE)操作,即将一个已有的分区拆分为多个分区。这个功能可以指定需要分裂的分区和新分区的分裂位点进行手动执行分区分裂命令,根据需求和数据增长情况对分区进行调整。
OceanBase 当前(4.4.0 及以下版本)仅支持对 Range / Range Columns 分区的一级分区表进行手动分区分裂操作。
示例:
test_tbl1
。
CREATE
TABLE
test_tbl1(col1
INT
, col2
INT
, PRIMARY
KEY
(col1))
PARTITION
BY
RANGE
(col1)
(
PARTITION
p0
VALUES
LESS
THAN
(
100
),
PARTITION
p1
VALUES
LESS
THAN
(
200
),
PARTITION
p2
VALUES
LESS
THAN
(
300
),
PARTITION
p_max
VALUES
LESS
THAN
(MAXVALUE));
test_tbl1
表的分区
p0
分裂成三个新的分区,分裂的位置是在
30
和
60
这两个值所对应的行上。分裂后,原来的分区
p0
被分成三个新的分区
p0_1
、
p0_2
和
p0
。
ALTER
TABLE
test_tbl1
REORGANIZE
PARTITION
p0
INTO
(
PARTITION
p0_1
VALUES
LESS
THAN
(
30
),
PARTITION
p0_2
VALUES
LESS
THAN
(
60
),
PARTITION
p0
VALUES
LESS
THAN
(
100
));
test_tbl1
表的分区
p_max
分裂成三个新的分区,分裂的位置是在
400
和
500
这两个值所对应的行上。分裂后,原来的分区
p_max
被分成三个新的分区
p_max_1
、
p_max_2
和
p_max_3
。
ALTER
TABLE
test_tbl1
REORGANIZE
PARTITION
p_max
INTO
(
PARTITION
p_max_1
VALUES
LESS
THAN
(
400
),
PARTITION
p_max_2
VALUES
LESS
THAN
(
500
),
PARTITION
p_max_3
VALUES
LESS
THAN
(MAXVALUE));
test_tbl1
的结构和定义。
SHOW
CREATE
TABLE
test_tbl1 \G
返回结果如下:
*************************** 1. row ***************************
Table: test_tbl1
Create Table: CREATE TABLE `test_tbl1` (
`col1` int(11) NOT NULL,
`col2` int(11) DEFAULT NULL,
PRIMARY KEY (`col1`)
) ORGANIZATION INDEX DEFAULT CHARSET = utf8mb4 ROW_FORMAT = DYNAMIC
partition by range(col1)
(partition `p0_1` values less than (30),
partition `p0_2` values less than (60),
partition `p0` values less than (100),
partition `p1` values less than (200),
partition `p2` values less than (300),
partition `p_max_1` values less than (400),
partition `p_max_2` values less than (500),
partition `p_max_3` values less than (MAXVALUE))
1 row in set (0.003 sec)
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70047136/viewspace-3093183/ ,如需转载,请注明出处,否则将追究法律责任。