etcd系列-----後端數據存儲mvcc模塊

etcd於2016年6月發佈了v3. 0.0版本,從此進入了etcdv3時代。在etcdv2和etcdv3 中,使用的raft模塊變化不大,但使用的後端存儲有很多不同之處。v2版本的存儲是一版完全基於內存的存儲,它並沒有將數據實時地寫入到磁盤。 在其需要進行持久化時,會將整個存儲的數據序列化成JSON格式的數據並寫入磁盤文件。在etcdv2 版本的存儲中, 數據是以樹形結構保存在內存中的。v3使用mvcc作爲後端存儲。mvcc對外提供的接口:

type KV interface {
	ReadView   
	WriteView

	// Read creates a read transaction.
	Read() TxnRead

	// Write creates a write transaction.
	Write() TxnWrite

	// Hash computes the hash of the KV's backend.
	Hash() (hash uint32, revision int64, err error)

	// HashByRev computes the hash of all MVCC revisions up to a given revision.
	HashByRev(rev int64) (hash uint32, revision int64, compactRev int64, err error)

	// Compact frees all superseded keys with revisions less than rev.
	Compact(rev int64) (<-chan struct{}, error)

	// Commit commits outstanding txns into the underlying backend.
	Commit()

	// Restore restores the KV store from a backend.
	Restore(b backend.Backend) error
	Close() error
}
type WriteView interface {

	DeleteRange(key, end []byte) (n, rev int64)

	Put(key, value []byte, lease lease.LeaseID) (rev int64)
}
type ReadView interface {

	FirstRev() int64

	// Rev returns the revision of the KV at the time of opening the txn.
	Rev() int64

	Range(key, end []byte, ro RangeOptions) (r *RangeResult, err error)
}

對同一個 key 每次修改都對應 一個revision , 一個key 在生命週期內可能被頻繁刪除,從創建到刪除的所有 revision 集合組成一個 generation (代), 每個 key 是由多個 generation 組成多版本。組織這個多版本 key 的結構體叫做 keyIndex。

type keyIndex struct {
	key         []byte
	modified    revision //最新版本revision
	generations []generation  //代
}
type generation struct {
	ver     int64
	created revision // 創建這個代的第一個版本
	revs    []revision//版本數組
}
type revision struct {
	//事務id,全局遞增
	main int64

	// 事務內遞增
	sub int64
}

需要說明的一點,一個key從創建到刪除形成一個generation,如果key被很多次創建和刪除就會形成很多的generation。

// For example: put(1.0);put(2.0);tombstone(3.0);put(4.0);tombstone(5.0) on key "foo"
// generate a keyIndex:
// key:     "foo"
// rev: 5
// generations:
//    {empty}
//    {4.0, 5.0(t)}
//    {1.0, 2.0, 3.0(t)}
//
// Compact a keyIndex removes the versions with smaller or equal to
// rev except the largest one. If the generation becomes empty
// during compaction, it will be removed. if all the generations get
// removed, the keyIndex should be removed.
//
// For example:
// compact(2) on the previous example
// generations:
//    {empty}
//    {4.0, 5.0(t)}
//    {2.0, 3.0(t)}
//
// compact(4)
// generations:
//    {empty}
//    {4.0, 5.0(t)}
//
// compact(5):
// generations:
//    {empty} -> key SHOULD be removed.
//
// compact(6):
// generations:
//    {empty} -> key SHOULD be removed.

//tombstone就是指delete刪除key,一旦發生刪除就會結束當前的generation,生成新的generation,小括號裏的(t)標識tombstone。
//compact(n)表示壓縮掉revision.main <= n的所有歷史版本,會發生一系列的刪減操作

1、數據存儲關係
    數據存儲分兩部分:

    (1)內存記錄由 key 和 keyIndex 組成的版本信息,存儲在內存的 btree 中,用於快速查找
    (2)真實的 kv 數據存放在 boltdb 中,key 是 revision, value 是序列化後的 pb

                                                   

 2、Btree

btree的數據結構和基本操作都很簡單

