DMDPC的关键技术

5.1 元数据服务

元数据服务又称字典信息服务。

在 DMDPC 中,为了简化 DDL 的处理,有专门的元数据服务器 MP 负责管理所有的字典对象元数据信息。所有 SP 和 BP 都是通过网络请求从 MP 获取字典对象的。

SP、BP 和 MP 的字典模块的构架基本不变,仍有字典缓存;不同的是,SP 和 BP 不再进行字典数据的存储,统一交给 MP 来存储。简单的可以理解为,SP 和 BP 的字典模块是 MP 字典模块的客户端。

图 5.1 元数据服务整体示意图.png

图5.1 元数据服务整体示意图

对 SP 和 BP 而言,它们获取表、视图、索引等字典对象的逻辑一致,现以 SP 为例说明其步骤如下:

  1. SP 在字典缓存中查找是否有该对象的缓存,如果有则直接使用该对象,否则执行步骤 2;
  2. SP 向 MP 发出请求,获取某个特定 ID 的字典对象;
  3. MP 接收请求后从系统表中加载(load)对应的字典对象,然后返回 SP;
  4. SP 解包 MP 返回的字典对象并添加到自身缓存中。

5.2 并行线程管理

并行线程英文全称为 Parallel Thread,简称为 PTHD。

DMDPC 环境中会有大量的并行计算,简单的、固定个数的并行线程的逻辑已经不能满足需求。需要一个可扩展的并行线程池(PTHD POOL)对其进行管理。当服务器负载大时,增加池内并行线程的数量;当负载减少时,服务器也可以自动释放一些并行线程以减少资源消耗。

5.3 数据存储

DMDPC 的数据存储在各个 BP 中。

用户在创建表对象时需要指定用户表空间。如果建表时未显式指定用户表空间,系统则随机选择用户表空间。如果没有创建任何用户表空间则建表会报错。

表空间的元数据信息由 MP 进行管理。所有本地表空间元信息都记录在 MP 的控制文件中。

在 DMDPC 中,MP 包含 SYSTEM、MAIN、ROLL 和 TEMP 表空间;SP 包含 TEMP 表空间;BP 包含 SYSTEM、ROLL、TEMP 和用户表空间。

5.4 数据交换与数据迭代操作符

为了在不同的实例之间或同一实例不同的线程之间进行数据交换,DMDPC 引入了两个高效的数据交换操作符:发送操作符 ESEND 和接收操作符 ERECV。为了控制工作线程获取表数据时的粒度,DMDPC 引入了 GI 操作符。下面对这三种操作符分别加以介绍。

  • 发送操作符 ESEND

ESEND 将孩子操作符的数据按照某种分发规则发送给特定 ERECV 操作符。

ESEND 操作符发送的内容为当前子计划的执行结果,有时也会包含一些附加信息。附加信息包含:如是否为最后一批数据、错误码、请求调度父层子任务等。ESEND 共有 6 种数据分发方式:BY HASH、BY RANGE、BY LIST、BY N_DEST、DIRECT 和 BROADCAST。下面分别详细介绍:

  1. BY HASH:按照发送列的 HASH 取值和并行度,决定发送给 ERECV 操作符中的第几个 worker 线程。例如,ERECV 所在并行度为 8,发送 key 为 C1,某条数据的 HASH(C1)% 8 = 5,那么这条记录将发送给 ERECV 操作中第 5 个 worker 线程。
  2. BY RANGE:按照发送列的范围判断发送给 ERECV 操作符的第几个 worker 线程,每个 worker 线程事先都约定了各自的接收数据范围。
  3. BY LIST:按照发送列的值判断发送给 ERECV 操作符的第几个 worker 线程,每个 worker 线程事先都约定了各自的接收数据值。
  4. BY N_DEST:类似于 BY HASH,都是按照 HASH(key)%N 决定发送给哪一个接收者线程。区别在于 BY HASH 中的 N 取值为 HASH 水平分区子表个数,通常用在 Partition Wise Join 连接的一侧,另一侧是 HASH 分区表。而 BY N_DEST 方式下,连接的两侧按照约定的目标线程个数进行数据划分,N 的取值由并行度决定,一般是成对出现在连接的两侧孩子中。
  5. DIRECT:直接发送给某个接收线程。如果接收线程数为 1,那么所有的数据全部发给了同一个线程;如果接收线程个数大于 1,则 ESEND 逐一发送给每个接收线程。
  6. BROADCAST:每一批数据都向所有接收线程发送。
  • 接收操作符 ERECV

ERECV 操作符用于接收某个 ESEND 操作符的输出结果,并将接收的结果向上层操作符传递。

  • 数据迭代操作符 GI

GI(Granule Iterator)操作符用于控制工作线程如何获取表中的数据,访问的粒度可以是单个子表,也可以是子表中的部分页。当数据来源是分区表且存在分区列条件过滤时,GI 会剔除不满足条件的分区,即进行分区裁剪优化。

GI 的数据访问粒度分为 RANDOM、PART_UNIT(LV2_PART_UNIT)和 USE_SQC_NO(LV2_SQC_NO)。RANDOM 表示粒度可以任意划分,既能以子表为单位也可以是子表中部分页为单位;后面几种粒度的访问策略都是以单个子表为单位。PART_UNIT 和 USE_SQC_NO 的区别在于后者通常出现在分区智能连接(PWJ),工作线程访问哪一张子表已经事先确定不能随意挑选。

