本文选自“字节跳动基础架构实践”系列文章。
“字节跳动基础架构实践”系列文章是由字节跳动基础架构部门各技术团队及专家倾力打造的技术干货内容,和大家分享团队在基础架构发展和演进过程中的实践经验与教训,与各位技术同学一起交流成长。
在字节跳动,随着搜索、推荐、广告等业务的启动和发展,在数据上有了跨分区数据一致性的需求。于是我们调研、设计并实现了第一代基于表格系统的分布式事务。本文详细介绍了该表格事务系统的关键设计以及一些至关重要的原创性优化,希望能分享给大家一些分布式事务设计的思路。
1. 背景
分布式表格存储系统在业界拥有广泛的应用场景。Google 先后发布了 Bigtable 和 Spanner 两代分布式表格存储系统,承接了其公司内部和外部云服务中的所有表格存储需求,其中 Bigtable 的开源实现 HBase 在国内外公司中都得到了广泛的使用。
在字节跳动,随着头条全网搜索项目等业务的启动和发展,业务需要一个全局有序、容量巨大同时性能高效的表格存储系统以存储整个互联网中所有链接和网页,并保证互联网上发生的所有变更都被能实时的更新到表格存储系统中。我们团队最初使用 HBase 提供服务,比如搜索场景在全网链接关系的实时更新需求下需要提供足够高的可用性和足够低的延时,由于其数据量极其庞大所以会创建极多的数据分片,集群的整体尾延时和可用性会随着数据分片实例数的增多而造成指数级别的恶化,因此对每一个分片实例的延时和可用性提出了更高的要求。但由于 HBase 存在尾延时较高和可用性较低的问题,并不能满足我们的需求,于是我们团队自研了第一代基于 Bigtable 数据模型的分布式表格存储系统 Bytable。
类似于 Google Percolator 中提到的问题,我们的业务也有跨分区数据一致性的需求,于是调研、设计并实现了我们的第一代分布式事务。本文介绍了系统的关键设计点以及一些原创优化。在第 2 节中介绍了事务相关的一些基础概念以及我们的选择。第 3 节介绍了数据模型和架构。第 4 节介绍了系统的设计和优化。第 5 节展望了下未来演进的方向。第 6 节加入了一些 FAQ,方便读者深入理解设计细节。总体上来讲干货多多,相信读者一定会有一个非常好的体验。
2. 事务简介
2.1 模型
目前常见的分布式事务模型有大致 4 种:
2PC(Prepare-Commit/Abort)
在分布式的情况下,没有办法一次性地使得分布式系统中牵涉的不同实例之间达到原子的状态。试想假设发起者所在的服务试图一次性一阶段执行涉及 s1 和 s2 两个服务的修改操作 op1 和 op2,那么在这一次性执行的过程中是没有办法保证 op1 和 op2 的原子性的,很可能或者 s1 或者 s2 由于各种原因失败了,这是分布式下 1PC 必然做不到的事情。那么 2PC(两阶段提交)怎么做到?
两阶段提交执行分为两个阶段,第一阶段 prepare,预写数据,第二阶段 commit,提交可见。具体的牵涉节点有两种角色,一个是协调者,一个是参与者。协调者负责整个事务的生命周期管理,调度管理整个事务的执行,参与者为相应的事务相关的操作节点。
a. 选出一个事务协调者,负责整个事务的调度和执行,协调者选出一个状态决议服务(可能是他自己,也可能全局一个不需要选)维护当前事务的状态,比如 TransactionManager(TM)。
b. 第一阶段:协调者先向参与者发送 prepare 请求,其中不仅有相应的 operation,还携带了事务状态决议服务的信息。把要做的修改预先写下但不对外生效,并记录状态决议的必要信息(比如 TM 信息以及事务 id)。
c. 第二阶段:协调者收集参与者的 prepare 执行结果,如果成功则向 TM 提交事务,并向参与者异步(同步)发送提交请求,如果失败则向 TM 取消事务。

为什么 2PC 能保证原子性?因为 prepare 的时候不仅持久化记录下了操作,还记录了事务状态决议的必要信息。即便在过程中有参与者或者协调者挂了,我们都可以最终根据事务状态信息决议自己该提交还是回滚。而分布式的情况下高可靠的系统一般实例都是有多个副本,能保证高可用,所以基本没有单点问题。
3PC
3PC 相比 2PC 增加了超时机制以及资源锁定预留阶段,其虽然解决了标准 2PC 的阻塞等问题,但是增加了 latency,并且依然存在数据不一致问题,而且其超时自动提交机制很鸡肋,数据不一致的来源之一,分布式下有多副本机制基本上没什么应用的地方。对于 2PC 和 3PC 存在的宕机等问题导致出问题的场景,根本上还是要靠分布式去保证,比如逻辑上通过 Quorum/Raft/Paxos 构造多副本,物理上通过 disk、server、rack、AZ(DC)、region 等做隔离。
TCC(Try-Confirm-Cancel)
TCC 其实就是 2PC 的一种特化实现,Percolator 一样也是 2PC 的一种特化实现。TCC 的 try 阶段相当于 2PC 的 prepare 阶段,confirm 相当于 2PC 的 commit,cancel 相当于 2PC 的 abort。2PC 的实现会与具体系统关联性较强,也就是对内高内聚,对外的表现更加透明、低耦合。而 TCC 设计上讲究的是让业务做更多,业务可以做决策,TCC 本身只是一个框架,资源层不同的参与事务的子系统只要实现 TCC 的接口即可,业务基于 TCC 框架调用接口就能实现子系统间的事务。一般 TCC 的子系统接口会具有幂等性或者支持事务,可以方便业务实现。由于 TCC 把 2PC 的决策过程放到了业务层中,而资源层子系统又会暴露较多信息给业务,这样业务其实就可以更好地跟踪事务状态以作出决策。比如对于 commit 超时这一结果的判定,2PC 的话只能通过业务读取结果去判定事务状态,而 TCC 业务可以直接通过查看事务 id 或者查看相应事务 id 在各个子系统中的状态去判定事务状态。
消息表
基于 MQ 的方式就是消息表的一种具体特化实现,这种机制的事务显而易见就是异步的,遵循的不是 ACID 而是 BASE 理论,讲究最终一致性,某些 case 下的弱一致性。通过 MQ 去做解偶,并利用 MQ 的可靠性等机制简化实现。
2.2 一致性
上面基于 BASE 理论的属于柔性事务,其对应的就是刚性事务。刚性事务(如单数据库)完全遵循 ACID 规范,即数据库事务正确执行的四个基本要素:
原子性(Atomicity)
一致性(Consistency)
隔离性(Isolation)
持久性(Durability)
柔性事务(如分布式事务)为了满足可用性、性能与降级服务的需要,降低一致性(Consistency)与隔离性(Isolation)的要求,遵循 BASE 理论:
基本可用性(Basic