go-ethereum 源码笔记(ethdb 模块)

通过前面的文章,我们知道了有工作量证明的区块链是怎么构建的,然而区块链一直在内存中当然是不行的,我们需要将区块链持久化到数据库中。

区块链的本质是一个分布式数据库,尽管如此,底层使用的数据库并没有太大要求,不需要分布式,分片等等特性,这些特性是在数据库的上层做的。所有的节点都是一个完整的实例,是它们组成了一个分布式系统。就实现业务来说,我们只需要一个简单的 k-v 数据库就够了。

geth 采用的是 LevelDB,回头来看这个选择还是有很多可以反思的地方,LevelDB 在顺序读写的场景下性能很好,但 geth 在运行智能合约时,面临的更多的场景是大量时间耗费在世界状态的读写上,通过前面对 MPT 的分析我们知道,这些 IO 大多是离散随机的,这导致了 geth 的性能瓶颈,特别是在合约里的数据大量分布在 Storage Trie 的情况下,因为树的高度可能很高,而每加载一个节点都需要进行一次 IO。这一点在以太坊(Ethereum) 的执行交易性能瓶颈这篇文章中有论述。邓草原 正在开发的 Kesque 也可以关注一下。这不是本文重点,就不展开了。

中本聪的最初论文并没有提具体要用什么数据库,尽管如此,社区使用最多的参考实现 Bitcoin Core 最初使用的是 Berkeley DB,后来转而使用 levelDB 存区块链索引,以太坊的 Go,C++, Python 实现的客户端使用的是 LevelDB 数据库,Rust 客户端 Parity 使用的是 RocksDB。

go-ethereum 对 LevelDB 的增删改查是通过 ethdb 这个模块来交互的。ethdb 实际上是对 github.com/syndtr/goleveldb/leveldb 这个库的封装,其中增加了收集 metric 的功能。

如果想要对 LevelDB 有更深层次的了解可以自行查询相关资料。

ethdb 模块的功能比较单一,就4个文件:

  • interface.go 定义数据库增删改查的接口
  • database.go 封装 levelDB 的代码
  • memory_database.go 基于内存的数据库,不会持久化到文件,只在测试时使用
  • database_test.go 测试用例

interface.go

1
2
3
4
5
6
7
8
type Database interface {
Putter
Get(key []byte) ([]byte, error)
Has(key []byte) (bool, error)
Delete(key []byte) error
Close()
NewBatch() Batch
}

Database 接口定义了所有数据库操作,Putter 接口定义批量操作和普通操作的接口。批量操作不能多线程同时使用。

memory_database.go

1
2
3
4
type MemDatabase struct {
db map[string][]byte
lock sync.RWMutex
}

这个文件跟 LevelDB 没什么关系,它只是封装了内存里的 key-value 结构,暴露的增删改查的接口(Put,Has,Get,Keys,Delete 等等)跟对 LevelDB 的封装基本一致。加了锁以在多线程的情况下对资源进行保护。

database.go

封装了 levelDB 接口,在 go-ethereum 中大量使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type LDBDatabase struct {
fn string
db *leveldb.DB

compTimeMeter metrics.Meter
compReadMeter metrics.Meter
compWriteMeter metrics.Meter
diskReadMeter metrics.Meter
diskWriteMeter metrics.Meter

quitLock sync.Mutex
quitChan chan chan error

log log.Logger
}

Path, Put, Has, Get 等方法是对 levelDB 的封装,没有什么特别的,也不需要用锁,因为 github.com/syndtr/goleveldb/leveldb 支持多线程访问。

Path

1
2
3
func (db *LDBDatabase) Path() string {
return db.fn
}

Put

1
2
3
func (db *LDBDatabase) Put(key []byte, value []byte) error {
return db.db.Put(key, value, nil)
}

Has

1
2
3
func (db *LDBDatabase) Has(key []byte) (bool, error) {
return db.db.Has(key, nil)
}

Get

1
2
3
4
5
6
7
func (db *LDBDatabase) Get(key []byte) ([]byte, error) {
dat, err := db.db.Get(key, nil)
if err != nil {
return nil, err
}
return dat, nil
}

Delete

1
2
3
func (db *LDBDatabase) Delete(key []byte) error {
return db.db.Delete(key, nil)
}

Metrics

compTimeMeter, compReadMeter, compWriteMeter, diskReadMeter, diskWriteMeter 这几个方法都通过 metrics.NewRegisteredMeter 注册得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (db *LDBDatabase) Meter(prefix string) {
if !metrics.Enabled {
return
}
db.compTimeMeter = metrics.NewRegisteredMeter(prefix+"compact/time", nil)
db.compReadMeter = metrics.NewRegisteredMeter(prefix+"compact/input", nil)
db.compWriteMeter = metrics.NewRegisteredMeter(prefix+"compact/output", nil)
db.diskReadMeter = metrics.NewRegisteredMeter(prefix+"disk/read", nil)
db.diskWriteMeter = metrics.NewRegisteredMeter(prefix+"disk/write", nil)

db.quitLock.Lock()
db.quitChan = make(chan chan error)
db.quitLock.Unlock()

go db.meter(3 * time.Second)
}

最后通过一个 goroutine 调用 db.meter。

该方法是一个死循环,过三秒钟就通过 db.db.GetProperty("leveldb.iostats") 获取一次 leveldb 的 stats,然后发到 metrics 系统。如果收到 quitChan 的信号就退出死循环。

业务相关代码

与业务直接相关的代码在 core/rawdb 目录下,基本上所有与底层数据库交互的代码都在这里,没有很复杂的内容,不过了解这些底层的数据结构对于理解业务很有帮助,可以自行浏览。

key->value

以下是一些 geth 中常见的 key-value 对(在 core/rawdb/schema.go 中定义)。

key value 描述
“h” + num + hash 区块头的数据 根据区块高度和区块哈希找到区块头数据(RLP 编码)
“h” + num + hash + “t” 区块总难度 根据区块高度,区块哈希找到 totalDifficulty,即累计的区块难度的和
“h” + num + “n” 区块头哈希值 根据存储的规范区块链高度找到区块头哈希值
“H” + hash 区块高度 根据哈希值找到区块高度
“b” + num + hash 区块体的数据 根据区块高度,区块哈希找到区块体的数据(RLP 编码)
“r” + num + hash 收据的数据 根据区块高度,区块哈希找到收据的数据(RLP 编码)
“l” + hash 交易,收据查找的 metadata 根据哈希值找到交易,收据查找的 metadata
“B” + bit + section + hash 布隆过滤器的结果 用于日志的过滤

References