etcd源碼閱讀筆記(二)backend

1. 簡介

etcd的backend模塊對於底層存儲引擎進行了抽象,默認使用上一篇文章中介紹的BoltDB。

etcd將鍵值對的每一個版本都存儲在BoltDB中,並在內存中構建BTree keyIndex索引。

image.png

  • 在BoltDB中存儲數據的key是revision,revision包含兩個id:main revision和sub revision;main revision每個事務加1,sub revision事務中的每次操作加1;
  • keyIndex索引的key是鍵值對的key,value是revision。

通過這種存儲模型,etcd很自然的支持了MVCC、歷史數據查詢和watcher。

2. backend

2.1 Backend和backend

Backend接口和backend結構體定義如下:

type Backend interface {
	// 創建一個只讀事務
	ReadTx() ReadTx
	// 創建一個批量事務
	BatchTx() BatchTx
	// 創建一個無阻塞併發讀事務
	ConcurrentReadTx() ReadTx

	// 創建一個快照
	Snapshot() Snapshot
	Hash(ignores map[IgnoreKey]struct{}) (uint32, error)
	// 返回存儲引擎已分配的物理空間大小
	Size() int64
	// 返回存儲引擎已使用的空間大小
	SizeInUse() int64
	// 返回打開的只讀事務數量
	OpenReadTxN() int64
	// 碎片整理
	Defrag() error
	// 提交批量讀寫事務
	ForceCommit()
	Close() error
}

type backend struct {
	// 已分配的字節數
	size int64
	// 實際使用的字節數
	sizeInUse int64
	// 提交次數
	commits int64
	// 已打開的讀事務數量
	openReadTxN int64

        // 鎖
	mu sync.RWMutex
        // BoltDB指針
	db *bolt.DB
...
}

2.2 只讀事務

只讀事務定義瞭如下接口:

type ReadTx interface {
	Lock()
	Unlock()
	RLock()
	RUnlock()

        // 在指定bucket中範圍搜索
	UnsafeRange(bucketName []byte, key, endKey []byte, limit int64) (keys [][]byte, vals [][]byte)
        // 在遍歷指定bucket,並執行回調方法
	UnsafeForEach(bucketName []byte, visitor func(k, v []byte) error) error
}

type readTx struct {
	// 讀寫鎖,控制對於 txReadBuffer 讀緩存的訪問
	mu  sync.RWMutex
	// 讀緩存
	buf txReadBuffer

	// 事務讀寫鎖,範圍搜索時,控制對於bucket的訪問
	txMu    sync.RWMutex
	// 底層引擎的事務
	tx      *bolt.Tx
	// 底層引擎的bucket的指針的map,key爲bucketName
	buckets map[string]*bolt.Bucket
...
}

範圍搜索的具體實現如下:

func (rt *readTx) UnsafeRange(bucketName, key, endKey []byte, limit int64) ([][]byte, [][]byte) {
        // endKey爲nil,表示查一個鍵值對
	if endKey == nil {
		limit = 1
	}
	if limit <= 0 {
		limit = math.MaxInt64
	}
	if limit > 1 && !bytes.Equal(bucketName, safeRangeBucket) {
		panic("do not use unsafeRange on non-keys bucket")
	}
        // 在txReadBuffer中範圍搜索
	keys, vals := rt.buf.Range(bucketName, key, endKey, limit)
	if int64(len(keys)) == limit {
		return keys, vals
	}

	// 如果沒有在txReadBuffer找到
	// 則搜索並緩存bucket
	bn := string(bucketName)
	// txMu讀鎖保護下,讀取bucket指針
	rt.txMu.RLock()
	bucket, ok := rt.buckets[bn]
	rt.txMu.RUnlock()
	if !ok {
	        // 如果沒有取到,則可能其他進程寫鎖正在修改
	        // txMu讀鎖保護下,讀取bucket指針
		rt.txMu.Lock()
		bucket = rt.tx.Bucket(bucketName)
		rt.buckets[bn] = bucket
		rt.txMu.Unlock()
	}

	if bucket == nil {
		return keys, vals
	}

	// txMu讀鎖保護下,獲取bucket遊標
	rt.txMu.Lock()
	c := bucket.Cursor()
	rt.txMu.Unlock()

	// 使用遊標範圍搜索bucket
	k2, v2 := unsafeRange(c, key, endKey, limit-int64(len(keys)))
	return append(k2, keys...), append(v2, vals...)
}

2.3 併發讀事務

