以太坊之LevelDB源码分析

最近研究以太坊的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是否为空,这个是读数据那边用到的,等看到读那边的逻辑再讲一下


基本上代码撸了一遍,但是只是粗略的过了一遍,很多细节尚未涉及,有机会再详细撸一遍。

猜你喜欢

转载自blog.csdn.net/csds319/article/details/80361450