type store struct {
	ReadView   //讀視圖
	WriteView   //寫視圖

	// consistentIndex caches the "consistent_index" key's value. Accessed
	// through atomics so must be 64-bit aligned.
	consistentIndex uint64

	// mu read locks for txns and write locks for non-txn store changes.
	mu sync.RWMutex

	ig ConsistentIndexGetter

	b       backend.Backend     //後端存儲(bbolt)
	kvindex index   //btree
}

type treeIndex struct {
	sync.RWMutex
	tree *btree.BTree
}
func (ti *treeIndex) Put(key []byte, rev revision) {
	keyi := &keyIndex{key: key}

	ti.Lock()
	defer ti.Unlock()
	item := ti.tree.Get(keyi)
	if item == nil {
		keyi.put(rev.main, rev.sub)
		ti.tree.ReplaceOrInsert(keyi)
		return
	}
	okeyi := item.(*keyIndex)
	okeyi.put(rev.main, rev.sub)
}

func (ti *treeIndex) Get(key []byte, atRev int64) (modified, created revision, ver int64, err error) {
	keyi := &keyIndex{key: key}
	ti.RLock()
	defer ti.RUnlock()
	if keyi = ti.keyIndex(keyi); keyi == nil {
		return revision{}, revision{}, 0, ErrRevisionNotFound
	}
	return keyi.get(atRev)
}

3、boltdb

bolt是一個DB,DB裏有多個bucket。在物理上,bolt使用單個文件存儲。
bolt在某一刻只允許一個 read-write 事務,但是可以同時允許多個 read-only 事務。其實就是讀寫鎖,寫只能順序來,讀可以併發讀。
DB.Update() 是用來開啓 read-write 事務的, DB.View() 則是用來開啓 read-only 事務的。由於每次執行 DB.Update() 都會寫入一次磁盤,可以使用 DB.Batch() 來進行批量操作。

bbolt中存儲的value是這樣一個json序列化後的結構,包括key創建時的revision(對應某一代generation的created),本次更新版本,sub ID(Version ver),Lease ID(租約ID)

kv := mvccpb.KeyValue{
		Key:            key,
		Value:          value,
		CreateRevision: c,
		ModRevision:    rev,
		Version:        ver,
		Lease:          int64(leaseID),
	}

4、put

put流程分成兩個部分,內存btree中插入一條新的revision,bolt中寫入一條新的k-v條目。put操作是事務操作,一次只能有一個寫事務,讀事務可以併發。一次寫事務可以多次寫,然後批量提交,這樣可以提升性能。

func (tw *storeTxnWrite) put(key, value []byte, leaseID lease.LeaseID) {
	rev := tw.beginRev + 1
	c := rev
	oldLease := lease.NoLease

	// if the key exists before, use its previous created and
	// get its previous leaseID
	_, created, ver, err := tw.s.kvindex.Get(key, rev)
	if err == nil {
		c = created.main
		oldLease = tw.s.le.GetLease(lease.LeaseItem{Key: string(key)})
	}

	ibytes := newRevBytes()
	idxRev := revision{main: rev, sub: int64(len(tw.changes))}
	revToBytes(idxRev, ibytes)

	ver = ver + 1
	kv := mvccpb.KeyValue{
		Key:            key,
		Value:          value,
		CreateRevision: c,
		ModRevision:    rev,
		Version:        ver,
		Lease:          int64(leaseID),
	}

	d, err := kv.Marshal()
	if err != nil {
		plog.Fatalf("cannot marshal event: %v", err)
	}

	tw.tx.UnsafeSeqPut(keyBucketName, ibytes, d)//插入bolt
	tw.s.kvindex.Put(key, idxRev)//插入btree
	tw.changes = append(tw.changes, kv)

	//watch相關
	if oldLease != lease.NoLease {
		if tw.s.le == nil {
			panic("no lessor to detach lease")
		}
		err = tw.s.le.Detach(oldLease, []lease.LeaseItem{{Key: string(key)}})
		if err != nil {
			plog.Errorf("unexpected error from lease detach: %v", err)
		}
	}
	if leaseID != lease.NoLease {
		if tw.s.le == nil {
			panic("no lessor to attach lease")
		}
		err = tw.s.le.Attach(leaseID, []lease.LeaseItem{{Key: string(key)}})
		if err != nil {
			panic("unexpected error from lease Attach")
		}
	}
}
func (t *batchTxBuffered) UnsafeSeqPut(bucketName []byte, key []byte, value []byte) {
	t.batchTx.UnsafeSeqPut(bucketName, key, value)
	t.buf.putSeq(bucketName, key, value)//加入讀視圖緩存
}

 真正寫入bolt

