文章详情

短信预约信息系统项目管理师 报名、考试、查分时间动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

BaikalDB技术实现内幕(一)-- 分布式事务实现

2016-05-06 20:05

关注

BaikalDB技术实现内幕(一)-- 分布式事务实现

本系列文章主要介绍HTAP数据库BaikalDB的技术实现细节。

作者介绍:罗小兵,百度商业平台研发部高级研发工程师,主要负责BaikalDB事务能力,全局二级索引等方向的研发工作。

欢迎关注 Star github.com/baidu/BaikalDB

一、概述

BaikalDB系统简介

BaikalDB是一个分布式可扩展的存储系统,兼容MySQL协议,整个系统的架构如下图所示:

图片

分布式事务

BaikalDB将数据按照range进行切分,每个分区称为region,连续的region组成了数据的整个区间。不同的region位于不同的BaikalStore实例,在不同的机器上,那么当一个SQL语句涉及多个region的更新时,如何保证所有的更新,要么全部成功,要么全部失败呢,这就需要分布式事务。讲到事务,我们先回顾一下事务的ACID特性:

分布式事务就是在分布式环境下实现和传统数据库事务一样的 ACID 功能,目前BaikalDB实现了分布式事务的功能,支持的事务相关的SQL语句包括:

二、分布式事务实现

BaikalDB通过两阶段提交协议(2PC),借助RocksDB的Pessimistic事务和Savepoint机制实现分布式事务。下面逐步介绍BaikalDB的分布式事务实现,当然,分布式事务非常复杂,BaikalDB的分布式事务还在不断的迭代开发。

两阶段提交协议(2PC)

二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务原子提交的解决办法,它有一个协调者(coordinator)和多个参与者(participant),可以保证在分布式事务中,要么所有参与者要么都提交事务,要么都取消事务,交互流程如图所示:

图片

协调者的提交过程分两个步骤分别与参与者交互:

BaikalDB的实现

BaikalDB的分布式事务通过2PC+RocksDB的单机事务实现悲观事务。

一次事务执行通常包括BEGIN语句,一个或多个DML语句,COMMIT语句或ROLLBACK语句,BaikalDB为每个语句都分配了一个seq_id,并且seq_id单调连续递增。

seq_id=1对应BEGIN语句,会在BaikalDB缓存,和第一条DML语句一起发送给BaikalStore,然后BEGIN语句会在BaikalStore创建一个RocksDB的Pessimistic事务,接着开始执行之后的DML语句,同时BaikalStore会记录当前执行过的DML语句的seq_id,用于事务幂等处理,COMMIT语句实际上分成了PREPARE + COMMIT,先发PREPARE到各个region,都返回成功,再发COMMIT。

下表介绍了与事务相关的数据信息:

数据 描述
事务ID(txn_id) 全局唯一都事务ID,由两部分组成,BaikalDB全局唯一的实例ID(下文中的server_instance_id,占用3 bytes)和BaikalDB实例内部ID(5 bytes,实例内部全局唯一且递增)
DML语句ID(seq_id) 当前命令在事务内部的序列号,在事务内由1开始连续递增
BaikalDB实例ID(server_instance_id) BaikalDB全局唯一的实例ID(占用3 bytes,由BaikalMeta统一分配)
待回滚的DML语句ID(need_rollback_seq) 实现多语句局部回滚
事务所有DML语句集合(cache_plans) 缓存了事务中的所有语句的执行计划,用于在事务执行过程中发生Region分裂或RAFT切主时进行重试

BaikalDB与BaikalStore事务交互的message为:

message TransactionInfo {
    required uint64 txn_id            = 1;  // 由baikaldb生成的全局事务id
    required int32  seq_id            = 2;  // 当前语句的seq_id
    optional int32  start_seq_id      = 3;  // the start_seq_id of the command in this request (include cached)
    optional bool   optimize_1pc      = 4;  // 单region 1pc优化
    repeated int32  need_rollback_seq = 5;  //因为在某些region上执行失败,需要全局回滚seq_id
    repeated CachePlan cache_plans    = 6;  //缓存的query的执行计划
    optional bool    autocommit       = 7;
};

下面举例说明多语句事务整个两阶段提交示意图:

图片

