Block-STM
论文Scaling Blockchain Execution by Turning Ordering Curse to a Performance Blessing主要讨论了一种用于区块链系统的并行执行引擎,称为Block-STM(基于软件事务内存的区块执行引擎)。该引擎旨在通过利用并行性来加速智能合约的执行,从而提升区块链系统的吞吐量。
问题定义
Block-STM的输入是一个包含多个交易的区块,记为BLOCK,其中包含n个交易,并定义了预设的序列化顺序: 。问题的核心在于如何并行执行这个区块,并生成一个最终状态,该状态与按照预设顺序逐个执行交易所产生的状态相同。
在 Block-STM 中,每个交易可能会被执行多次,我们将第i次执行称为该交易的第i次 "incarnation"。当系统决定需要重新执行并增加执行次数时,我们称该 "incarnation" 被终止。一个版本由交易索引和 "incarnation" 次数组成。为了支持可能同时执行的交易进行读写操作,Block-STM 维护了一个内存中的多版本数据结构,该结构分别存储了每个内存位置由交易写入的最新值及其相关联的交易版本。对于每次 "incarnation",Block-STM 维护一个写集合(write-set)和一个读集合(read-set)。当交易读取某个内存位置时,它从多版本数据结构(MVMemeory)中获取该位置由出现在它之前的最高交易写入的值及其相关版本。例如,交易可以读取交易写入的值,即使交易也对该位置进行了写入。如果没有较小的交易对某个位置进行了写入,那么读取(例如的所有读取操作)会基于块执行前的状态从存储中解决。
依赖关系估计
Block-STM 不会预先计算依赖关系。相反,对于每个交易,Block-STM 将被终止的 "incarnation" 的写集合视为下一次 "incarnation" 写集合的估计值。结合多版本数据结构和预设的顺序,它可以有效地减少终止率,通过高效检测潜在的依赖关系来实现。当一个 "incarnation" 因验证失败而被终止时,多版本数据结构中对应其写集合的条目将被替换为一个特殊的估计标记(estimate marker)。这表明下一次 "incarnation" 可能会写入相同的内存位置。特别是,当交易的某次 "incarnation" 读取由较低交易写入的标记为估计的值时(),该交易将被立即终止。这是一种优化,提前终止一个可能因未来的验证失败而终止的 "incarnation",避免了不必要的资源浪费。
协调器
Block-STM 引入了一个协作调度器,用于在线程之间协调验证和执行任务。共识的顺序规定了交易必须按顺序提交,因此即使某次 "incarnation" 验证成功,也不意味着它可以立即提交。这是因为一个较早交易的终止和重新执行可能会使更高交易的验证集无效,从而需要重新执行。因此,当一个交易终止时,所有更高的交易将被安排重新验证。同一个 "incarnation" 可能由不同的线程多次验证,甚至可能并行进行,但 Block-STM 确保每个版本只会成功终止一次。由于交易必须按顺序提交,Block-STM 的调度器会优先处理与低索引交易相关的任务(验证和执行)。
调度算法
Block中的每个transaction初始化为待执行的task,加入集合中
- Initialization: { | 待执行的task}, { | 待验证的task}
- Find next task: 在和中,执行序号最小的:
- Execution task: 执行的下一个分身,如果读到了ESTIMATE状态的值,停止并放回中。否者:
(a) 如果当前中不等于上一次的,所有不在中需要加入到中重新验证;
(b) 否者,创建的validation task并加入中。 - Validation task: 验证的最近incarnation。如果验证成功,继续;如果失败,abort:
(a) 将中当前写入的数据置为ESTIMATE;
(b) 将所有不在中的加入到中重新验证;
(c) 增加的,并加入中重新执行。
- Execution task: 执行的下一个分身,如果读到了ESTIMATE状态的值,停止并放回中。否者:
验证成功的条件(Succeed)
一个 "incarnation" 在以下情况下会被认为是成功的:
- 读集一致性验证: 在执行过程中,交易读取了某些内存位置的值,这些读取操作会被记录在一个读集合(read-set)中。验证过程中,系统会重新读取这些内存位置,并检查这些位置的值和版本是否与 "incarnation" 执行时读取到的一致。如果读取的值和版本未发生变化,则验证成功。
- 没有检测到依赖冲突: 如果该 "incarnation" 在验证时没有依赖于被标记为估计(estimate)的内存位置值,并且没有其他的交易对它的结果产生影响,则它可以成功完成验证。
验证失败的条件(Failed)
"Incarnation" 的验证在以下几种情况下会失败:
- 读集中的值已过时: 如果验证过程中重新读取某个内存位置时,发现其值或版本与该 "incarnation" 在执行时读取的不一致(例如,其他交易在此期间修改了该内存位置的值),则验证失败。这表明该 "incarnation" 读取的值在执行之后已经被其他交易更新,因此其操作基于过时的数据,需要重新执行。
- 依赖冲突: 如果某个 "incarnation" 读取了一个被标记为估计的值(这意味着较低索引的交易可能会在后续重新执行时修改这个内存位置),那么该 "incarnation" 将被立即终止。这种情况下,交易无法继续进行,因为未来可能的修改会使得当前的执行无效。
线程逻辑(Thread logic)
在 Block-STM 论文中,Algorithm 1 描述了线程逻辑(Thread logic)的基本流程。这个算法负责管理线程如何在 Block-STM 系统中执行和验证交易的“incarnation”。
Algorithm 1: Thread Logic 流程
- 初始化 (Initialization) :
- 线程首先初始化一个
task
变量为⊥
,表示当前没有正在处理的任务。
- 线程首先初始化一个
- 主循环 (Main Loop) :
- 线程进入一个循环 (
while
循环),直到调度器(Scheduler)确定所有任务都已完成 (Scheduler.done()
返回true
)。
- 线程进入一个循环 (
- 处理执行任务 (Handle Execution Task) :
- 如果当前
task
不是空 (⊥
) 并且它是一个执行任务 (EXECUTION_TASK
),线程会调用try_execute()
函数处理该任务。 try_execute()
函数会尝试执行任务并可能返回一个验证任务 (VALIDATION_TASK
) 或者返回⊥
以表示没有新的验证任务。
- 如果当前
- 处理验证任务 (Handle Validation Task) :
- 如果
task
不是空 (⊥
) 并且它是一个验证任务 (VALIDATION_TASK
),线程会调用needs_reexecution()
函数来处理该任务。 needs_reexecution()
函数会检查该任务是否需要重新执行,并可能返回一个新的执行任务,或者返回⊥
表示没有新的执行任务。
- 如果
- 请求新任务 (Request New Task) :
- 如果当前
task
为空 (⊥
),线程会调用Scheduler.next_task()
请求一个新的任务。该任务可能是一个新的执行任务或验证任务。
- 如果当前
关键函数解释:
- try_execute(version) :
- 该函数负责执行特定版本的交易。在执行过程中,如果遇到读取错误(如依赖冲突),则可能需要重新执行该任务,或者添加依赖关系。如果执行成功,函数将把写入的数据记录到多版本内存结构中,并更新任务的状态。
- needs_reexecution(version) :
- 该函数用于验证交易的读集是否仍然有效。如果验证失败,系统会尝试终止该 "incarnation",并将其写入的数据标记为估计值。此时,需要重新执行该任务。
- Scheduler.next_task() :
- 这是调度器分配任务的函数,根据当前系统的状态分配下一个需要执行的任务。
多版本内存(MVMemory)模块
在 Block-STM 论文中,Algorithm 2 描述了 MVMemory 模块(即多版本内存模块)的工作流程。这个模块负责管理交易过程中内存位置的多版本数据,以支持并行执行和验证。
MVMemory 模块的主要功能是维护一个多版本数据结构,该结构存储每个内存位置的多个版本的值,这些版本对应不同的交易“incarnation”。此外,该模块还负责处理内存读写操作、记录写入数据、验证读集的有效性等。
关键组件和变量:
- write_set:
- read_set:
- data: :这是一个映射(Map),用于存储每个内存位置的值和版本。键是内存位置和交易索引,值是一个包含“incarnation”编号和写入值的对,或者一个特殊的估计标记(ESTIMATE marker)。
- last_written_locations: :一个数组,用于存储每个交易的最后一次完成执行时写入的内存位置集合。
- last_read_set: :一个数组,用于存储每个交易的最后一次完成执行时读取的内存位置及其相应的版本。
主要功能和流程:
- apply_write_set(txn_index, incarnation_number, write_set) :
- 该函数将写集合(write_set)应用到多版本数据结构
data
中。对于每一个(内存位置,值)对,函数会将其存储到data
中,并将其与相应的交易索引和“incarnation”编号关联。
- 该函数将写集合(write_set)应用到多版本数据结构
- rcu_update_written_locations(txn_index, new_locations) :
- 该函数更新
last_written_locations
中的写入位置集合,并从data
中移除未被新写集合覆盖的旧条目。该操作可以原子地更新存储的位置集合,并检查是否有新的位置被写入。
- 该函数更新
- record(version, read_set, write_set) :
- 这个函数首先应用写集合到
data
中,然后调用rcu_update_written_locations
更新最后写入的位置集合,并存储读集合(read_set)到last_read_set
中。它还会返回一个标志,指示是否写入了新的内存位置。
- 这个函数首先应用写集合到
- convert_writes_to_estimates(txn_idx) :
- 当一个“incarnation”失败时,这个函数会将其写入的数据标记为估计值(ESTIMATE)。这确保了如果依赖于这些数据的后续交易在验证时读取到这些估计值时,将能够正确处理依赖关系。
- read(location, txn_idx) :
- 该函数用于读取指定交易索引之前的最高版本的内存位置的值。如果找到的版本是估计值(ESTIMATE),则会返回一个读取错误(READ_ERROR),并指示调用者暂时推迟交易的执行。
- validate_read_set(txn_idx) :
- 该函数重新读取
last_read_set
中记录的内存位置,并验证这些位置的值和版本是否与原始读取时一致。如果验证失败,说明这些数据在执行后已被其他交易修改,验证失败。
- 该函数重新读取
- snapshot() :
- 在 Block-STM 完成所有交易后,调用该函数来获取每个内存位置的最终值。
虚拟机(VM)模块
在 Block-STM 论文中,Algorithm 3 描述了 VM 模块(即虚拟机模块)的算法流程。这个模块的主要功能是处理交易的执行,捕获读写操作,并记录读集和写集。VM 模块是 Block-STM 的核心组件之一,它负责在执行交易时进行内存访问操作,并与多版本内存数据结构(MVMemory)进行交互。VM 模块主要通过 VM.execute
函数来执行交易,并在执行过程中跟踪和记录交易的读集(read-set)和写集(write-set)。
关键步骤和流程:
- 初始化读集和写集 (Initialization) :
- 当执行交易时,首先初始化
read_set
和write_set
,这两个集合分别用于存储交易执行过程中读取和写入的内存位置和对应的值。 - write_set:
- read_set:
- 当执行交易时,首先初始化
- 执行交易 (Executing the Transaction) :
- 交易的执行过程包括对内存位置的读取和写入操作。
- 每当交易试图写入一个内存位置时,新的(位置,值)对会被加入
write_set
中。如果该位置已经存在于write_set
中,则用新值替换旧值。
- 处理写操作 (Handling Write Operations) :
- 如果交易试图写入一个内存位置,虚拟机(VM)会将该位置和新值添加到
write_set
中。 - 在
write_set
中只会保留最新的写入操作,因此每个位置最多只会有一个值。
- 如果交易试图写入一个内存位置,虚拟机(VM)会将该位置和新值添加到
- 处理读操作 (Handling Read Operations) :
- 如果交易试图读取一个内存位置,首先检查该位置是否已经存在于
write_set
中。如果存在,则直接读取write_set
中的值。 - 如果
write_set
中没有该位置,VM 会调用MVMemory.read
函数,从多版本数据结构中读取该位置的值。 - 如果从
MVMemory
中读取到的结果是NOT_FOUND
,则表示该位置没有被之前的交易写入,VM 会从主存储(Storage)中读取值,并记录到read_set
中。 - 如果读取到的是
READ_ERROR
,则表示遇到了依赖冲突,VM 会停止执行并返回错误和阻塞的交易索引。 - 如果读取成功(返回
OK
状态),则将该位置和相应的版本记录到read_set
中,并读取值。
- 如果交易试图读取一个内存位置,首先检查该位置是否已经存在于
- 返回读集和写集 (Returning Read-Set and Write-Set) :
- 在交易执行完成后,VM 将
read_set
和write_set
返回给调用者。这些集合将用于后续的验证和提交操作。
- 在交易执行完成后,VM 将
调度器模块(Scheduler Module)
在 Block-STM 论文中,Algorithm 4 描述了调度器模块(Scheduler module)的算法流程。这个模块负责管理并调度执行和验证任务,并在多线程环境下协调这些任务的执行顺序。调度器模块是 Block-STM 的核心组件,它确保交易的执行和验证能够按照正确的顺序进行,同时最大化系统的并行度。
调度器模块的主要功能包括管理任务的状态、分配新的执行和验证任务、以及判断所有任务是否已经完成。以下是该算法的主要步骤和流程:
关键变量和组件:
- execution_idx:用于跟踪下一个需要执行的交易索引。
- validation_idx:用于跟踪下一个需要验证的交易索引。
- decrease_cnt:计数器,用于记录
execution_idx
或validation_idx
被减少的次数。 - num_active_tasks:记录当前正在进行的验证和执行任务的数量。
- done_marker:标志,用于指示所有任务是否已经完成。
- txn_dependency:一个数组,每个元素是一个交易的依赖集合,用于管理交易之间的依赖关系。
- txn_status:一个数组,每个元素是一个由交易索引和状态组成的对,状态包括
READY_TO_EXECUTE
(准备执行)、EXECUTING
(正在执行)、EXECUTED
(已执行)和ABORTING
(正在终止)。
主要功能和流程:
- decrease_execution_idx(target_idx) :
- ,并增加
decrease_cnt
。
- ,并增加
- decrease_validation_idx(target_idx) :
- ,并增加
decrease_cnt
。
- ,并增加
- done() :
- 返回
done_marker
,指示是否所有任务都已完成。
- 返回
- check_done() :
- 检查
execution_idx
和validation_idx
是否已经超过所有交易的数量,以及num_active_tasks
是否为 0。如果条件满足,则设置done_marker
为true
,指示所有任务已经完成。
- 检查
- try_incarnate(txn_idx) :
- 尝试将交易索引
txn_idx
的状态从READY_TO_EXECUTE
改为EXECUTING
,并返回该交易的incarnation
号。如果txn_idx
已经超出块的大小,则返回⊥
。
- 尝试将交易索引
- next_version_to_execute() :
- 从
execution_idx
中获取下一个需要执行的交易索引,并尝试为该交易创建一个执行任务。如果交易已准备好执行,则返回该交易的版本,否则返回⊥
。
- 从
- next_version_to_validate() :
- 从
validation_idx
中获取下一个需要验证的交易索引,并检查该交易是否已执行。如果已执行,则返回该交易的版本,否则返回⊥
。
- 从
- next_task() :
- 这是调度器的核心函数,决定下一个要获取的任务是执行任务还是验证任务。它通过比较
execution_idx
和validation_idx
的值来确定优先级。如果需要验证的交易序号更小,则优先返回验证任务,否则返回执行任务。
- 这是调度器的核心函数,决定下一个要获取的任务是执行任务还是验证任务。它通过比较
依赖关系(Dependencies)处理
在 Block-STM 论文中,Algorithm 5 描述了调度器模块中与依赖关系处理和任务完成逻辑相关的算法流程。这个部分主要涉及如何管理交易之间的依赖关系、如何处理任务的状态转换,以及如何在任务完成后更新调度器的状态。
这个算法的主要任务是确保交易的依赖关系得到正确处理,并在交易执行和验证任务完成后,正确更新系统状态。以下是该算法的关键步骤和流程:
关键函数和流程:
- add_dependency(txn_idx, blocking_txn_idx) :
- 当某个交易在执行过程中遇到依赖时(例如,它读取的内存位置被另一个交易写入),调用此函数来记录该依赖关系。
- 具体操作是将当前交易的状态设置为
ABORTING
,并将该交易添加到阻塞交易(blocking_txn_idx
)的依赖集合中。 - 如果在添加依赖关系前阻塞交易已经执行完毕,则返回
false
,表示不需要再处理该依赖。
- set_ready_status(txn_idx) :
- 将指定交易的状态从
ABORTING
更新为READY_TO_EXECUTE
,同时将其incarnation
编号增加 1,使得该交易可以重新执行。
- 将指定交易的状态从
- resume_dependencies(dependent_txn_indices) :
- 恢复所有依赖于特定交易的其他交易的状态,使这些交易可以重新执行。
- 通过调用
set_ready_status()
函数将依赖交易的状态设置为READY_TO_EXECUTE
,并确保执行索引execution_idx
被正确更新。
- finish_execution(txn_idx, incarnation_number, wrote_new_path) :
- 当一个交易的执行任务完成后,该函数将该交易的状态设置为
EXECUTED
。 - 然后,该函数会处理与此交易相关的所有依赖关系,调用
resume_dependencies()
恢复所有依赖交易的状态。 - 如果该交易写入了新的内存位置,还会更新验证索引
validation_idx
,以安排对该交易及其依赖交易的重新验证。
- 当一个交易的执行任务完成后,该函数将该交易的状态设置为
- try_validation_abort(txn_idx, incarnation_number) :
- 尝试将某个交易的状态从
EXECUTED
转变为ABORTING
。此操作用于处理验证失败的情况。 - 如果状态转换成功,返回
true
,表示验证失败并需要重新执行该交易。
- 尝试将某个交易的状态从
- finish_validation(txn_idx, aborted) :
- 在验证任务完成后调用此函数。如果验证失败(即
aborted = true
),则将交易的状态设置为READY_TO_EXECUTE
,并更新验证索引validation_idx
以便重新执行该交易。 - 如果验证成功且没有依赖任务,则任务完成,计数器
num_active_tasks
减少。
- 在验证任务完成后调用此函数。如果验证失败(即
状态转移
Figure 2 展示了 Block-STM 系统中交易(transaction)在不同阶段的状态转移流程。这些状态转移描述了在并行执行环境下,交易在其生命周期中的不同阶段可能经历的状态变化。以下是这些状态转移的详细解释:
- READY_TO_EXECUTE (i) :
- 状态描述:交易的第 次“incarnation”准备执行。此时交易已经被调度,但尚未开始执行。
- 可能的转移:
- EXECUTING (i) :当调度器选择执行该交易时,其状态会从
READY_TO_EXECUTE (i)
转换为EXECUTING (i)
,表示交易的第 次“incarnation”正在执行中。
- EXECUTING (i) :当调度器选择执行该交易时,其状态会从
- EXECUTING (i) :
- 状态描述:交易的第 次“incarnation”正在执行。此时交易正在读取或写入内存位置。
- 可能的转移:
- EXECUTED (i) :当交易成功执行完毕时,其状态会转换为
EXECUTED (i)
,表示第 次“incarnation”已经执行完成。 - ABORTING (i) :如果在执行过程中检测到依赖冲突或其他问题,交易可能会被中止,此时状态转换为
ABORTING (i)
,准备重新执行。
- EXECUTED (i) :当交易成功执行完毕时,其状态会转换为
- EXECUTED (i) :
- 状态描述:交易的第 次“incarnation”已经成功执行,但尚未验证。
- 可能的转移:
- ABORTING (i) :如果在验证阶段发现交易的读集数据不再有效,交易的状态将从
EXECUTED (i)
转换为ABORTING (i)
,表示需要重新执行。 - 没有进一步状态转移:如果验证成功,则不需要进一步状态转移,交易将保持在
EXECUTED (i)
状态。
- ABORTING (i) :如果在验证阶段发现交易的读集数据不再有效,交易的状态将从
- ABORTING (i) :
- 状态描述:交易的第 次“incarnation”被终止,准备进行下一次执行(即第 次“incarnation”)。
- 可能的转移:
- READY_TO_EXECUTE (i+1) :在终止完成后,系统会为交易创建一个新的“incarnation”,即第 次“incarnation”,并将状态更新为
READY_TO_EXECUTE (i+1)
,以便重新开始执行。
- READY_TO_EXECUTE (i+1) :在终止完成后,系统会为交易创建一个新的“incarnation”,即第 次“incarnation”,并将状态更新为
整体流程:
- 从 READY_TO_EXECUTE 到 EXECUTING:
- 当交易准备好并被调度执行时,其状态从
READY_TO_EXECUTE
转换为EXECUTING
。此时,交易正式开始执行。
- 当交易准备好并被调度执行时,其状态从
- 从 EXECUTING 到 EXECUTED 或 ABORTING:
- 在执行过程中,如果交易顺利完成而没有冲突,其状态会变为
EXECUTED
。如果在执行过程中发生依赖冲突或者其他错误,则状态会转换为ABORTING
,准备中止并重新执行。
- 在执行过程中,如果交易顺利完成而没有冲突,其状态会变为
- 从 EXECUTED 到 ABORTING:
- 如果在交易执行完成后但在验证阶段发现问题(例如,其他交易修改了读集中的数据),交易的状态将从
EXECUTED
转变为ABORTING
,准备重新执行。
- 如果在交易执行完成后但在验证阶段发现问题(例如,其他交易修改了读集中的数据),交易的状态将从
- 从 ABORTING 到 READY_TO_EXECUTE (i+1) :
- 在交易中止后,调度器会重新安排交易执行,生成一个新的“incarnation”(即 次执行),并将状态设置为
READY_TO_EXECUTE
,等待重新执行。
- 在交易中止后,调度器会重新安排交易执行,生成一个新的“incarnation”(即 次执行),并将状态设置为
实验效果
- 高性能并行执行:实验表明,Block-STM 能够在低冲突(low-contention)工作负载下几乎完美地扩展,最高可以达到 160,000 tps(每秒交易处理量),这是顺序执行的 16 倍。
- 适应性强:Block-STM 对不同程度的工作负载冲突具有很强的适应性。在高冲突工作负载下(如有较多交易冲突),Block-STM 依然表现出显著的性能优势,相比顺序执行提高了多达 8 倍。
- 低开销:在完全顺序的工作负载(即没有并行机会的场景)下,Block-STM 的开销控制在 30% 以内。这表明,即使在最不利的情况下,Block-STM 也能保持相对较低的性能损耗。
- 相较于现有方案的优势:实验结果还显示,Block-STM 在大多数情况下的性能与 Bohm 算法相当,甚至在某些情况下优于 Bohm 和 LiTM,特别是在使用多线程的场景下。
总的来说,Block-STM 通过智能的任务调度和冲突处理,实现了区块链系统中交易并行执行的显著加速,同时保证了系统的一致性和可靠性。