func (t *batchTx) unsafePut(bucketName []byte, key []byte, value []byte, seq bool) {
	bucket := t.tx.Bucket(bucketName)
	if bucket == nil {
		plog.Fatalf("bucket %s does not exist", bucketName)
	}
	if seq {
		// it is useful to increase fill percent when the workloads are mostly append-only.
		// this can delay the page split and reduce space usage.
		bucket.FillPercent = 0.9
	}
	if err := bucket.Put(key, value); err != nil {
		plog.Fatalf("cannot put key into bucket (%v)", err)
	}
	t.pending++
}

寫入bolt的數據同時要寫入讀視圖中,提供用戶查詢

func (txw *txWriteBuffer) putSeq(bucket, k, v []byte) {
	b, ok := txw.buckets[string(bucket)]
	if !ok {
		b = newBucketBuffer()
		txw.buckets[string(bucket)] = b
	}
	b.add(k, v)
}
func (bb *bucketBuffer) add(k, v []byte) {
	bb.buf[bb.used].key, bb.buf[bb.used].val = k, v
	bb.used++
	if bb.used == len(bb.buf) {
		buf := make([]kv, (3*len(bb.buf))/2)
		copy(buf, bb.buf)
		bb.buf = buf
	}
}

在end()函數中將版本號遞增,同時還會判斷如果達到事務提交的閾值,會對事務進行一次提交

func (tw *storeTxnWrite) End() {
	// only update index if the txn modifies the mvcc state.
	if len(tw.changes) != 0 {
		tw.s.saveIndex(tw.tx)
		// hold revMu lock to prevent new read txns from opening until writeback.
		tw.s.revMu.Lock()
		tw.s.currentRev++
	}
	tw.tx.Unlock()
	if len(tw.changes) != 0 {
		tw.s.revMu.Unlock()
	}
	tw.s.mu.RUnlock()
}
func (t *batchTx) Unlock() {
	if t.pending >= t.backend.batchLimit {
		t.commit(false)
	}
	t.Mutex.Unlock()
}

 5、delete

刪除只是打標記,並不是真正刪除,真正刪除在後面將到的compact和Defrag

func (tw *storeTxnWrite) delete(key []byte, rev revision) {
	ibytes := newRevBytes()
	idxRev := revision{main: tw.beginRev + 1, sub: int64(len(tw.changes))}
	revToBytes(idxRev, ibytes)
	ibytes = appendMarkTombstone(ibytes)//加“t”標記

	kv := mvccpb.KeyValue{Key: key}

	d, err := kv.Marshal()
	if err != nil {
		plog.Fatalf("cannot marshal event: %v", err)
	}

	tw.tx.UnsafeSeqPut(keyBucketName, ibytes, d)//從blot中刪除就是寫一條刪除數據,該數據表明這之前的數據都無效,真正的刪除是在compact、Defrag階段處理
	err = tw.s.kvindex.Tombstone(key, idxRev)//btree中刪除也就是結束當前的generation,append一條empty就是結束當前generation。
	if err != nil {
		plog.Fatalf("cannot tombstone an existing key (%s): %v", string(key), err)
	}
	tw.changes = append(tw.changes, kv)

	item := lease.LeaseItem{Key: string(key)}
	leaseID := tw.s.le.GetLease(item)

	if leaseID != lease.NoLease {
		err = tw.s.le.Detach(leaseID, []lease.LeaseItem{item})
		if err != nil {
			plog.Errorf("cannot detach %v", err)
		}
	}
}

內存中刪除key就是結束該key的generation。

func (ki *keyIndex) tombstone(main int64, sub int64) error {
	if ki.isEmpty() {
		plog.Panicf("store.keyindex: unexpected tombstone on empty keyIndex %s", string(ki.key))
	}
	if ki.generations[len(ki.generations)-1].isEmpty() {
		return ErrRevisionNotFound
	}
	ki.put(main, sub)
	ki.generations = append(ki.generations, generation{})//append一條empty的generation
	keysGauge.Dec()
	return nil
}