PART_UNIT 和 LV2_PART_UNIT 的区别是,后者针对二级分区表,且叶子子表分布方式一致,此时单个线程会将中间主表下所有相同序号的叶子子表作为一个整体进行全部扫描。例如,二级分区表 T1,中间层有 P1、P2、P3 三个子表,每个中间层表 Pi 有 S1~S4 个叶子子表,如果采用了 LV2_PART_UNIT 的粒度策略,t1.p1.s1、t1.p2.s1 和 t1.p3.s1 会交由同一个工作线程扫描处理。LV2_PART_UNIT 的出现通常是因为连接、分组条件只用到了二级分区列。

USE_SQC_NO 和 LV2_SQC_NO 的区别与 PART_UNIT 和 LV2_PART_UNIT 的区别一样,也是一个线程扫描所有中间主表下相同序号的叶子子表。

5.5 DMDPC 的计划生成

DMDPC 的执行计划是在单机数据库执行计划的基础上插入了数据交换操作符 ESEND/ERECV 后形成的。同 MPP 架构类似,数据交换操作符的插入只发生在特定的操作符中,例如连接、分组、排序等。

在处理每一个单机操作符时,优化器会考虑各种可能的数据分发方式。例如哈希内连接,既可以左侧广播右侧无任何操作,也可以两侧同时按照连接列进行分布,当然如果连接两侧数据已经按照连接条件分布好了,那么可以直接连接不用任何数据交换操作符。分组、排序等也是一样,优化器穷举各种可能的路径,根据代价模型依次计算代价值,最后挑选出最小代价。

用户可以通过 EXPLAIN 命令查看执行计划,通过 10053 TRACE 信息(需设置 10053 trace event)观察到计划的优化过程。

以下是一个例子,从中我们可以观察到连接、分组和排序是如何挑选分布式计划路径的。

一,使用 EXPLAIN 查看语句的执行计划:

explain
select TOP 10
l_orderkey,
sum(l_extendedprice*(1-l_discount)) as revenue,
o_orderdate,
o_shippriority
from customer, orders, lineitem
where c_mktsegment = 'BUILDING'
and c_custkey = o_custkey
and l_orderkey = o_orderkey
and o_orderdate < date'1995-03-15'
and l_shipdate > date'1995-03-15'
group by l_orderkey, o_orderdate, o_shippriority
order by revenue desc, o_orderdate;

得到的计划如下:

1   #NSET2: [579065, 10, 116]	
2     #ERECV: [579065, 10, 116]; stask_no(-1), l_stask_no(3), n_key(0), recv_in_turn(0)
3       #ESEND: [579065, 10, 116]; stask_no(3), type(DIRECT), sites(2:1), sql_invoke(0), table(-)
4         #PRJT2: [579065, 10, 116]; exp_num(4), is_atom(FALSE)
5           #ERECV: [579065, 10, 116]; stask_no(3), l_stask_no(2), n_key(2), recv_in_turn(0)
6             #ESEND: [579065, 10, 116]; stask_no(2), type(DIRECT), sites(2:19, 3:19, 4:19, 5:19), sql_invoke(0), table(-)
7               #SORT3: [579065, 10, 116]; key_num(2), is_distinct(FALSE), top_flag(1), is_adaptive(0)
8                 #HAGR2: [341666, 15221235, 116]; grp_num(3), sfun_num(1); slave_empty(0) keys(DMTEMPVIEW_16778491.TMPCOL0, DMTEMPVIEW_16778491.TMPCOL1, DMTEMPVIEW_16778491.TMPCOL2)
9                   #PRJT2: [103926, 15221235, 116]; exp_num(4), is_atom(FALSE)
10                    #HASH2 INNER JOIN: [103926, 15221235, 116];  KEY_NUM(1); KEY(ORDERS.O_ORDERKEY=LINEITEM.L_ORDERKEY) KEY_NULL_EQU(0)
11                      #ERECV: [19597, 6997382, 80]; stask_no(2), l_stask_no(0), n_key(0), recv_in_turn(0)
12                        #ESEND: [19597, 6997382, 80]; stask_no(0), type(HASH), sites(2:19, 3:19, 4:19, 5:19), sql_invoke(0), table(LINEITEM)
13                          #HASH2 INNER JOIN: [19597, 6997382, 80];  KEY_NUM(1); KEY(CUSTOMER.C_CUSTKEY=ORDERS.O_CUSTKEY) KEY_NULL_EQU(0)
14                            #GI: [710, 1540563, 52]; policy(USE_SQC_NO), gi_unit[0..0]
15                              #HFLKUP2: [710, 1540563, 52]; (CUSTOMER)
16                                #SLCT2: [710, 1540563, 52]; CUSTOMER.C_MKTSEGMENT = 'BUILDING'
17                                  #HFSEK2: [710, 1540563, 52]; (CUSTOMER), scan_type(EQU)['BUILDING','BUILDING']
18                            #ERECV: [16149, 37598752, 28]; stask_no(0), l_stask_no(1), n_key(0), recv_in_turn(0)
19                              #ESEND: [16149, 37598752, 28]; stask_no(1), type(HASH), sites(2:64, 3:64, 4:64, 5:64), sql_invoke(0), table(CUSTOMER)
20                                #GI: [16149, 37598752, 28]; policy(RANDOM), gi_unit[0..0]
21                                  #HFLKUP2: [16149, 37598752, 28]; (ORDERS)
22                                    #SLCT2: [16149, 37598752, 28]; ORDERS.O_ORDERDATE < 1995-03-15
23                                      #HFSEK2: [16149, 37598752, 28]; (ORDERS), scan_type(L)(null2,1995-03-15)
24                      #GI: [71905, 167431981, 36]; policy(USE_SQC_NO), gi_unit[0..0]
25                        #HFLKUP2: [71905, 167431981, 36]; (LINEITEM)
26                          #SLCT2: [71905, 167431981, 36]; LINEITEM.L_SHIPDATE > 1995-03-15
27                            #HFSEK2: [71905, 167431981, 36]; (LINEITEM), scan_type(G)(1995-03-15,max)

