ETCD 原理剖析
ETCD

etcd 官方地址
https://etcd.io/etcd 是 CoreOS 公司, 最初用于解决集群管理系统中 OS 升级时的分布式并发控制、配置文件的存储于分发等问题。
- etcd 被设计为提供高可用、强一致性的小型
key/value数据存储服务。
- etcd 被设计为提供高可用、强一致性的小型
etcd 目前已经隶属于
CNCF (Cloud Native Computing Foundation)基金会, 被包含 AWS 、Google、Microsoft、Alibaba 等大型互联网公司广泛使用。etcd 最早在 2013年6月份 于
github中开源。etcd 于 2014年6月份 正式被
kubernetes使用, 用于存储kubernetes集群中的元数据。etcd 于 2015年2月份 发布 2.0 版本, 正式支持分布式协议 Raft, 并支持 1000/s 的并发
writes。etcd 于 2017年1月份 发布 3.1 版本, 全面优化了
etcd, 新的API、重写了强一致性(read)读的方法,并提供了 gRPC Proxy 接口, 以及大量gc的优化。etcd 于 2018年11月 加入 CNCF 基金会。
etcd 于 2019年 发布 etcd v3.4 该版本又
Google、AWS、Alibaba等大公司联合打造的一个版本。将进一步优化稳定性以及性能。
Etcd 架构与内部机制
Etcd 术语简介
| 名称 | 含义 |
|---|---|
| Raft | etcd 使用的一致性算法 |
| WAL | 预写Log, 持久化数据, 防止节点重启等导致数据丢失 |
| Snapshot | 快照, 数据更新超过阈值时, 会通过快照的方式来压缩 WAL 文件大小 |
| MVCC | 多版本并发控制 |
| DB | boltdb/bboltdb, 实际存储 etcd v3 的数据 |
| Revision | 版本号, 作为 etcd 数据的逻辑时钟 |
| Auth revision | 鉴权操作所用的版本号, 为了避免 TOCTOU 问题引入 |
| Propose | 发起一次 Raft 请求提案 |
| Committed | 一半以上的节点同意这次请求后的状态, 此时数据可以被应用层 apply |
| Apply | 应用层实际将 Committed 的数据应用到 DB |
| Compact | 压缩历史版本数据 |
| Defrag | 碎片整理, 压缩 etcd 的 db 存储大小 |
| Endpoint | 客户端指定的 etcd 访问地址 |
| Node | 组成 etcd 集群的节点 |
| Term | Leader 任期, 每进行一次 leader 选举 Term 会增加 1 |
| Index | 单调递增, 每次经过 Raft 模块发起变更操作时由 leader 增加 |
| CommittedIndex | 经过 Raft 协议同意提交的数据 Index |
| AppliedIndex | 已经被应用层应用的 Index |
| ConsistentIndex | 为保证不重复 Apply 同一条数据引入, 保证 Apply 操作的幂等性 |
| ReadIndex | 通过 Raft 模块获取 leader 当前的 committedIndex |
etcd 基础概念
etcd 是一个
分布式、强一致性可靠、key/value存储系统。 它主要用于存储分布式系统中的 关键数据。etcd
key/value存储是按照 有序key排列的, 可以顺序遍历。因为
key有序, 所以etcd支持按目录结构高效遍历。支持复杂事务, 提供类似
if ... then ... else ...的事务能力。基于租约机制实现
key的TTL过期。
etcd 包含二种状态:
LeaderFolloweretcd 集群通常由 奇数(最低3)个 etcd 组成。
集群中多个 etcd 通过
Raft consensus algorithm算法进行协同, 多个 etcd 会通过Raft算法选举出一个Leader, 由Leader节点进行数据同步, 以及数据分发。etcd通过boltdb持久化存储数据。当
Leader出现故障时, 集群中的 etcd 会投票选举出另一个Leader, 并重新进行数据同步以及数据分发。客户端从任何一个 etcd 都可以进行
读/写操作。在 etcd 集群中 有一个关键概念
quorum、quorum = ( n + 1 ) / 2, 也就是说超过集群中半数节点组成的一个团体。 集群中可以容忍故障的数量, 如(3 + 1) / 2 = 1可以容忍的故障数为1台。在 etcd 集群中 任意两个 quorum 的成员之间一定会有交集, 只要有任意一个
quorum存活, 其中一定存在某一个节点它包含 etcd 集群中最新的数据, 基于这种假设,Raft一致性算法就可以在一个 quorum 之间采用这份最新的数据去完成数据的同步。quorum: 在Raft中超过一半以上的人数就是法定人数。