6、compact、Defrag

如果重複更新某些k的值,bolt文件就會越來越來,當然我們不會讓他無限制的增大,這個時候就需要compact、Defrag定期清理數據,將無用的或者過期的數據刪除,將空間釋放。先來看看compact操作,compact包括刪除內存數據和刪除bolt數據兩部分,內存btree的刪除很好理解,bolt中數據的真正刪除還是需要等到Defrag纔會真正的吧空間歸還出來。

func (s *store) Compact(rev int64) (<-chan struct{}, error) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.revMu.Lock()
	defer s.revMu.Unlock()

	if rev <= s.compactMainRev {
		ch := make(chan struct{})
		f := func(ctx context.Context) { s.compactBarrier(ctx, ch) }
		s.fifoSched.Schedule(f)
		return ch, ErrCompacted
	}
	if rev > s.currentRev {
		return nil, ErrFutureRev
	}

	start := time.Now()

	s.compactMainRev = rev

	rbytes := newRevBytes()
	revToBytes(revision{main: rev}, rbytes)

	tx := s.b.BatchTx()//獲取事務
	tx.Lock()
	tx.UnsafePut(metaBucketName, scheduledCompactKeyName, rbytes)//記錄一次壓縮
	tx.Unlock()
	// ensure that desired compaction is persisted
	s.b.ForceCommit()//強制提交

	keep := s.kvindex.Compact(rev)//這裏執行內存btree的數據壓縮,返回結果是需要保留的k
	ch := make(chan struct{})
	var j = func(ctx context.Context) {
		if ctx.Err() != nil {
			s.compactBarrier(ctx, ch)
			return
		}
		if !s.scheduleCompaction(rev, keep) {//遍歷所有的k,除了在keep中的不刪,其他k全部從bolt中刪除。上面也說了這裏的刪除並不能吧空間歸還出來。
			s.compactBarrier(nil, ch)
			return
		}
		close(ch)
	}

	s.fifoSched.Schedule(j)

	indexCompactionPauseDurations.Observe(float64(time.Since(start) / time.Millisecond))
	return ch, nil
}

內存btree是在Compact()函數中做壓縮。

func (ti *treeIndex) Compact(rev int64) map[revision]struct{} {
	available := make(map[revision]struct{})
	var emptyki []*keyIndex
	plog.Printf("store.index: compact %d", rev)
	// TODO: do not hold the lock for long time?
	// This is probably OK. Compacting 10M keys takes O(10ms).
	ti.Lock()
	defer ti.Unlock()
	ti.tree.Ascend(compactIndex(rev, available, &emptyki))//遍歷過濾出那些k需要保留,哪些需要刪除
	for _, ki := range emptyki {//如果是需要刪除的就從btree中刪除
		item := ti.tree.Delete(ki)
		if item == nil {
			plog.Panic("store.index: unexpected delete failure during compaction")
		}
	}
	return available
}

具體到每一個keyIndex中進行壓縮

func (ki *keyIndex) compact(atRev int64, available map[revision]struct{}) {
	if ki.isEmpty() {
		plog.Panicf("store.keyindex: unexpected compact on empty keyIndex %s", string(ki.key))
	}

	genIdx, revIndex := ki.doCompact(atRev, available)//具體的compact,就是將atRev記錄之前的revision都刪除,返回刪除後有效的代和代內revIndex

	g := &ki.generations[genIdx]
	if !g.isEmpty() {
		// remove the previous contents.
		if revIndex != -1 {
			g.revs = g.revs[revIndex:]
		}
		// remove any tombstone
		if len(g.revs) == 1 && genIdx != len(ki.generations)-1 {//如果最後一個revision被打上刪除的標記,也視作無效,需要刪除
			delete(available, g.revs[0])
			genIdx++
		}
	}

	// remove the previous generations.
	ki.generations = ki.generations[genIdx:]
}

Defrag很簡單首先創建一個臨時db,然後把當前db文件中的有效數據拷貝到臨時文件中,最後把臨時文件重命名。

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