整个多语句事务语句执行的序列为BEGIN,D1,D2,D3,D4,P,C。

  1. BaikalDB充当协调者的角色,执行BEGIN / START TRANSACTION后创建BEGION(seq_id=1)并缓存在cache_plans,D1(seq_id=2)涉及到region1和region2,将BEGIN和D1一起发送到region1和region2执行;
  2. D2(seq_id=3),D3(seq_id=4),D4(seq_id=5)分别涉及region1和region2,分别执行;
  3. P(seq_id=6) 即PREPARE,向region1和region2都发送PREPARE,PREPARE之前的指令都在Raft的Leader上执行的,PREPARE指令执行时会将之前的语句和PREPARE打包成一条Raft日志提交,之后Follower也有了整个事务的操作;
  4. C(seq_id=7)即COMMIT,向region1和region2都发送COMMIT,COMMIT提交一条Raft日志最终整个事务提交完成。

多语句局部回滚

对于多语句事务,某些DML失败不影响整个事务的提交,当其中某一条DML语句涉及多个region时,可能一些region执行成功,一些region执行失败,当有region执行失败时需要回滚整条DML已经操作过的region,不然多region之间数据会不一致,BaikalDB利用了RocksDB的Savepoint机制来保证数据一致性。

具体实现

  1. 在每个region在执行一条语句之前,会调用RocksDB事务的txn->SetSavePoint()设置保存点,并将当前的seq_id保存到栈 std::stack _save_point_seq中;
  2. 然后执行该语句,如果执行失败,BaikalDB将需要回滚的seq_id添加need_rollback_seq中,在下一次请求BaikalStore时携带上need_rollback_seq到对应的region;
  3. 在region上以逆序的方式遍历need_rollback_seq,然后和栈顶_save_point_seq.top比较,若seq_id与栈顶相等,表明该region之前执行了该seq_id对应的语句,则调用RocksDB的txn->RollbackToSavePoint()回滚保存点。

通过上面的步骤来实现局部语句的回滚,下面举例说明整个流程,假设多语句事务:

sql seq_id 涉及的region 执行结果
B 1 R1,R2,R3
D1 2 R1 成功
D2 3 R2,R3 成功
D3 4 R1,R2,R3 R3失败,整条语句需要回滚
D4 5 R3 成功
D5 6 R1,R2 R2失败,整条语句需要回滚
P 7 R1,R2,R3 成功
C 8 R1,R2,R3 成功

整个_save_point_seq的变化如下表所示:

图片

三、异常宕机处理

两阶段提交协议的缺点是在整个交互过程中,所有节点都处于阻塞状态,RocksDB的Pessimistic事务在未提交之前key都处于锁定状态。

当协调者发生故障后,参与者会一直阻塞下去,更严重的是在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这回导致只有一部分参与者接受到了commit请求,导致数据不一致。

本节将介绍BiakalDB如何处理阻塞、协调者单点以及数据不一致的问题。

BaikalDB异常处理

BaikalDB充当着两阶段提交的协调者角色,如果事务执行过程中BaikalDB宕机可能会对整个系统之后的操作产生影响,参与者将处于不确定状态,如图所示,不同参与者状态不一致了。

图片

下面对不同阶段BaikalDB宕机可能产生的影响总结如下:

图片

BaikalDB宕机可能导致事务残留,残留事务除了阻塞后续写请求外,如上表所示最坏的情况可能会导致数据不一致,这里要解决的问题有两个:

  1. 残留事务如何清除;
  2. 部分commit导致数据不一致如何处理。

这里BaikalDB参考Percolator事务模型的机制,随机选取第一条DML指令的某一个region为primary region(sync point),在执行COMMIT/ROLLBACK的时候首先向primary region发送请求,保证primary region执行成功,再向其他region发送COMMIT/ROLLBACK,让primary region来充当事务协调者的角色,具体流程:

指令 序号seq_id 涉及region_id
BEGIN 1 1,2,3
D1 2 1
D2 3 2
D3 4 1,2,3
D4 5 3
PREPARE 6 1,2,3
COMMIT 7 1,2,3

图片

说明:

  1. BEGIN实际会和每个region的第一条DML1一起打包执行,执行时随机指定一个region作为primary region;
  2. COMMIT指令之前的命令都是并发执行,执行出错直接给用户返失败;
  3. COMMIT首先在primary region执行,并且必须保证执行成功;
  4. primary region执行COMMIT成功后剩下的region并发执行COMMIT。

