最近研究以太坊的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
compaction是把数据一级一级的往下写,leveldb实现了minor compaction和major compaction
minor compaction,leveldb里面的mCompaction goroutine做的事情,就是把memory中的数据写入到level 0文件中
major compaction,leveldb里面tCompaction goroutine做的事情,就是把低层的level文件合并写入高层的level文件中
mCompaction
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代码,实现都在这里
tCompaction
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是否为空,这个是读数据那边用到的,等看到读那边的逻辑再讲一下
基本上代码撸了一遍,但是只是粗略的过了一遍,很多细节尚未涉及,有机会再详细撸一遍。