太长不看
- 我们评估了三种BAL设计——全BAL、批量 I/O BAL和并行 I/O BAL它们在执行吞吐量和BAL大小之间进行了不同的权衡。
- 我们研究开销最低的设计——并行 I/O BAL在多大程度上能够接近 Full BAL的吞吐量。
- 并行 I/O BAL 的吞吐量约为 10.8 GGas/s,而完整BAL 的吞吐量约为 13.9 GGas/s,并行 I/O BAL 仅需完整BAL大小的 33% 即可提供 78% 的吞吐量。
块级访问列表 ( BAL ) 通过在The Block空间中显式编码块执行期间访问的所有账户和存储及其执行后的值,实现了包括并行 I/O 和并行执行在内的并行性。在我们之前的文章中,我们研究了包含事务后状态差异以及块读取键值对的完整BAL的并行执行性能。在一台 16 核商用机器上,我们在兆块大小的场景下实现了约 15 GGas/s 的纯并行执行吞吐量。
然而,这项研究忽略了两个主要限制因素:I/O 和较大的BAL开销。在非预热场景下,I/O 约占总块处理时间的 70%。尽管BAL支持并行磁盘读取,但 BAL 实现的并行 I/O 的有效性可能取决于BAL本身嵌入了多少读取信息。更详细的读取提示可以提高 I/O 并行度,但也会增加BAL 的大小,直接影响网络带宽和存储成本。因此, BAL存在多种设计变体,每种变体都代表了可实现的并行度和BAL大小之间的不同权衡。根据读取提示的精确度,主要设计包括:全BAL、批处理 I/O BAL和并行 I/O BAL。
| 姓名 | 细节 | 并行执行 | 并行 I/O | BAL大小(RLP 编码)* |
|---|---|---|---|---|
| 满BAL | 交易后状态差异和块前读取的键和值 | 每笔交易 | 每个提示(仅用于验证) | 213 KB |
| 批量 I/OBAL | 交易后状态差异和块前读取键 | 每笔交易 | 每提示 | 110 KB |
| 并行 I/OBAL | 交易后状态差异 | 每笔交易 | 每笔交易 | 71 kb(最低,占完整BAL的 33%,占批处理 I/O BAL的 64%) |
*样本取自第 23,770,000至 23,771,999 个区块
理想情况下,我们希望在最大化吞吐量的同时,最小化BAL 的大小。虽然完整的BAL可以提供最高的性能,但同时也会带来最大的开销。这就引出了一个关键问题:开销最低的设计——并行 I/O BAL在多大程度上能够接近完整的BAL的吞吐量?解决这个问题是本文的核心目标。
为了回答这个问题,我们构建了一个执行环境,该环境明确地包含了通过 I/O 读取进行状态加载的功能,具体设置如下:
- Reth 中使用的扁平化数据库,用于存储账户、存储和合约代码。
- 预先恢复的交易发送方,利用大多数客户端中已实现的发送方恢复并行机制
- 本文省略了状态根计算和状态树提交,因为它们的成本可以分摊到大块数据中,并非本研究的重点。
利用此设置,我们对不同BAL设计下的逐事务并行执行(包括并行 I/O 和并行执行)进行了基准测试。结果表明,在 16 核普通机器上,即使采用并行 I/O BAL,在兆块大小的设置下,其吞吐量仍能达到约 10.8 GGas/s,与完整BAL 的约 13.9 GGas/s 相当。这表明,相对于完整BAL,并行 I/O BAL仅需其 33% 的BAL大小即可达到完整BAL 78% 的吞吐量,从而在吞吐量和BAL大小开销之间实现了有效的权衡。
以太坊执行中的 I/O 瓶颈
以太坊正在持续扩容 L1 缓存。Fusaka 升级已将 gas 上限从 4500 万提升至 6000 万,预计 Glamsterdam 升级将进一步提高上限。 我们之前的研究表明, BAL可以将执行吞吐量提升一个数量级,为更高的 gas 上限奠定了坚实的基础。
尽管取得了这些进展,但 I/O 仍然是当今区块处理流水线中的一个主要瓶颈。在非预热配置下,I/O 大约占总执行时间的 70%。以 Reth 为例:
- 使用MDBX进行单线程I/O执行仅能达到约350MGas/s的吞吐量。
- 预热后,I/O 开销降至约 20%,吞吐量提高至约 700 MGas/s。
尽管预热有所帮助,但仍有很大的性能提升空间。I/O 性能的根本限制在于顺序 I/O 访问模式。虽然现代 NVMe SSD 支持深度 I/O 队列(通常高达 64 个队列),但大多数以太坊客户端仍然按顺序执行状态读取操作,未能充分利用可用的 I/O 并行性。
BAL通过启用并行 I/O 来解决这一限制,但这需要付出一定的代价。事务后状态差异对于并行执行至关重要——我们之前的研究表明,它们能够比顺序执行快 10 倍。然而,读取值和读取提示加起来的大小可能与状态差异相当,而它们相对于这些额外的网络和存储开销所带来的性能提升则不太明确。
这就引出了一个重要的设计问题:如果无需包含读取值(甚至读取提示)即可实现接近最优的性能,则可以显著减小BAL 的大小,从而在不牺牲吞吐量的前提下降低网络和存储成本。为了验证这一假设,我们重点研究并行 I/O BAL,它仅包含事务后的状态差异,并在执行期间按需进行状态读取。
实验方法
为了评估并行 I/O BAL所能达到的最终性能极限,我们构建了一个简化的执行环境,移除了上述无关部分。这使我们能够测量 BAL 并行处理能力的真实上限。
利用 reth 的高性能执行引擎和 RocksDB 的多线程读取功能,我们修改了 reth 客户端,使其能够转储执行依赖项(包括块、BAL 和最近 256 个块哈希),使用 REVM 作为 EVM 执行引擎,并引入基于 RocksDB 的状态提供程序,用于帐户、代码和存储访问。
简化 I/O 执行仿真
- 所有交易都已恢复发送方身份(发送方恢复可以事先完全并行化)。
- 执行后不进行状态根计算或 trie 提交(只进行扁平状态提交),因为这些成本与本研究的重点无关。
工程工作及搭建
- 修改了 Reth 客户端,使其支持转储完整的执行依赖项,包括区块、BAL 和最后 256 个区块哈希。
- 为 Revm 添加了 RocksDB 状态提供程序,用于加载帐户、代码和存储状态。
- Reth 的 MDBX 绑定最初经过测试,但在多线程环境下性能下降;因此改用 RocksDB,并使用迁移工具将 MDBX 数据库转换为 RocksDB。
- 对于并行 I/O,使用共享缓存层来避免跨事务的冗余读取。
- 每次实验前都清除了页面缓存。
- 并行粒度 = 逐事务
- 硬件:
- AMD Ryzen 9 5950X(16 个物理核心,或 32 个超线程核心)
- 128 GB 内存
- 7TB RAID-0 NVMe SSD(4k 块随机读取 IOPS 约为 960k,带宽为 3.7GB/s)
- 数据集:2000 个主网区块( #23770000 –23771999)。
- 指标:每秒 Gas 用量 = 总 Gas 使用量 / 执行时间(含 I/O 时间)。
基准测试套件可在此处获取:
GitHub - dajuguan/evm-benchmark
结果
我们首先评估了以太坊主网区块在并行 I/O 和并行执行(使用不同线程数)下的性能,并给出了并行 I/O 的BAL。结果显示存在一条明显的关键路径,该路径主要由运行时间最长的交易构成。为了缓解这一问题,我们模拟了更高的区块 gas 限制,这在使用BAL时可以显著提高并行性。
使用 16 个线程和一个 1G 的 gas 块,并行 I/O BAL 的吞吐量约为 10.8 GGas/s ,接近完整BAL吞吐量(约 13.8 GGas/s)的 78% 。更重要的是,这种性能的实现仅需平均约 71 KB 的BAL大小,与完整BAL相比减少了约 67% 。
并行I/O和并行执行中的关键路径分析
为了评估实际加速效果以及阿姆达尔定律对事务级并行性的影响,我们进行了逐事务并行执行实验,以量化运行时间最长的事务对可实现加速效果的影响。
详细结果如下所示(其中“最长交易延迟”是指每个区块中运行时间最长的交易的总执行时间(含 I/O 时间)):
| 线程 | 吞吐量(百万燃气/秒) | 最长传输延迟 | 总时间 |
|---|---|---|---|
| 1 | 740 | 6.85秒 | 60.62秒 |
| 2 | 1,447 | 6.75秒 | 31.00秒 |
| 4 | 2,167 | 8.11秒 | 20.70秒 |
| 8 | 2,994 | 9.02秒 | 14.98秒 |
| 16 | 3,220 | 8.92秒 | 13.93秒 |
| 32 | 3,253 | 9.57秒 | 13.79秒 |
总体而言,结果与阿姆达尔定律基本吻合。虽然吞吐量随着线程数的增加而提高,但总执行时间受到最长事务的限制。在 16 个线程以下,最长事务约占总执行时间的 75%,因此加速比仅为约 4 倍,而非理想的 16 倍。
为了克服这一限制,我们尝试提高The Block气体限制。
当线程数超过物理核心数(例如,16 个核心上运行 32 个线程)时,性能不再提升。虽然 I/O 本身可以扩展到物理核心数之外,但这很可能受到 RocksDB 缓存查找(索引、布隆过滤器、数据块)以及 CPU 密集型值编码/解码的限制。
巨型模块实现大规模并行处理
为了克服每个区块的关键路径限制,我们尝试使用更高 gas 费用的“巨型区块”(类似于我们之前的工作),以提高并行性。为了模拟这种情况,我们并行执行多个连续主网区块(即巨型区块或批次)的交易,并在批次中的所有交易完成后才将状态提交到数据库。这有效地将多个区块聚合为一个大型执行单元。
我们评估了一批包含 50 个代码块的数据集,模拟了不同线程数下平均代码块 gas 消耗量为 1121 M 的情况。完整结果如下所示:
| 线程 | 吞吐量(百万燃气/秒) | 最长传输延迟 | 总时间 |
|---|---|---|---|
| 1 | 943 | 0.53秒 | 47.55秒 |
| 2 | 1,857 | 0.53秒 | 24.16秒 |
| 4 | 3,505 | 0.56秒 | 12.80秒 |
| 8 | 6,524 | 0.57秒 | 6.88秒 |
| 16 | 10,842 | 0.61秒 | 4.13秒 |
| 32 | 10,794 | 1.07秒 | 4.14秒 |
使用巨型区块后,运行时间最长的交易不再主导关键路径——在 16 个线程下,它们仅占总执行时间的不到 15%。吞吐量几乎与线程数呈线性关系,达到约 10.8 GGas/s——相当于完整BAL性能的 78%——同时BAL大小比完整BAL减少了 67%。
| BAL设计 | RLP编码的BAL大小 | 16线程吞吐量 |
|---|---|---|
| 满BAL | 213 KB | 13,881 兆天然气/秒 |
| 并行 I/OBAL | 71 KB(占 213 KB 的 33%) | 10,842 兆天然气/秒 |
结论
本研究表明,并行 I/O BAL在显著减小BAL大小的同时,性能接近完整BAL 。在超大块部署环境下,并行 I/O BAL可维持约 10.8 GGas/s 的吞吐量(约为完整BAL吞吐量的 78%),同时将BAL大小开销降低至完整BAL的约 33%。这使得并行 I/O BAL成为一种实用高效的设计选择,可在吞吐量与网络和存储开销之间取得平衡。
总的来说,这些结果为并行 I/O BAL 支持的并行执行建立了一个实际的上限,并为以太坊客户端优化和未来的 L1 扩容工作提供了可操作的见解。
其他作品
除了执行基准测试外,我们还在合成随机读取工作负载和 EVM 执行下比较了 RocksDB 和 MDBX,并研究了不同块 gas 限制下并行 I/O BAL和批量 I/O BAL之间的权衡。
MDBX 与 RocksDB 随机读取基准测试
我们首先在与先前实验相同的硬件上对 MDBX 和 RocksDB 的原始随机读取性能进行了基准测试,并通过改变读取线程数来评估其可扩展性。数据库配置如下:
| 物品 | 价值 |
|---|---|
| 钥匙尺寸 | 16 字节 |
| 价值大小 | 32 字节 |
| 条目 | 16亿 |
| RocksDB 的大小 | 85 GB |
| MDBX 大小 | 125 GB |
详细结果:
| 线程 | 数据库 | IOPS | 平均延迟(微秒) | CPU 使用率 (%) |
|---|---|---|---|---|
| 2 | RocksDB | 12K | 160 | 1.1 |
| 2 | MDBX | 21K | 85 | 0.8 |
| 4 | RocksDB | 30K | 130 | 2.2 |
| 4 | MDBX | 48K | 84 | 1.3 |
| 8 | RocksDB | 85K | 92 | 4.5 |
| 8 | MDBX | 97K | 83 | 2.5 |
| 16 | RocksDB | 180K | 90 | 8 |
| 16 | MDBX | 180K | 86 | 6 |
| 32 | RocksDB | 320K | 110 | 24 |
| 32 | MDBX | 360K | 90 | 13 |
RocksDB 和 MDBX 的吞吐量几乎都随线程数线性增长,即使超过 16 个物理核心也是如此。一旦线程数超过 8 个,这两个数据库在 IOPS 和延迟方面的差异就变得微乎其微了。
基准测试套件可在以下位置获取: GitHub - dajuguan/ioarena:用于 libmdbx、rocksdb、lmdbETC的嵌入式存储基准测试工具。
MDBX 与 RocksDB EVM 执行基准测试(并行 I/O 设置)
然后,我们使用 MDBX 评估了 EVM 在并行 I/O 下的执行吞吐量,并将其与 RocksDB 进行了比较,区块 gas 使用量为 1,121 M。详细结果如下:
| 线程 | 数据库 | 吞吐量(百万燃气/秒) |
|---|---|---|
| 8 | MDBX | 2,369 |
| 8 | RocksDB | 6,524 |
| 16 | MDBX | 3,705 |
| 16 | RocksDB | 10,842 |
| 32 | MDBX | 5,748 |
| 48 | MDBX | 6,662 |
| 64 | MDBX | 6,525 |
尽管原始 I/O 性能相近,但使用 MDBX 的执行吞吐量却显著降低。这种差异很可能是由于 reth 当前 MDBX 绑定的使用方式所致,该绑定未能充分利用底层 I/O 并行性。特别是,妥善管理跨线程的共享读取器可以提升性能,但我们尚未找到有效的方法。
并行 I/O 与批量 I/O 在气体限制下的比较
之前的分析主要集中于并行 I/O,即在执行过程中按需获取状态。然而,在某些事务 I/O 密集度极高的场景下,批量 I/O 可能更具优势,因为它可以更好地利用物理 CPU 核心数之外的 I/O 并行性。
为了评估这种权衡,我们比较了不同 I/O 负载模式下的并行 I/O BAL和批量 I/O BAL ,并测量了两种BAL设计下的执行吞吐量如何扩展。
基于主网数据的平均 I/O 负载分析
我们首先进行平均情况分析,其中存储读取仅占每次交易执行指令总数的一小部分——这种设置与典型的主网工作负载非常接近。下表总结了不同BAL设计、线程数和区块 gas 使用量下的吞吐量结果。
| I/O 类型 | 线程 | 块批次大小 | 平均块状气体(M) | 吞吐量(百万燃气/秒) |
|---|---|---|---|---|
| 批量处理 | 16 | 1 | 22 | 3,587 |
| 批量处理 | 32 | 1 | 22 | 3,333 |
| 平行线 | 16 | 1 | 22 | 2,893 |
| 批量处理 | 16 | 10 | 224 | 7,221 |
| 批量处理 | 32 | 10 | 224 | 6,725 |
| 平行线 | 16 | 10 | 224 | 6,842 |
| 批量处理 | 16 | 50 | 1,121 | 10,159 |
| 批量处理 | 32 | 50 | 1,121 | 10,259 |
| 平行线 | 16 | 50 | 1,121 | 10,842 |
| 批量处理 | 16 | 100 | 2,243 | 11,129 |
| 批量处理 | 32 | 100 | 2,243 | 11,266 |
| 平行线 | 16 | 100 | 2,243 | 11,292 |
随着块级气体使用量的增加,两种设计的吞吐量均持续提升。然而,批量 I/O BAL的相对优势逐渐降低,从小块尺寸时的约 20% 降至大块尺寸时的几乎为零。
此外,对于批量 I/O BAL,将线程数从 16 增加到 32 并没有带来多少性能提升,这表明工作负载已变为 CPU 瓶颈而非 I/O 瓶颈。这种现象可能是由于 RocksDB 缓存查找和 CPU 密集型值编码/解码造成的,这些操作限制了 I/O 的进一步扩展。
| BAL设计 | RLP编码的BAL大小 |
|---|---|
| 并行 I/O BAL (无读取) | 110 KB |
| 批量 I/OBAL(含读取) | 71 KB(缩小 35%) |
至关重要的是,批量 I/O 的平均 RLP 编码BAL大小比并行 I/O 大约大 35%。考虑到大数据块除了 I/O 读取之外还会暴露执行瓶颈,这种额外的网络和存储开销使得并行 I/O 成为更具吸引力的BAL设计选择。上述基准测试套件中也提供了详细的BAL大小测量数据。
基于模拟数据的最坏情况 I/O 负载分析
为了补充平均情况的结果,我们现在考虑最坏情况的 I/O 负载场景,其中磁盘读取主导事务执行。
为了模拟这种情况,我们构建了能够最大限度增加存储访问压力的合成交易。具体来说,我们生成的交易的操作码流中充满了对合约的调用,这些合约会重复执行SLOAD(x)操作,其中x是一个随机值的哈希值。由于没有 BAL 提供的读取位置,这类交易必须按顺序执行SLOAD操作码才能获取存储状态,这代表了一种最坏情况下的 I/O 密集型工作负载。
鉴于目前每笔交易的 gas 上限为 1600 万,且每次槽位状态读取成本约为:
-
SLOAD需要 2000 gas,加上 keccak 哈希开销约 39 gas,
单笔交易最多可执行: \frac{16,000,000}{2039} \approx 7,845 16,000,000 2039 ≈ 7,845
不同的存储读取操作。使用此配置,我们模拟主网数据库最坏情况下的 I/O 负载事务。
下面显示了批处理 I/O 设计与并行 I/O 设计的性能对比结果:
| I/O 类型 | 线程 | 总执行时间(毫秒) | 平均块状气体(M) | 吞吐量(百万燃气/秒) |
|---|---|---|---|---|
| 批量处理 | 16 | 14.4 | 64 | 4,571 |
| 批量处理 | 32 | 11.2 | 64 | 5,818 |
| 批量处理 | 48 | 10.7 | 64 | 6,400 |
| 批量处理 | 64 | 10.7 | 64 | 5,333 |
| 平行线 | 4 | 82.5 | 64 | 780 |
| 批量处理 | 16 | 42.6 | 640 | 11,034 |
| 批量处理 | 32 | 58.2 | 640 | 12,307 |
| 批量处理 | 32 | 60.3 | 640 | 10,158 |
| 平行线 | 16 | 82.2 | 640 | 7,804 |
在较低的块 gas 消耗量(64M gas)下,批量 I/O BAL在 48 个线程时达到最佳吞吐量,几乎是并行 I/O BAL吞吐量的 8 倍。这证实了当存储读取操作占主导地位时,显式 I/O 批量处理非常有效。
然而,解读这些结果时,必须考虑端到端的执行环境。即使在最坏的 I/O 负载场景下,并行 I/O BAL 的总执行时间也远低于当前的验证截止时间(约 3 秒)。此外,由于此场景下不存在状态变更,并行执行排除了 merklization 和状态提交的成本,而这两项成本在实际的并行执行流水线中几乎占总执行时间的 50%。
在 10 倍 gas 使用量巨型块设置(640 M gas)中,性能差距进一步缩小:批量 I/O BAL 的性能仅比并行 I/O BAL高出约1.6 倍,而两者都轻松保持在验证时间限制内。
| I/O 类型 | 平均块状气体(M) | 最佳吞吐量(百万加仑/秒) | RLP编码的BAL大小 |
|---|---|---|---|
| 批量处理 | 64 | 6,400 | 251 KB |
| 平行线 | 64 | 6,400 | 0 千字节 |
| 批量处理 | 640 | 15,238 | 2,511 KB |
| 平行线 | 640 | 6,153 | 0 千字节 |
综合来看,在最坏情况下的 I/O 密集型工作负载下,我们观察到以下现象:
- 在当前主网Gas 限制下:
- 批量 I/O BAL 的吞吐量比并行 I/O BAL最高可提升 8 倍。然而,就端到端块处理时间而言,I/O 读取并非此方案中的主要瓶颈。
- 低于气体限制的 10 倍:
- 批量 I/O BAL的性能优势显著缩小,吞吐量仅为并行 I/O BAL 的1.6 倍,同时还会产生约2.5 MiB 的额外BAL大小开销,这是不可忽略的。
这些结果强化了一个关键见解:虽然批量 I/O BAL在病态的、I/O 饱和的工作负载下提供了最佳性能,但并行 I/O BAL即使在最坏的情况下也仍然足够稳健,而不会产生批量处理引入的额外BAL大小开销。
基准测试套件可在此处获取:
GitHub - dajuguan/evm-benchmark



