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文件中的有效數據拷貝到臨時文件中,最後把臨時文件重命名。