etcd 提供了如下
APIPut(key, value)增加Delete(key)删除Get(key)/Get(keyFrom, keyEnd)查询Watch(key)/Watch(key前缀)事件监听, 监听key的变化Transactions(if / then / else ops).Commit()事务操作, 指定某些条件, 为true时执行其他操作。Leasesi Grant / Revoke / KeepAlive
etcd 写操作流程
1.
etcd任一节点的etcd Server模块收到Client写请求- 1.1. 如果是
follower节点, 会先通过Raft模块将请求转发至leader节点处理。
- 1.1. 如果是
2.
etcd Server将请求封装为Raft请求, 然后提交给Raft模块处理。3.
leader通过Raft协议与集群中follower节点进行交互, 将消息复制到follower节点, 于此同时, 并行将日志持久化到WAL。4.
follower节点对该请求进行响应, 回复自己是否同意该请求。5. 当集群中超过半数节点
((n/2)+1 members )同意接收这条日志数据时, 表示该请求可以被Commit,Raft模块通知etcd Server该日志数据已经Commit, 可以进行Apply。6. 各个节点的
etcd Server的applierV3模块异步进行Apply操作, 并通过MVCC模块写入后端存储BoltDB。7. 当
client所连接的节点数据apply成功后, 会返回给客户端apply的结果。
etcd 读操作流程
1.
etcd任一节点的etcd Server模块收到客户端读请求(Range 请求)。2. 判断读请求类型, 如果是串行化读
(serializable)则直接进入Apply流程。3. 如果是线性一致性读
(linearizable), 则进入Raft模块。4.
Raft模块向leader发出ReadIndex请求, 获取当前集群已经提交的最新数据Index。5. 等待本地
AppliedIndex大于或等于ReadIndex获取的CommittedIndex时, 进入Apply流程。6.
Apply流程: 通过Key名从KV Index模块获取Key最新的Revision, 再通过Revision从BoltDB获取对应的Key和Value。
etcd 数据版本号机制
etcd 的数据版本号机制非常重要
term: 全局单调递增 (64bits),term表示整个集群中leader的任期, 当集群中发生leader切换, 如:leader节点故障、leader网络故障、整个集群重启都会发生leader切换, 这个时候term = term + 1。revision: 全局单调递增 (64bits),revision表示在整个集群中数据变更版本号, 当集群中数据发生变更, 包括创建、修改、删除的时候,revision = revision + 1。Key/Vaule:Create_revision: 表示在当前Key/Value中在整个集群数据中创建时(revision)的版本号。每个Key/Value都有一个Create_revision。mod_revision: 表示当前Key/Value等于当前修改时的全局的版本数 (revision) 既mod_version = 当前 revision。version: 表示当前Key/Value被修改了多少次。

实际例子操作
- 查看 key 的相关版本信息
| |
etcd leader 选举机制
集群选举
Leader需要半数以上节点参与节点
revision版本最大的允许选举为Leader节点中
revision相同, 则term越大的允许选举为Leader
etcd mvcc && watch
mvcc: 全称Multi-Version Concurrency Control即多版本并发控制。mvcc: 是一种并发控制的方法, 一般在数据库管理系统中, 实现对数据库的并发访问。
在
etcd中, 支持对同一个Key发起多次数据修改。因为已经知道每次数据修改都对应一个版本号(mod_revision), 多次修改就意味着一个key中存在多个版本, 在查询数据的时候可以通过不指定版本号查询Get key, 这时etcd会返回该数据的最新版本。当我们指定一个版本号查询数据后Get --rev=1 Key, 可以获取到一个Key的历史版本。在
watch的时候指定数据的版本, 创建一个watcher, 并通过这个watcher提供的一个数据管道, 能够获取到指定的revision之后所有的数据变更。如果指定的revision是一个旧版本, 可以立即拿到从旧版本到当前版本所有的数据更新。并且,watch的机制会保证etcd中, 该Key的数据发生后续的修改后, 依然可以从这个数据管道中拿到数据增量的更新。在
etcd中 所有的数据都存储在一个b + tree中。b + tree是保存在磁盘中, 并通过mmap的方式映射到内存用来查询操作。mmap是一种内存映射文件的方法, 即将一个文件或者其对象映射到进程的内存地址空间, 实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。
b + tree维护着revision到value的映射关系。也就是说当指定revision查询数据的时候, 就可以通过该b + tree直接返回数据。当我们通过watch来订阅数据的时候, 也可以通过这个b + tree维护的revision到value映射关系, 从而通过指定的revision开始遍历这个b + tree,拿到所有的数据更新。
在
etcd内部还维护着另外一个b + tree。它管理着key到revision的映射关系。当需要查询Key对应数据的时候, 会通过etcd内部的b + tree, 将key翻译成revision。再通过磁盘中的b + tree的revision获取到对应的value。在
etcd中 一个数据是存在多个版本的。在
etcd持续运行过程中会不断的发生修改, 意味着etcd中内存及磁盘的数据都会持续增长。这对资源有限的场景来说是无法接受的。因此在etcd中会周期性的运行一个Compaction的机制来清理历史数据。 对于一个Key的历史版本数据, 可以选择清理掉。
etcd mini-transactions
- etcd 的
transaction机制比较简单, 基本可以理解为一段if else程序, 在if中可以提供多个操作。
| |
在
etcd内部会保证整个事务操作的原子性。也就是说If操作所有的比较条件, 其看到的视图, 一定是一致的。同时它能够确保在争执条件中, 多个操作的原子性不会出现etc仅执行了一半的情况。通过
etcd提供的事务操作, 我们可以在多个竞争中去保证数据读写的一致性, 比如Kubernetes, 它正是利用了etcd的事务机制, 来实现多个Kubernetes API server对同样一个数据修改的一致性。

