etcd 是强一致性、分布式的key-value存储,为分布式系统或集群提供一种可靠的数据存储方式。它在网络分区期间优雅地处理leader选举,并且可以容忍机器故障,甚至在leader节点中也是如此。etcd 是典型的读多写少存储,在我们实际业务场景中,读一般占据 2/3 以上的请求。
目前较常用的etcd 3.5的性能较v3.1约翻倍提升,如下表所示(官网数据Performance | etcd):
Number of keys | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Target etcd server | Average write QPS | Average latency per request | Average server RSS |
---|---|---|---|---|---|---|---|---|
10,000 | 8 | 256 | 1 | 1 | leader only | 583 | 1.6ms | 48 MB |
100,000 | 8 | 256 | 100 | 1000 | leader only | 44,341 | 22ms | 124MB |
100,000 | 8 | 256 | 100 | 1000 | all members | 50,104 | 20ms | 126MB |
表一 etcd3.5写性能测试数据
Number of requests | Key size in bytes | Value size in bytes | Number of connections | Number of clients | Consistency | Average read QPS | Average latency per request |
---|---|---|---|---|---|---|---|
10,000 | 8 | 256 | 1 | 1 | Linearizable | 1,353 | 0.7ms |
10,000 | 8 | 256 | 1 | 1 | Serializable | 2,909 | 0.3ms |
100,000 | 8 | 256 | 100 | 1000 | Linearizable | 141,578 | 5.5ms |
100,000 | 8 | 256 | 100 | 1000 | Serializable | 185,758 | 2.2ms |
表二 etcd3.5读性能测试数据
etcd 的基础架构:
- Client 层:Client 层包括 client v2 和 v3 两个大版本 API 客户端库
- API 网络层:API 网络层主要包括 client 访问 server 和 server 节点之间的通信协议。一方面,client 访问 etcd server 的 API 分为 v2 和 v3 两个大版本。v2 API 使用 HTTP/1.x 协议,v3 API 使用 gRPC 协议。同时 v3 通过 etcd grpc-gateway 组件也支持 HTTP/1.x 协议,便于各种语言的服务调用。另一方面,server 之间通信协议,是指节点间通过 Raft 算法实现数据复制和 Leader 选举等功能时使用的 HTTP 协议。
- Raft 算法层:Raft 算法层实现了 Leader 选举、日志复制、ReadIndex 等核心算法特性,用于保障 etcd 多个节点间的数据一致性、提升服务可用性等,是 etcd 的基石和亮点。
- 功能逻辑层:etcd 核心特性实现层,如典型的 KVServer 模块、MVCC 模块、Auth 鉴权模块、Lease 租约模块、Compactor 压缩模块等,其中 MVCC 模块主要由 treeIndex 模块和 boltdb 模块组成。
- 存储层:存储层包含预写日志 (WAL) 模块、快照 (Snapshot) 模块、boltdb 模块。其中 WAL 可保障 etcd crash 后数据不丢失,boltdb 则保存了集群元数据和用户写入的数据。
要理解etcd 源码,首先要理解以下内容:
1. Raft协议
可通过动画该协议理解:Raft 分布式共识算法动画演示
(1)Leader 选举
(2)日志复制
(3)数据一致性保证
2. MVCC多版本控制
MVCC 机制正是基于多版本技术实现的一种乐观锁机制.有乐观锁就有悲观锁,两种锁都能保证数据的安全,不存在孰优孰劣,分适用场景。悲观锁比较好理解就是先加锁,加锁既可以是对代码块加锁(如Java的synchronized关键字),也可以是对数据加锁(如MySQL中的排它锁)。
乐观锁实现方式常见的实现方式有两种:CAS(Compare And Swap)机制和版本号机制。
etcd v3存储的逻辑视图是一个扁平的二进制键空间。etcd的键空间可维护多个revision.每个原子的修改操作都会在键空间创建一个新的version,之前version 的所有数据均保持不变。新建一个key,version为1;删除key,version重置为0来结束key的生命周期;对key的每一次修改都会增加其version,因此,key的version在key的一次生命周期中是单调递增的。
但是reversion是一直增加的。boltdb中存储的key是reversion,value是etcd自己的key-value组合,即上面说的。etcdv3会在boltdb中保存每个版本,从而实现多版本机制。新建、更新、删除都会记录一个新的revision.
3. boltdb存储
etcd 数据存储在基于 B+ tree和 mmap 的数据库的 boltdb中,下图是db 文件磁盘布局图。从图中的左边部分你可以看到,文件的内容由若干个 page 组成,一般情况下 page size 为 4KB.
etcdv2的每个key只保留一个value,所以数据库并不大,可以直接放在内存中。但是etcd v3实现了MVCC后,每个key的value都需要保存多个历史版本,内存中存不下,因此使用boltdb将数据存储中磁盘中。
boltdb只提供简单的key/value存储,官网上说是一种GO语言嵌入式键值v数据库。代码精简(小于3KB),非常适合在其上构建更加复杂的数据库功能。BoltDB基本原理是用mmap将磁盘的page映射到内存的page,而操作系统则是通过 COW ( copy - on - write )技术进行 page 管理,通过COW 技术,系统可实现无锁的读写并发,但是无法实现无锁的写写并发,这就注定了这类数据库读性能超高,但写性能一般,因此非常适合于"读多写少"的场景。同时 BoltDB 支持完全可序列化的 ACID 事务。因此最适合作为 etcd 的底层存储引擎。
B+树中键值对的key即revison,是一个2元组(main, sub),其中main是该revison的主版本号,sub是同一个revison的副版本号,用于区分同一个revision的不同key.例如通过etcdctl来写入两条数据put key1 'v1' key2 'v2';然后再更新一下put key1 'v12' key2 'v22';那么boltdb中包含4条数据:
rev={3,0}, key=key1, value='v1'
rev={3,1}, key=key2, value='v2'
rev={4,0}, key=key1, value='v12'
rev={4,1}, key=key2, value='v22'
4. 日志和快照管理
etcd 对数据的持久化,采用的是 binlog (日志,也称为WAL,即 Write - Ahead - Log )加 Snapshot (快照)的方式。
在计算机科学中,预写式日志( Write - Ahead - Log , WAL )是关系数据库系统中用于提供原子性和持久性( ACID 属性中的两个)的一系列技术。在使用 WAL 的系统中,所有的修改在提交之前都要先写入 log 文件中。
□ wal :用于存放预写式日志,其最大的作用是记录整个数据变化的全部历程。在 etcd 中,所有数据的修改在提交前,都要先写入 WAL 中。使用 WAL 进行数据的存储使得 etcd 拥有故障快速恢复和数据回滚这两个重要功能。WAL 文件存储在 etcd 数据目录的 member/wal
子目录中。
□ snap :用于存放快照数据。 etcd 为防止 WAL 文件过多会创建快照, snap 用于存储 etcd 的快照数据状态。生成快照后,etcd 可以删除包含在 Snapshot 中的旧 WAL 文件,从而减少磁盘使用量和加快恢复速度。Snapshot 文件存储在 etcd 数据目录的 member/snap
子目录中。
既然有了 WAL 实时存储所有的变更,那么为什么还需要做快照呢?因为随着使用量的增加, WAL 存储的数据会暴增,为了防止磁盘很快就爆满, etcd 默认每10000条记录做一次快照,做过快照之后的 WAL 文件就可以删除。而通过 API 可以查询的历史 etcd 操作默认为1000条。另外一个场景:如果某个节点故障下线后,又添加了一个小节点,那么向这个新节点拷贝数据时,可以只需要先复制整个快照,然后复制剩下的较少量的 binlog即可。
另外,BoltDB 是 etcd 的底层存储引擎,用于存储实际的数据。读写时都会通过boltdb,WAL和snapshot只是日志副本工具。etcd 启动的时候,会通过 mmap 机制将 db 文件映射到内存,后续可从内存中快速读取文件中的数据。写请求通过 fwrite 和 fdatasync 来写入、持久化数据到磁盘。WAL只在写数据时会用到,snapshot和读写都没关系,是一些场景触发的快照。boltdb通过高效的 B+ 树数据结构提供键值对存储和检索功能,并支持 ACID 特性的事务,保证数据的一致性和完整性。BoltDB 文件存储在 etcd 数据目录的 member
子目录中,通常是 member/snap/db.例如,/var/lib/etcd/member/snap/db.
最后放两张etcd读写流程图便于理解:
etcd读请求执行过程
etcd写请求执行过程
最后,留一个思考题,etcd既然是基于raft协议的,那么会不会出现数据不一致的情况呢?
参考:
1. 杜军 等,《云原生分布式存储基石——etcd深入解析》,机械工业出版社
2. 唐聪,《etcd实战课》,极客时间