每日一博 | BaikalDB 技术实现内幕(一)—— 分布式事务实现
本系列文章主要介绍HTAP数据库BaikalDB的技术实现细节。
作者介绍:罗小兵,百度商业平台研发部高级研发工程师,主要负责BaikalDB事务能力,全局二级索引等方向的研发工作。
欢迎关注 Star github.com/baidu/BaikalDB 国内加速镜像库gitee
一、概述
BaikalDB系统简介
BaikalDB是一个分布式可扩展的存储系统,兼容MySQL协议,整个系统的架构如下图所示:
- BaikalStore 负责数据存储,数据分区按region组织,三个Store的三个region形成一个 Raft-group 实现三副本,多实例部署,Store实例宕机可以自动迁移 Region数据;
- BaikalMeta 负责元信息管理,包括分区,容量,权限,均衡等, Raft 保障的3副本部署,Meta 宕机只影响数据无法扩容迁移,不影响数据读写;
- BaikaDB 负责前端SQL解析,查询计划生成执行,无状态全同构多实例部署,宕机实例数不超过 qps 承载极限即可;
分布式事务
BaikalDB将数据按照range进行切分,每个分区称为region,连续的region组成了数据的整个区间。不同的region位于不同的BaikalStore实例,在不同的机器上,那么当一个SQL语句涉及多个region的更新时,如何保证所有的更新,要么全部成功,要么全部失败呢,这就需要分布式事务。讲到事务,我们先回顾一下事务的ACID特性:
- Atomicity(原子性):一个事务(transaction)中的所有操作,要么全部完成,要么全部失败,不会出现中间状态。事务在执行过程中发生错误,会被 回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样;
- Consistency(一致性):数据库在事务执行之前和执行之后处于一致性状态,完整性没有被破坏;
- Isolation(隔离性):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致;
- Durability(持久性):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
分布式事务就是在分布式环境下实现和传统数据库事务一样的 ACID 功能,目前BaikalDB实现了分布式事务的功能,支持的事务相关的SQL语句包括:
- 事务开始 BEGIN / START TRANSACTION
- 事务提交COMMIT
- 事务回滚ROLLBACK
- 设置自动提交状态SET AUTOCOMMIT=0/1
二、分布式事务实现
BaikalDB通过两阶段提交协议(2PC),借助RocksDB的Pessimistic事务和Savepoint机制实现分布式事务。下面逐步介绍BaikalDB的分布式事务实现,当然,分布式事务非常复杂,BaikalDB的分布式事务还在不断的迭代开发。
两阶段提交协议(2PC)
二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务原子提交的解决办法,它有一个协调者(coordinator)和多个参与者(participant),可以保证在分布式事务中,要么所有参与者要么都提交事务,要么都取消事务,交互流程如图所示:
协调者的提交过程分两个步骤分别与参与者交互:
- 第一阶段:发送prepare请求给所有参与者 ,询问是否可能提交。
- 第二阶段:如果所有参与者都回复YES,则发起第 2 阶段的 提交(commit) 真正提交;如果任意一个 参与者 回复NO或者超时无响应,则第 2 阶段改为 中止(abort) 回滚之前的操作。
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。
- BaikalDB充当协调者的角色,执行BEGIN / START TRANSACTION后创建BEGION(seq_id=1)并缓存在cache_plans,D1(seq_id=2)涉及到region1和region2,将BEGIN和D1一起发送到region1和region2执行;
- D2(seq_id=3),D3(seq_id=4),D4(seq_id=5)分别涉及region1和region2,分别执行;
- P(seq_id=6) 即PREPARE,向region1和region2都发送PREPARE,PREPARE之前的指令都在Raft的Leader上执行的,PREPARE指令执行时会将之前的语句和PREPARE打包成一条Raft日志提交,之后Follower也有了整个事务的操作;
- C(seq_id=7)即COMMIT,向region1和region2都发送COMMIT,COMMIT提交一条Raft日志最终整个事务提交完成。
多语句局部回滚
对于多语句事务,某些DML失败不影响整个事务的提交,当其中某一条DML语句涉及多个region时,可能一些region执行成功,一些region执行失败,当有region执行失败时需要回滚整条DML已经操作过的region,不然多region之间数据会不一致,BaikalDB利用了RocksDB的Savepoint机制来保证数据一致性。
具体实现:
- 在每个region在执行一条语句之前,会调用RocksDB事务的txn->SetSavePoint()设置保存点,并将当前的seq_id保存到栈 std::stack<int> _save_point_seq中;
- 然后执行该语句,如果执行失败,BaikalDB将需要回滚的seq_id添加need_rollback_seq中,在下一次请求BaikalStore时携带上need_rollback_seq到对应的region;
- 在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宕机可能导致事务残留,残留事务除了阻塞后续写请求外,如上表所示最坏的情况可能会导致数据不一致,这里要解决的问题有两个:
- 残留事务如何清除;
- 部分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 |
说明:
- BEGIN实际会和每个region的第一条DML1一起打包执行,执行时随机指定一个region作为primary region;
- COMMIT指令之前的命令都是并发执行,执行出错直接给用户返失败;
- COMMIT首先在primary region执行,并且必须保证执行成功;
- 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可能得宕机时刻如下图所示,下面说明在不同时刻宕机重启事务的恢复流程。
- ① 或②时宕机,此时之前的语句都在leader上执行,宕机后Raft重新选主,之后请求发送到新leader时根据请求的start_seq_id判断让BaikalDB重新发送之前的语句来恢复事务状态(region第一次执行事务时start_seq_id都会置为1,如果请求的start_seq_id>1但BaikalStore重启后因为没有执行过事务,记录的seq_id为0,需要重传之前的语句);
- ③或④时宕机,此时PREPARE执行成功,语句已经通过Raft复制到整个Raft组。在重启时通过RocksDB的GetAllPreparedTransactions拿到已经prepare的事务txn_id,再根据write_meta_before_prepared记录的log_index+txn_id,拿到执行过prepare事务的log_index,根据log_index读取Raft日志恢复事务状态;
- ⑤时宕机,此时commit已经执行,通过RocksDB的GetAllPreparedTransactions拿不到txn_id,通过检查是否存在pre_commit信息,可以知道宕机在commit 和 write_meta_after之间,此时重新执行write_meta_after_commit即可;
- ⑥时宕机,此时事物已经全部执行完成,无需任何操作。
其他异常处理
当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的并发度。
实现要点:
- DML语句在leader上执行是在Raft外,在执行成功后再提交一条Raft log,follower的DML语句是在Raft状态机内执行,这样一来状态机不会被卡住;
- 执行第一条语句时需要记录Raft的log_index到RocksDB,如果BaikalStore宕机,重启时从记录的最小Raft的log_index开始遍历Raft log,恢复尚未提交的事务。
只读事务优化
只读事务在执行commit时按照当前的实现,PREPARE和CPOMMIT会两次raft日志,但这是没有必要的,只需要在Leader上进行判断,对只读事务,直接执行prepare和commit即可,更进一步,对只读事务将prepare和commit转化为rollback处理,RocksDB事务执行rollback性能更好。
后记
本文从实现原理、宕机恢复等方面介绍了BaikalDB分布式事务的实现,支持单语句事务(autocommit=1模式下的DML形成单语句事务)和使用显式控制命令的多语句事务。 binlog,MVCC,子查询等功能还正在不断完善中,如果你有兴趣,欢迎加入github.com/baidu/BaikalDB。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
OSCHINA APP v4.9.8 发布
OSCHINA APP v4.9.8 版本发布了。此版本修复了一些 bug。 新版更新日志: 升级了 QQSDK,适配 QQAndroid 10 纯图分享 修复了问答评论转发动弹失效的 bug 修复分享图片二维码失效的 bug 修复网络异常状态下动弹详情闪退的 bug 删除了博客打赏 修复主页内存被回收,tab 状态显示异常的 bug 安卓下载:→戳我戳我戳我← 扫码下载↓↓↓
- 下一篇
KDE 社区迁移至 Gitlab
2019 年 11 月,KDE 社区宣布计划迁移至 Gitlab。目前,它已正式完成迁移计划的第一阶段,并加入了 GitLab 的开源计划。 KDE是一个国际性的自由软件社区,拥有一系列跨平台应用程序。它最著名的产品 Plasma 桌面是许多Linux 发行版的默认桌面环境。这个社区共同创建和维护了 200 多个应用程序和不计其数的插件,还包括 1000 多个存储库、80 多个 Qt 框架,以及超过 2600 个项目。 KDE e.V 主席 Aleix Pol 表示,降低门槛、简化贡献者的工作是选择 Gitlab 的主要原因。他还认为,“使项目贡献者轻松参与的测试和交付方式无疑将成为我们生态系统的转折点。” 在迁移过程中,KDE 面临的最大挑战是要处理的巨大数据量,以及如何将其集成到众多工具中。因此,KDE 决定分阶段进行迁移,分别处理不同的数据类型。他们还创建了自定义工具,用于迁移中的批量更新。 KDE 的整个迁移计划大致如下: 阶段 1:代码托管和审核 阶段 2:CI 阶段 3:开发人员的任务管理 现在 KDE 仅完成了第一阶段的迁移任务。除了解决数据的迁移问题外,接下来,团队和成...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- Mario游戏-低调大师作品
- 2048小游戏-低调大师作品
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Red5直播服务器,属于Java语言的直播服务器