KunlunBase DDL事务处理原理和技术简介
KunlunBase DDL事务处理原理和技术简介
在关系数据库系统中,用户通过DDL语句定义数据库系统中所有类型的数据库对象,比如database, schema, table, index, view, materialized view, role/user, trigger, stored procedure,以及设定这些数据库对象的访问控制权限等。在KunlunBase中也是这样,并且KunlunBase支持所有这些DDL功能。
可靠地实现DDL 功能,对于一个分布式数据库系统来说不仅意味着DDL语句要原子地执行,集群节点因为各种软硬件故障或者网络故障和异常而导致流程中断的话,不能残留中间状态和中间数据 --- 对于单机数据库也有这个要求 --- 而且每个DDL所描述的元数据更新在集群所有节点中扩散的过程要么完全严格同步,要么分布式数据库系统必须能发现元数据扩散不同步导致的集群元数据与节点本地数据和元数据不一致,避免导致程序崩溃或者故障,至少要将错误告知客户端,以便客户端重新执行。
KunlunBase DDL功能简介
在KunlunBase中,用户可以配置启动任意数量的计算节点,所有这些计算节点是对等关系,客户端可以连接到任何一个计算节点执行DDL或者DML SQL语句。
KunlunBase的计算节点本地存储着用户数据的所有元数据,包括KunlunBase支持的每种数据库对象(database、schema、表、视图、索引、序列、物化视图、列、运算符、domain、用户、权限、CHECK约束、存储过程、触发器等等)的元数据,以及KunlunBase集群拓扑结构信息(集群的所有计算节点,存储shard及其存储节点,元数据节点,计算机服务器的详细信息)。
KunlunBase能够确保在执行DDL语句期间集群节点故障不会导致集群的数据或者元数据不一致,同时KunlunBase通过DDL replication实现的最终一致性确保集群所有计算节点(包括后来新增的计算节点)的本地元数据完全相同,因而客户端才可以连接任何一个计算节点都可以正确地执行SQL语句。
这给应用软件开发者带来了极大的便利,也简化了DBA的维护管理工作。分布式数据库系统要做到这一点面临一系列技术挑战,本文介绍KunlunBase是如何做到的。
KunlunBase DDL事务处理架构
DDL Log和全局一致的执行顺序
在元数据集群中,每个KunlunBase集群有一个叫做DDL_logs的元数据表,其中记录着这个集群执行过的来自客户端的所有DDL语句以及(如有的话)对应每个DDL语句发送给存储节点的DDL语句,以及其他状态信息。每条这样的信息记为ddl_log。KunlunBase 把DDL_logs表作为一个队列来使用,最新执行的DDL事务的ddl_log 会被追加到队尾。
一个计算节点CN每次执行一个DDL语句期间,它会发起一个分布式事务ddl_txn,,ddl_txn的参与方包括CN自身、元数据集群和存储集群。在CN内部的事务分支中,CN查询和修改、插入相关数据库对象的元数据到其本地的元数据表,以便执行此DDL涉及的元数据更新;如果执行的DDL涉及到用户数据表,那么就需要存储节点参与,于是CN会发送适当的DDL语句(通常与客户端发送的原始DDL语句完全不同)给数据表所在的存储节点去执行;同时,ddl_txn还需要在元数据集群中写ddl_log,因此在元数据集群中发起的分布式事务分支中CN依次做这三件事:
锁住DDL_logs队列的队尾,这样可以阻止其他并发的DDL事务追加ddl_log 到DDL_logs中并且会等待其他并行执行的DDL事务执行完毕。
确保它已经执行完毕了DDL_logs表中所有的现有语句,包括正在执行的DDL语句。没有完成就一直等待。
追加自己的ddl_log到DDL_logs
DDL log 及其排队追加写入
在一个DDL事务ddl_txn做上述操作的过程中,其他并发执行的DDL会因为DDL_logs锁互斥而等待ddl_txn提交后才能继续执行。这样就确保了DDL_logs表中的所有DDL语句在全集群范围内一定是排队依次执行的,并且集群所有计算节点执行这些DDL的顺序完全相同,因而确保了集群的所有计算节点的元数据完全等价。这里说等价而不是说相同,是因为数据库对象的OID是本地分配的,在不同的计算节点实例中,同一个数据库对象的OID未必相同,但是其名称和其他属性及其关联关系一定是相同的,因而是等价的。
DDL分布式事务处理
(DDL事务执行流程示例)
在KunlunBase中执行的每一个DDL语句是一个分布式事务,为方便描述记为ddl_txn。KunlunBase通过分布式事务处理机制来确保其原子性[1],由DDL事务管理器(后文记为ddl_trx_mgr)来协调ddl_txn事务的执行和提交。上图所示例子执行过程中,在关键节点1,2,3,4如果这个DDL事务的参与方(计算节点、存储节点、元数据节点)发生各种软硬件故障,那么KunlunBase能够确保DDL事务的ACID属性,有极个别情况下需要DBA手动介入,详见下文。
DDL事务协调器(后文记为ddl_trx_mgr)位于计算节点,ddl_txn的参与者包括:
1、计算节点
在计算节点的事务分支中更新本地元数据和执行坐标到pg_ddl_log_progress元数据表。
2、存储集群
如果DDL涉及到用户数据,比如CREATE/DROP/ALTER DATABASE/SCHEMA/TABLE/INDEX/SEQUENCE/MATERIALIZED VIEW等语句,需要在存储节点执行DDL语句做数据表的创建、更新、删除等操作。这就需要存储节点的事务分支参与到ddl_txn中在目标存储节点执行对应的DDL语句。多数DDL不涉及存储集群,比如view,存储过程,trigger, policy,user, role,权限等。
3、元数据集群
每个DDL事务都需要写入DDL_logs表。在上一节我们已经详述了对DDL_logs表的写入流程。
前文已经介绍了一个DDL语句的执行流程,本节重点介绍在执行流程中任何相关节点的故障及其恢复方法。
KunlunBase DDL容灾恢复
在KunlunBase中一个DDL事务的执行过程中,如果参与该事物执行的计算节点、存储节点、元数据节点发生故障(重启、硬件故障、宕机、断电、断网等),那么KunlunBase可以确保DDL事务的ACID属性。
下面以CREATE TABLE语句为例来说明。客户端发送CREATE TABLE语句给计算节点,计算节点的DDL事务协调器 ddl_trx_mgr首先在计算节点内部启动事务分支trx_cn,在其中执行CREATE TABLE语句,修改相关元数据表(比如pg_class等)。然后在元数据节点启动XA事务分支trx_meta并执行前述的写入ddl_log到DDL_logs表的操作。
失败情形1
如果写入ddl_log失败则回滚trx_cn和trx_meta并返回错误给客户端。
(DDL_logs写入失败的处理)
失败情形2
在trx_meta中写入ddl_log成功后,如果这个DDL需要存储节点参与,计算节点ddl_trx_mgr就会发送针对存储节点的DDL语句(记为ddl_storage,不是客户端发给KunlunBase的DDL语句)给存储节点。如果ddl_storage失败,则ddl_trx_mgr回滚trx_meta和trx_cn,然后返回错误给客户端。存储节点继承了社区版MySQL-8.0的能力,可以确保ddl_storage失败的话不残留任何中间状态和数据。如下图所示。
如果一个DDL语句不涉及存储节点,比如针对view, trigger, stored procedure, user, role, privilege的DDL语句,也就不需要发送ddl_storage,所以KunlunBase一定可以干净地回滚这个ddl_txn,不残留任何中间状态。
(存储节点执行DDL失败的处理)
失败情形3
如果ddl_storage成功后,提交trx_meta之前计算节点退出(比如因为服务器故障,断电,网络故障等原因),则trx_cn自动回滚了,而trx_meta会残留下来。KunlunBase的cluster_mgr组件会定期在元数据集群查找这样的trx_meta并回滚它们。如下图所示。这种情况下在存储节点创建好的表需要DBA手动删除,具体原因见下节。
如果一个DDL语句不涉及存储节点,比如针对view, trigger, stored procedure, user, role, privilege的DDL语句,也就不需要发送ddl_storage,所以KunlunBase一定可以干净地回滚这个ddl_txn,不残留任何中间状态。
而对于发送ddl_storage的语句,比如CREATE/DROP/ALTER DATABASE/SCHEMA/TABLE/INDEX/SEQUENCE/MATERIALIZED VIEW 等语句,则暂时需要DBA手动处理故障,见下节详述。
(计算节点宕机的处理)
MySQL 原子DDL的限制带来的问题
由于MySQL-8.0的DDL是原子的,但是无法在显式事务中执行,因此,无法在存储节点发起一个XA事务分支来执行发送给存储节点的DDL语句并且在需要的时候做回滚来去除掉这个DDL导致的效果。所以在需要回滚一个DDL事务时,如果需要存储节点参与并且ddl_storage已经执行成功,那么KunlunBase无法回滚掉在存储节点执行的DDL产生的效果,需要DBA手动处理。
一些情况下,DBA可以轻易处理,比如CREATE TABLE,回滚的方法就是直接删掉存储节点上创建的表即可;但是有些情况下DBA无法处理,比如DROP TABLE,在存储节点上表已经被drop掉了不存在了,无法轻易找回。
为了防止DDL可能失败造成的无法恢复的问题,DBA可以使用KunlunBase的逻辑数据备份和恢复功能 ,在执行一个DDL之前,先逻辑备份这个表。如果DDL执行失败需要重建该表,那么用刚才对该表做的逻辑备份数据做表级逻辑恢复即可快速找回该表。
另一个种方法是,用户可以使用KunlunBase提供的Online DDL功能,先把需要创建的目标表创建好,其表结构和定义可以等价于做了所有需要针对该表做的DDL和repartition操作而形成的表。然后调用cluster_mgr API或者使用XPanel的GUI 来将源表数据灌入目标表然后切换使用新建的表。
在KunlunBase-1.3版本中,我们会把存储节点的DDL语句做成可以在XA事务中做两阶段提交的,这样第二阶段既可以提交也可以回滚,这样就可以完全解决这个问题。
失败情形4
如果ddl_storage语句执行成功,ddl_trx_mgr就发送提交命令给trx_meta,将其提交,然后提交trx_cn。如果在提交trx_meta之后,提交trx_cn之前,计算节点宕机,那么trx_cn自然回滚,待此计算节点重启后,ddl_applier线程在启动之初会从pg_ddl_log_progress记录的位置开始查找本节点上次执行的DDL并复制执行它,这样就把上次执行时对本地元数据应做的更新再次做好了。如下图所示。
(计算节点重启后的恢复)
DDL 复制及其故障恢复机制
每个计算节点CN通过DDL复制机制,把其所在的KunlunBase集群内其他计算节点执行过的DDL对其本地元数据的更新在CN 复制过来,以便集群内所有计算节点的本地元数据是等价的。下图是DDL复制的功能示意图。
(DDL复制功能示意图)
一个计算节点CN的后台进程ddl_applier会持续地从元数据集群的DDL log执行不加锁的SELECT 查询(因而不会被正在执行的DDL事务阻塞)来获取其他计算节点执行过的并且本节点还没有执行的下一条DDL语句及其在DDL_logs的编号ddl_op_id作为复制坐标,并且在CN本地的事务ddl_repl_trx中来复制执行此DDL对本节点元数据的修改,从而复制其他计算节点执行过的DDL 对其本地元数据表的更新。Ddl_applier复制执行DDL语句时不执行对存储节点的修改因为此操作在该DDL最初执行时已经在目标存储节点上执行过了。
(DDL复制的交互示意图)
在ddl_repl_trx中复制执行了DDL语句之后,还要存储复制坐标ddl_op_id到专门的元数据表pg_ddl_log_progress 中,然后再提交ddl_repl_trx。DDL复制事务ddl_repl_trx因为任何原因而中断的话,它就会自动回滚。同时,Ddl_applier每次启动总是从表pg_ddl_log_progress 记录的复制坐标开始复制。因此,DDL replication 随时可以中断,随后计算节点总是可以精确地恢复续传,不会有遗漏和重复。
(pg_ddl_log_progress元数据表示例)
通过DDL复制实现的集群元数据最终一致性
确保DDL事务的一致性,就是要以下三者保持一致:
计算节点元数据
元数据集群的DDL log
可选的,存储集群的数据对象
DDL复制实现的一致性,是一种最终一致性,也就是说,一个KunlunBase集群的多个计算节点之间,以及计算节点和存储节点之间,由于执行DDL和复制执行DDL的微小时间差,难免会有微小的时间窗口内前述各节点元数据不同。在此时间窗口内执行DML语句就可能因为相关节点的元数据不一致导致错误。KunlunBase可以发现元数据不一致并报错,客户端再试几次(很少超过1次,等DDL复制将最新的DDL复制执行后)就成功了。为了集群性能考虑,KunlunBase不做全局锁定来实现完全一致的元数据对齐和验证,否则DML的性能就会严重损失。
计算节点与存储节点
由于每个计算节点做DDL复制的微小延迟,理论上有可能出现计算节点CNx的本地元数据没有及时更新就执行了一个DML语句来读写数据表tx,而存储节点上的数据表tx与CNx的tx的元数据不一致,从而导致错误。
比如,客户端连接conn1连接到计算节点CN1创建表t1,然后客户端连接conn2立刻发送DML语句到计算节点CN2来读写t1,但是CN2报错说t1不存在。客户端在conn2中再次发送同样的DML语句一次或者若干次就成功了。
再比如,在客户端连接conn1中drop掉了t1的一列colx,然后在conn2中立刻执行SELECT查询colx,那么CN2会受到存储节点的错误说colx不存在。在conn2中再发送同样的SELECT语句,则CN2已经复制执行了相关DDL,知道colx已经被drop了,因此直接报错给客户端说colx不存在。
计算节点之间
如果在conn1中发送DDL语句创建了存储过程、视图等,在conn2中立刻执行DML来使用这个存储过程或者视图那么此DML可能会失败,因为CN2还没有复制执行相关的 CREATE PROCEDURE/CREATE VIEW 语句,报错说所用的存储过程或者视图不存在。稍等再试则成功了。
总结
KunlunBase的DDL事务处理和DDL复制功能,确保了KunlunBase集群可以可靠地执行DDL语句,并且让集群内所有计算节点具有相同的元数据。DDL事务处理可以在绝大多数故障情况下自动恢复。DDL复制则总是可以自动正确恢复和继续复制执行。
在极特殊的情况下,发生节点故障时,DDL事务可能在存储节点遗留中间数据需要DBA手动恢复处理。为此可以使用KunlunBase的Online DDL和/或逻辑备份恢复来做可靠的DDL执行,详见正文。
为了性能考虑,DDL复制的最终一致性会导致有微小的时间窗口内集群的多个计算节点之间以及计算节点和存储节点之间元数据有可能不一致,KunlunBase能够发现这些不一致并且会返回错误给客户端,客户端需要重试即可正确执行相关的DDL或DML语句。