太長不看
- 我們評估了三種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