primary region事务执行完commit/rollback后,second region如果一定时间阈值未收到commit/rollback则通过反查primary region来判断事务是commit还是rollback,这里primary region事务的commit/rollback状态需要记录,我们只对rollback在RocksDB中记录信息,second region反查时首先查看事务是否还存在,如果存在则不做操作,如果不存在再读取RocksDB判断是否有事务的rollback记录,如果有就执行rollback,如果没有就执行commit,这样保证了整个事务commit/rollback的一致性。 而对于残留的事务,primary region超过一定时间阈值会主动rollback,其他的region则通过反查的方式最终保证事务被及时清除。 通过这样的方式解决事务的阻塞、协调者单点以及数据不一致的问题。

BaikalStore异常处理

BaikalStore作为存储层,也是2PC的参与者,需要保证高可用。BaikalStore用 region 组织,三个 Store 的 三个region形成一个 Raft group 实现三副本实现高可用,Raft库使用的braft。BaikalStore要考虑在PREPARE/COMMIT前后宕机如何恢复,为此我们需要记录相关的元数据信息,再利用Raft的日志(把Raft日志作为redo log)来恢复事务状态,相关元信息包括:

元信息记录 说明
write_meta_before_prepared 在执行RocksDB的prepare之前记录prepare时Raft的log_index+txn_id
write_pre_commit 在执行RocksDB的commit/rollback之前记录Raft的log_index+txn_id
write_meta_after_commit 在执行RocksDB的commit/rollback之后记录Raft的log_index,同时删除write_pre_commit和write_meta_before_prepared记录的Raft的log_index+txn_id

BaikalStore可能得宕机时刻如下图所示,下面说明在不同时刻宕机重启事务的恢复流程。

图片

  1. ① 或②时宕机,此时之前的语句都在leader上执行,宕机后Raft重新选主,之后请求发送到新leader时根据请求的start_seq_id判断让BaikalDB重新发送之前的语句来恢复事务状态(region第一次执行事务时start_seq_id都会置为1,如果请求的start_seq_id>1但BaikalStore重启后因为没有执行过事务,记录的seq_id为0,需要重传之前的语句);
  2. ③或④时宕机,此时PREPARE执行成功,语句已经通过Raft复制到整个Raft组。在重启时通过RocksDB的GetAllPreparedTransactions拿到已经prepare的事务txn_id,再根据write_meta_before_prepared记录的log_index+txn_id,拿到执行过prepare事务的log_index,根据log_index读取Raft日志恢复事务状态;
  3. ⑤时宕机,此时commit已经执行,通过RocksDB的GetAllPreparedTransactions拿不到txn_id,通过检查是否存在pre_commit信息,可以知道宕机在commit 和 write_meta_after之间,此时重新执行write_meta_after_commit即可;
  4. ⑥时宕机,此时事物已经全部执行完成,无需任何操作。

其他异常处理

当Raft发生Leadership Transfer时,如果此时事务已经执行了Prepare命令,说明整个事务已经通过Raft复制给follower,无需特殊处理。否则需要将Old Leader上的事务回滚,并在收到BaikalDB发送的下一条事务命令时,在New Leader上利用BaikalDB的重试机制补发缺失的命令,从头开始执行事务。

四、优化

事务功能上线运行的过程中,我们发现了很多可以优化的地方,本节简单介绍两点。

多语句Raft复制优化

如前所述,事务执行流程为,先在leader执行,并将指令缓存在leader,待prepare时通过Raft复制将所以指令复制给follower。因为leader已经在Raft外部执行完成,在日志应用(on_apply)时leader不再执行,而follower从begin到prepare所有指令全部在on_apply中执行,根据Raft语义,on_apply为串行执行,所以导致follower执行缓慢效率低。优化为leader执行一条指令成功后立即通过raft复制给follower执行,提高follower的并发度。

图片

实现要点:

只读事务优化

只读事务在执行commit时按照当前的实现,PREPARE和CPOMMIT会两次raft日志,但这是没有必要的,只需要在Leader上进行判断,对只读事务,直接执行prepare和commit即可,更进一步,对只读事务将prepare和commit转化为rollback处理,RocksDB事务执行rollback性能更好。

后记

本文从实现原理、宕机恢复等方面介绍了BaikalDB分布式事务的实现,支持单语句事务(autocommit=1模式下的DML形成单语句事务)和使用显式控制命令的多语句事务。 binlog,MVCC,子查询等功能还正在不断完善中,如果你有兴趣,欢迎加入github.com/baidu/BaikalDB。

阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-数据库
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