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文件中的有效数据拷贝到临时文件中,最后把临时文件重命名。

发布了48 篇原创文章 · 获赞 9 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/cyq6239075/article/details/105513609