实现mvcc_一文读懂 etcd 的 mvcc 实现
提到事務必談 ACID 特性, 基于悲觀鎖的實現會有讀寫沖突問題,性能很低,為了解決這個問題,主流數據庫大多采用版本控制 mvcc[1] 技術,比如 oracle, mysql, postgresql 等等。讀可以不加鎖,只需要讀歷史版本即可 (寫寫還是沖突). 根據事務能看到不同版本的數據,還產生了隔離級別的問題,比如 mysql 默認的 repeatable-read, oracle 默認的 read-commited. 本文暫時只講 mvcc, 隔離實現放到下文。
mvcc 不同數據庫實現也不同,mysql 原地更新數據,將多版本保存到 undo, 而 postgresql 直接插入不同版本數據,過期的數據由 vacuum 來刪除。etcd 的實現類似 pg, 本次分享看一下 etcd 的實現原理。
Revision
可以先閱讀我的文章 etcd 中讓人頭大的 version, revision, createRevision, modRevision[2] 來了解下幾個版本的概念。
type?revision?struct?{?//?main?is?the?main?revision?of?a?set?of?changes?that?happen?atomically.
?main?int64
?//?sub?is?the?sub?revision?of?a?change?in?a?set?of?changes?that?happen
?//?atomically.?Each?change?has?different?increasing?sub?revision?in?that
?//?set.
?sub?int64
}
main 是版本 id, 邏輯時間戳全局遞增。sub 表示當前事務內操作 changes 的順序 id, 從 0 開始遞增。
靜態存儲
etcd 的 mvcc 數據存儲分兩部分:內存保存所有 key 對應的版本信息,用于快速范圍查詢與點查,而磁盤存儲所有不同版本的真實數據。
kvindex btree
內存數據由 btree 來維護,從圖上可以看到,key 是用戶真實的 key, value 是對應所有的版本信息。
type?keyIndex?struct?{?key?????????[]byte
?modified????revision?//?the?main?rev?of?the?last?modification
?generations?[]generation
}
//?generation?contains?multiple?revisions?of?a?key.
type?generation?struct?{
?ver?????int64
?created?revision?//?when?the?generation?is?created?(put?in?first?revision).
?revs????[]revision
}
keyIndex 保存 key 的所有版本信息,每刪除一次都會生成一個 generation, 每個 generation 保存了這個生命周期內從創建到刪除中間的所有版本號。
磁盤 boltdb
磁盤負責存儲所有數據,key 是 revision, value 是 mvccpb.KeyValue, 存儲引擎是 boltdb
type?KeyValue?struct?{?//?key?is?the?key?in?bytes.?An?empty?key?is?not?allowed.
?Key?[]byte?`protobuf:"bytes,1,opt,name=key,proto3"?json:"key,omitempty"`
?//?create_revision?is?the?revision?of?last?creation?on?this?key.
?CreateRevision?int64?`protobuf:"varint,2,opt,name=create_revision,json=createRevision,proto3"?json:"create_revision,omitempty"`
?//?mod_revision?is?the?revision?of?last?modification?on?this?key.
?ModRevision?int64?`protobuf:"varint,3,opt,name=mod_revision,json=modRevision,proto3"?json:"mod_revision,omitempty"`
?//?version?is?the?version?of?the?key.?A?deletion?resets
?//?the?version?to?zero?and?any?modification?of?the?key
?//?increases?its?version.
?Version?int64?`protobuf:"varint,4,opt,name=version,proto3"?json:"version,omitempty"`
?//?value?is?the?value?held?by?the?key,?in?bytes.
?Value?[]byte?`protobuf:"bytes,5,opt,name=value,proto3"?json:"value,omitempty"`
?//?lease?is?the?ID?of?the?lease?that?attached?to?key.
?//?When?the?attached?lease?expires,?the?key?will?be?deleted.
?//?If?lease?is?0,?then?no?lease?is?attached?to?the?key.
?Lease?int64?`protobuf:"varint,6,opt,name=lease,proto3"?json:"lease,omitempty"`
}
mvccpb.KeyValue 存儲本次操作的 key, value, 還有相關的所有版本信息。
Range 查找
每次數據操作,都會在 etcdserver 層開啟一個事務 txn, Range 操作是 Read 讀事務,然后調用 txn 的 Range 方法,直接看 mvcc 目錄下 kvstore_txn.go 文件的實現。
func?(tr?*storeTxnRead)?Range(key,?end?[]byte,?ro?RangeOptions)?(r?*RangeResult,?err?error)?{?return?tr.rangeKeys(key,?end,?tr.Rev(),?ro)
}
func?(tr?*storeTxnRead)?rangeKeys(key,?end?[]byte,?curRev?int64,?ro?RangeOptions)?(*RangeResult,?error)?{
?rev?:=?ro.Rev
?if?rev?>?curRev?{
??return?&RangeResult{KVs:?nil,?Count:?-1,?Rev:?curRev},?ErrFutureRev
?}
?if?rev?<=?0?{
??rev?=?curRev
?}
?if?rev???return?&RangeResult{KVs:?nil,?Count:?-1,?Rev:?0},?ErrCompacted
?}
??
?revpairs?:=?tr.s.kvindex.Revisions(key,?end,?rev)
??......
?kvs?:=?make([]mvccpb.KeyValue,?limit)
?revBytes?:=?newRevBytes()
?for?i,?revpair?:=?range?revpairs[:len(kvs)]?{
??revToBytes(revpair,?revBytes)
??_,?vs?:=?tr.tx.UnsafeRange(keyBucketName,?revBytes,?nil,?0)
????......
??if?err?:=?kvs[i].Unmarshal(vs[0]);?err?!=?nil?{
????......
??}
?}
?tr.trace.Step("range?keys?from?bolt?db")
?return?&RangeResult{KVs:?kvs,?Count:?len(revpairs),?Rev:?curRev},?nil
}
省略部份無關代碼,直接看主干部份
Put 更新數據
etcdserver 層同樣要開啟事務,只不過是寫事務。然后實現直接看 mvcc 目錄下 kvstore_txn.go
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)})
?}
?tw.trace.Step("get?key's?previous?created_revision?and?leaseID")
?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()
??......
?tw.tx.UnsafeSeqPut(keyBucketName,?ibytes,?d)
?tw.s.kvindex.Put(key,?idxRev)
?tw.changes?=?append(tw.changes,?kv)
?tw.trace.Step("store?kv?pair?into?bolt?db")
??......
}
省去不太相關的 lease 操作,只看主干邏輯
Delete 刪除
同樣需要開啟寫事務,直接看源碼
func?(tw?*storeTxnWrite)?DeleteRange(key,?end?[]byte)?(int64,?int64)?{?if?n?:=?tw.deleteRange(key,?end);?n?!=?0?||?len(tw.changes)?>?0?{
??return?n,?tw.beginRev?+?1
?}
?return?0,?tw.beginRev
}
func?(tw?*storeTxnWrite)?deleteRange(key,?end?[]byte)?int64?{
?rrev?:=?tw.beginRev
?if?len(tw.changes)?>?0?{
??rrev++
?}
?keys,?_?:=?tw.s.kvindex.Range(key,?end,?rrev)
?if?len(keys)?==?0?{
??return?0
?}
?for?_,?key?:=?range?keys?{
??tw.delete(key)
?}
?return?int64(len(keys))
}
同樣需要先查找內存 kvindex, 找到所有符合的待刪除版本,然后調用 delete 去刪
func?(tw?*storeTxnWrite)?delete(key?[]byte)?{?ibytes?:=?newRevBytes()
?idxRev?:=?revision{main:?tw.beginRev?+?1,?sub:?int64(len(tw.changes))}
?revToBytes(idxRev,?ibytes)
?if?tw.storeTxnRead.s?!=?nil?&&?tw.storeTxnRead.s.lg?!=?nil?{
??ibytes?=?appendMarkTombstone(tw.storeTxnRead.s.lg,?ibytes)
?}?else?{
??//?TODO:?remove?this?in?v3.5
??ibytes?=?appendMarkTombstone(nil,?ibytes)
?}
?kv?:=?mvccpb.KeyValue{Key:?key}
?d,?err?:=?kv.Marshal()
?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)
?err?=?tw.s.kvindex.Tombstone(key,?idxRev)
?......
}
數據過期
與 pg 比較像,過期與刪除數據都是惰性刪除的。etcd 可以配置只保留固定時間的數據,所以會周期性的 Compact. 同樣分為兩部分,內存 btree 數據如果發現當前 generation 為空,并且最大 Revision 己過期,那就從 btree 中刪除。
磁盤數據由 boltdb 維護,只會標記為刪除,磁盤空間可以回收利用,但是不會自動釋放,只有調用 Defrag 才會重建磁盤文件。
另外說到存儲引擎 boltdb, 這個東西性能一般,除了 etcd 沒有什么知名項目在用。讀事務是并發,但是寫事務是串行的,所以內部會將盡可能多的寫入 batch 一起操作,異步提交。
小結
這次分享就這些,以后面還會分享更多關于 etcd 的內容,如果感興趣,可以關注并轉發(:
參考資料
[1]什么是 mvcc: https://en.wikipedia.org/wiki/Multiversion_concurrency_control,
[2]etcd 中讓人頭大的 version, revision, createRevision, modRevision: https://mp.weixin.qq.com/s/TFcSEBBMnb0wJ_A3R4Jfqw,
總結
以上是生活随笔為你收集整理的实现mvcc_一文读懂 etcd 的 mvcc 实现的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: verilog赋多位值_verilog赋
- 下一篇: DellEMC UnityVSA 部署指