Aptos 实验室新推出的 Move 特性 “聚合器”(Aggregators),使得即使在读写冲突的情况下,Aptos 区块链上的智能合约也能并行执行。自从主网上线以来,Aptos 网络已经成功利用聚合器来实时跟踪其原生代币的总供应量。尽管每一笔交易都会修改这个值,但借助聚合器,交易的并行处理并不会受到影响。根据最新的 AIP-43 协议,聚合器也可以应用于数量有限的数字资产集合(比如 NFT),使其铸造速度大大提升。最新的预览网络数据显示,利用聚合器技术,在 Aptos 上铸造 100 万个 NFT 只需不到 90 秒。
#1.
并行执行的背景
Block-STM —— Aptos 支持的并行执行引擎 —— 采用乐观并发和多版本数据结构技术,可以并行地对交易进行推测性执行。如果某个交易的推测执行出错,Block-STM 就会发现并终止该交易,稍后再重新执行。
像 Block-STM 这样的并行引擎,其性能在很大程度上取决于工作负载,特别是读写冲突的频率。举个例子,考虑两个按顺序进行的交易:Alice 向 Bob 转账,然后是 Carol 向 David 转账。这两个交易可以并行执行,因为它们互不影响,没有读写集重叠。
再来看另外一对交易:Alice 向 Bob 转账,紧接着 Bob 又转账给 Carol。在这种情况下,Block-STM 可以先尝试执行第二个交易,但会发现它与先前的交易(Alice 向 Bob 的转账)发生冲突,因为第二个交易读取了 Bob 的余额,但第一个交易尚未更新。因此,Bob 向 Carol 转账的操作会被中断,并会在 Alice 向 Bob 的转账完成后再次执行。
#2.
顺序工作负载的问题
存在大量读写冲突的工作负载无法有效地进行并行处理。顺序工作负载是一个极端案例,其中所有交易都互相冲突。在这种情况下,顺序执行交易显然是最理想的选择。然而,不幸的是,在多个区块链应用场景中,工作负载可能是顺序的,以下是一些例子。
供应量和余额计数器读写冲突问题
许多区块链系统会燃烧部分交易手续费。因此,每笔交易都会间接更新原生代币的总供应量,导致所有交易发生冲突。
这意味着,即便是简单的点对点非冲突转账,也会因为对供应量计数器的读写冲突而变成顺序工作负载。这是一个普遍问题,导致一些区块链系统不再在智能合约里跟踪原生货币的总供应量。为了维持并行处理的能力,这些系统会通过协议实现(比如使用原子计数器)来跟踪供应量。
对于数字资产集合,比如非同质化代币(NFT),通常也会跟踪其集合的大小。例如,这些集合可以作为音乐节的门票,由于门票数量有限,因此对应的 NFT 数量也是有限的。
与跟踪原生代币的总供应量类似,每笔铸造或销毁代币的交易都需要更新 NFT 集合的大小。而且,非常重要的一点是,铸造的 NFT 数量不能超过其创作者设定的集合上限。
在这些限制下,NFT 只能逐个顺序铸造,这大大降低了网络的吞吐量。据我们所知,许多区块链在快速铸造大量 NFT 方面面临挑战,这通常需要数小时才能完成。
最后,读写冲突的另一来源是账户余额问题,因为每笔交易都需要支付一定的手续费。这笔费用从账户余额中扣除,所以所有使用同一付费账户的交易都会发生冲突。这阻碍了单一发送方的并行处理,比如一个账户短时间内发送大量交易,在游戏等应用中尤为重要。
NFT 的索引和命名读写冲突问题
NFT 通常会根据铸造时间进行顺序索引,并且它们的名称中会包含索引,例如“Star #17”。因此,即使不跟踪 NFT 集合的大小,从而避免了对集合大小计数器的读写冲突,但由于索引的原因,代币仍无法并行铸造,因为索引过程本身引入了另一种顺序执行的瓶颈问题。
Aptos 的创新之举
为了解决上述问题,Aptos 实验室的工程师们在 AIP-47 中提出了一种名为“聚合器”(Aggregators)的 Move 特性。这一功能使得 Aptos 网络能够并行处理那些本应是顺序执行的工作负载。
#3.
我们对该问题的看法
我们发现,有些表面上看起来按顺序执行的工作负载实际上并不包含很多读写冲突。而且,即使有冲突,也对事务的最终结果影响不大。我们可以通过延迟从区块链状态读取信息并推迟写入信息来规避这些冲突。
在处理事务时,可以捕捉到冲突的部分,并对其执行结果进行推测性预测。这样一来,事务的主体部分就可以完全并行处理。以下是一些具体例子。
通过延后读取来实现并行聚合
我们可以不直接更新一个计数器(比如,原生代币的供应量或 NFT 集合的大小),而是借助 Block-STM 所采用的多版本数据结构来记录这次更新,但并不立即执行它。
以这样一个简单的交易序列为例,所有这些交易都在更新存储在区块链状态中的某个计数器,但互不冲突。
在这个例子中,第一笔交易减去了 10,因此记录的变化值为 -10。第二笔交易加了 20 又减了 17。我们可以不直接读取计数器的值和写入新值,而是记录两个变化值:+20 和 -17,这样这笔交易造成的总变化值就是 +3。同理,第三笔和第四笔交易的变化值分别是 -10 和 -20。
如果这些交易之间没有其他读写冲突,它们可以并行执行,最后以任意顺序更新计数器。例如,如果计数器最初的值是 100,那么执行完这四笔交易后,它将变为 63。总的来说,更新全局计数器可能引起的读写冲突可以被避免,从而实现工作负载的完全并行执行。
只有在冲突不可避免时,才会采用顺序执行。例如,如果第三笔交易读取了计数器,那么这次读取操作就会强制要求所有前面的更新(来自第一笔和第二笔交易)必须预先计算,这限制了并行处理的可能性。
处理整数上溢和下溢
更新全局计数器时可能会遇到上溢或下溢的问题。比如,在下面的例子中,第二笔交易中计数器出现了上溢。不过,只要我们能正确预测这种上溢,就仍然可以并行处理计数器的更新。如果预测失败,这笔交易就会被 Block-STM 重新执行,就像处理其他任何冲突的交易一样。我们希望的是,这种推测性预测通常是准确的,而且溢出或下溢并不常见。
快照 —— 无冲突地写入区块链状态
即使全局计数器的更新已经并行化,这仍然无法解决 NFT 铸造时遇到的问题。在 NFT 持久化保存到区块链状态之前,索引(即 NFT 的铸造时间)和代币名称(可能也包括索引)会读取计数器的值。
不过,我们可以通过推迟写入区块链状态的操作来解决这个问题。与其立即将代币索引写入状态,不如先取一个当前索引值的“快照”,然后在交易提交时再存储。从本质上讲,快照记录了全局计数器变量当前的部分更新。
#4.
Aptos上的聚合器
在 Aptos 实验室,我们开发了一个通用库,用于处理前文提及的应用场景,这个库可以在 aptos-framework 下找到。
这个库定义了 “聚合器(Aggregators)”,它是一种并发计数器的实现方式。开发者可以利用它来避免在并行执行交易时遇到的顺序执行瓶颈,例如,用于跟踪他们的代币集合的大小。
聚合器本质上是对整数的封装,可以初始化为零。创建聚合器时,用户还可以自定义一个上限值,即聚合器能存储的最大数值。考虑到聚合器是无符号整数,其最小可能值是 0。
// Type parameter allows one to support different integer bit widths,
// e.g., u64 or u128.
struct Aggregatorhas store {
value: IntTy,
max_value: IntTy,
}
let counter = aggregator::create(/*max_value=*/100);
和正常的整数一样,聚合器可以通过加法和减法(称为 try_add 和 try_sub)进行更新,而且是并行操作的!这些方法会返回一个布尔值,用以表明聚合器的值是否发生了变化(如果没有溢出,结果是真;如果溢出,则为假)。
let success = aggregator::try_add(&mut counter, 20);
let success = aggregator::try_sub(&mut counter, 40);
if (success) {
// Counter has been modified.
} else {
// Error handling goes here.
};
一个关键点是,聚合器的并发更新并不会导致交易之间发生冲突。这是因为 Block-STM 所采用的多版本数据结构已经解决了写-写冲突问题。而且,聚合器的行为与顺序执行的结果是一致的,也就是说,无论是顺序还是并行执行,结果都是一样的。
聚合器也可以进行读取操作,读取的是聚合器中存储的值。但是,这会限制多个交易间的并行度,如果可能的话,最好避免这种操作。
// This limits the parallelism of the system.
let value = aggregator::read(&counter);
为了在不受顺序执行影响的情况下读取聚合器的值(比如,用于索引或命名 NFT 集合),用户也可以对聚合器进行一次快照。聚合器快照可以记录某一时刻聚合器的值,但不会透露实际的数值。
struct AggregatorSnapshothas store {
value: Ty,
}
// Snapshot captures the value of the counter.
// This does not affect the parallelism of the system.
let snapshot = aggregator::snapshot(&counter);
如果需要知道快照的确切值,那么它可以像普通聚合器一样进行读取。
// Limits the parallelism of the system, but returns the actual
// value stored inside of a snapshot.
let value = aggregator::read_snapshot(&snapshot);
在当前的实现中,快照还可以被格式化成字符串形式。
let name = aggregator::concat("star #", &snapshot, "");
我们在最新的预览网络(类主网)上针对有代表性的工作负载对聚合器进行了评估,重点测试了 NFT 铸造。我们使用的 NFT 集合具有限量供应,并且是顺序命名的。我们成功地在 90 秒内铸造了 100 万个集合,或在 8 分钟内铸造了 500 万个集合,实现了持续的高吞吐量。与没有使用聚合器的执行相比,性能显著提高了大约 10 倍。
此外,我们还使用了合成基准测试来检验聚合器的性能。我们发现,通过聚合器在 Move 中更新一个无上限的全局计数器速度提升了近 9 倍。这归功于其更好的并行性。
#5.
生产环境中的聚合器
自从 Aptos 主网上线以来,聚合器就被用于其代码库中,以追踪 Aptos 原生代币的总供应量,这意味着即使在燃烧交易手续费的情况下,也不会影响系统的并行处理能力。虽然每一笔交易都会更新总供应量,但工作负载仍能被并行处理。
AIP-47 提出了聚合器和快照等更新更强大的 API,并使它们公开可用。通过 AIP-43,聚合器还可以部署在框架内。这一特性使得 Aptos 平台上的数字资产集合即便在追踪其大小的情况下也能并行地铸造和销毁。此外,这些集合还可以被索引,并拥有格式化的名称。如果这些 AIP 得到批准,数字资产的操作将变得可以并行处理,并有望在 2024 年第一季度在主网上实现。
#6.
后续文章
那么,聚合器是如何实现的呢?它是如何减少 Block-STM 中交易间的冲突,并随着线程的增加而扩展?所有这些问题的答案都将在即将发布的文章中揭晓。敬请关注!
以上内容均转载自互联网,不代表AptosNews立场,不是投资建议,投资有风险,入市需谨慎,如遇侵权请联系管理员删除。