Go开发的同学平时接触到Etcd的机会比较多,今天邀请到做过DBA的研发老兵董大哥给大家分享一下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         []bytemodified    revision // the main rev of the last modificationgenerations []generation
}// generation contains multiple revisions of a key.
type generation struct {ver     int64created 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.Revif rev > curRev {return &RangeResult{KVs: nil, Count: -1, Rev: curRev}, ErrFutureRev}if rev <= 0 {rev = curRev}if rev < tr.s.compactMainRev {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
}

省略部份无关代码,直接看主干部份

  1. 检查所查找的 rev 版本是否有效,超过当前版本不行,被 compact 删除的也不行

  2. 根据指定版本去 kvindex 即内存 btree 中查找,所有符合 rev 版本从 key 到 end 的版本信息

  3. 遍历所有版本,UnsafeRange 去底层磁盘 boltdb 中获取真实 key/value

Put 更新数据

etcdserver 层同样要开启事务,只不过是写事务。然后实现直接看 mvcc 目录下 kvstore_txn.go

func (tw *storeTxnWrite) put(key, value []byte, leaseID lease.LeaseID) {rev := tw.beginRev + 1c := revoldLease := 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.mainoldLease = 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 + 1kv := 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 操作,只看主干逻辑

  1. 根据当前版本 key, rev 查找内存 kvindex, 看看是否有当前 key 的版本记录。主要是获取这个 key 当前的 createdRevisionVersion

  2. 生成 mvccpb.KeyValue 信息,主要是确定这次操作的 ModRevision

  3. UnsafeSeqPut 操作写磁盘 boltdb, key 是 Revision, value 是 mvccpb.KeyValue 序列化后的数据

  4. 最后更新 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.beginRevif 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.5ibytes = 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)......
}
  1. 生成 ibytes, 然后追加一个 appendMarkTombstone 标记,表示这个 revision 是 delete,并且生成一个只含有 key 的 mvccpb.KeyValue

  2. UnsafeSeqPut 删除磁盘 boltdb, 注意这里底层只是标记删除,磁盘空间并不释放

  3. 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,

一文读懂 etcd 的 mvcc 实现相关推荐

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

    提到事务必谈 ACID 特性, 基于悲观锁的实现会有读写冲突问题,性能很低,为了解决这个问题,主流数据库大多采用版本控制 mvcc[1] 技术,比如 oracle, mysql, postgresql ...

  2. mysql 默认事务隔离级别_一文读懂MySQL的事务隔离级别及MVCC机制

    回顾前文: <一文学会MySQL的explain工具> <一文读懂MySQL的索引结构及查询优化> (同时再次强调,这几篇关于MySQL的探究都是基于5.7版本,相关总结与结论 ...

  3. 从实验室走向大众,一文读懂Nanopore测序技术的发展及应用

    关键词/Nanopore测序技术    文/基因慧 随着基因测序技术不断突破,二代测序的发展也将基因检测成本大幅降低.理想的测序方法,是对原始DNA模板进行直接.准确的测序,消除PCR扩增带来的偏差, ...

  4. 一文读懂Faster RCNN

    来源:信息网络工程研究中心本文约7500字,建议阅读10+分钟 本文从四个切入点为你介绍Faster R-CNN网络. 经过R-CNN和Fast RCNN的积淀,Ross B. Girshick在20 ...

  5. 福利 | 一文读懂系列文章精选集发布啦!

    大数据时代已经悄然到来,越来越多的人希望学习一定的数据思维和技能来武装自己,虽然各种介绍大数据技术的文章每天都扑面而来,但纷繁又零散的知识常常让我们不知该从何入手:同时,为了感谢和回馈读者朋友对数据派 ...

  6. ​一文读懂EfficientDet

    一文读懂EfficientDet. 今年年初Google Brain团队在 CVPR 2020 上发布了 EfficientDet目标检测模型, EfficientDet是一系列可扩展的高效的目标检测 ...

  7. 一文读懂序列建模(deeplearning.ai)之序列模型与注意力机制

    https://www.toutiao.com/a6663809864260649485/ 作者:Pulkit Sharma,2019年1月21日 翻译:陈之炎 校对:丁楠雅 本文约11000字,建议 ...

  8. AI洞观 | 一文读懂英特尔的AI之路

    AI洞观 | 一文读懂英特尔的AI之路 https://mp.weixin.qq.com/s/E9NqeywzQ4H2XCFFOFcKXw 11月13日-14日,英特尔人工智能大会(AIDC)在北京召 ...

  9. 一文读懂机器学习中的模型偏差

    一文读懂机器学习中的模型偏差 http://blog.sina.com.cn/s/blog_cfa68e330102yz2c.html 在人工智能(AI)和机器学习(ML)领域,将预测模型参与决策过程 ...

最新文章

  1. Atlas Samples Suse Linux 10.1
  2. 【转】利用matlab生成随机数函数
  3. Keil MDK-ARM下载 安装与和谐教程
  4. rest framework错误笔记——身份验证和权限
  5. 【算法图解|5】javaScript求两个数的最大公约数
  6. 剑指offer(7)斐波那契数列
  7. 三款旗舰手机、四大高端生态新品,Redmi发布K50系列等七大重磅新品
  8. Python envoy 模块源码剖析
  9. A股收盘:深证区块链50指数跌3.80%,爱迪尔等9股涨停
  10. 平昌一中高考2021成绩查询,2019年四川省平昌中学高考喜报
  11. 反射 java 例子 get_Java反射实例
  12. java毕向东学习笔记——day09
  13. 许鹏:从零开始学习,Apache Spark源码走读(一)
  14. java实现数字转英文_Java实现数字转成英文的方法
  15. 计算机大类专业分流问题,2019级计算机大类专业分流实施细则
  16. ColdFusion CGI or Application variables
  17. 计算机考研复习资料推荐(转载)
  18. 驱动开发:Win10内核枚举SSDT表基址
  19. 用Obspy读取segy的文件头并保存到csv数据库
  20. pvpgn mysql d2gs_pvpgn战网命令集

热门文章

  1. java基础-02数据类型
  2. 高仿真机器人助力临床医学发展
  3. 父类、派生类、方法重写、实例化后的执行顺序
  4. 《数学建模:基于R》——1.1 数据的描述性分析
  5. php的SAPI,CLI SAPI,CGI SAPI
  6. 【Cocos得知】技术要点通常的积累
  7. Hadoop1.9安装配置
  8. 你们觉得这个时代好还是父母那个时代好?
  9. 【C语言期末实训】学生学籍管理系统
  10. Sr Software Engineer - Big Data Team