什麼是 MVCC
MVCC 是 Multi-Version Concurrency Control 的縮寫,即多版本併發控制。它是一種併發控制的方法,用於在資料庫系統中實現事務的隔離性。MVCC 是一種樂觀鎖機制,它透過儲存資料的多個版本來實現事務的隔禽性。在 etcd 中,MVCC 是用於實現資料的版本控制的。而且可以檢視歷史版本的資料。
測試
# 新增資料
etcdctl put /test t1
OK
etcdctl put /test t2
OK
# 檢視資料
etcdctl get /test
/test
t2
# 檢視 json 格式資料
etcdctl get /test --write-out=json
# {"header":{"cluster_id":8735285696067307020,"member_id":7131777314758672153,"revision":15,"raft_term":4},"kvs":[{"key":"L3Rlc3Q=","create_revision":14,"mod_revision":15,"version":2,"value":"dDI="}],"count":1}
# 檢視歷史版本
etcdctl get /test --rev=14
/test
t1
可以看到,透過 --rev
引數可以檢視歷史版本的資料。也就是我第一次新增的資料。那麼 json 中 revision 是什麼意思呢?
revision
reversion 中是 etcd 中的一個概念,它是一個遞增的整數,用於標識 etcd 中的資料版本。他是一個 int64 型別。沒操作一次 etcd 資料(增,刪,改),reversion 就會遞增。
# 刪除資料
etcdctl del /test
1
# 檢視 revision
etcdctl get / -wjson
# {"header":{"cluster_id":8735285696067307020,"member_id":7131777314758672153,"revision":16,"raft_term":4}}
# 剛才是 15 現在是 16
# 新增 /test2 資料
etcdctl put /test2 t3
OK
# 檢視 revision
etcdctl get / -wjson
# {"header":{"cluster_id":8735285696067307020,"member_id":7131777314758672153,"revision":17,"raft_term":4}}
儲存結構
etcd mvcc 中,維護了兩個資料結構,分別是 treeindex 和 boltDB。treeindex 是一個 B 樹,用於儲存 key 和 revision 之間的對映關係,它主要維護在記憶體中。而 boltDB 是一個 key-value 資料庫,用於儲存 key 和 value 之間的對映關係, 它主要維護在磁碟中, 用於持久化資料,雖然 boltdb 使用了 mmap 機制,但是它還是一個磁碟資料庫。
treeindex
為什麼 etcd 的 treeindex 使用 B-tree 而不使用雜湊表、平衡二叉樹?
因為 etcd 需要範圍查詢,所以雜湊表不適合。而且etcd 中的 key 過多,平衡二叉樹的查詢效率不高,所以使用 B tree。
b-tree:
在 treeindex 中,資料的每個 key 是一個 keyIndex 結構,它儲存了 key 和 revision 之間的對映關係。keyIndex 結構如下:
type keyIndex struct {
key []byte // key 的值
modified Revision // 最後一次修改的 main revision
generations []generation // 儲存了 key 的歷史版本 沒刪除一次然後新增一次就是一個 generation
}
type Revision struct {
// 就是 revision 的值,比如上邊的 15 等
Main int64
// 子 revision 的值 主要是在事務中使用 比如事務中多個操作 那麼就是 0 1 2 3 等
Sub int64
}
// generation 儲存了 key 的歷史版本
type generation struct {
ver int64 // 版本號
created Revision // 最後一次被建立的 revision
revs []Revision // 儲存了 key 的歷史 revision
}
在 treeindex 中,每個 keyIndex 儲存了 key 的歷史版本,而且每個 keyIndex 中的 generations 儲存了 key 的歷史版本。而且每個 generation 中的 revs 儲存了 key 的歷史 revision。這樣就可以實現歷史版本的查詢。
獲取 resersion 的值
func (ti *treeIndex) Get(key []byte, atRev int64) (modified, created Revision, ver int64, err error) {
ti.RLock()
defer ti.RUnlock()
return ti.unsafeGet(key, atRev)
}
func (ti *treeIndex) unsafeGet(key []byte, atRev int64) (modified, created Revision, ver int64, err error) {
keyi := &keyIndex{key: key}
// 從 B 樹中獲取 keyIndex
if keyi = ti.keyIndex(keyi); keyi == nil {
return Revision{}, Revision{}, 0, ErrRevisionNotFound
}
// 從 keyIndex 中獲取 revision
return keyi.get(ti.lg, atRev)
}
func (ti *treeIndex) keyIndex(keyi *keyIndex) *keyIndex {
if ki, ok := ti.tree.Get(keyi); ok {
return ki
}
return nil
}
func (ki *keyIndex) get(lg *zap.Logger, atRev int64) (modified, created Revision, ver int64, err error) {
if ki.isEmpty() {
lg.Panic(
"'get' got an unexpected empty keyIndex",
zap.String("key", string(ki.key)),
)
}
// 找到 key 的 generation
g := ki.findGeneration(atRev)
if g.isEmpty() {
return Revision{}, Revision{}, 0, ErrRevisionNotFound
}
// 從 generation 中獲取 revision 找到第一次小於 atRev 的 revision
n := g.walk(func(rev Revision) bool { return rev.Main > atRev })
if n != -1 {
return g.revs[n], g.created, g.ver - int64(len(g.revs)-n-1), nil
}
return Revision{}, Revision{}, 0, ErrRevisionNotFound
}
// 基本的意思就是從後往前找到第一個 revision 小於 atRev 的 generation
func (ki *keyIndex) findGeneration(rev int64) *generation {
lastg := len(ki.generations) - 1
cg := lastg
for cg >= 0 {
if len(ki.generations[cg].revs) == 0 {
cg--
continue
}
g := ki.generations[cg]
if cg != lastg {
// 如果當前 generation 的最後一個 revision 小於等於 rev 那麼就返回 nil
if tomb := g.revs[len(g.revs)-1].Main; tomb <= rev {
return nil
}
}
if g.revs[0].Main <= rev {
return &ki.generations[cg]
}
cg--
}
return nil
}
// walk 從後往前遍歷 generation
func (g *generation) walk(f func(rev Revision) bool) int {
l := len(g.revs)
for i := range g.revs {
ok := f(g.revs[l-i-1])
if !ok {
return l - i - 1
}
}
return -1
}
boltdb
上邊的 treeindex 拿到 revision 之後,並沒有拿到 value,那麼如何拿到 value 呢?這就需要用到 boltdb 了。boltdb 是一個 key-value 資料庫,用於儲存 key 和 value 之間的對映關係。在 etcd 中,boltdb 主要用於持久化資料。
在 etcd 中,boltdb 報錯的不是 etcd key-value 資料,而他的 ket 是 revision,value 是後設資料。
func (tr *storeTxnCommon) rangeKeys(ctx context.Context, key, end []byte, curRev int64, ro RangeOptions) (*RangeResult, error) {
rev := ro.Rev
// 如果 rev 大於當前的 revision 那麼就返回 ErrFutureRev
if rev > curRev {
return &RangeResult{KVs: nil, Count: -1, Rev: curRev}, ErrFutureRev
}
// 如果 rev 小於等於 0 那麼就是當前的 revision
if rev <= 0 {
rev = curRev
}
// 如果 rev 小於 compactMainRev 那麼就返回 ErrCompacted
if rev < tr.s.compactMainRev {
return &RangeResult{KVs: nil, Count: -1, Rev: 0}, ErrCompacted
}
// 如果 re.Count 代表 count 操作 查出來直接返回數量就可以了 不需要在查 value
if ro.Count {
total := tr.s.kvindex.CountRevisions(key, end, rev)
tr.trace.Step("count revisions from in-memory index tree")
return &RangeResult{KVs: nil, Count: total, Rev: curRev}, nil
}
// 查好需要的 revision 之後,從 boltdb 中查出 value ,revpairs 是從 treeindex 中查出來的 revisions
revpairs, total := tr.s.kvindex.Revisions(key, end, rev, int(ro.Limit))
tr.trace.Step("range keys from in-memory index tree")
if len(revpairs) == 0 {
return &RangeResult{KVs: nil, Count: total, Rev: curRev}, nil
}
limit := int(ro.Limit)
if limit <= 0 || limit > len(revpairs) {
limit = len(revpairs)
}
kvs := make([]mvccpb.KeyValue, limit)
revBytes := NewRevBytes()
// 對於每個 revision 從 boltdb 中查出 value
for i, revpair := range revpairs[:len(kvs)] {
select {
case <-ctx.Done():
return nil, fmt.Errorf("rangeKeys: context cancelled: %w", ctx.Err())
default:
}
// 把 revision 轉換成 bytes
revBytes = RevToBytes(revpair, revBytes)
// 從 boltdb 中查出 value
_, vs := tr.tx.UnsafeRange(schema.Key, revBytes, nil, 0)
if len(vs) != 1 {
tr.s.lg.Fatal(
"range failed to find revision pair",
zap.Int64("revision-main", revpair.Main),
zap.Int64("revision-sub", revpair.Sub),
zap.Int64("revision-current", curRev),
zap.Int64("range-option-rev", ro.Rev),
zap.Int64("range-option-limit", ro.Limit),
zap.Binary("key", key),
zap.Binary("end", end),
zap.Int("len-revpairs", len(revpairs)),
zap.Int("len-values", len(vs)),
)
}
// 把 value 轉換成 mvccpb.KeyValue
if err := kvs[i].Unmarshal(vs[0]); err != nil {
tr.s.lg.Fatal(
"failed to unmarshal mvccpb.KeyValue",
zap.Error(err),
)
}
}
tr.trace.Step("range keys from bolt db")
return &RangeResult{KVs: kvs, Count: total, Rev: curRev}, nil
}
// boltdb 的 key 結構
type BucketKey struct {
Revision
// 墓碑標誌 當刪除的時候 先標記一下
tombstone bool
}
func (baseReadTx *baseReadTx) UnsafeRange(bucketType Bucket, key, endKey []byte, limit int64) ([][]byte, [][]byte) {
if endKey == nil {
// forbid duplicates for single keys
limit = 1
}
if limit <= 0 {
limit = math.MaxInt64
}
if limit > 1 && !bucketType.IsSafeRangeBucket() {
panic("do not use unsafeRange on non-keys bucket")
}
// 從快取中拿出資料
keys, vals := baseReadTx.buf.Range(bucketType, key, endKey, limit)
if int64(len(keys)) == limit {
return keys, vals
}
// find/cache bucket
bn := bucketType.ID()
baseReadTx.txMu.RLock()
bucket, ok := baseReadTx.buckets[bn]
baseReadTx.txMu.RUnlock()
lockHeld := false
if !ok {
baseReadTx.txMu.Lock()
lockHeld = true
bucket = baseReadTx.tx.Bucket(bucketType.Name())
baseReadTx.buckets[bn] = bucket
}
// ignore missing bucket since may have been created in this batch
if bucket == nil {
if lockHeld {
baseReadTx.txMu.Unlock()
}
return keys, vals
}
if !lockHeld {
baseReadTx.txMu.Lock()
}
c := bucket.Cursor()
baseReadTx.txMu.Unlock()
// 從 boltdb 中查出資料
k2, v2 := unsafeRange(c, key, endKey, limit-int64(len(keys)))
return append(k2, keys...), append(v2, vals...)
}
流程
- 使用者透過
etcdctl get /b
命令獲取資料 - etcd 透過 treeindex 獲取 key 的 revision 資訊
{man: 19, sub: 0}
- etcd 透過 key =
{man: 19, sub: 0, tombstone: false}
從 boltdb 中獲取 value 值 他是一個protobuf 序列化的資料 - etcd 將 value 值反序列化成 mvccpb.KeyValue
- etcd 將 mvccpb.KeyValue 返回給使用者