1-etcd快速入门
.1 认识etcd
.1.1 etcd 概念
从哪里说起呢?官网第一个页面,有那么一句话:”A distributed, reliable key-value store for the most critical data of a distributed system”。也就是说 etcd 是一个分布式、可靠 key-value 存储的分布式系统。当然,它不仅仅用于存储,还提供共享配置及服务发现。
.1.2 etcd vs Zookeeper
提供配置共享和服务发现的系统比较多,其中最为大家熟知的是 Zookeeper,而 etcd 可以算得上是后起之秀了。在项目实现、一致性协议易理解性、运维、安全等多个维度上,etcd 相比 zookeeper 都占据优势。
本文选取 Zookeeper 作为典型代表与 etcd 进行比较,而不考虑 Consul 项目作为比较对象,因为 Consul 的可靠性和稳定性还需要时间来验证(项目发起方自身服务并未使用 Consul,自己都不用)。
- 一致性协议:
etcd使用Raft协议,Zookeeper使用ZAB(类PAXOS协议),前者容易理解,方便工程实现; - 运维方面:
etcd方便运维,Zookeeper难以运维; - 数据存储:
etcd多版本并发控制(MVCC)数据模型 , 支持查询先前版本的键值对 - 项目活跃度:
etcd社区与开发活跃,Zookeeper 感觉已经快死了; - API:
etcd提供HTTP+JSON,gRPC 接口,跨平台跨语言;Zookeeper需要使用其客户端; - 访问安全方面:
etcd支持HTTPS访问,Zookeeper在这方面缺失;
…
.1.3 etcd 应用场景
etcd 比较多的应用场景是用于服务发现。
服务发现 (Service Discovery) 要解决的是分布式系统中最常见的问题之一,即在同一个分布式集群中的进程或服务如何才能找到对方并建立连接。和 Zookeeper 类似,etcd 有很多使用场景,包括:
- 配置管理
- 服务注册发现
- 选主
- 应用调度
- 分布式队列
- 分布式锁
.1.4 etcd 工作原理
.1.4.1 如何保证一致性
etcd 使用 raft 协议来维护集群内各个节点状态的一致性。简单说,etcd 集群是一个分布式系统,由多个节点相互通信构成整体对外服务,每个节点都存储了完整的数据,并且通过 Raft 协议保证每个节点维护的数据是一致的。
每个 etcd 节点都维护了一个状态机,并且,任意时刻至多存在一个有效的主节点。主节点处理所有来自客户端写操作,通过 Raft 协议保证写操作对状态机的改动会可靠的同步到其他节点。
.1.4.2 数据模型
etcd 的设计目标是用来存放非频繁更新的数据,提供可靠的 Watch 插件,它暴露了键值对的历史版本,以支持低成本的快照、监控历史事件。这些设计目标要求它使用一个持久化的、多版本的、支持并发的数据数据模型。
当 etcd 键值对的新版本保存后,先前的版本依然存在。从效果上来说,键值对是不可变的,etcd 不会对其进行 in-place 的更新操作,而总是生成一个新的数据结构。为了防止历史版本无限增加,etcd 的存储支持压缩(Compact)以及删除老旧版本。
.1.4.2.1 逻辑视图
从逻辑角度看,etcd 的存储是一个扁平的二进制键空间,键空间有一个针对键(字节字符串)的词典序索引,因此范围查询的成本较低。
键空间维护了多个修订版本(Revisions),每一个原子变动操作(一个事务可由多个子操作组成)都会产生一个新的修订版本。在集群的整个生命周期中,修订版都是单调递增的。修订版同样支持索引,因此基于修订版的范围扫描也是高效的。压缩操作需要指定一个修订版本号,小于它的修订版会被移除。
一个键的一次生命周期(从创建到删除)叫做 代 (Generation),每个键可以有多个代。创建一个键时会增加键的版本(version),如果在当前修订版中键不存在则版本设置为1。删除一个键会创建一个**墓碑(Tombstone)**,将版本设置为0,结束当前代。每次对键的值进行修改都会增加其版本号 — 在同一代中版本号是单调递增的。
当压缩时,任何在压缩修订版之前结束的代,都会被移除。值在修订版之前的修改记录(仅仅保留最后一个)都会被移除。
.1.4.2.2 物理视图
etcd 将数据存放在一个持久化的 B+ 树中,出于效率的考虑,每个修订版仅仅存储相对前一个修订版的数据状态变化(Delta)。单个修订版中可能包含了 B+ 树中的多个键。
键值对的键,是三元组(major,sub,type):
- major:存储键值的修订版
- sub:用于区分相同修订版中的不同键
- type:用于特殊值的可选后缀,例如 t 表示值包含墓碑
键值对的值,包含从上一个修订版的 Delta。B+ 树 —— 键的词法字节序排列,基于修订版的范围扫描速度快,可以方便的从一个修改版到另外一个的值变更情况查找。
etcd 同时在内存中维护了一个 B 树索引,用于加速针对键的范围扫描。索引的键是物理存储的键面向用户的映射,索引的值则是指向 B+ 树修该点的指针。
.1.5 etcd 读写性能
按照官网给出的数据, 在 2CPU,1.8G 内存,SSD 磁盘这样的配置下,单节点的写性能可以达到 16K QPS, 而先写后读也能达到 12K QPS。这个性能还是相当可观。
.1.6 etcd 术语

