1. 簡介
etcd的backend模塊對於底層存儲引擎進行了抽象,默認使用上一篇文章中介紹的BoltDB。
etcd將鍵值對的每一個版本都存儲在BoltDB中,並在內存中構建BTree keyIndex索引。
- 在BoltDB中存儲數據的key是revision,revision包含兩個id:main revision和sub revision;main revision每個事務加1,sub revision事務中的每次操作加1;
- keyIndex索引的key是鍵值對的key,value是revision。
通過這種存儲模型,etcd很自然的支持了MVCC、歷史數據查詢和watcher。
2. backend
2.1 Backend和backend
Backend接口和backend結構體定義如下:
type Backend interface {
// 創建一個只讀事務
ReadTx() ReadTx
// 創建一個批量事務
BatchTx() BatchTx
// 創建一個無阻塞併發讀事務
ConcurrentReadTx() ReadTx
// 創建一個快照
Snapshot() Snapshot
Hash(ignores map[IgnoreKey]struct{}) (uint32, error)
// 返回存儲引擎已分配的物理空間大小
Size() int64
// 返回存儲引擎已使用的空間大小
SizeInUse() int64
// 返回打開的只讀事務數量
OpenReadTxN() int64
// 碎片整理
Defrag() error
// 提交批量讀寫事務
ForceCommit()
Close() error
}
type backend struct {
// 已分配的字節數
size int64
// 實際使用的字節數
sizeInUse int64
// 提交次數
commits int64
// 已打開的讀事務數量
openReadTxN int64
// 鎖
mu sync.RWMutex
// BoltDB指針
db *bolt.DB
...
}
2.2 只讀事務
只讀事務定義瞭如下接口:
type ReadTx interface {
Lock()
Unlock()
RLock()
RUnlock()
// 在指定bucket中範圍搜索
UnsafeRange(bucketName []byte, key, endKey []byte, limit int64) (keys [][]byte, vals [][]byte)
// 在遍歷指定bucket,並執行回調方法
UnsafeForEach(bucketName []byte, visitor func(k, v []byte) error) error
}
type readTx struct {
// 讀寫鎖,控制對於 txReadBuffer 讀緩存的訪問
mu sync.RWMutex
// 讀緩存
buf txReadBuffer
// 事務讀寫鎖,範圍搜索時,控制對於bucket的訪問
txMu sync.RWMutex
// 底層引擎的事務
tx *bolt.Tx
// 底層引擎的bucket的指針的map,key爲bucketName
buckets map[string]*bolt.Bucket
...
}
範圍搜索的具體實現如下:
func (rt *readTx) UnsafeRange(bucketName, key, endKey []byte, limit int64) ([][]byte, [][]byte) {
// endKey爲nil,表示查一個鍵值對
if endKey == nil {
limit = 1
}
if limit <= 0 {
limit = math.MaxInt64
}
if limit > 1 && !bytes.Equal(bucketName, safeRangeBucket) {
panic("do not use unsafeRange on non-keys bucket")
}
// 在txReadBuffer中範圍搜索
keys, vals := rt.buf.Range(bucketName, key, endKey, limit)
if int64(len(keys)) == limit {
return keys, vals
}
// 如果沒有在txReadBuffer找到
// 則搜索並緩存bucket
bn := string(bucketName)
// txMu讀鎖保護下,讀取bucket指針
rt.txMu.RLock()
bucket, ok := rt.buckets[bn]
rt.txMu.RUnlock()
if !ok {
// 如果沒有取到,則可能其他進程寫鎖正在修改
// txMu讀鎖保護下,讀取bucket指針
rt.txMu.Lock()
bucket = rt.tx.Bucket(bucketName)
rt.buckets[bn] = bucket
rt.txMu.Unlock()
}
if bucket == nil {
return keys, vals
}
// txMu讀鎖保護下,獲取bucket遊標
rt.txMu.Lock()
c := bucket.Cursor()
rt.txMu.Unlock()
// 使用遊標範圍搜索bucket
k2, v2 := unsafeRange(c, key, endKey, limit-int64(len(keys)))
return append(k2, keys...), append(v2, vals...)
}
2.3 併發讀事務
// concurrentReadTx定義
type concurrentReadTx struct {
buf txReadBuffer
txMu *sync.RWMutex
tx *bolt.Tx
buckets map[string]*bolt.Bucket
txWg *sync.WaitGroup
}
func (b *backend) ConcurrentReadTx() ReadTx {
// 申請讀鎖
b.readTx.RLock()
// defer 釋放讀鎖
defer b.readTx.RUnlock()
// WaitGroup計數器置爲1
b.readTx.txWg.Add(1)
// 創建concurrentReadTx
return &concurrentReadTx{
buf: b.readTx.buf.unsafeCopy(),
tx: b.readTx.tx,
txMu: &b.readTx.txMu,
buckets: b.readTx.buckets,
txWg: b.readTx.txWg,
}
}
ConcurrentReadTx的創建過程其實就是從backend的readTx克隆一份新的buf緩衝區,tx、txMu、buckets、txWg均指向readTx。
read_tx.go
func (rt *concurrentReadTx) Lock() {}
func (rt *concurrentReadTx) Unlock() {}
func (rt *concurrentReadTx) RLock() {}
func (rt *concurrentReadTx) RUnlock() { rt.txWg.Done() }
ConcurrentReadTx的申請鎖、釋放鎖實現如上,Lock、Unlock、RLock時,不做任何操作,僅在RUnlock時將WaitGroup計數器減一。
2.4 批量事務
批量事務提供批量讀寫數據庫的能力。
batch_tx.go
type BatchTx interface {
ReadTx
UnsafeCreateBucket(name []byte)
UnsafePut(bucketName []byte, key []byte, value []byte)
UnsafeSeqPut(bucketName []byte, key []byte, value []byte)
UnsafeDelete(bucketName []byte, key []byte)
// 提交事務,並開始一個新事務
Commit()
// 提交事務,但不開始一個新事務
CommitAndStop()
}
type batchTx struct {
// 互斥鎖
sync.Mutex
// BoltDB事務實例
tx *bolt.Tx
// 關聯的backend實例
backend *backend
// 待提交的指令數量
pending int
}
同樣的,BatchTx定義了一系列不安全的讀寫方法,需要調用方自行控制併發。
util.go
func WriteKV(be backend.Backend, kv mvccpb.KeyValue) {
// 創建BoltDB中的key
ibytes := newRevBytes()
revToBytes(revision{main: kv.ModRevision}, ibytes)
// 鍵值對組織成byte數組
d, err := kv.Marshal()
if err != nil {
panic(fmt.Errorf("cannot marshal event: %v", err))
}
// 加鎖
be.BatchTx().Lock()
// 寫BoltDB,key爲revision,value爲鍵值對
be.BatchTx().UnsafePut(keyBucketName, ibytes, d)
// 釋放鎖
be.BatchTx().Unlock()
}
3. KeyIndex內存索引
index.go
type treeIndex struct {
sync.RWMutex
tree *btree.BTree
lg *zap.Logger
}
treeIndex主要由一個讀寫鎖、一個BTree和一個日誌工具組成,BTree使用的是google開源項目。
key_index.go
type keyIndex struct {
// 調用方傳入的鍵值對的key
key []byte
// 最後一次修改的main revison
modified revision
// 該key對應的每代的版本信息
generations []generation
}
keyIndex是BTree上的一個鍵值對的值的數據結構。其中generations []generation中存儲着每次編輯的對應的main revision 和 sub revision。
type generation struct {
ver int64
created revision
// 記錄了多次修改的revision
revs []revision
}
每個generation實例記錄了多次修改的revision,知道調用tombstone,終止一個generation
func (ki *keyIndex) tombstone(lg *zap.Logger, main int64, sub int64) error {
if ki.isEmpty() {
lg.Panic(
"'tombstone' got an unexpected empty keyIndex",
zap.String("key", string(ki.key)),
)
}
// 判斷最後一個generation是否合法
if ki.generations[len(ki.generations)-1].isEmpty() {
return ErrRevisionNotFound
}
ki.put(lg, main, sub)
// generations最後追加一個tombstone,其實就是個空的generation實例
ki.generations = append(ki.generations, generation{})
keysGauge.Dec()
return nil
}
4. 總結
綜上所述,etcd的backend模塊,基於BoltDB和內存BTree索引,在讀多寫少的場景下表現優異。