日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

实现mvcc_一文读懂 etcd 的 mvcc 实现

發布時間:2023/12/15 c/c++ 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 实现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
}

省略部份無關代碼,直接看主干部份

  • 檢查所查找的 rev 版本是否有效,超過當前版本不行,被 compact 刪除的也不行
  • 根據指定版本去 kvindex 即內存 btree 中查找,所有符合 rev 版本從 key 到 end 的版本信息
  • 遍歷所有版本,UnsafeRange 去底層磁盤 boltdb 中獲取真實 key/value
  • 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 操作,只看主干邏輯

  • 根據當前版本 key, rev 查找內存 kvindex, 看看是否有當前 key 的版本記錄。主要是獲取這個 key 當前的 createdRevision 與 Version
  • 生成 mvccpb.KeyValue 信息,主要是確定這次操作的 ModRevision
  • UnsafeSeqPut 操作寫磁盤 boltdb, key 是 Revision, value 是 mvccpb.KeyValue 序列化后的數據
  • 最后更新 kvindex btree
  • 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)
    ?......
    }
  • 生成 ibytes, 然后追加一個 appendMarkTombstone 標記,表示這個 revision 是 delete,并且生成一個只含有 key 的 mvccpb.KeyValue
  • UnsafeSeqPut 刪除磁盤 boltdb, 注意這里底層只是標記刪除,磁盤空間并不釋放
  • Tombstone 結束當前生命周期,生成一個新的空 generation, 更新 kvindex
  • 數據過期

    與 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 实现的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。