Kubernetes在使用etcd做为元数据存储后元数据实现高可用, 无单点故障
系统无状态, 故障修复相对容易
系统可水平扩展, 横向提升性能以及容量
简化整体架构, 降低维护的复杂度
etcd lease
lease是分布式系统中一个常见的概念, 用于代表一个租约。通常情况下, 在分布式系统中需要去检测一个节点是否存活的时候, 就需要租约机制。etcd通过CreateLease(时间)来创建一个租约。如:lease = CreateLease(10s)创建一个 10s 过期的一个租约。通过 Put(key1, value1, lease) 可以将之前创建的 租约绑定到
key1中(同一个租约可以绑定多个key)。当租约过期时,etcd会自动清理key1对应的value1值。KeepAlive方法: 可以续约租期。- 比如说需要检测分布式系统中一个进程是否存活, 那么就会在这个分布式进程中去访问
etcd并且创建一个租约, 同时在该进程中去调用KeepAlive的方法, 与etcd保持一个租约不断的续约。当进程挂掉了, 租约在进程挂掉的一段时间就会被etcd自动清理掉。所以可以通过这个机制来判定节点是否存活。
- 比如说需要检测分布式系统中一个进程是否存活, 那么就会在这个分布式进程中去访问
etcd 性能优化
etcd 性能分析

Etcd Server 硬件需求
etcd硬件需求 (如下为官方提供的参考数据)小型集群少于100个客户端, 每秒少于200个请求, 存储数据少于100MB。如: 少于50节点的Kubernetes集群。
| CPU | 内存 | 最大并发 | 磁盘吞吐量 |
|---|---|---|---|
| 2核 | 4G | 1500IOPS | 50MB/S |
中型集群少于500个客户端, 每秒少于1000个请求, 存储数据少于500MB。如: 少于250节点的Kubernetes集群。
| CPU | 内存 | 最大并发 | 磁盘吞吐量 |
|---|---|---|---|
| 4核 | 16G | 5000IOPS | 100MB/S |
大型集群少于1500个客户端, 每秒少于10000个请求, 存储数据少于1GB。 如: 少于1000节点的Kubernetes集群。
| CPU | 内存 | 最大并发 | 磁盘吞吐量 |
|---|---|---|---|
| 8核 | 32G | 8000IOPS | 200MB/S |
超大型集群大于1500个客户端, 每秒处理大于10000个请求, 存储数据大于1GB。如: 少于3000个节点的Kubernetes集群
| CPU | 内存 | 最大并发 | 磁盘吞吐量 |
|---|---|---|---|
| 16核 | 64G | 15000IOPS | 300MB/S |
etcd 运维
etcd 集群数据备份
- 准备一些测试数据
| |
备份 Etcd 快照
snapshot save命令备份。在集群状态正常的情况下对任意一个节点进行数据备份都可以。
etcd v3.4只能一个节点进行数据备份。Snapshot can only be requested from one etcd node
在使用
ETCD API3 的情况下,只会备份 3 的数据。
| |
- 删除创建的数据
| |
etcd 集群数据恢复
恢复集群数据
etcdctl snapshot restore命令恢复恢复数据, 需要将快照覆盖到所有
ETCD集群节点。
节点-1--data-dir指定恢复数据的目录, 如果数据目录存在会报错, 可以选择删除原来的, 或者备份为新的目录。--name每个节点的name都不相同initial-cluster配置为集群所有节点的 2380 内部通讯端口initial-cluster-token必须配置与原来相同initial-advertise-peer-urls配置为恢复节点的IP:2380
| |
节点-2--data-dir指定恢复数据的目录, 如果数据目录存在会报错, 可以选择删除原来的, 或者备份为新的目录。--name每个节点的name都不相同initial-cluster配置为集群所有节点的 2380 内部通讯端口initial-cluster-token必须配置与原来相同initial-advertise-peer-urls配置为恢复节点的IP:2380
| |
节点-3--data-dir指定恢复数据的目录, 如果数据目录存在会报错, 可以选择删除原来的, 或者备份为新的目录。--name每个节点的name都不相同initial-cluster配置为集群所有节点的 2380 内部通讯端口initial-cluster-token必须配置与原来相同initial-advertise-peer-urls配置为恢复节点的IP:2380
| |
重启所有
etcd服务- 如上恢复数据到新的数据目录
--data-dir
- 如上恢复数据到新的数据目录
| |
- 备份原来的数据目录
| |
- 使用新的数据
| |
| |
- 查看节点的情况
| |
- 查询恢复数据
| |