etcd原始碼閱讀(五):mvcc
ofollow,noindex" target="_blank">MVCC 是資料庫中常見的一種併發控制的方式,即儲存資料的多個版本,在同一個事務裡, 應用所見的版本是一致的。
但是,我還是很想吐槽etcd的mvcc實現,有點亂,在我看來,是過度抽象了。為了理解mvcc,我們需要預先了解這些東西(下文,mvcc如無特別說明,都是指代mvcc資料夾下,etcd的mvcc實現):
- mvcc底層使用bolt 實現,bolt是一個基於B+樹的KV儲存。
-
kv.go
這個檔案定義了大量的介面,然後介面之間又各種組合,但是其實最後 etcdserver 使用的就是ConsistentWatchableKV
這個介面。
此處,我們預先了解bolt的一些東西,但是暫時不去探究bolt的實現,bolt的實現粗略的瞄了一眼,如果要寫的話,有的寫了:
https://github.com/etcd-io/bbolt
- bolt的頂級是一個DB,DB裡有多個bucket。在物理上,bolt使用單個檔案儲存。
- bolt在某一刻只允許一個 read-write 事務,但是可以同時允許多個 read-only 事務。其實就是讀寫鎖,寫只能順序來,讀可以併發讀。
-
DB.Update()
是用來開啟 read-write 事務的,DB.View()
則是用來開啟 read-only 事務的。由於每次執行DB.Update()
都會寫入一次磁碟,可以使用DB.Batch()
來進行批量操作。
store是上面所說的ConsistentWatchableKV
的底層實現:
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 bbackend.Backend kvindex index le lease.Lessor // revMuLock protects currentRev and compactMainRev. // Locked at end of write txn and released after write txn unlock lock. // Locked before locking read txn and released after locking. revMu sync.RWMutex // currentRev is the revision of the last completed transaction. currentRev int64 // compactMainRev is the main revision of the last compaction. compactMainRev int64 // bytesBuf8 is a byte slice of length 8 // to avoid a repetitive allocation in saveIndex. bytesBuf8 []byte fifoSched schedule.Scheduler stopc chan struct{} lg *zap.Logger }
可以看到其中有幾個很重要的東西:
mu sync.RWMutex b backend.Backend kvindex index
接下來探究一下Put
是怎麼工作的,這樣我們就可以粗略的瞭解 mvcc 是怎麼工作的。
store 本身並沒有實現Put
方法, 但是卻可以呼叫Put
方法,因為在最上邊,它嵌套了一個匿名的WriteView
,從而獲得了
這個方法:
type store struct { ReadView WriteView
而具體的實現則在NewStore
這個函式裡可以找到:
s.ReadView = &readView{s} s.WriteView = &writeView{s}
那我們就去看writeView
怎麼實現的:
func (wv *writeView) Put(key, value []byte, lease lease.LeaseID) (rev int64) { tw := wv.kv.Write() defer tw.End() return tw.Put(key, value, lease) }
看看wv.kv.Write
返回的是個啥嘎達(注意,wv.kv是一個符合KV這個interface的東東):
type KV interface { ReadView WriteView // Read creates a read transaction. Read() TxnRead // Write creates a write transaction. Write() TxnWrite
那我們就要去看TxnWrite.Put
是怎麼實現的:
// TxnWrite represents a transaction that can modify the store. type TxnWrite interface { TxnRead WriteView // Changes gets the changes made since opening the write txn. Changes() []mvccpb.KeyValue }
原來又是介面,Put
是在WriteView
裡定義的。所以呢,繞了一圈,我們又繞回來了,所以,etcd搞得這麼複雜幹啥呢。。。為了找出具體實現,我們得去NewStore
裡翻翻,具體傳進去的是什麼。原來傳進去的就是自己啊,那就說明,type store struct
這玩意兒,肯定實現了Write
這個方法。但是呢,
我找來找去,就是沒有發現。最後我只能開啟搜尋大法,然後在kvstore_txn.go
這個檔案裡找到了:
// 哇好繞啊,又繞到這裡來了,我是佩服的 func (s *store) Write() TxnWrite { s.mu.RLock() tx := s.b.BatchTx() tx.Lock() tw := &storeTxnWrite{ storeTxnRead: storeTxnRead{s, tx, 0, 0}, tx:tx, beginRev:s.currentRev, changes:make([]mvccpb.KeyValue, 0, 4), } return newMetricsTxnWrite(tw) }
我是服氣的。
接下來就要去翻TxnWrite
的實現,也就是storeTxnWrite
的Put
的實現了,然後你會發現,Put
呼叫了put
:
// etcdctl put foo bar最後到了這裡 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() // revision的bytes 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() // kv的bytes if err != nil { if tw.storeTxnRead.s.lg != nil { tw.storeTxnRead.s.lg.Fatal( "failed to marshal mvccpb.KeyValue", zap.Error(err), ) } else { plog.Fatalf("cannot marshal event: %v", err) } } tw.tx.UnsafeSeqPut(keyBucketName, ibytes, d) // 所以最後儲存的,以revision為key,kv為value儲存下來了 tw.s.kvindex.Put(key, idxRev) tw.changes = append(tw.changes, kv) 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 { if tw.storeTxnRead.s.lg != nil { tw.storeTxnRead.s.lg.Fatal( "failed to detach old lease from a key", zap.Error(err), ) } else { 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") } } }
這裡可以看出來,bolt裡儲存的KV,實際上並不是使用者給出的KV。Key是revision,而Value是使用者給出的KV。所以,才需要那個b樹做索引, 把使用者的key換成revision,然後再到bolt裡,把revision換成真正的KV。
之後我們再看mvcc實現裡的其他特性,例如watch是怎麼實現的,實際上這玩意兒儲存的時候,是有序儲存的。
好了。這一節就看到這裡了。