Nethermind 推出了一种名为 Flat DB 的新型状态存储架构,通过将账户和存储数据从传统的默克尔树(Trie)遍历路径中分离到扁平的键值列中,解决了状态增长带来的读取性能瓶颈。该架构在早期测试中提升了约 20% 的执行吞吐量,并引入了分层快照系统和 Trie 节点预热机制,以优化以太坊执行客户端的状态访问效率。

……早期测试显示,与 HalfPath 相比,吞吐量提高了约 20%
以太坊状态规模庞大、结构复杂,且随着每个区块不断增长。客户端如何存储和检索该状态决定了区块处理的速度。Nethermind 引入了一种新的状态存储架构:Flat DB。
Flat DB 是 Nethermind 以太坊执行客户端的一种新状态存储架构。它将账户数据、存储槽数据和 Trie 节点分离到专用的 RocksDB 列中,而不是所有读取都依赖于以 Trie 为中心的布局。
它还引入了分层快照和 Compaction 系统,旨在减少区块处理过程中的读取开销。
关键转变很简单。客户端不再总是通过遍历 Merkle Trie 来读取状态,而是可以直接从扁平的键值列中查找账户和存储。Trie 仍然被维护用于根计算和验证,但它不再是主要的读取路径。
Flat DB 针对的是区块执行期间状态访问的成本。EVM 通过 SLOAD 和 SSTORE 操作与状态交互。在基于 Trie 的存储中,这些操作需要从根节点开始遍历多个 Trie 节点才能检索到值。随着状态的增长,这种遍历变得越来越昂贵。
Nethermind 之前的模型基于 HalfPath,将状态存储在 Trie 结构中。相比传统的基于哈希的布局,HalfPath 显著提高了缓存局部性,但读取状态时仍需遍历 Trie。随着状态的增长,这种遍历持续增加成本。
Flat DB 解决的主要低效问题是读取期间的 Trie 遍历。
Flat DB 通过将账户、存储和 Trie 节点分离到不同的列中,并在纯读取工作负载中完全跳过 Trie 遍历来解决这个问题。
图 1:在 HalfPath 下,单次账户查找需要遍历 4-6 个 Trie 节点,每个节点都可能是一次磁盘读取。Flat DB 在一次读取中直接从扁平键值列解析相同的查找。Trie 仍被单独维护以进行根验证。
Flat DB 还减少了状态访问期间读取的数据量。Trie 条目显著大于扁平条目,这增加了遍历期间的带宽和内存开销。在测试中,基于 Trie 的读取所需的数据量远多于扁平条目,有时甚至达到数十倍。直接的扁平查找减少了这种开销并提高了读取性能。
扁平化状态存储已被多次尝试。之前的实现通常提高了读取性能,但在 Commit 期间减慢了状态计算。在实践中,这抵消了大部分收益。
Nethermind 已经包含了激进的缓存和推测执行。Prewarmer 在执行之前处理了大部分状态读取。这意味着 Flat DB 必须优于已经高度优化的读取路径。
因此,仅改进状态访问是不够的。Flat DB 还必须解决 Commit 时间、缓存行为和内存压力,以提供有意义的性能提升。
全局状态编排位于 FlatDbManager 中,它位于 IPersistence 抽象层之上。这允许在不改变高层逻辑的情况下使用不同的数据库布局。在实践中,使用的是 RocksDB。
Flat DB 引入了两层快照。第一层包含包含近期写入的单区块快照。第二层包含聚合了多个区块的压缩快照。这些压缩快照的大小取决于区块编号。具体来说,大小是能整除区块编号的最大的 2 的幂,最大可配置为 32 个区块。
这使得快照层数相对于 Reorg 规模呈对数增长。在读取期间需要遍历的层数更少,尤其是在处理较深的 Reorg 时。
ReadOnlySnapshotBundle 是世界状态使用的共享读取视图,通常遍历约 7 层即可满足 Snap Sync 所需的 128 层 Reorg 深度。
然而,区块处理仍然需要计算状态根。这意味着即使有了扁平状态访问,仍然需要获取 Trie 节点。优化这一路径对 Flat DB 的性能至关重要。
有两个组件是此类优化的核心:
TrieNodeCache,一个由路径和哈希索引的分片哈希表TrieNodeWarmer,预取 Trie 节点以减少读取延迟如果没有这些优化,Flat DB 不会比 HalfPath 显著更快。
图 2:全局状态布局,显示了Layer1单区块快照、Layer2压缩快照、持久化层(Persistence)和 Trie 缓存。
图 3:FlatWorldStateScope 组件分解,显示了状态树、存储树、ITrieWarmer、SnapshotBundle 和 TrieNodeCache。
Flat DB 依靠快照 Compaction 和 Trie 预热来实现性能提升。较小的快照会逐步合并到较大的压缩快照中,从而减少读取器必须遍历的层数。这有助于在实际工作负载(包括 Snap Sync 场景)下最大限度地降低延迟并保持性能。
Flat DB 构建在可插拔的持久化层之上。IPersistence 抽象允许在不更改高级状态逻辑的情况下使用不同的数据库后端和键布局。
这使得针对不同用例尝试备选存储引擎和布局变得更加容易。该设计允许尝试 RocksDB、LMDB 或 Paprika 等数据库,以及针对延迟、内存使用或磁盘效率优化的不同键结构。然而,到目前为止,RocksDB 仍然是性能最强的解决方案,也是当前的生产后端。
Flat DB 目前包含三种具有不同权衡的布局:
默认的 Flat 布局旨在实现最低延迟,同时保持功能兼容性。数据库经过调整,以实现快速读取和最小访问开销,即使以更高的内存使用量为代价。Flat 布局还使用哈希地址的 20 个字节作为键。这减少了比较时的 CPU 开销,同时保持了较低的冲突风险。
这包括在内存中保留较大的 RocksDB 索引。在主网上,这大约是 2.5 GB 的 RocksDB 索引数据,高于之前的配置。目标是减少区块处理过程中的查找延迟。
该布局使用七个 RocksDB 列:Metadata、Account、Storage、StateTopNodes、StateNodes、StorageNodes 和 FallbackNodes。
账户使用精简的 RLP 编码且不进行压缩。存储键的结构有助于 RocksDB 进行比较。顶级 Trie 节点被分离到专用列中,以减轻 Compaction 压力并提高缓存命中率。
主网上的数据库总大小约为 260 GB。此布局适用于高内存环境,测试中参考内存约为 32 GB。
PreimageFlat 与 Flat 布局相同,不同之处在于账户和存储键使用原始值而不是哈希键。
由于键未经过哈希处理,此布局无法支持同步。它无法导入现有状态或使用 Snap Sync。因此,它主要用于实验。
理论上,移除键哈希应该会减少查找开销并提高性能。然而,由于该布局目前无法导入或进行 Snap Sync,因此很难测量性能差异。
因此,PreimageFlat 主要作为一种实验性配置存在,用于评估键哈希对状态访问性能的影响。
FlatInTrie 专为内存受限的环境设计,或者网络状态足够大以至于 Flat 布局的内存开销变得不可接受的环境。
FlatInTrie 不维护单独的扁平列,而是将账户和存储数据嵌入到与 Trie 相关的列中。这显著减少了保留在内存中的索引数据量。常驻索引大小仅为几兆字节。
较小的数据库规模还提高了操作系统的缓存利用率,这有助于减少内存受限环境中的磁盘访问开销。
权衡之处在于性能。FlatInTrie 明显慢于默认的 Flat 布局,因为更多的查找依赖于压缩存储和分区索引,而不是大型内存索引。
因此,FlatInTrie 适用于低内存机器或非常庞大的网络,在这些场景中,内存效率比最大执行吞吐量更重要。
在早期测试中,与 HalfPath 相比,Flat DB 的执行吞吐量(以每秒 MGas 为单位)高出约 20%。Flat DB 主要改进了较慢的状态访问路径。Nethermind 已经在推测执行期间预热了大部分读取。Flat DB 在这些缓存未命中时减少了延迟。在更高的 Gas 限制和存储密集型工作负载下,这种改进变得更加明显。
测试还表明,Flat DB 在内存压力下保持了更稳定的性能。随着可用内存的减少,基于 Trie 的布局性能下降更显著,而扁平状态访问则保持得更一致。
在涉及重型区块的更大压力测试中,还观察到了约 40% 的改进。这些增益主要出现在高状态访问的工作负载中,其中减少 Trie 遍历和更少的快照层产生的影响最大。
这种改进源于读取期间 Trie 遍历的减少、更低的快照遍历深度以及 Trie 节点的缓存。
这些结果基于 PR 讨论中引用的内部测试。仍需要在各种工作负载和配置中进行更广泛的验证。Flat DB 还减少了纯状态读取期间的原始带宽使用,因为 Trie 条目显著大于扁平条目。然而,区块处理仍然需要读取 Trie 节点以计算状态根。因此,整体性能影响可能会根据工作负载以及读取时间与 Commit 时间的比例而有所不同。
压缩快照可以在后台持久化,而不是逐个区块写入,这有助于减少状态持久化期间的 CPU 开销。
Flat DB 目前处于 Alpha 阶段,默认不启用。实现仍在积极开发中,已知问题包括偶发的卡死和随机崩溃。目前尚不建议用于生产环境。
Flat DB 也不会自动应用于新的同步。运营商在测试该功能时必须明确选择加入。Flat DB 目前在测试时需要明确配置。迁移和同步工作流程仍在演进中。
Flat DB 的 Snap Sync 支持已经合并。然而,由于实现仍处于 Alpha 阶段,运营商仍应预料到不稳定性以及持续的变化。
关键考虑因素:
有兴趣测试 Flat DB 的运营商应将其视为实验性的,并预料到随着实现的稳定会发生进一步的变化。
Flat DB 的 Snap Sync 支持已经实现。然而,协调变得更加复杂,因为必须在同一区块高度的读取中保持一致的状态。这种行为仍在验证中。
Flat DB 为状态数据引入了快照分层和 Compaction。这改变了状态在内部存储和访问的方式。它不改变区块或交易数据。
Flat DB 已合并到当前分支,但仍处于 Alpha 阶段且 默认禁用。实现仍处于积极开发中,稳定性工作正在进行。
已知局限性:
⚠️ 运行期间可能会出现诸如“unexpected old snapshot”之类的诊断日志。此行为仍在调查中。
Flat DB 降低了区块处理期间状态访问的成本。随着执行变得更加并行且区块大小增加,状态访问在执行时间中所占的比例越来越大。因此,降低状态访问延迟变得越来越重要。
图 4:HalfPath、Flat DB 和理论上经过优化的区块级访问列表(Block-Level Access List)实现中每个区块的时间分解。Flat DB 将瓶颈从状态访问转移到了状态根计算,这也是 EIP-7928 的目标。
Flat DB 将扁平状态访问与 Trie 计算分离。这为未来的工作和进一步的性能调优创造了更清晰的结构。这包括:
与此同时,Flat DB 尚未提供某些功能:
Flat DB 是 Nethermind 存储和访问状态方式的一次基础性改变。早期结果显示,在测试设置中吞吐量更高。进一步的验证将决定它在不同工作负载和环境下的表现。
- 原文链接: nethermind.io/blog/flat-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!