最近研究以太坊的LevelDB使用,看了看代码,大致介绍下使用流程(网上介绍的leveldb大多是c++版本的,以太坊使用的是go语言版本的),我使用的是mac book开发环境。介绍中会忽略一些细节,如有重要遗漏或者错误欢迎指出。
读此篇文章默认leveldb的基本知识都了解,可以参见我的另外一篇文章介绍
https://blog.csdn.net/csds319/article/details/80333187
在ethdb/database.go的NewLDBDataBase()函数中,
db, err := leveldb.OpenFile(file, &opt.Options{
OpenFilesCacheCapacity: handles,
BlockCacheCapacity: cache / 2 * opt.MiB,
WriteBuffer: cache / 4 * opt.MiB, // Two of these are used internally
Filter: filter.NewBloomFilter(10),
})
file就是leveldb的路径,以太坊的默认路径是/Users/$Owner/Library/Ethereum/geth/chaindata
OpenFilesCacheCapacity:以太坊设置的是1024,作用应该是可打开的文件数吧,后续代码中再确认一下
BlockCacheCapacity:设置的是cache的一半,是384M
WriteBuffer:设置的是cache的1/4,是192M,这个是memtable的size。为什么是1/4呢,因为cache是设置的leveldb总共使用的大小,一半给了BlockCacheCapacity,另外一半是给memtable的。而leveldb写数据的流程是先写memtable,等写满了把这个memtable forzen,然后启用minor compaction到level 0文件,同时new一个memtable供新写入。所以cache的一半是给memtable和frozon memtable用的,单个memory的大小就是1/4
Filter:bloom filter,每个level文件会建filter,10的意思是每个key hash的次数。bloom的位数需要代码确认下
OpenFile就会直接调用到leveldb的db.go文件中
经过一些列初始化,恢复log文件等,建立了若干个goroutine,看代码
func openDB(s *session) (*DB, error) {
....
// Doesn't need to be included in the wait group.
go db.compactionError()
go db.mpoolDrain()
if readOnly {
db.SetReadOnly()
} else {
db.closeW.Add(2)
go db.tCompaction()
go db.mCompaction()
// go db.jWriter()
}
}
compactionError:看代码是监听一些channel做处理,暂未深究,后续补充
mpoolDrain:启动一个30s的ticker读取mempool chan,具体作用暂未深究,后续补充
mCompaction: minor compaction,就是把memory的内容写入到level 0的文件
tCompaction:major compaction,就是合并不同层级的level文件。比如level 0满了(已经有大于等于4个文件了),此goroutine监听到了,就会将level 0的某个文件和level 1的某些文件合并成新的level 1文件
到这里leveldb的初始化就成功了,新建几个goroutine监听是否compaction,基本流程大值如此了
leveldb提供了一些接口来写数据,以太坊做了包装,具体看ethdb/interface.go
// Putter wraps the database write operation supported by both batches and regular databases.
type Putter interface {
Put(key []byte, value []byte) error
}
// Database wraps all database operations. All methods are safe for concurrent use.
type Database interface {
Putter
Get(key []byte) ([]byte, error)
Has(key []byte) (bool, error)
Delete(key []byte) error
Close()
NewBatch() Batch
}
// Batch is a write-only database that commits changes to its host database
// when Write is called. Batch cannot be used concurrently.
type Batch interface {
Putter
ValueSize() int // amount of data in the batch
Write() error
// Reset resets the batch for reuse
Reset()
}
定义了三个interface,Putter,Database和Batch与LevelDB读写交互
写数据又分为写新数据、更新数据和删除数据
leveldb为了效率考虑(如果删除数据和更新数据用传统的方式做的话,需要查找所有数据库找到原始key,效率比较低),此三种情况统统使用插入数据的方式,删除数据是写一个删除标志,更新数据是写一样key带不同的value
那么问题来了,如果更新或删除数据,整个数据库中有两个或更多个相同的key,什么时候合并,查找的时候怎么确定哪个是正确的
答案:
(1)什么时候合并
如果有两个或多个相同的key(或者是删除,key的v是删除标志),一直到major compaction的时候才会执行合并动作或者删除动作,这样可以提升效率
(2)如何查找到正确的值
因为leveldb的分层概念,读数据的时候先查memory,然后再从level 0到level N逐层查询,查询到了就不再查询,这里有个新鲜度的概念,层级越低,新鲜度越高,memory中新鲜度最高。所以对于更新操作来说,即便是某个时刻数据库中有两个或者更过个相同key的kv,会以新鲜度高的为准。如果查询到了key为删除标志,那么直接返回not found即可
为了减少leveldb的交互,写数据的时候一般会以Batch进行,就是先往batch里写一堆数据,然后再统一把这个Batch写到leveldb。
即便是单个kv的写入,leveldb内部也是使用batch来写入的,但是这个batch也会即时写入memory和log
以太坊的core/blockchain.go中写block的时候就是新建Batch,然后把Batch写入leveldb
// WriteBlockWithState writes the block and all associated state to the database.
func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts ...) {
...
// Write other block data using a batch.
batch := bc.db.NewBatch()
if err := WriteBlock(batch, block); err != nil {
return NonStatTy, err
}
....
if err := batch.Write(); err != nil {
return NonStatTy, err
}
....
}
我们来看看batch.Write的实现,在leveldb的db_write.go代码里:
func (db *DB) Write(batch *Batch, wo *opt.WriteOptions) error {
…
// 这段代码的意思是当batch的内容长度大于memory table的长度(以太坊是192M),
// 一次性写入memory(当写满的时候会触发minor compaction,然后接着写memory直到把内容全部写完)
if batch.internalLen > db.s.o.GetWriteBuffer() && !db.s.o.GetDisableLargeBatchTransaction() {
tr, err := db.OpenTransaction()
if err != nil {
return err
}
if err := tr.Write(batch, wo); err != nil {
tr.Discard()
return err
}
return tr.Commit()
}
…
return db.writeLocked(batch, nil, merge, sync)
}
接着看writeLocked代码:
func (db *DB) writeLocked(batch, ourBatch *Batch, merge, sync bool) error {
// flush的功能是看是否触发minor compaction
mdb, mdbFree, err := db.flush(batch.internalLen)
…
// Write journal. 写Log文件
if err := db.writeJournal(batches, seq, sync); err != nil {
db.unlockWrite(overflow, merged, err)
return err
}
// Put batches. 写batch数据到memory
for _, batch := range batches {
if err := batch.putMem(seq, mdb.DB); err != nil {
panic(err)
}
seq += uint64(batch.Len())
}
….
// Rotate memdb if it's reach the threshold.
// 如果memory不够写batch的内容,调用rotateMem,就是把memory frezon触发minor compaction
if batch.internalLen >= mdbFree {
db.rotateMem(0, false)
}
db.unlockWrite(overflow, merged, nil)
return nil
}
有点没看懂为什么先batch.putMem然后判断batch.internalLen与mdbFree比大小再rotateMem,理应是先判断mdbFree...
还有个merge与一堆channel的交互没看明白,后续接着看
再看rotateMem的实现
func (db *DB) rotateMem(n int, wait bool) (mem *memDB, err error) {
retryLimit := 3
retry:
// Wait for pending memdb compaction.
err = db.compTriggerWait(db.mcompCmdC)
if err != nil {
return
}
retryLimit--
// Create new memdb and journal.
// 新建log文件和memory,同时把现在使用的memory指向为frozenMem,minor compaction的时候写入frozenMem到level 0文件
mem, err = db.newMem(n)
if err != nil {
if err == errHasFrozenMem {
if retryLimit <= 0 {
panic("BUG: still has frozen memdb")
}
goto retry
}
return
}
// Schedule memdb compaction.
// 触发minor compaction
if wait {
err = db.compTriggerWait(db.mcompCmdC)
} else {
db.compTrigger(db.mcompCmdC)
}
return
}
至此数据写完,如果memory空间够,直接写入memory
如果memory空间不够,等待执行minor compaction(compTrigger内会等待compaction的结果)再写入新建的memory db(是从mempool中拿的,应该是mempool中就两块儿memory,待写入的memory和frozon memory)中
先看插入新数据的接口,更新数据也是调用这个一样的接口:
func (db *DB) Put(key, value []byte, wo *opt.WriteOptions) error {
return db.putRec(keyTypeVal, key, value, wo)
}
插入数据是插入一个type为keyTypeVal,key/value的数据
再看删除数据的接口
func (db *DB) Delete(key []byte, wo *opt.WriteOptions) error {
return db.putRec(keyTypeDel, key, nil, wo)
}
删除数据的代码其实就是插入一个type为keyTypeDel,key/nil的数据,当做一个普通的数据插入到memory中
等后续做major compaction的时候找到原始的key再执行删除动作(更新数据也是在major compaction的时候进行)
具体major compaction的代码还未看明白,后续看明白了再贴上来
读数据是依次从memtable和各个level文件中查找数据,db.go的接口:
func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
err = db.ok()
if err != nil {
return
}
// 关于snapshot未做研究,后续有研究再贴一下
se := db.acquireSnapshot()
defer db.releaseSnapshot(se)
return db.get(nil, nil, key, se.seq, ro)
}
func (db *DB) get(auxm *memdb.DB, auxt tFiles, key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, err error) {
ikey := makeInternalKey(nil, key, seq, keyTypeSeek)
if auxm != nil {
if ok, mv, me := memGet(auxm, ikey, db.s.icmp); ok {
return append([]byte{}, mv...), me
}
}
// 拿到memdb和frozon memdb依次查找
em, fm := db.getMems()
for _, m := range [...]*memDB{em, fm} {
if m == nil {
continue
}
defer m.decref()
if ok, mv, me := memGet(m.DB, ikey, db.s.icmp); ok {
return append([]byte{}, mv...), me
}
}
// 拿到version后从version中各个level的文件中依次查找
v := db.s.version()
value, cSched, err := v.get(auxt, ikey, ro, false)
v.release()
if cSched {
// Trigger table compaction.
db.compTrigger(db.tcompCmdC)
}
return
}
compaction是把数据一级一级的往下写,leveldb实现了minor compaction和major compaction
minor compaction,leveldb里面的mCompaction goroutine做的事情,就是把memory中的数据写入到level 0文件中
major compaction,leveldb里面tCompaction goroutine做的事情,就是把低层的level文件合并写入高层的level文件中
func (db *DB) mCompaction() {
var x cCmd
for {
select {
case x = <-db.mcompCmdC:
switch x.(type) {
case cAuto:
db.memCompaction()
x.ack(nil)
x = nil
default:
panic("leveldb: unknown command")
}
case <-db.closeC:
return
}
}
}
还记得写数据的时候rotateMem中会写channel mcompCmdC吗,这个goroutine起来后一直在监听该channel等待做compaction的事情,所以看memCompaction的实现
func (db *DB) memCompaction() {
// rotateMem的时候把当前使用的memory指向到frozonMem,这里读出来写入level 0文件
mdb := db.getFrozenMem()
// Pause table compaction.
// 这里的作用是minor compaction的时候要先暂停major compaction
resumeC := make(chan struct{})
select {
case db.tcompPauseC <- (chan<- struct{})(resumeC):
case <-db.compPerErrC:
close(resumeC)
resumeC = nil
case <-db.closeC:
db.compactionExitTransact()
}
// Generate tables. 创建level 0文件然后写memory到文件
// flushMemdb是把memory内容写到新建的level 0文件,然后把level 0文件加入到addedTables record中
// 代码里把level 0~N的文件叫做table
db.compactionTransactFunc("memdb@flush", func(cnt *compactionTransactCounter) (err error) {
stats.startTimer()
flushLevel, err = db.s.flushMemdb(rec, mdb.DB, db.memdbMaxLevel)
stats.stopTimer()
return
}, func() error {
for _, r := range rec.addedTables {
db.logf("memdb@flush revert @%d", r.num)
if err := db.s.stor.Remove(storage.FileDesc{Type: storage.TypeTable, Num: r.num}); err != nil {
return err
}
}
return nil
})
rec.setJournalNum(db.journalFd.Num)
rec.setSeqNum(db.frozenSeq)
// Commit.
// 就是最终存储tables,写入到version记录。。。后续深入看下
stats.startTimer()
db.compactionCommit("memdb", rec)
stats.stopTimer()
db.logf("memdb@flush committed F·%d T·%v", len(rec.addedTables), stats.duration)
for _, r := range rec.addedTables {
stats.write += r.size
}
db.compStats.addStat(flushLevel, stats)
// Drop frozen memdb.
// minor compaction之后把指向frozon的memory重新放回mempool中
db.dropFrozenMem()
// Resume table compaction.
// 恢复major compaction
if resumeC != nil {
select {
case <-resumeC:
close(resumeC)
case <-db.closeC:
db.compactionExitTransact()
}
}
// Trigger table compaction.
// tcompCmdC就是major compaction要监听的channel,这里写数据到此channel
db.compTrigger(db.tcompCmdC)
}
后续需要继续完善compactionCommit代码,实现都在这里
func (db *DB) tCompaction() {
for {
if db.tableNeedCompaction() {
select {
case x = <-db.tcompCmdC:
case ch := <-db.tcompPauseC:
db.pauseCompaction(ch)
continue
case <-db.closeC:
return
default:
}
} else {
for i := range ackQ {
ackQ[i].ack(nil)
ackQ[i] = nil
}
ackQ = ackQ[:0]
select {
case x = <-db.tcompCmdC:
case ch := <-db.tcompPauseC:
db.pauseCompaction(ch)
continue
case <-db.closeC:
return
}
}
if x != nil {
switch cmd := x.(type) {
case cAuto:
ackQ = append(ackQ, x)
case cRange:
x.ack(db.tableRangeCompaction(cmd.level, cmd.min, cmd.max))
default:
panic("leveldb: unknown command")
}
x = nil
}
db.tableAutoCompaction()
}
}
计算是否要执行major compaction
func (v *version) computeCompaction() {
for level, tables := range v.levels {
var score float64
size := tables.size()
if level == 0 {
// We treat level-0 specially by bounding the number of files
// instead of number of bytes for two reasons:
//
// (1) With larger write-buffer sizes, it is nice not to do too
// many level-0 compaction.
//
// (2) The files in level-0 are merged on every read and
// therefore we wish to avoid too many files when the individual
// file size is small (perhaps because of a small write-buffer
// setting, or very high compression ratios, or lots of
// overwrites/deletions).
score = float64(len(tables)) / float64(v.s.o.GetCompactionL0Trigger())
} else {
score = float64(size) / float64(v.s.o.GetCompactionTotalSize(level))
}
if score > bestScore {
bestLevel = level
bestScore = score
}
statFiles[level] = len(tables)
statSizes[level] = shortenb(int(size))
statScore[level] = fmt.Sprintf("%.2f", score)
statTotSize += size
}
v.cLevel = bestLevel
v.cScore = bestScore
}
计算是否要compaction是逻辑是:计算一个分数,level 0是文件个数/4,level 0以上就是文件的总大小/预设的每个level的文件大小总量;最后找出算出的值最大的一个赋值到v.cScore,level赋值到v.cLevel
最终使用的时候是判断这个cScore是否>=1来决定是否要进行compaction
func (v *version) needCompaction() bool {
return v.cScore >= 1 || atomic.LoadPointer(&v.cSeek) != nil
}
还有一个判断是v.cSeek是否为空,这个是读数据那边用到的,等看到读那边的逻辑再讲一下
基本上代码撸了一遍,但是只是粗略的过了一遍,很多细节尚未涉及,有机会再详细撸一遍。