简要说明 RECV/SEND/GI 操作符后的部分字段含义。以序号 2、3、24 的操作符为例。

2 #ERECV: [579065, 10, 116]; stask_no(-1), l_stask_no(3), n_key(0),recv_in_turn(0)

该 RECV 操作符所在的子任务序号是-1(可以理解为最根层的子任务),左孩子任务序号为 3,RECV 的 n_key 为 0,即不需要进行归并排序。

3 #ESEND: [579065, 10, 116]; stask_no(3), type(DIRECT), sites(2:1),sql_invoke(0), table(-)

该 SEND 操作符所在的子任务序号是 3,数据发送方式是 DIRECT,当前子任务会被分配到 RAFT_ID 为 2 的实例执行,并行度为 1。

24 #GI: [71905, 167431981, 36]; policy(USE_SQC_NO), gi_unit[0..0]

该 GI 的数据访问粒度是 USE_SQC_NO,意味着以子表为单位扫描,控制所在子任务的第 0 个 SCN/SEK 操作符。

由上可看出,要想了解每一个子任务会在哪些站点执行,并行度是多少,只要观察子任务的根操作符 sites(M:N)字段即可,子任务的根操作符可以是 SEND、SPOOL 和 HEAP TAB。sites(1:16,2:16,3:16)表示该子计划会在 RAFT_ID 为 1、2、3 的站点执行,每个站点上并行度为 16。

二,使用 10053 trace 文件查看执行计划生成。

下面截取 10053 trace 文件中和 DMDPC 计划生成相关的执行计划。

*** PHASE F STARTED...
<<处理哈希内连接
*** probe best distribute method for hash inner join[0x7f38a42db178], restricts[0x18], self cost 10791.797
<<各种通讯代价
	left motion cost: broadcast = 1909.954, dis = 477.488, gather = 477.488
	right motion cost: broadcast = 100135.803, dis = 25033.951, gather = 25033.951
<<各种分发方式的路径探测
	> try L NO R NO(401), not available
	> try L BROADCAST(403), cost 122237.445, n_parallel 64, best*
	> try R BROADCAST(404), cost 6408691.754, n_parallel 64
	> try L DIS R DIS(405), cost 25511.830, n_parallel 64, best*
	> try R DIS ONLY(407), cost 25034.341, n_parallel 64, best*
	> try L GAT R GAT(408), cost 36303.237, n_parallel 0

*** probe best distribute method for hash inner join[0x7f38a42da8b8], restricts[0x18], self cost 19362.614
<<各种通讯代价
	left motion cost: broadcast = 103365.639, dis = 25841.410, gather = 25841.410
	right motion cost: broadcast = 143708.022, dis = 35927.006, gather = 35927.006
<<各种分发方式的路径探测
	> try L NO R NO(401), not available
	> try L BROAD THD DIS(402), cost 6615401.259, n_parallel 64, best*
	> try L BROADCAST(403), cost 6615401.259, n_parallel 64
	> try R BROADCAST(404), cost 9197313.800, n_parallel 64
	> try L DIS R DIS(405), cost 61768.806, n_parallel 64, best*
	> try L DIS ONLY(406), cost 25841.800, n_parallel 64, best*
	> try L GAT R GAT(408), cost 81131.029, n_parallel 0
<<处理哈希分组
*** probe best distribute method for group[0x7f38a42d8d38], restricts[0x1f], self cost 944515.546
	left motion cost: broadcast = 167297.602, dis = 41824.401, gather = 41824.401
	self motion cost: broadcast = 167297.602, dis = 41824.401, gather = 41824.401
	> try X ONLY(410), cost 944515.546, n_parallel 0, best* 
	> try X DIS X(411), cost 49267.428, n_parallel 64, best*
	> try X GAT X(412), cost 990029.460, n_parallel 64
	> try GAT X(413), cost 986339.946, n_parallel 0
	> try DIS X(414), cost 45577.914, n_parallel 64, best*
<<处理排序
*** probe best distribute method for order[0x7f38a42d8458], restricts[0x45], self cost 943452.726
<<各种通讯代价
	left motion cost: broadcast = 167297.602, dis = 41824.401, gather = 41824.401
	self motion cost: broadcast = 0.028, dis = 0.007, gather = 0.007
<<各种分发方式的路径探测
	> try X ONLY(410), not available
	> try GAT X(413), cost 985277.126, n_parallel 0, best*
	> try X MERGE(417), cost 12171.229, n_parallel 0, best*
