Two-Phase Commit Using Blockchain
Benjamin Marks, Heron Yang, Yuewei Na
目录
- 1 介绍
- 1.0.1 可扩展性
- 1.0.2 可扩展性
- 1.0.3 可靠性
- 2 设计概述
- 2.1 架构和组件
- 2.1.1 区块链
- 2.1.2 数据库
- 2.1.3 协调员
- 2.1.4 队列
- 2.2 系统交互(a.k.a Life of a Transaction)
- 2.3 处理故障
- 2.3.1 协调器崩溃
- 2.3.2 队列崩溃
- 2.3.3 慢区块链
- 3 实施
- 3.1 环境
- 3.1.1 区块链
- 3.1.2 数据库
- 3.1.3 协调员
- 3.1.4 队列
- 4 总结
- 4.0.1 延迟权衡
- 4.0.2 分布式协调器
- 参考
摘要:
我们创建了一个去中心化系统,该系统使用公共区块链作为两阶段提交 (2PC) 协调器,因此用户可以在任何支持我们接口的数据库中安全地提交原子事务。用户将我们的系统视为一个带有 ACID 的大型数据库,尽管它由许多不知道彼此存在的独立数据库组成。通过使用区块链作为协调器,它可以抵御网络中断、断电和磁盘故障等导致传统 2PC 实现阻塞的故障。我们最初的集成是与以太坊区块链和 LMDB 数据库;但是,我们的系统提供了一个通用接口来与其他区块链和数据库集成。
1 介绍
两阶段提交(2PC)是一种被广泛采用的协议,它允许分布式系统达成分布式事务的共识。它确保事务可以在所有分布式机器上提交或中止。然而,阻塞被证明 [1] 是不可避免的,即使在可以可靠估计延迟界限的同步系统中也是如此。
自比特币发明以来,底层区块链技术不仅受到以太坊等加密货币系统的关注 [2],而且受到有关计算机系统的学术研究的关注。区块链的核心是一种使用工作量证明达成共识的协议。先前的工作[3]表明,同步区块链可以增加原始2PC协议的可靠性,并且在发生各种故障时也可以使其不阻塞。
在本文中,我们通过优化的系统架构和适当设计的接口进一步扩展了之前的工作。这些贡献使 2PC 与区块链在生产和规模上工作。与传统版本的2PC协议相比,我们提出的方法增加了可扩展性、可扩展性和可靠性。
1.0.1 可扩展性
我们可以将任何区块链,无论它是公共/无许可还是私有/许可,都可以插入我们的区块链接口,该接口公开 StartVoting()、Vote()、GetVotingDecision()。我们还可以将任何支持 ACID 的数据库系统插入到我们的事务委托接口中,该接口实现常见的事务操作 - Begin()、Commit()、Abort()、Get()、Put()。
1.0.2 可扩展性
我们将协调器设计为轻量级以处理高吞吐量并具有水平可扩展性。这是通过将协调和投票工作委托给区块链来实现的。委托使协调器无状态,并允许多个协调器作业共存以平衡负载并容忍作业失败。向系统添加群组就像让潜在客户知道新添加的群组的地址一样简单,因此我们的系统可以横向扩展。
1.0.3 可靠性
我们的系统比传统的 2PC 协议具有更好的可靠性,原因有二。首先,系统利用区块链来维护协调状态。只要知道参与的队列,无状态协调器就可以轻松恢复。其次,协调器是无状态的和水平可扩展的。我们可以让多个作业同时运行,并在其上放置一个负载平衡层,以容忍单个协调节点故障。
2 设计概述
2.1 架构和组件
我们将事务定义为以 Begin() 开头的操作列表,后跟多个 Get() orPut() 并以 Commit() 结尾。客户端创建一个事务并请求协调器提交。协调者将事务分发给相应的群组进行准备,然后将剩余的 2PC 责任卸载到区块链上。每个群组在本地准备事务,如果准备好则投票给区块链,检查区块链以查看是否应提交事务,最后将事务提交到其数据库。
该架构类似于传统的 2PC 架构,我们有一个协调员和多个群组 - 除了每个群组直接与区块链对话以进行投票并在协调员将 2PC 投票责任转移到区块链时查找投票决定。
我们在剩下的 2.1 中描述了每个组件的细节,在 2.2 中处理成功事务的完整流程,以及在 2.3 中我们如何处理失败。
2.1.1 区块链
区块链组件执行 2PC 协议,因此它负责跟踪每个事务的投票状态并决定是提交还是中止。该实现使用以太坊智能合约在内部存储每笔交易的以下状态(由交易 ID 索引):
- 事务状态存储:事务的每个队列的投票状态。
- 事务配置:存储每个事务的事务配置,包括队列数和投票超时时间。
我们设计了一个 BlockchainAdapter 接口,它在外部向协调者和群组公开以下 API:
- StartVoting(交易ID、群组、投票超时时间)
由协调器调用以开始对事务进行投票。群组是稍后将对此事务进行投票的群组数量。投票超时时间是 2PC 停止投票时区块链上的绝对时间。 - 投票(交易 ID、群组 ID、选票)
由群组调用以在超时前对事务进行 COMMIT 或 ABORT 投票。 - GetVotingDecision(交易 ID)
由任何组件调用以检查事务的投票状态。
StartVoting() 初始化交易的状态,Vote() 更新交易的投票状态。GetVotingDecision() 按照以下逻辑做出决定:
- 如果收到任何 ABORT 投票,则返回 ABORTdecision。
- 如果收到所有 COMMIT 投票,则返回 COMMIT 决定。
- 如果不是(1)且不是(2),如果区块链时间已超过投票超时时间,则返回 ABORT 决定;否则,返回 PENDING 决定。
2.1.2 数据库
数据库组件负责在每个队列中提交子事务,并在 DatabaseAdapter 中使用五个 API 抽象出典型的 ACID 数据库:
- Begin() 开始一个事务。
- Commit() 提交一个已启动的事务。
- Abort() 中止已启动的事务。
- Put(key, value) 将 value 放在主键为 key 的行中。
- Get(key) 从主键为 key 的行中获取值。
我们要求底层数据库是 ACID,因为该数据库可以原生处理锁和事务,而不需要群组为 Put 和 Get 操作实现复杂的锁定逻辑。但是,对于仅支持弱快照隔离的数据库(例如 LMDB [5]),队列需要单个写锁来防止启动其他写事务以确保事务被序列化。
2.1.3 协调员
协调器向客户端公开两个 API:
- CommitAtomicTransaction( CommitAtomicTransactionRequest, CommitAtomicTransactionResponse)
- GetTransactionResult( GetTransactionResultRequest, GetTransactionResultResponse)
数据键空间被拆分为命名空间,每个命名空间对应于负责该范围内所有键的单个群组。当协调者收到来自客户端的 CommitAtomicTransaction() 调用时,它首先将 StartVoting() 发送到区块链以跟踪来自群组的投票状态。然后它根据每个操作的命名空间将事务分解为子事务,这些子事务将在队列中执行。在事务分解之后,协调器将带有子事务的 PrepareTransaction() 请求发送到群组。与传统的 2PC 协议不同,我们设计协调器将投票和协调工作委托给区块链。从这里开始,除非客户端请求 GetTransactionResult(),否则协调器将不再处理任何工作。
当客户端向协调器发送 GetTransactionResult() 时,协调器从区块链中读取交易状态。如果区块链返回 PENDING 或 ABORTED 状态,coordinator 将直接返回状态。如果事务已提交,则协调器将向群组发送请求以获取 Get 结果,然后将状态与 Get 结果一起返回。如果并非所有群组都返回 Get 结果(例如由于崩溃或网络延迟),协调器将向客户端发送部分响应,其中包含响应的响应以及响应尚未完成的指示符。
如 2.1.1 中所述,协调者不需要将任何信息保存到磁盘,因为所有投票信息都存储在区块链中,并且客户端也知道所有其他信息(例如交易中涉及的群组列表)。
2.1.4 队列
群组向协调器公开了两个 API:
- PrepareTransaction( PrepareTransactionRequest, PrepareTransactionResponse)
- GetTransactionResult( GetTransactionResultRequest, GetTransactionResultResponse)
PrepareTransactionRequest() 包含一个事务 id、子事务的操作列表和投票超时时间,超过此时间区块链将不接受投票。当一个群组收到来自协调者的 PrepareTransaction() 请求时,它会采取以下步骤:
- 在线程池中启动一个新线程。
- 创建一个 DatabaseAdapter 对象。
- 使用 DatabaseAdapter:: Begin() 启动事务。
- 获取子事务的相关锁。
- 在子事务中执行一系列请求的 Put 或 Get 操作。
- 如果数据库报告任何操作无效或无法及时获取锁,队列将通过 BlockchainAdapter::Vote() 向区块链发送 ABORT 投票。
- 如果所有操作都成功,它会将检索到的 Get 值存储在内存中的 hashmap 中,并永久存储在由事务 id 键入的磁盘上,以便为将来的 GetTransactionResult() 调用做准备。
- 根据输入投票超时时间设置定时器。
- 如果计时器达到投票超时时间,它将向区块链发送请求 BlockchainAdapter::GetVotingDecision(transaction id)。
a. 如果区块链声明事务应该提交,它将调用DatabaseAdapter::Commit() 并释放任何获得的锁。
b. 如果区块链声明事务应该中止,它将调用DatabaseAdapter::Abort() 并释放任何获得的锁。
当群组收到 GetTransactionResult() 请求并已提交事务时,它会根据请求中的事务 id 简单地读取内存中的 hashmap,并将获取的值作为响应返回。如果它还没有收到来自区块链的提交或中止响应,它会返回一个挂起的响应。
2.2 系统交互(a.k.a Life of a Transaction)
本节和图 1 描述了没有失败的事务的端到端生命周期。
图 1. 该图显示了我们架构中的主要组件以及成功提交事务的示例步骤。
- 客户端编写事务请求并通过 CommitAomticTransaction() 将其传递给协调器。协调器返回一个事务ID,供客户端稍后查找事务结果。
- 协调者通过 StartVoting() 要求区块链跟踪新交易的投票。
- 协调器将事务分解为子事务,并通过 PrepareTransaction() 将它们分发到相应的队列。
- 每个群组通过获取锁并将操作转发到数据库直到 Commit() 之前的点来执行子事务。群组将条目写入重做日志以在崩溃时恢复。另外,队列写入Get操作结果存储到磁盘以便在崩溃和恢复后更快地响应。
- 队列通过Vote() 向区块链投票,表明它想要提交或中止交易。
- 队列等待到预期的交易投票超时时间,并通过 GetVotingDecision() 从区块链获取投票决定。
- 如果投票决定提交事务,则队列将子事务提交到数据库并释放任何获取的锁;否则,它会中止数据库中的子事务并释放任何获取的锁。
- (独立于前面的步骤)客户端等到投票超时时间,通过GetTransactionResult()向协调者请求交易结果,使用步骤1中给出的交易ID。
- 协调者通过 GetTransactionResult() 从区块链获取(1)查询交易的投票决定和(2)从每个队列获取操作结果。
2.3 处理故障
如上所述,与传统的 2PC 实施相比,我们系统的主要优势之一是提高了弹性。通过将投票和协调委托给区块链,我们的系统不会阻塞任何一台崩溃或具有分区网络的机器。
2.3.1 协调器崩溃
协调器的状态仅包括每个活动事务中涉及哪些群组的列表。因此,如果协调器崩溃,当它恢复时,它将通过指示它无法找到事务来响应客户端对事务的 Get 响应的请求。因此,客户端可以直接询问同类交易的 Get 响应是什么。与传统的 2PC 中协调器存储每个队列的投票方式不同,在我们的设计中,此信息存储在区块链中。我们的系统对此类崩溃具有弹性,因为事务不需要阻止协调器的恢复,即使协调器没有响应,队列也可以提交或中止。
2.3.2 队列崩溃
我们的系统也能抵御群组崩溃,就像这些崩溃一样不会导致任何其他同类群组的阻塞时间超过投票超时时间。如果一个群组在发送投票提交之前崩溃,那么区块链将等到投票超时时间,所有群组都将中止交易。如果一个群组在投票提交后崩溃并且区块链决定该事务应该提交,那么该群组将重播在它崩溃和恢复时提交的所有事务。为了重放事务,队列将获取与事务相同的锁,并将相关的 Putand Get 请求发送到数据库并提交数据库中的事务。由于它已经计算了响应,如果协调器请求获取任何这些事务的响应,队列可以安全地返回它们,即使它们尚未提交到数据库中。这可以在未来实现,但我们决定不在我们的第一个版本中包含它。
2.3.3 慢区块链
我们使用以太坊来实现我们的区块链。根据 [6],在以太坊上处理区块链交易大约需要 15 秒到 5 分钟,这表明与我们系统中的其他组件相比,区块链相对较慢。缓慢的区块链增加了在投票超时时间之前无法收集到所有提交投票的可能性,这导致更多的交易最终中止;然而,区块链的缓慢并不影响承诺的原子性。如果需要,客户端或协调器可以指定更长的投票超时时间。
3 实施
在本节中,我们首先描述我们整个系统的实现。然后,我们说明各个组件的实现。我们实现的源代码可在 [7] 获得,根据 GNU 宽通用公共许可证 [8] 获得许可。我们的实现依赖于以下库:truffle [9]、ganache [10]、sqlite3 [11]、grpc [12]、web3 [13]、abseil-cpp [14]、rule nodejs [15]、gflags [16] 、glog [17]、openssl [18]、线程池 [19]、lmdb [20] 和 lmdbxx [21]。
3.1 环境
我们用大约 2800 行 C++ 代码实现了协调器和队列。对于组件之间的通信,我们使用了 gRPC [12] 和 ProtoBuf [22]。为了提供交换不同数据库和区块链实现的可扩展性,我们提供了通用接口。我们在一台机器上测试了端到端的行为,其中不同的进程模拟了每个组件。
3.1.1 区块链
我们使用 Solidity 编写了两阶段提交智能合约,并使用 Truffle [9] 开发了它。 Truffle 为我们提供了开发环境来编译智能合约,对其进行测试,并将其部署到本地进行调试。一旦我们部署了我们的智能合约,协调者和群组通过智能合约客户端库与合约进行交互。但是,我们没有找到合适的 C++ 智能合约客户端库来使用,所以我们使用了 JavaScript 库(web3.js [13]),并添加了一个额外的 gRPC 层来转换为 C++。
在 Truffle 开发中进行测试时,我们了解到用于成功提交两个群组的交易的 gas 约为 0.33 美元 [23](请参阅附录 A 了解细目)。
3.1.2 数据库
我们设计了 DatabaseAdapter 接口并使用 LMDB 原语实现了这些方法。 LMDB 提供面向过程的函数,如 open()、begin()、commit()、put()、get()。我们面向对象的 C++ 实现有将近 500 行代码。我们选择 LMDB 是因为它快速、轻量并且易于在内存中测试。该接口可扩展到其他数据库,如 SQLite [24]。
3.1.3 协调员
协调器根据客户端提供的标识符以及客户端的主机名和端口确定性地计算事务的全局标识符。如果客户端多次发送相同的请求,这允许协调器避免向群组发送重复的(可能是非幂等的)请求。如果客户端没有及时听到协调器对其原始请求的响应并尝试重新发送它,则可能会发生这种情况。通过使用 SHA256 生成事务标识符,来自不同客户端或具有不同客户端标识符的事务极不可能具有相同的全局标识符。
3.1.4 队列
我们对群组使用的一项优化是 PrepareTransactionRequest() 包含一个指示符,如果这是交易的唯一群组。在这种情况下,我们避免了对区块链的昂贵请求,并且队列可以完成在数据库中的准备后立即提交有效事务。这是安全的,因为它知道没有其他队列可以中止事务,并且保证它在自己的数据库中是原子的。
4 总结
4.0.1 延迟权衡
我们的方法使客户能够在延迟和提交事务的可能性之间进行权衡。当客户端为投票超时时间设置更长的时间时,它更有可能从所有群组收集足够的投票来做出提交决定,但如果群组崩溃,则可能会有更高的延迟。最小超时有一个限制,不能短于完成一个新区块链块所需的时间。
作为未来努力的一部分,我们可以通过让群组将投票发送给协调者和区块链来扩展系统以在进一步减少延迟和安全之间进行权衡。如果协调者收到来自所有群组的提交投票,它可以更新区块链中的决定并告诉所有群组提交。在这个过程中,我们不需要等待在区块链中挖掘一个新块来提交交易。安全性的损失来自于区块链处理交易缓慢并且最终没有及时收到投票并因此决定中止的边缘情况。如果一些群组收到来自协调者的提交消息而其他群组没有收到,然后向区块链询问决定,则收到提交消息的群组将提交,而其他群组将中止交易。由于我们预计这只会影响极少数事务,因此客户端将能够指示他们的事务是否可以容忍很小的非原子性机会以实现更快的延迟。
4.0.2 分布式协调器
目前协调器是无状态的,因此很容易添加新的协调器服务器,只要每个协调器处理一组不相交的事务。但是,如果协调器崩溃,则客户端无法向其他协调器询问事务的 Get 响应,因为这些协调器将不知道如何找到事务的队列。作为未来工作的一部分,我们可以让协调器相互发送每个事务的队列,这样即使该请求的原始协调器崩溃,他们也可以响应客户端请求。
参考
[1] Dale Skeen. Nonblocking commit protocols. In Proceedings of the 1981 ACM SIGMOD international conference on Management of data, pages 133–142, 1981.
[2] Gavin Wood et al. Ethereum: A secure decentralised generalised transaction ledger. Ethereum project yellow paper, 151(2014):1–32, 2014.
[3] Paul Ezhilchelvan, Amjad Aldweesh, and Aad van Moorsel. Non-blocking two-phase commit using blockchain. Concurrency and Computation: Practice and Experience, 32(12):e5276, 2020.
[4] Butler Lampson and David Lomet. A new presumed commit optimization for two phase commit. In 19th International Conference on Very Large Data Bases (VLDB’93), pages 630–640, 1993.
[5] Gavin Henry. Howard chu on lightning memory-mapped database. Ieee Software, 36(06):83–87, 2019.
[6] EG Station. How long does an ethereum transaction really take. https://legacy.ethgasstation.info/blog/ ethereum-transaction-how-long/. Accessed: 2022-06-03.
[7] benjmarks22/blockchain-2pc: Cs 244b project to use a public blockchain as a two-phase commit coordinator to securely commit an atomic transaction across any two systems of a database. https://github.com/benjmarks22/ blockchain-2pc/. Accessed: 2022-06-03.
[8] Gnu lesser general public license v3.0 - gnu project - free software foundation. https://www.gnu.org/licenses/lgpl3.0.en.html. Accessed: 2022-06-03.
[9] trufflesuite/truffle: A tool for developing smart contracts. crafted with the finest cacaos. https://github. com/trufflesuite/truffle. Accessed: 2022-06-03.
[10] trufflesuite/ganache: A tool for creating a local blockchain for fast ethereum development. https:// github.com/trufflesuite/ganache. Accessed: 2022-06-03.
[11] Tryghost/node-sqlite3: Asynchronous, non-blocking sqlite3 bindings for node.js. https://github.com/ TryGhost/node-sqlite3. Accessed: 2022-06-03.
[12] grpc/grpc: The c based grpc (c++, python, ruby, objective-c, php, c#). https://github.com/grpc/grpc. Accessed: 2022-06-03.
[13] Chainsafe/web3.js: Ethereum javascript api. https:// github.com/ChainSafe/web3.js. Accessed: 2022-06-03.
[14] abseil/abseil-cpp: Abseil common libraries (c++). https: //github.com/abseil/abseil-cpp. Accessed: 2022-06-03.
[15] bazelbuild/rules nodejs: Javascript and nodejs rules for bazel. https://github.com/bazelbuild/rules nodejs. Accessed: 2022-06-03.
[16] gflags/gflags: The gflags package contains a c++ library that implements commandline flags processing. https: //github.com/gflags/gflags. Accessed: 2022-06-03.
[17] google/glog: C++ implementation of the google logging module. https://github.com/google/glog. Accessed: 202206-03.
[18] openssl/openssl: Tls/ssl and crypto library. https:// github.com/openssl/openssl/. Accessed: 2022-06-03.
[19] Barak Shoshany. A C++17 Thread Pool for HighPerformance Scientific Computing. arXiv e-prints, May 2021.