系列文章目录
浅谈分布式系统与一致性协议(一)
浅谈分布式系统与一致性协议(二)
浅谈分布式系统与一致性协议(三)
深入浅出之etcd
深入浅出之etcd(二)
etcd版本之v3
etcd之安全性阐述
在数据库领域,并发控制是一个具有挑战性的领驭。常见的并发控制方式包括悲观并发控制,乐观并发控制和多版本并发控制
悲观并发控制
在关系型数据库中,并发控制(又名悲观锁,Pessimistic Concurrency Control ,PCC)是一种并发控制的方法。它可以阻止一个事物以影响其他用户的方式来修改数据。它可以阻止一个事务以影响其他用户的方式来修改数据。如果一个事物执行的操作对某行数据应用了锁,那么只有在这个事务将锁释放后,其他事务才能执行与该锁冲突的操作。悲观并发控制主要用于数据竞争激烈的环境,以及发生并发冲突时使用锁保护数据的成本要低于回滚事务的成本环境中。
乐观并发控制
乐观并发控制(又名乐观锁)也是一种并发控制的方法。它假设多用户并发的事务在处理时彼此之间互不影响,各个事务能够在不产生锁的情况下处理格子影响的那部分数据。在提交数据更新之前,每个事务都会检查在该事务读取数据之后,有没有其他事务又修改了数据。如果其他事务有更新的化,那么提交的事务会发生回滚。
乐观并发控制多用于数据竞争不大,冲突较少的环境。在这种环境下,偶尔发生事务回滚的成本要低于读取数据时锁定数据的成本,因此,这种环境下乐观并发控制可以获得比其他并发控制方法更高的吞吐量
多版本并发控制
多版本并发控制(Multiversion Concurrency Control ,MVCC)并不是一个与乐观并发控制和悲观并发控制对立的概念。它能够与两者很好的结合以增加事务的并发量,目前最流行的SQL数据库MySQL和PostgreSQL都对MVCC进行实现。MVCC每一个写操作都会创建一个新版本的数据,读操作会从有限多个版本的数据中挑选一个最合适的(要么是最新版本,要么是指定版本)的结果返回。通过这种方式,读写操作之间的冲突不再需要收到关注。
为什么选择MVCC
对一个系统进行优化时,相应的思路并不是凭空产生的,而实存在一定的方法论,首先我们应该分析etcd的使用场景,然后才能进行针对性的优化。首先etcd定位是一个分布式,一致的key-value存储,主要用途时共享配置和服务发现,它不是一个类似于ceph那样存储海量数据的存储体系,也不是类似于MySQL这样的SQL数据库。它存储的其实是一些非常重要的元数据,元数据的写操作是非常少的,但是会有很多客户端同时watch这些元数据的变更。也就是说etcd使用场景是一种“读多写少”的场景,etcd的一个key其实不不会频繁变更,但是一旦发生变更,etcd就需要通知监听这个key的所有客户端
因为同一时间可能会存在很多用户连接,那么这段时间一定会存在许多并发问题,比如数据竞争。etcd必须保证并发操作产生的结果是安全的。etcd v2是一个纯内存数据库,整个数据库有一个stop the world的锁,可以通过所锁机制来解决并发带来的数据问题,但是通过锁的方式存在一些缺点:
- 锁的粒度不好控制,每次操作stop the world时都会锁住整个数据库
- 读锁和写锁会互相阻塞(block)
- 如果使用基于锁的隔离机制,并且有一段很长的读事务,那么在这段时间内这个对象就会无法被改写,后面的事务会被阻塞,直到这个事务完成为。这种机制对于并发性能来说影响很大
多版本并发控制(MVCC)则以一种优雅的方式解决了所带来的问题。在MVCC中,每当想要更改或者删除某个数据对象时,DBMS不会在原地删除或者修改这个已有的数据对象本身,而实针对该数据对象创建一个新的版本,这样一来,并发读取操作仍然可以读取老版本的数据,而写数据就可以同时进行。这个模式的好处在于,可以读取操作不再阻塞,事实上根本不需要锁。
etcd v2存储机制实现
etcd v2是一个纯内存数据库,写操作通过Raft复制日志文件,复制成功后将数据写入到内存,整个数据库在内存中是一个简单的树形结构。etcd v2并未实时地将内存中的数据写入到磁盘,持久化是靠快照实现的,具体实现就是将整个内存中的数据复制一份,然后序列化成JSON,写入磁盘,成为一个快照。做快照的时候使用的是复制出来的数据库,客户端读写请求依然落在原始的数据库上,这样的话,做快照的操作不会阻塞客户端的读写请求
etcd v3数据模型
etcd v3可靠的存储不经常更新的数据,并且提供可靠的watch查询。etcd v3与etcd v2不同的是,它支持暴露旧版本的键值来支持高效的快照和watch历史事件。一个持久化的,多版本并发控制的数据模型非常适合etcd v3使用场景——因为如果仅仅维持一个key,一个value的数据模型,那么连续更新就只能保存最后一个value,历史版本无法追溯,而多版本则可以解决这个问题
etcd v3将数据存储在一个多版本的持久化key-value存储里面。值得注意的是,作为key-value存储的etcd 会将数据存储在另一个key-value数据库中。当持久简直存储的值发生变化时,持久化键值保存先前版本的键值对。etcd 后台的键值存储实际是不可变的,etcd操作不会就地更新结构,而实始终生成一个更新之后的结构。发生修改后,key先前的版本的所有制仍然可以访问和watch。为了防止数据存储随时间的推移无限增长,并且为了维护旧版本,etcd可能会压缩(删除)key的旧版本数据
逻辑视图
etcd v3存储的逻辑视图是一个扁平的二进制键空间。该键空间对key有一个此法排序索引,因此此范围查询的成本很低
etcd键空间可能维护很多revision。每个原子修改(例如,一个事务操作可能包含多个操作)都会在键空间上创建一个新的revision,之前revision所有数据均保持不变,旧版本(version)的key仍然可以通过之前的revision进行访问。同样,revision也是被索引的,因此Watcher可以实现高效的范围watch。revision在etcd中可以起到逻辑时钟的作用。revision在集群的声明周期时单调递增的。如果因为节省空间而压缩空间,那么在此revision之前的的revision都会被删除,只保留之后的revision
我们将key创建和删除的过程称为一个生命周期。在etcd中,每个key都可能有多个生命周期,也就是说被创建,删除多次。创建一个key时,如果在当前revision中该key不存在(即之前没有创建过),那么它的revision就会被设置为1.删除key就会生成一个key的墓碑。可以通过将其version重置0来结束key的生命周期。对key的每一次修改都会增加其version,因此,key的version在key的一次生命周期中是单调递增的。
revision是集群状态的版本号,存储状态每一次更新(例如,写,删除,事务等)都会让revision值加1。version特指etcd键空间某个key从创建开始被修改的次数,即KeyValue.Version。
物理视图
etcd将物理数据存储为一棵持久化B+树中的键值对。为了高效,每个revision的存储状态都只包含相较于之前revision的增量。一个revision可能对应于树中的多个key
B+树中的键值对的key即revision,revision是一个2元组(main,sub),其中main是该revision的主版本号,sub是同一revision的副版本号,其用于区分同一个revision的不同key。B+树中的键值对的value包含了相对于之前revision的修改,即相对于之前revision的一个增量
B+树按key的字典字节序进行排序。这样,etcd v3对revision增量的范围查询(range query,即从某个revision到另一个revision)会很快——因为我们已经记录了从一个特定revision到其他revision的修改量。etcd v3的压缩操作会删除过时的键值对
etcd v3还在内存中维护一个基于B树的二级索引来加快对key的范围查询。该B树索引的key是向用户暴露的etcd v3存储的key,而该B树索引的value则是一个指向上文谈论的持久化B+树的增量的指针。etcd v3的压缩操作会删除指向B树索引的无效指针
etcd v3的MVCC实现
etcd v2二点每一个key都只保留一个value,所以数据库并不大,可以直接放到内存中。但是etcd v3实现了每一个MVCC以后 ,每一个key的value都会保存,即存在多个历史版本。对此一个自然的解决方案就是将数据存储在磁盘中。etcd v3当前使用BoltDB将数据存储到磁盘中。
BoltDB是根据Howard Chu的LMDB项目开发的一个存粹的Go语言版的key/value存储。它的目标是为项目提供一个简单,高效,可嵌入式的,可序列化的键/值数据库,而不是要求像一个MySQL那样完整的数据库服务器。BoltDB还是一个支持事务的键值存储etcd的事务就是基于BoltDB的事务实现的。
BoltDB只提供简单的key/value存储,没有其他特性,也因此BoltDB可以做到代码精简,质量高,非常适合yiBoltDB为基础在其之上构建更加复杂的数据库功能。由于BoltDB的设计适合“读多写少”的场景
etcd在BoltDB中存储的key是revision,value是etcd自己的key-value组合,也就是说etcd会在BoltDB中保存每个版本,从而实现多版本机制
revision主要有两部分组成,第一部分是main rev,每操作以此事务就加一第二部分是sub rev,同一事务每进行以此操作就加1。这样的实现方式带来的问题就是整个数据库会越来越大,最终超过磁盘容量。因此MVCC还需要定期删除老的版本,etcd提供了命令行工具以及配置选项,供用户手动删除老版本数据操作为数据压缩
了解了etcd v3的磁盘存储之后,可以看到想要从BoltDB查询数据,必须通过revision,但是客户端都通过key来查询value的,所以etcd v3在内存中维护一个kvindex,保存的就是key与revision的映射关系,用来加速查询。kvindex,是基于Google开源的Golang的B树实现的,也就是前文提到的etcd v3在内存中维护的二级索引。这样客户端通过key来查询value时候,会先在kvindex中查询这个key的所有revision,然后通过revision从这BoltDB中查询数据。
之前讲过,etcd v2的数据持久化机制是依靠定期做快照来实现的,即将内存中整个数据库都复制一份,然后序列化到磁盘,做快照会对磁盘造成较大的压力。而etcd v3实现了MVCC之后,数据是实时写入BoltDB数据库的,数据持久化其实已经分摊每次对key的写请求上了,因此etcd v3就不需要做快照了。
需要注意的是,etcd v3虽然不需要做快照,但是需要定期对数据库进行压缩,因为磁盘的容量是有限的,不可能保存key的所有历史版本的value。