<<经过DMDPC阶段后的最终计划, ESEND/ERECV是新增的
*** BEST PLAN FOR THIS STATEMENT AFTER PHF ***
 ERECV[0x7f38a457a708]  (cost: 2007335.29974, rows: 10);
   ESEND[0x7f38a457a0e8] send_type(DIRECT) sites(2:1) (cost: 2007335.29974, rows: 10);
     project[0x7f38a42d7a28]   (cost: 2007335.29974, rows: 10);
       ERECV[0x7f38a45797d8]  (cost: 2007335.29974, rows: 10);
         ESEND[0x7f38a45791b8] send_type(DIRECT) sites(2:64, 3:64, 4:64, 5:64) (cost: 2007335.29974, rows: 10);
           order[0x7f38a42d8458]  (cost: 2007335.29974, rows: 10);
             group[0x7f38a42d8d38]  (cost: 1063882.57389, rows: 60491120);
               ERECV[0x7f38a4578738]  (cost: 119367.02828, rows: 60491120);
                 ESEND[0x7f38a4578118] send_type(N_DEST) sites(2:19, 3:19, 4:19, 5:19) (cost: 119367.02828, rows: 60491120);
                   project(DMTEMPVIEW_16778402) [0x7f38a42d9768]   (cost: 119367.02828, rows: 60491120);
                     hash inner join[0x7f38a42da8b8]  (cost: 119367.02828, rows: 60491120);
                       ERECV[0x7f38a4577000]  (cost: 28098.98457, rows: 54193363);
                         ESEND[0x7f38a45769e0] send_type(HASH) sites(2:19, 3:19, 4:19, 5:19) (cost: 28098.98457, rows: 54193363);
                           select[0x7f38a42dec20]  (cost: 28098.98457, rows: 54193363); (ORDERS.O_ORDERDATE < 1995-03-15)
                             hash inner join[0x7f38a42db178]  (cost: 28098.98457, rows: 54193363);
                               base table[0x7f38a42dc5d0] (CUSTOMER, INDEX33556384, EQU SEARCH) (cost: 710.93447, rows: 1540563); (CUSTOMER.C_MKTSEGMENT = 'BUILDING')
                               ERECV[0x7f38a4575a50]  (cost: 16596.25267, rows: 149999999);
                                 ESEND[0x7f38a4575430] send_type(HASH) sites(2:64, 3:64, 4:64, 5:64) (cost: 16596.25267, rows: 149999999);
                                   base table[0x7f38a42dd2d8] (ORDERS, INDEX33555914, FULL SEARCH) (cost: 16596.25267, rows: 149999999);
                       base table[0x7f38a42ddf18] (LINEITEM, INDEX33555456, G SEARCH) (cost: 71905.42958, rows: 167431981); (LINEITEM.L_SHIPDATE > 1995-03-15)

5.6 子计划分割与调度

子计划分割以 ESEND/ERECV 操作符为界限分割原始计划得到一系列的子计划。每个子计划之间相互独立执行,有各自的并行度,采用不同的线程。

子计划一般以 ESEND 操作符为根节点,以 ERECV 为叶子操作符节点。一个子计划最多包含一个 ESEND 操作符,可以没有 ERECV 操作符,也可以有一个或多个 ERECV 操作符。

相邻的一对 ESEND/ERECV 操作符所在的子计划被称为父子关系子计划,因为 ESEND 所在的子计划发送的数据是要依靠 ERECV 所在的父亲子计划来接收的,二者之间存在着依赖关系,可以称为 Parent SPLAN 和 Child SPLAN。一个 SPLAN 最多只有一个 Parent SPLAN,但是可以有零个、一个或多个 Child SPLAN。

父子关系子计划是一个相对的概念,某个子计划 A 相对于子计划 B 是其父亲子计划,但是相对于子计划 C,有可能是其孩子子计划。

下面是一个完整的子计划划分例子:

create table t1(id int, c2 int) partition by hash(id) partitions 3;
create table t2(id int, d2 int) partition by range(id) (
partition p1 values less than (10),
partition p2 values less than (20),
partition p3 values less than (30));

假定 T1,T2 数据分布如下图 5.2 所示:

图 5.2 T1 和 T2 表的数据分布.png

图5.2 T1和T2表的数据分布

查询 Q1:

select count(*) from t1, t2 where c2 = d2 and t1.id = 10 and t2.id between 15 and 25;

假设 t1.id=10 位于 P1 分区,执行时计划可以是这样的:

图 5.3 Q1 语句的执行计划.png

图5.3 Q1语句的执行计划

调度顺序是从无依赖的子任务开始,大体按照中序遍历顺序。当 ESEND 操作符第一次输出数据时,SQC 通知 QC 调度它所依赖的父层子任务。

上述计划的执行顺序是:

第一步,QC 通知调度执行 Sub plan1 和 Sub plan3。Sub plan1 附加信息:Sub plan1 的并行度=1,需要访问 T1.P1 分区,执行的 BP 节点是 BP1;Sub plan3
附加信息:t2.p2,t2.p3@BP3,并行度=2;

第二步,ESEND1 第一次对外输出数据时,向 QC 发送请求调度执行 Sub plan2。Sub plan2 附加信息:Sub plan2 的并行度=4,需要访问 T2.P2 和 T2.P3 分区,执行的 BP 节点是 BP3;ESEND2 发送数据后由于 ERECV2 没有接收数据(此时 sub plan2 忙于执行 ERECV1 和 HASH 表构造),ESEND2 也会暂时挂起;

第三步,Sub plan1 完成后,sub plan2 和 sub plan3 协同工作直至 sub plan3 和 sub plan2 分别完成。

ESEND1 和 ESEND2 的分发方式是 BY N_DEST(4);

ESEND1_p1:1->4,1 个 sender 线程发送给 4 个 recver 线程;

ESEND2_p2:2->4,2 个 sender 线程发送给 4 个 recver 线程;

