舊文重發(fā),之前的文章被簡書封了添诉,迷之尷尬
感謝 Gary大佬的細(xì)心解答
之前轉(zhuǎn)過一篇文章 如何解析 Bitcoin 的數(shù)據(jù)潭陪,Bitcoin 將 p2p 網(wǎng)絡(luò)同步來的數(shù)據(jù),保存在了LevelDB數(shù)據(jù)庫中培己。我們可以通過 rpc 的方式請求數(shù)據(jù),這些請求來的數(shù)據(jù)是從LevelDB(以下簡稱 ldb)獲取的胚泌。如果我們能直接讀取 ldb 的數(shù)據(jù)省咨,就可以繞過 rpc請求,直接讀取诸迟。
為什么繞過 rpc 茸炒,直接讀取 ldb 呢
一個字 快,二個字真快阵苇。
我們可以通過 tcp 直接讀取 mysql 數(shù)據(jù),也可以通過
navicat這樣的 GUI 工具讀取感论。有時候為了快速讀取數(shù)據(jù)绅项,我們會舍棄GUI這樣美觀的工具,而用更直接的方式比肄。
直接讀取 Bitcoin 的 ldb 數(shù)據(jù)庫快耿, 就是舍棄了 美觀的 rpc 工具,顯而易見的好處是快芳绩。
由于 Bitcoin 使用 Ldb 保存區(qū)塊鏈數(shù)據(jù)掀亥,之后的很多鏈沿用了這個技術(shù)路線,Ethereum也同樣使用 Ldb 保存數(shù)據(jù)妥色。
什么是 LevelDB
Ldb是Google 工程師Jeff Dean和Sanjay Ghemawat開發(fā)的NoSQL 存儲引擎庫搪花,是現(xiàn)代分布式存儲領(lǐng)域的一枚原子彈。在它的基礎(chǔ)之上嘹害,F(xiàn)acebook 開發(fā)出了另一個 NoSQL 存儲引擎庫 RocksDB撮竿,沿用了 Ldb 的先進(jìn)技術(shù)架構(gòu)的同時還解決了 LevelDB 的一些短板。現(xiàn)代開源市場上有很多數(shù)據(jù)庫都在使用 RocksDB 作為底層存儲引擎笔呀,比如大名鼎鼎的 TiDB幢踏。
在使用 Ldb 時,我們可以將它看成一個 Key/Value 內(nèi)存數(shù)據(jù)庫许师。它提供了基礎(chǔ)的 Get/Set API房蝉,我們在代碼里可以通過這個 API 來讀寫數(shù)據(jù)僚匆。你還可以將它看成一個無限大小的高級 HashMap,我們可以往里面塞入無限條 Key/Value 數(shù)據(jù)搭幻,只要磁盤可以裝下咧擂。
Ldb有多種儲存結(jié)構(gòu),這些結(jié)構(gòu)并不是平坦的粗卜,而是分層組織(Level)的屋确,這也是LevelDB名字的來源。如下所示:
Ldb基本使用示例
Ethereum 使用最廣的開發(fā)版本是 go-eth续扔,我們自然用 golang做些基本的代碼事例气破。
// 讀或?qū)憯?shù)據(jù)庫
db, err := leveldb.OpenFile("path/db", nil)
...
defer db.Close()
...
///////////////////////
// 讀或?qū)憯?shù)據(jù)庫,返回數(shù)據(jù)結(jié)果不能修改
data, err := db.Get([]byte("key"), nil)
...
err = db.Put([]byte("key"), []byte("value"), nil)
...
err = db.Delete([]byte("key"), nil
///////////////////////
// 數(shù)據(jù)庫遍歷
iter := db.NewIterator(nil, nil)
for iter.Next() {
key := iter.Key()
value := iter.Value()
...
}
iter.Release()
err = iter.Error()
...
如何讀取 Eth 的 Ldb 數(shù)據(jù)呢辆它?
我們先研究下 v1.8.7
版本缀去,這個版本有很多裸露的線索。在示例中讀取 ldb 需要有 key
识脆,Eth有key
這種東西么设联?
https://github.com/ethereum/go-ethereum/blob/v1.8.7/core/database_util.go#L53
headerPrefix = []byte("h") // headerPrefix + num (uint64 big endian) + hash -> header
numSuffix = []byte("n") // headerPrefix + num (uint64 big endian) + numSuffix -> hash
bodyPrefix = []byte("b") // bodyPrefix + num (uint64 big endian) + hash -> block body
從這句話中headerPrefix + num (uint64 big endian) + hash -> header
推斷 headerPrefix + x + y
組成了一個 key, 用代碼試試
// 先連接ldb數(shù)據(jù)庫
db, _ := leveldb.OpenFile("/mnt/eth/geth/chaindata", nil)
num := 46147 // 任意的區(qū)塊高度
blkNum := make([]byte, 8)
binary.BigEndian.PutUint64(blkNum, uint64(num)) // 把num變?yōu)?uint64 big endian類型的數(shù)據(jù)
hashKey := append(headerPrefix, blkNum...) // headerPrefix + blkNum
hashKey = append(hashKey, numSuffix...) // blkNum + headerPrefix + numSuffix
// 查找hashKey 對應(yīng)的 value
blkHash, _ := db.Get(hashKey, nil)
從 Ldb 讀取出blkHash
是真有數(shù)據(jù)灼捂, 替換不同的 num 得到的數(shù)據(jù)不同±肜現(xiàn)實中我是看 GetCanonicalHash
函數(shù)得到的提示。
得到了 blkHash
key , 下一步得到 headerKey
key
headerKey := append(headerPrefix, blkNum...) // headerPrefix + blkNum
headerKey = append(headerKey, blkHash...) // headerPrefix + blkNum + blkHash
blkHeaderData, _ := db.Get(headerKey, nil) // headerKey是新的key
_byteData := bytes.NewReader(blkHeaderData)
blkHeader := new(types.Header)
rlp.Decode(_byteData, blkHeader)
fmt.Printf("Block Hash: %x \n", blkHeader.Hash())
fmt.Printf("Block Coinbase: %x \n", blkHeader.Coinbase)
rlp.Decode(_byteData, blkHeader)
這里解釋下悉稠,如果大家讀Eth的一些文章宫蛆,ldb的保存數(shù)據(jù)是用rlp編碼后,這里是解碼為types.Header
結(jié)構(gòu)的數(shù)據(jù)的猛。
更詳細(xì)代碼請看 ethLeveldb.go
------------------------------ 華麗麗的分割線-------------------------------
go-eth 在 2019 年 7 月推出了v1.9.x
版本耀盗,在同步數(shù)據(jù)、讀取Ldb方面做了大量的封裝卦尊,使用起來更為方便了叛拷。 使用 syncmode=archive
同步方式的時候, 同步時間從 62 天變成了 13 天岂却,5 倍 忿薇。
上面的代碼在讀取1.9.x
版本數(shù)據(jù)的時候,讀取不成功淌友。
1.9.x
在數(shù)據(jù)方面做了重新整理煌恢,大概有以下兩個非兼容的改動:
歷史的區(qū)塊鏈數(shù)據(jù)(header,body震庭, receipts等)被挪到一個flaten file存儲中瑰抵,因為這部分?jǐn)?shù)據(jù)已經(jīng)是不會更改的了
更改了部分?jǐn)?shù)據(jù)結(jié)構(gòu)的scheme,例如receipt器联。原先很多字段不需要存到db二汛,是可以在read之后重新計算出來的婿崭。這部分會占據(jù)大量的存儲空間,在1.9把這些字段刪去了肴颊。
有一個 inspect
命令氓栈,分別統(tǒng)計下 fast 、full 不同模式的數(shù)據(jù)庫詳細(xì)信息
geth inspect --datadir fastdata
+-----------------+--------------------+------------+
| DATABASE | CATEGORY | SIZE |
+-----------------+--------------------+------------+
| Key-Value store | Headers | 211.40 KiB |
| Key-Value store | Bodies | 44.00 B |
| Key-Value store | Receipts | 42.00 B |
| Key-Value store | Difficulties | 19.07 KiB |
| Key-Value store | Block number->hash | 17.24 KiB |
| Key-Value store | Block hash->number | 845.67 KiB |
| Key-Value store | Transaction index | 0.00 B |
| Key-Value store | Bloombit index | 0.00 B |
| Key-Value store | Trie nodes | 4.79 MiB |
| Key-Value store | Trie preimages | 547.13 KiB |
| Key-Value store | Clique snapshots | 0.00 B |
| Key-Value store | Singleton metadata | 149.00 B |
| Ancient store | Headers | 5.97 MiB |
| Ancient store | Bodies | 851.64 KiB |
| Ancient store | Receipts | 182.32 KiB |
| Ancient store | Difficulties | 279.10 KiB |
| Ancient store | Block number->hash | 769.77 KiB |
| Light client | CHT trie nodes | 0.00 B |
| Light client | Bloom trie nodes | 0.00 B |
+-----------------+--------------------+------------+
| TOTAL | 14.40 MIB |
+-----------------+--------------------+------------+
geth inspect --datadir fulldata
+-----------------+--------------------+------------+
| DATABASE | CATEGORY | SIZE |
+-----------------+--------------------+------------+
| Key-Value store | Headers | 42.89 MiB |
| Key-Value store | Bodies | 8.90 MiB |
| Key-Value store | Receipts | 3.76 MiB |
| Key-Value store | Difficulties | 3.93 MiB |
| Key-Value store | Block number->hash | 3.33 MiB |
| Key-Value store | Block hash->number | 3.07 MiB |
| Key-Value store | Transaction index | 544.56 KiB |
| Key-Value store | Bloombit index | 1.60 MiB |
| Key-Value store | Trie nodes | 4.03 MiB |
| Key-Value store | Trie preimages | 1.01 MiB |
| Key-Value store | Clique snapshots | 0.00 B |
| Key-Value store | Singleton metadata | 139.00 B |
| Ancient store | Headers | 6.00 B |
| Ancient store | Bodies | 6.00 B |
| Ancient store | Receipts | 6.00 B |
| Ancient store | Difficulties | 6.00 B |
| Ancient store | Block number->hash | 6.00 B |
| Light client | CHT trie nodes | 0.00 B |
| Light client | Bloom trie nodes | 0.00 B |
+-----------------+--------------------+------------+
| TOTAL | 73.05 MIB |
+-----------------+--------------------+------------+
+-----------------+--------------------+------------+
| DATABASE | CATEGORY | SIZE |
+-----------------+--------------------+------------+
| Key-Value store | Headers | 50.70 MiB |
| Key-Value store | Bodies | 15.60 MiB |
| Key-Value store | Receipts | 6.51 MiB |
| Key-Value store | Difficulties | 4.74 MiB |
| Key-Value store | Block number->hash | 3.93 MiB |
| Key-Value store | Block hash->number | 6.77 MiB |
| Key-Value store | Transaction index | 3.03 MiB |
| Key-Value store | Bloombit index | 3.54 MiB |
| Key-Value store | Trie nodes | 6.83 MiB |
| Key-Value store | Trie preimages | 1.50 MiB |
| Key-Value store | Clique snapshots | 0.00 B |
| Key-Value store | Singleton metadata | 139.00 B |
| Ancient store | Headers | 23.94 MiB |
| Ancient store | Bodies | 4.97 MiB |
| Ancient store | Receipts | 1.43 MiB |
| Ancient store | Difficulties | 1.12 MiB |
| Ancient store | Block number->hash | 3.01 MiB |
| Light client | CHT trie nodes | 0.00 B |
| Light client | Bloom trie nodes | 0.00 B |
+-----------------+--------------------+------------+
| TOTAL | 137.62 MIB |
+-----------------+--------------------+------------+
這里的flaten file存儲
婿着,其實是把歷史數(shù)據(jù)挪到了 ancient
文件夾授瘦,不在用 Ldb,而用普通的二進(jìn)制格式儲存數(shù)據(jù)竟宋。
.
├── 000034.ldb
├── MANIFEST-000162
└── ancient
├── 000006.log
├── bodies.0000.cdat
├── bodies.cidx
├── diffs.0000.rdat
├── diffs.ridx
├── hashes.0000.rdat
├── hashes.ridx
├── headers.0000.cdat
├── headers.cidx
├── receipts.0000.cdat
└── receipts.cidx
那讀取是不是更麻煩了呢提完?非也
dbPath = "/mnt/eth/geth/chaindata"
ancientPath = dbPath + "/ancient" // 必須是絕對路徑
ancientDb, _ := rawdb.NewLevelDBDatabaseWithFreezer(dbPath, 16, 1, ancientPath, "")
for i := 1; i <= 10; i++ {
// ReadCanonicalHash retrieves the hash assigned to a canonical block number.
blkHash := rawdb.ReadCanonicalHash(ancientDb, uint64(i))
if blkHash == (common.Hash{}) {
fmt.Printf("i: %v\n", i)
} else {
fmt.Printf("blkHash: %x\n", blkHash)
}
// ReadBody retrieves the block body corresponding to the hash.
blkHeader := rawdb.ReadHeader(ancientDb, blkHash, uint64(i))
fmt.Printf("blkHeader Coinbase: 0x%x\n", blkHeader.Coinbase)
}
更詳細(xì)代碼請看newEthLeveldb.go
通過LevelDB讀取的數(shù)據(jù)
本人1 年前用了 1 個月時間追到了 500w 高度。現(xiàn)在看起來真香丘侠,所以做人要淡定啊徒欣, 只要你熬得夠久,你就是藝術(shù)家了啊蜗字。
參考:
http://qyuan.top/2019/10/12/mpt-2/
https://golangnote.com/topic/81.html
https://johng.cn/leveldb-intro/
https://catkang.github.io/2017/01/07/leveldb-summary.html
https://juejin.im/post/5c22e049e51d45206d12568e
https://draveness.me/bigtable-leveldb.html
https://blog.ethereum.org/2019/07/10/geth-v1-9-0/