.2 安装和运行
.2.1 构建
需要 Go 1.9 以上版本:
1 | cd $GOPATH/src |
使用 build 脚本构建会在当前项目的 bin 目录生产 etcd 和 etcdctl 可执行程序。
etcd就是etcd server了,etcdctl主要为etcd server提供了命令行操作。
.2.2 静态集群
如果 Etcd 集群成员是已知的,具有固定的 IP 地址,则可以静态的初始化一个集群。
每个节点都可以使用如下环境变量:
1 | ETCD_INITIAL_CLUSTER="radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380" |
或者如下命令行参数
1 | --initial-cluster radon=http://10.0.2.1:2380,neon=http://10.0.3.1:2380 |
来指定集群成员。
.2.3 初始化集群
完整的命令行示例:
1 | etcd --name radon --initial-advertise-peer-urls http://10.0.2.1:2380 |
.2.4 使用TLS
Etcd 支持基于 TLS 加密的集群内部、客户端-集群通信。每个集群节点都应该拥有被共享 CA 签名的证书:
1 | # 密钥对、证书签名请求 |
etcd –name radon –initial-advertise-peer-urls https://10.0.2.1:2380
–listen-peer-urls https://10.0.2.1:2380
–listen-client-urls https://10.0.2.1:2379,https://127.0.0.1:2379
–advertise-client-urls https://10.0.2.1:2380
所有以-initial-cluster开头的选项,在第一次运行(Bootstrap)后都被忽略
–initial-cluster-token etcd.gmem.cc
–initial-cluster radon=https://10.0.2.1:2380,neon=https://10.0.3.1:2380 # 指定集群成员列表
–initial-cluster-state new # 初始化新集群时使用
–initial-cluster-state existing # 加入已有集群时使用
客户端TLS相关参数
–client-cert-auth
–trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt
–cert-file=/opt/etcd/cert/radon.crt
–key-file=/opt/etcd/cert/radon.key
集群内部TLS相关参数
–peer-client-cert-auth
–peer-trusted-ca-file=/usr/share/ca-certificates/GmemCA.crt
–peer-cert-file=/opt/etcd/cert/radon.crt
–peer-key-file=/opt/etcd/cert/radon.key
1 |
|
$ etcdctl put foo bar
OK
1 |
|
foo = bar
foo1 = bar1
foo2 = bar2
foo3 = bar3
a = 123
b = 456
z = 789
1 |
|
$ etcdctl get foo
foo // key
bar // value
1 |
|
$ etcdctl get foo –print-value-only
bar
1 |
|
$ etcdctl get foo –hex
\x66\x6f\x6f
\x62\x61\x72
1 |
|
$ etcdctl get foo foo3
foo
bar
foo1
bar1
foo2
bar2
1 |
|
$ etcdctl get –prefix foo
foo
bar
foo1
bar1
foo2
bar2
foo3
bar3
1 |
|
$ etcdctl get –limit=2 –prefix foo
foo
bar
foo1
bar1
1 |
|
$ etcdctl get –from-key b
b
456
z
789
1 |
|
foo = bar # revision = 2
foo1 = bar1 # revision = 3
foo = bar_new # revision = 4
foo1 = bar1_new # revision = 5
1 |
|
$ etcdctl get –prefix foo # 访问最新版本的key
foo
bar_new
foo1
bar1_new
$ etcdctl get –prefix –rev=4 foo # 访问第4个版本的key
foo
bar_new
foo1
bar1
$ etcdctl get –prefix –rev=3 foo # 访问第3个版本的key
foo
bar
foo1
bar1
$ etcdctl get –prefix –rev=2 foo # 访问第3个版本的key
foo
bar
$ etcdctl get –prefix –rev=1 foo # 访问第1个版本的key
1 |
|
foo = bar
foo1 = bar1
foo3 = bar3
zoo = val
zoo1 = val1
zoo2 = val2
a = 123
b = 456
z = 789
1 |
|
$ etcdctl del foo
1
1 |
|
$ etcdctl del –prev-kv zoo
1
zoo
val
1 |
|
$ etcdctl del foo foo9
2
1 |
|
$ etcdctl del –prefix zoo
2
1 |
|
$ etcdctl del –from-key b
2
1 |
|
$ etcdctl watch foo
1 |
|
$ etcdctl put foo 123
OK
$ etcdctl put foo 456
OK
$ ./etcdctl del foo
1
1 |
|
$ etcdctl watch foo
PUT
foo
123
PUT
foo
456
DELETE
foo
1 |
|
$ etcdctl lock mutex1
mutex1/326963a02758b52d
1 |
|
$ etcdctl lock mutex1
1 |
|
$ etcdctl lock mutex1
mutex1/694d6ee9ac069436
1 |
|
$ etcdctl put user frank
OK
$ ./etcdctl txn -i
compares:
value(“user”) = “frank”
success requests (get, put, del):
put result ok
failure requests (get, put, del):
put result failed
SUCCESS
OK
$ etcdctl get result
result
ok
1 |
|
$ etcdctl compact 5
compacted revision 5
$ etcdctl get –rev=4 foo
Error: etcdserver: mvcc: required revision has been compacted
1 |
|
$ etcdctl lease grant 30
lease 694d6ee9ac06945d granted with TTL(30s)
$ etcdctl put –lease=694d6ee9ac06945d foo bar
OK
1 |
|
$ etcdctl lease revoke 694d6ee9ac06945d
lease 694d6ee9ac06945d revoked
$ etcdctl get foo
1 |
|
$ etcdctl lease grant 10
lease 32695410dcc0ca06 granted with TTL(10s)
1 |
|
$ etcdctl lease keep-alive 32695410dcc0ca06
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
lease 32695410dcc0ca06 keepalived with TTL(10)
…
1 |
|
$ etcdctl lease grant 200
lease 694d6ee9ac06946a granted with TTL(200s)
$ etcdctl put demo1 val1 –lease=694d6ee9ac06946a
OK
$ etcdctl put demo2 val2 –lease=694d6ee9ac06946a
OK
1 |
|
$ etcdctl lease timetolive 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(178s)
1 |
|
$ etcdctl lease timetolive –keys 694d6ee9ac06946a
lease 694d6ee9ac06946a granted with TTL(200s), remaining(129s), attached keys([demo1 demo2])
1 |
|
这里我们先建立一个 etcd client,然后把它的 key API 放进 master 里面,这样我们以后只需要通过这个 API 来跟 etcd 进行交互。 Endpoints 是指 etcd 服务器们的地址,如 ”http://127.0.0.1:2379“ 等。
go master.WatchWorkers() 这一行启动一个 Goroutine 来监控节点的情况。下面是 WatchWorkers 的代码:
1 | func (master *Master) WatchWorkers() { |
worker 这边也跟 master 类似,保存一个 etcd KeysAPI,通过它与 etcd 交互,然后用 heartbeat 来保持自己的状态,在 heartbeat 定时创建租约,如果租用失效,master 将会收到 delete 事件。代码如下:
1 | func NewWorker(name, IP string, endpoints []string) *Worker { |
启动的时候需要有多个 worker 节点(至少一个)和一个 master 节点,所以我们在启动程序的时候,可以传递一个 “role” 参数。代码如下:
1 | var role = flag.String("role", "", "master | worker") |
项目地址: https://github.com/chapin666/etcd-service-discovery
.5 总结
- etcd 默认只保存 1000 个历史事件,所以不适合有大量更新操作的场景,这样会导致数据的丢失。
- etcd 典型的应用场景是配置管理和服务发现,这些场景都是读多写少的。
- 相比于
zookeeper,etcd使用起来要简单很多。不过要实现真正的服务发现功能,etcd还需要和其他工具(比如registrator、confd等)一起使用来实现服务的自动注册和更新。