为了解决 ESEND 发送过快而 ERECV 来不及处理的情况,DMDPC 引入了流量控制管理,详细信息参考[5.8 链路通讯](#5.8 链路通讯)。

5.7 生产者、消费者并行执行模型

DM MPP 架构,包括本地并行 LPQ,大体而言属于一种对称计划设计,即在单机计划基础上插入了数据交换操作符,但是计划仍然是一个整体。

生产者、消费者并发执行模型中,将父子子任务分别视为消费者、生产者子任务。

生产者子任务的角色是生产数据,只要有数据的存放空间,生产动作可以一直进行下去直至生产结束。

消费者子任务的角色是消费数据,只要有新的数据进来,消费动作可以一直进行下去。

当生产者、消费者子任务被同时调度起来后,数据就可以在二者之间形成类似流水线上的协同操作,两组线程各司其职,互相配合极大提升查询性能。我们称互相协作的一组消费者、生产者子任务为形成了一组流水。

该执行模型弱化了实例间和实例内部线程的数据交换区分,统一用 ESEND、ERECV 处理,简化了此前 MPP,LPQ 的两层并行执行模型。

如上一小节中的计划,Sub Plan1 和 Sub Plan2 被同时调度后形成一组流水,当 Sub Plan1 执行完成后,Sub Plan2 和 Sub Plan3 协同工作,形成新的流水。在实际调度执行中,Sub Plan1 和 Sub Plan3 可能被同时调度,这样可以提高并行效率。Sub Plan3 发送的数据被缓存在 Sub Plan2 的数据接收缓冲区内,在需要时,Sub Plan2 可以直接从本地的数据缓冲区获取到 Sub Plan3 发送的数据而无需调度并等待 Sub Plan3 的执行。

不同的子任务允许有不同的并行度,同一个子任务在不同 BP 上的并行度也可以不同,并行度设置的灵活性能大大地提升线程资源的利用效率。

用户也可以通过查询 V$DPC_STASK_THRD 来了解子任务的执行情况。

5.8 链路通讯

系统使用 XMAL 系统管理各部件节点网络信息,建立节点间的通讯机制。用户只需要将 SP 和 BP 节点的 IP 地址和端口等信息注册到 MP 上即可,使得增删节点的部署能够做到快速且便捷。

单个查询的各子计划按照数据驱动进行调度,尽管存在调度的先后顺序,但大多数情况会并行执行。数据由下往上传输,可能造成上层子计划的数据来不及处理而堆积。例如客户端执行查询后并不把所有数据都取完,而 BP 节点还在不断往 SP 节点发送数据,或者连接查询子计划需要左孩子全部取完数据后,才会取右节点数据,此时右节点也可能产生数据堆积。

DMDPC 内引入一种限流机制,当参数 MPP_MOTION_SYNC 不为 0 时开启。数据发送的目的地是消息盒子,在消息盒子接收端增加一种由信号量控制的授权机制,发送端发送数据前,需要请求目标消息盒子获取发送授权,对每个来源都进行发送数据的控制,如果消息盒子的资源不足,表示已经堆积。不再授权后续的发送,避免数据堆积。为了提高授权的效率,每次请求一批资源进行授权。

5.9 计算与存储分离

为了克服传统数据库系统中将计算和存储耦合在一起导致的瓶颈问题,DMDPC 采用了计算与存储分离的架构。将数据获取相关的子计划放在 BP 上,中间层计算相关的子任务放在一个或多个 SP 上执行。

当连接和分组、去重等操作需要用到多个计算节点时(例如连接时两侧分发 L_DIS_R_DIS 或者两阶段聚合分组 X_DIS_X),优化器会从 DMDPC 集群选择多个 SP 节点,具体规则如下:

  1. 假设应用登录的 SP 节点 S1 所属 SP 组为 G1,选择 G1 下所有的 SP 节点;
  2. 若集群中未定义任何 SP 组,选择和当前计划涉及的 BP 节点在同一台机器上的 SP 节点;
  3. 如果按上一条规则选择的 SP 节点数过少(不足 4 个或者远低于 BP 节点个数),选择集群中所有 SP 节点,最多 16 个。

5.10 动态增删节点

在 DMDPC 使用过程中,可对节点进行动态增删。动态增删分为两种情况:一是横向增删 SP 或 BP 节点。二是纵向增删 MP 或 BP 多副本系统中的副本节点。

5.10.1 横向增删

横向增删是指增删 DMDPC 系统中的 SP 单机节点或 BP 单机节点的操作,以此来扩大和缩小集群规模,灵活应对业务变化。MP 不支持横向增删。单机 DMDPC 或多副本 DMDPC 中均支持横向增删。

横向增删既可以登录到 SP 上执行,也可以登录到 MP 上执行。

如果现有主机有可用的硬件资源,可以添加新的 SP、BP 节点到当前集群。新节点直接部署到已有的主机上。下图展示了一个 DMDPC 集群含有 2 个 SP、1 个 MP 和 2 个 BP,现通过横向增加 1 个 SP,增加 1 个 BP,将其改变成为一个含有 3 个 SP、1 个 MP 和 3 个 BP 的集群。

图 5.4 横向增加节点示意图.png

图5.4 横向增加节点示意图

添加新的 BP 节点后,后续数据存储便可以利用这些 BP。由于 BP 上并不持有元数据信息,因此新加入的 BP 节点可以马上提供服务。新加入的 SP 节点可以用于响应更多的客户请求,分担子计划的执行。

支持动态增删的内容如下:

  • 支持删除 SP 组、BP 组、RAFT 组、BP 节点或 SP 节点。删除节点时需满足下列要求:
  1. 只有正常退出的 SP 节点才能删除;
  2. 没有数据的 RAFT 组内节点才能删除。如果想删除包含数据的 BP 节点,必须先将此节点上的数据迁移到其他分区,具体见[8.7 数据迁移](#8.7 数据迁移)。
  3. 删除 RAFT 组必须先删除组内包含的所有节点;
  • 支持修改 SP 和 BP 节点中的部分配置:ap_port、inst_port 和 ip_addr。
  • 支持手动失效/恢复 BP 模式的 RAFT 组。

5.10.2 纵向增删

纵向增删是指对 MP 多副本系统或 BP 多副本系统中的副本节点进行增删。因为 SP 不支持多副本,所以 SP 不支持纵向增删。多副本系统内的节点数必须是大于等于 3 的基数,因此一次性增删节点的个数必须为偶数,并且增删之后该 RAFT 组内节点总数依然要大于等于 3。

不支持对多副本系统中的主节点进行增删。

纵向增删 MP 副本节点,需要登录到 MP 主节点上执行。纵向增删 BP 多副本节点,需要以 BS 模式登录到 BP 主节点上执行。

下图展示了一个 DMDPC 集群含有 2 个 BP 节点,其中一个 BP 配置成 3 副本,另外一个 BP 配置成 5 副本。通过纵向增删节点之后,一个 BP 变成了 7 副本,另外一个 BP 变成了 3 副本。

图 5.5 纵向增删节点示意图.png

图5.5 纵向增删节点示意图

5.11 分布式事务一致性

在分布式系统架构下,不同的服务器之间通过网络远程协作而完成的事务,称为分布式事务。

DMDPC 通过两阶段提交技术来保证多个 BP 之间的分布式事务一致性。两阶段提交的参与者为 SP 和 BP。SP 作为全局事务的协调者,统一处理全局事务。BP 作为参与者,是被 SP 调度并执行事务的节点。SP 根据 BP 的响应来决定是否真正的执行并提交事务。所有参与者 BP 要么一起提交要么一起回滚,始终保持事务一致性状态。

DMDPC 是否采用两阶段提交由 INI 参数 DPC_2PC 来控制。

两阶段分为预提交阶段和提交阶段。

5.11.1 第一阶段 预提交

  1. 客户端向 SP 发起事务提交请求。
  2. SP 向所有参与者 BP 广播预提交命令。询问是否可以进行事务 COMMIT,并等待 BP 的响应。
  3. BP 接收到预提交命令之后,将相关操作生成回滚日志写入回滚段并响应 SP。

图 5.6 预提交流程(左成功 右失败).png

图5.6 预提交流程(左成功/右失败)
  1. 如果 SP 收到所有参与者 BP 的预提交成功的消息,则进入第二阶段进行提交。否则,当存在 BP 没有响应导致 SP 无法确定该 BP 是否执行了预提交时,SP 先尝试通知存活 BP 执行回滚。若有 BP 回滚成功,SP 就可以直接响应“事务提交失败”给客户端;否则 SP 只能响应“未知的提交结果”给客户端。客户端不论是收到“事务提交失败”还是“未知的提交结果”,都表示事务执行失败。

图 5.7 回滚流程.png

图5.7 回滚流程

5.11.2 第二阶段 提交

如果 SP 收到所有参与者 BP 的预提交成功的消息,那么说明可以进入提交阶段。

  1. SP 向所有参与者 BP 广播 COMMIT 命令。
  2. BP 接收到提交命令之后,执行二阶段的提交任务,释放事务资源,并将 COMMIT 成功结果反馈给 SP。

图 5.8 提交流程.png

图5.8 提交流程
  1. SP 收到所有参与者 BP 的 COMMIT 成功消息之后,直接响应客户端事务提交成功。二阶段提交时若遇到 BP 与 SP 通信中断,SP 会将提交任务交由异步任务进行处理并立即响应客户端事务提交成功。待故障 BP 与 SP 重新建立连接后,异步任务会自动重新执行二阶段提交,直到提交成功。客户端收到事务提交成功的消息,表示事务执行成功完成。

5.12 分布式事务的数据可见性

在分布式集群环境下,事务通常会跨多个 BP 节点,不同 BP 节点修改数据后执行提交操作的时间差将无法保证各节点上事务的隔离性。为此,DM 借助全局时钟系统(GTS)来帮助 BP 节点对数据的可见性进行判断,进而保证各节点上事务的隔离性。

全局时钟值由 MP 节点统一管理。BP 节点执行事务内的每条语句、执行预提交、提交操作时均会向 MP 申请当前的全局时钟值。

5.12.1 CA

CA 是 BP 用来存放事务信息的数组,CA 是一个全局变量。CA 中登记的事务信息如下:

图 5.9 CA 数组结构图.png

图5.9 CA数组结构图

BP 节点执行事务的执行预提交、提交操作时均会向 MP 申请当前的全局时钟值。然后,BP 将执行操作时的事务号 TID、事务状态和时钟值登记在数组 CA 中。事务状态分为预提交和提交两种,分别表示一阶段预提交和二阶段提交。

事务的 TID 是全局唯一且不变的。当事务从预提交状态进入提交状态,CA 中的事务状态和时钟值会进行同步更新。

5.12.2 MCA

MCA 是 MP 用来存放事务信息的数组。MCA 中登记的事务信息如下:

图 5.10 MCA 数组结构图.png

图5.10 MCA数组结构图

当事务执行提交操作向 MP 申请全局时钟值时,MP 会将该事务的 TID 和当前时钟值登记在 MCA 中。MCA 中登记的事务均为已经执行过提交操作的事务。

5.12.3 事务信息登记流程

  1. 当事务对数据进行修改时,日志记录中会登记修改该数据的事务号 TID。
  2. 当事务执行一阶段预提交时,CA 中会登记 TID、预提交状态和预提交时的时钟值。
  3. 当事务二阶段提交时,MCA 会登记 TID 和提交时的时钟值,CA 会将第 2 步中登记的信息更新为最终的提交状态和提交时的时钟值。

整体流程如下:

图 5.11 事务信息登记流程图.png

图5.11 事务信息登记流程图

5.12.4 数据可见性判断

事务对数据进行操作时,BP 都会向 MP 申请当前的全局时钟值 cur_seq。只要是当前事务操作时钟值 cur_seq 之前的已提交事务修改的数据,对当前事务都是可见的。

BP 节点判断某一数据的可见性时,首先找到日志记录中登记的修改该数据的事务 TID,通过 TID 在 CA 中查询到对应的事务状态 ca_flag 和时钟值 ca_seq,然后将 ca_seq 与 cur_seq 进行比较,最后通过比较结果和 ca_flag 来综合判断出数据的可见性。

具体可见性判断原则如下:

  1. 在 CA 中判断

如果 ca_seq≤cur_seq,且 ca_flag 为“提交”,说明生成该数据的事务在 cur_seq 之前就已提交,则该数据对当前事务可见。

如果 ca_seq≤cur_seq,但 ca_flag 为“预提交”,说明生成该数据的事务在 cur_seq 之前已经成功预提交。那么需要结合 MCA 来进一步判断该事务是否已提交,进入下一步 MCA 中继续判断。

  1. 在 MCA 中判断

如果 MCA 中找到了对应的 TID 和时钟值 mca_seq,且 mca_seg≤cur_seq,则该数据对当前事务可见。

  1. 其他情况均为不可见。

5.13 自动选举主库

多副本系统中,基于 RAFT 协议的选举规则,结合达梦 REDO 日志包的特点,制定了一套适合达梦多副本系统的选举规则,各节点实例根据此规则自动选举出领导者(Leader)和跟随者(Follower),其中 Leader 角色的实例会自动切换为主库模式,Follower 角色的实例会自动切换为备库模式。

选举是集群废黜旧 Leader 产生新 Leader 的过程,其作用是使集群在旧 Leader 故障后及时产生新 Leader。Leader 选举成功后,会定时发送心跳消息到其他节点,如果某个节点在选举超时的间隔内没有收到 Leader 的消息就会认为其故障,并会发起新的选举。

每次发起选举时,节点会切换为 Candidate 角色,并将自己的任期号加 1,从而废黜旧 Leader,然后向其余节点发送投票请求。选举过程通过投票完成,每个任期每个节点有且仅有一张选票,每个节点只会投给与自己相比符合选举要求的节点(有效日志比自己多)。发起选举的节点会直接投给自己,其余节点先收到的投票请求先处理,符合要求就会直接投出选票,不会考虑之后的投票请求。当一个节点收到超过半数(包括自己)的选票,就会成为这个新任期的 Leader。

被选举为 Leader 的数据库实例,会自动将自己切换到主库模式和 Open 状态,其他实例则会自动将自己切换到备库模式和 Open 状态,然后整个多副本系统继续正常对外提供服务。

5.14 RAFT 归档

多副本系统中的主库通过 RAFT 归档方式向备库同步数据。

与本地归档写入保存在磁盘中的日志文件不同,RAFT 归档将主库产生的 Redo 日志通过 XMAL 模块传递到备库,RAFT 归档是多副本系统的实现基础,RAFT 归档只在主库生效,一个主库可以配置 2~8 个 RAFT 备库,归档目标个数必须是偶数(确保总的实例个数是奇数)。

RAFT 归档的执行流程是主库在 Redo 日志(RLOG_PKG)写入联机日志文件前,将 Redo 日志发送到备库,并且不需要等待备库的响应消息,主库继续往下正常执行。备库收到 Redo 日志(RLOG_PKG)后,将日志包加入日志重演任务系统,在日志包写入本地日志文件后,发送日志刷盘消息给主库,主库根据此消息确定是否需要推进 C_SEQNO 和 C_LSN。

5.15 自动同步日志

在多副本系统中,主库通过 XMAL 模块自动将日志包发送给备库,备库在日志刷盘完成后发送刷盘消息给主库,主库在收到多数备库的刷盘消息后,向前推进已提交到的日志包序号和 LSN。

日志的发送与接收,详细介绍如下:

日志发送

主库产生的日志,以日志包为单位发送给备库重演。

每个日志包上会记录日志产生时的任期号以及一个全局递增的日志包序号 G_SEQNO,通过任期号和 G_SEQNO 可以唯一确定一个日志包。

主库每产生一个新的日志包后,就会立即并行、异步地将日志包发送给其余备库节点。如果日志发送失败或者备库校验日志失败,则将对应备库节点置为失效状态,并对其启动故障处理流程。如果主库发现自己过时(其他节点选举出来新主库),则停止发送日志并将自己切换为备库(Follower)模式。

日志接收

多副本系统中,备库收到日志包后会进行有效性校验。先校验是否是当前任期内的主库发送的日志,如果发现日志包来自旧主库,会通知其已经过时,再校验日志是否连续,如果发现日志缺失或者失效,会通知主库校验失败,由主库后续启动异步恢复流程。

备库会对收到的日志包进行排序、缓存,逐个交给日志重演系统进行重演,避免日志包乱序重演导致校验失败。日志重演系统会重构、拆分收到的日志包,采用并行方式重演日志,在日志包刷盘成功后会发送消息通知主库自己的刷盘信息。

5.16 C_SEQNO/C_LSN 维护

在多副本系统中,C_SEQNO 是已经提交的日志包序号,C_LSN 是已经提交的 C_SEQNO 日志包中的最大 LSN。日志包只有在超过半数(包括自己)节点刷盘之后才可以被提交。C_SEQNO 与 C_LSN 由主库推进,备库跟随主库调整。

主库会记录所有节点(包括自己)已刷盘完成的日志包序号和 LSN,当某一日志包在超过半数(包括自己)节点刷盘后,主库会主动将此日志包的包序号和最大 LSN 设为 C_SEQNO 和 C_LSN。主库会在日志包和心跳消息中附加当前的 C_SEQNO 和 C_LSN 发送给备库,备库收到后,根据此信息推进自己本地的 C_SEQNO 和 C_LSN 值。

备库在日志刷盘完成后,会将已刷盘的日志包的 G_SEQNO 和对应的 LSN 发给主库供主库推进 C_SEQNO 和 C_LSN 使用。

特别地,主库和备库均不会将 C_SEQNO 和 C_LSN 推到超过自己已刷盘完成的最大的日志包序号和 LSN,另外处于异步恢复中的备库不参与 C_SEQNO 和 C_LSN 的推进。

5.17 数据页刷盘和检查点推进

在多副本系统中,由于 RAFT 协议中允许对未提交的日志进行截断,因此需要对数据页刷盘和检查点推进做一定的条件限制。

要求数据页上的 LSN 必须小于或等于 C_LSN,也就是所有修改数据页产生的 Redo 日志已经提交到多数节点之后,才允许将数据页写入本地磁盘。

同样的,检查点 LSN 最多只允许推进到 C_LSN 位置,而不是本地已刷盘的最大 LSN。检查点推进过程中,可能碰到某个数据页上又有新的修改而无法刷盘的情况,此时检查点只能推进到修改此页的最小 LSN 的前一个位置,这个过程中需要将检查点从目标位置回调。

5.18 自动故障处理

在多副本系统中,在主库发生故障时,根据达梦多副本的选举规则可以安全的从活动备库中选出新主库,保证已提交的数据不会丢失;在少数备库发生故障或者出现网络延迟时,不会影响主库的正常运行。主备库的故障处理如下:

1. 主库故障处理

当出现硬件故障(掉电、存储损坏等)原因导致主库无法启动,或者是主库内部网卡故障导致主库短期不能恢复正常的情况下,剩余活动节点会自动启动选举流程,选出新的主库,其他节点仍然作为备库运行,选举完成后,多副本集群仍可对外提供服务。

选举新主库的前提条件是活动节点个数需超过配置的总节点个数的一半。也就是要保证多数节点处于活动状态,才可以正常发起选举,如果多数节点都发生故障,则暂时无法启动选举流程。

2. 备库故障处理

备库出现故障时,主库发送心跳或者日志包失败后,会将其归档状态失效,不再向故障备库同步数据。

如果只是少数备库故障,则不会影响到主库正常的日志提交动作,多副本系统仍然能够正常运行,如果多数备库都发生故障,则主库新产生的日志无法提交到多数节点,主库上的事务提交动作可能会被挂起。

5.19 故障恢复

在多副本系统中,在故障实例恢复时,主库可以自动发起异步的数据同步,最终可将备库数据同步到和主库完全一致。

主备库的故障恢复如下:

1.主库故障恢复

主库故障恢复时,多副本系统中如果已经有新主库,则会自动将老主库切换为备库重新加入多副本系统,并自动向其发起异步数据同步,直到主备库数据完全同步一致。

主库故障恢复时,如果多副本系统中没有活动主库,在多数节点都处于活动状态的情况下,则会重新发起选举,如果老主库再次选举成功,则直接将其切换为 Open 状态即可,否则仍然是作为备库重新加入多副本系统。

需要注意的是,主库故障前,本地日志文件中可能已经写入有未提交成功的日志,在作为备库重新加入多副本系统时,如果这些未提交的日志在当前主库上没有找到,则需要对这些日志进行截断处理。

2. 备库故障恢复

备库故障恢复后,多数情况下仍然是作为跟随者重新加入多副本系统,除非重新加入时没有活动主库才会尝试选举为新主库。

如果发起选举流程并且选举成功,则会自动切换为主库模式及 Open 状态。

如果仍然是作为备库重新加入多副本系统,则当前主库会自动向其发起异步恢复流程,直到和主库数据完全一致后,将其归档状态设置为有效状态,然后再次回到正常的数据同步方式并参与主库的日志提交流程。

需要注意的是,如果在备库故障期间,主库发生过多次切换,并且备库本地有未提交成功的日志,则备库异步恢复时,也需要先对本地多写入的日志进行截断处理。

5.20 多副本影子库

在部署有影子库的多副本集群中,可以将所有节点划分为两种:RAFT 库和影子库。RAFT 库为多副本集群中正常修改数据文件,有完整数据,可以正常提供服务的节点;多副本影子库,即影子库(SHADOW 库),为多副本集群中的特殊节点,不修改数据文件(没有数据),但正常参与选举和事务提交,仅提供 V$ 动态视图查询。

可以通过查询 V$RLOG_RAFT_INFO 中 RAFT_SHADOW 字段判断当前库是否为影子库,值为 0 表示是 RAFT 库;值为 1 表示是影子库。

影子库必须配置为多副本的少数节点,不能超过半数。

不支持对影子库执行动态增加和删除。

可以通过配置 SHADOW_CHECK_INTERVAL 参数,定期清理影子库的本地归档文件。

多副本影子库搭建的具体内容请参考 7.5 命令行工具搭建多副本影子库

微信扫码
分享文档
扫一扫
联系客服