// concurrentReadTx定義
type concurrentReadTx struct {
	buf     txReadBuffer
	txMu    *sync.RWMutex
	tx      *bolt.Tx
	buckets map[string]*bolt.Bucket
	txWg    *sync.WaitGroup
}
func (b *backend) ConcurrentReadTx() ReadTx {
        // 申請讀鎖
	b.readTx.RLock()
        // defer 釋放讀鎖
	defer b.readTx.RUnlock()

        // WaitGroup計數器置爲1
	b.readTx.txWg.Add(1)

        // 創建concurrentReadTx
	return &concurrentReadTx{
		buf:     b.readTx.buf.unsafeCopy(),
		tx:      b.readTx.tx,
		txMu:    &b.readTx.txMu,
		buckets: b.readTx.buckets,
		txWg:    b.readTx.txWg,
	}
}

ConcurrentReadTx的創建過程其實就是從backend的readTx克隆一份新的buf緩衝區,tx、txMu、buckets、txWg均指向readTx。

read_tx.go

func (rt *concurrentReadTx) Lock()   {}
func (rt *concurrentReadTx) Unlock() {}
func (rt *concurrentReadTx) RLock() {}
func (rt *concurrentReadTx) RUnlock() { rt.txWg.Done() }

ConcurrentReadTx的申請鎖、釋放鎖實現如上,Lock、Unlock、RLock時,不做任何操作,僅在RUnlock時將WaitGroup計數器減一。

2.4 批量事務

批量事務提供批量讀寫數據庫的能力。
batch_tx.go

type BatchTx interface {
	ReadTx
	UnsafeCreateBucket(name []byte)
	UnsafePut(bucketName []byte, key []byte, value []byte)
	UnsafeSeqPut(bucketName []byte, key []byte, value []byte)
	UnsafeDelete(bucketName []byte, key []byte)
	// 提交事務,並開始一個新事務
	Commit()
	// 提交事務,但不開始一個新事務
	CommitAndStop()
}

type batchTx struct {
        // 互斥鎖
	sync.Mutex
       // BoltDB事務實例
	tx      *bolt.Tx
        // 關聯的backend實例
	backend *backend
        // 待提交的指令數量
	pending int
}

同樣的,BatchTx定義了一系列不安全的讀寫方法,需要調用方自行控制併發。

util.go

func WriteKV(be backend.Backend, kv mvccpb.KeyValue) {
        // 創建BoltDB中的key
	ibytes := newRevBytes()
	revToBytes(revision{main: kv.ModRevision}, ibytes)

        // 鍵值對組織成byte數組
	d, err := kv.Marshal()
	if err != nil {
		panic(fmt.Errorf("cannot marshal event: %v", err))
	}
       
        // 加鎖
	be.BatchTx().Lock()
        // 寫BoltDB,key爲revision,value爲鍵值對
	be.BatchTx().UnsafePut(keyBucketName, ibytes, d)
        // 釋放鎖
	be.BatchTx().Unlock()
}

3. KeyIndex內存索引

index.go

type treeIndex struct {
	sync.RWMutex
	tree *btree.BTree
	lg   *zap.Logger
}

treeIndex主要由一個讀寫鎖、一個BTree和一個日誌工具組成,BTree使用的是google開源項目。

key_index.go

type keyIndex struct {
        // 調用方傳入的鍵值對的key
	key         []byte
        // 最後一次修改的main revison
	modified    revision
       // 該key對應的每代的版本信息
	generations []generation
}

keyIndex是BTree上的一個鍵值對的值的數據結構。其中generations []generation中存儲着每次編輯的對應的main revision 和 sub revision。

image.png

type generation struct {
	ver     int64
	created revision
        // 記錄了多次修改的revision
	revs    []revision
}

每個generation實例記錄了多次修改的revision,知道調用tombstone,終止一個generation

func (ki *keyIndex) tombstone(lg *zap.Logger, main int64, sub int64) error {
	if ki.isEmpty() {
		lg.Panic(
			"'tombstone' got an unexpected empty keyIndex",
			zap.String("key", string(ki.key)),
		)
	}
       // 判斷最後一個generation是否合法
	if ki.generations[len(ki.generations)-1].isEmpty() {
		return ErrRevisionNotFound
	}
	ki.put(lg, main, sub)
       // generations最後追加一個tombstone,其實就是個空的generation實例
	ki.generations = append(ki.generations, generation{})
	keysGauge.Dec()
	return nil
}

4. 總結

綜上所述,etcd的backend模塊,基於BoltDB和內存BTree索引,在讀多寫少的場景下表現優異。

5. 引用

etcd源代碼
BTree

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章