菜单

存储

相关源文件

本文档概述了 Go-Ethereum (Geth) 中的存储系统,涵盖了区块链数据的存储、访问和管理方式。存储系统负责持久化所有区块链数据,包括区块、交易、收据和以太坊状态。

有关状态管理的具体信息,请参阅状态管理

存储架构

Geth 采用两层存储架构,旨在平衡性能和磁盘使用量。

  1. 键值数据库 - 用于活动和近期数据
  2. 历史数据库(Freezer) - 用于历史性的、不可变的数据

这种架构使 Geth 能够高效地处理频繁访问的近期数据和不常访问的历史数据。

来源:ethdb/database.go72-83 core/rawdb/database.go40-67

数据库接口

Geth 在 ethdb 包中为其存储需求定义了清晰的接口,将不同存储操作的关注点分开。

来源:ethdb/database.go26-186

键值数据库

键值数据库用于存储

  1. 近期区块及其元数据
  2. 状态 Merkle Trie 节点
  3. 活动链索引
  4. 交易查找

Geth 支持多种键值数据库后端

  • LevelDB - 原始后端(v1.10 之前的默认设置)
  • PebbleDB - 更高性能的替代方案(v1.11 起的默认设置)

这两种数据库都是持久化的键值存储,针对快速读取和批量写入进行了优化。

来源:cmd/utils/flags.go103-108 core/rawdb/database.go37-83

历史数据存储(Freezer)

历史数据存储,也称为 Freezer,是一个仅追加的数据库,专为很少发生变化的历史数据而设计。它按类型将数据组织到单独的文件中

  • Headers - 区块头
  • Bodies - 区块体
  • Receipts - 交易收据
  • Hashes - 区块哈希
  • Difficulty - 难度值

每种文件类型由一个“freezer table”管理,该表负责索引和高效存储。

来源:core/rawdb/freezer.go1-190 core/rawdb/freezer_table.go1-400

数据存储

存储系统根据预定义的模式组织区块链数据,使用特定的键格式来存储不同类型的数据。

键格式

不同类型的数据使用不同的键前缀和格式

数据类型键格式描述
区块头headerPrefix + num + hash按编号和哈希查找的区块头
区块体bodyPrefix + num + hash按编号和哈希查找的区块体
区块收据blockReceiptsPrefix + num + hash按区块编号和哈希查找的收据
交易txLookupPrefix + hash按哈希查找的交易数据
规范哈希headerPrefix + num + canonicalSuffix指定编号下规范区块的哈希
头部区块headBlockKey当前头部区块的哈希
状态 Merkle Trie 节点将流量定向到响应时间最快的服务器按哈希查找的状态 Merkle Trie 节点

来源:core/rawdb/schema.go1-100 core/rawdb/accessors_chain.go1-200

状态存储

以太坊的状态使用修改过的 Merkle Patricia Trie 结构来存储。这种数据结构提供了

  1. 加密验证 - 每次状态更改都会反映在根哈希中
  2. 高效更新 - 只需更新已修改的路径
  3. 紧凑证明 - 为任何状态元素提供高效的包含性证明

来源:trie/trie.go32-60 core/state/statedb.go68-157

配置选项

Geth 提供多个命令行选项来配置存储系统

数据库配置

标志描述默认
--datadir数据库的数据目录~/.ethereum
--datadir.ancient历史数据目录datadir/chaindata 内部
--db.engine数据库后端pebble
--cache用于缓存的内存分配4096 MB (主网)
--cache.database数据库缓存的百分比50%
--nocompaction禁用数据库压缩false

状态保留选项

标志描述默认
--gcmode垃圾回收模式full
--state.scheme状态存储方案hash
--cache.gcGC 的百分比25%
--snapshot启用快照数据库模式true
--history.transactions交易历史保留约 1 年
--history.state状态历史保留90,000 个区块

来源:cmd/utils/flags.go91-114 cmd/utils/flags.go251-301 cmd/geth/config.go1-120

数据库存储模型

Geth 支持不同的存储模型以适应各种用例

完整节点(Full Node)

完整节点存储所有近期状态和区块,同时修剪旧状态。这是默认的存储模式,在磁盘使用量和功能之间取得了平衡。

  • 近期状态完全可用
  • 区块及其数据保留整个链
  • 历史状态根据配置的设置被修剪

存档节点(Archive Node)

存档节点存储所有区块的完整状态历史,需要显著更多的磁盘空间,但支持完整的历史查询。

  • 保留所有区块的所有状态
  • 不修剪任何数据
  • 通过 --gcmode=archive 启用

来源:eth/backend.go378-380 cmd/utils/flags.go251-290

状态方案

Geth 支持两种不同的状态存储方案

  1. 哈希方案(默认) - 传统的基于哈希查找的方法
  2. 路径方案 - 基于路径存储的新方法

路径方案在某些操作上更有效,并为状态历史提供更好的支持。

来源:cmd/utils/flags.go262-267 core/rawdb/database.go135-144

Freezer 迁移

当区块链数据超过特定阈值时,Geth 会自动将旧数据从键值数据库迁移到历史存储。这个过程

  1. 识别“历史”区块,这些区块不太可能被重写
  2. 在后台进程中将其数据移至 Freezer
  3. 从键值存储中删除迁移的数据
  4. 更新索引以指向正确的位置

此迁移是自动进行的,对用户透明。

来源:core/rawdb/freezer.go380-520

修剪机制

Geth 实现了多种修剪机制来控制磁盘使用量

状态修剪

状态修剪器会移除不再需要的历史状态数据

  • 在“完整”模式下,状态在达到足够旧后会被修剪
  • 可以使用 --gcmode=archive 禁用修剪
  • 状态修剪阈值可以通过 --history.state 进行配置

交易历史修剪

可以修剪交易查找以减少磁盘使用量

  • 默认情况下,保留约 1 年的交易历史记录
  • 可以通过 --history.transactions 进行配置
  • 设置为 0 会保留所有历史记录

日志修剪

事件日志也可以修剪以节省空间

  • 默认情况下,搜索索引中保留约 1 年的日志
  • 可以通过 --history.logs 进行配置
  • 可以通过 --history.logs.disable 禁用

来源: cmd/utils/flags.go262-301 eth/backend.go140-142

数据库初始化

Geth 启动时,会按此顺序初始化数据库系统

  1. 打开键值数据库(LevelDB/PebbleDB)
  2. 检测或创建历史数据库
  3. 初始化状态树数据库
  4. 检查是否需要数据库迁移或升级
  5. 验证数据库版本兼容性

如果检测到损坏数据,Geth 可能会尝试进行恢复操作。

来源: eth/backend.go129-147 core/genesis.go290-320

数据恢复机制

Geth 包含多种数据库恢复机制

  1. 版本检查 - 确保数据库兼容性
  2. 数据库修复 - 尝试修复损坏
  3. 链重下载 - 如果损坏严重,则从头同步
  4. 快照恢复 - 使用快照数据加速恢复

如果数据库损坏,可以通过再次从网络同步来重建数据库。

来源: eth/backend.go175-195

结论

Geth 中的存储系统是一个复杂的多层架构,旨在高效处理以太坊节点多样化的数据存储需求。通过将活动数据与历史数据分开,并采用各种修剪策略,Geth 在性能、功能和资源使用之间取得了平衡。

具有键值存储和历史数据库的两层架构,以及可配置的修剪选项,使得 Geth 能够部署在各种环境中,从资源受限的设备到专用的归档节点。

来源: eth/backend.go1-527 core/rawdb/database.go1-200 ethdb/database.go1-186