大家好,我是此林。
etcd 是一个高可用的键值对存储系统,常用于分布式系统中保存配置、服务发现和协调信息。它是 CNCF 旗下的项目之一,也是 Kubernetes 的核心组件之一,用来存储集群状态。
可以说,云原生场景下经常使用到 etcd,一般我们会把 etcd 作为注册中心来使用。
1. etcd 是什么?
etcd 单词可以拆分成 etc + d,etc 就是 Linux 里的 /etc 目录,d 就是 distribution,代表分布式。
-
使用 Raft 一致性算法保证分布式系统数据一致性(强一致性、保障了CP)
-
提供 键值对存储接口(KV)
-
支持 Watch 机制(监听键变化)
-
提供 Lease / TTL(用于临时键,比如服务注册)
2. etcd 常见使用场景
场景 | 说明 |
---|---|
配置中心 | 分布式系统中的配置动态变更通知 |
服务发现 | 服务注册自己的信息,客户端从 etcd 查询 |
分布式锁 | 利用 etcd 的原子操作实现分布式锁 |
领导选举 | 通过 Lease 和 Watch 实现 leader 的自动选举和变更通知 |
Kubernetes 存储 | K8s 所有 API 对象最终存储在 etcd 中 |
3. etcd 安装
这里使用版本 3.5.21。
Release v3.5.21 · etcd-io/etcd
下载完成后,上传到 Linux 服务器,使用下面的命令解压。
tar -zxvf etcd-v3.5.21-linux-amd64.tar.gz
进入目录后,其实最核心的也就是三个二进制可执行文件,etcd(主服务Server端),etcdctl(etcd客户端连接工具),etcdutl(etcd工具集)。
其他都是些文档。
把这三个二进制文件复制到 /usr/local/bin 里。
# 创建目标目录(如果不存在)
sudo mkdir -p /usr/local/bin# 赋予可执行权限
chmod +x etcd etcdctl etcdutl# 复制到系统路径
sudo cp etcd etcdctl etcdutl /usr/local/bin/
配置环境变量,/usr/local/bin 加入 /etc/profile 文件末尾。
vim /etc/profile
文件末尾有这一行就行。
export PATH=$PATH:/usr/local/bin
使环境变量生效。
source /etc/profile
4. 快速开始
输入 etcd 启动 etcd 服务端。
打开另一个终端,输入命令:
etcdctl put greeting "hello, etcd"etcdctl get greeting
当然,也可以格式化输出为 json。
etcdctl --endpoints=$ENDPOINTS --write-out="json" get foo
输出:
{"header":{"cluster_id":289318470931837780,"member_id":14947050114012957595,"revision":3,"raft_term":4, "kvs":[{"key":"Zm9v","create_revision":2,"mod_revision":3,"version":2,"value":"SGVsbG8gV29ybGQh"}]}}
看到这里,恭喜你,已经 etcd 入门了!
5. 角色用户配置
下面是官方文档给出的配置示例,不急,我们慢慢来看。
1. 设置环境变量
export ETCDCTL_API=3
# 设置使用 etcdctl 的 v3 API(新版 etcd 默认使用 v3)
# 这个只在当前shell生效,重启会失效,永久设置要在/etc/profile里ENDPOINTS=localhost:2379
# 设置 etcd 访问地址(如果是集群也可写多个:127.0.0.1:2379,127.0.0.2:2379)
2. 创建 root 角色并查看
etcdctl --endpoints=${ENDPOINTS} role add root
# 添加一个名为 root 的角色etcdctl --endpoints=${ENDPOINTS} role get root
# 查看 root 角色的权限信息(刚创建时是空的)
可以发现,root 角色有键值对读写权限。
3. 创建 root 用户并绑定 root 角色
etcdctl --endpoints=${ENDPOINTS} user add root
# 添加一个用户 root(执行后会提示你输入密码)etcdctl --endpoints=${ENDPOINTS} user grant-role root root
# 给用户 root 分配 root 角色etcdctl --endpoints=${ENDPOINTS} user get root
# 查看用户 root 的角色信息
4. 创建 role0 角色并设置权限,创建 user0 用户并赋予角色
etcdctl --endpoints=${ENDPOINTS} role add role0
# 添加一个角色 role0etcdctl --endpoints=${ENDPOINTS} role grant-permission role0 readwrite foo
# 给角色 role0 授予对 key "foo" 的读写权限(readwrite)
# 你也可以设定范围权限,例如:--prefixetcdctl --endpoints=${ENDPOINTS} user add user0
# 添加用户 user0,会提示输入密码etcdctl --endpoints=${ENDPOINTS} user grant-role user0 role0
# 将角色 role0 分配给用户 user0
这里,我们使用 grant-permission 对 role0 的权限进行了修改,role0 只有对 key "foo" 才有读写权限
5. 启用认证
etcdctl --endpoints=${ENDPOINTS} auth enable
# 启用 etcd 的认证功能
# 启用后,所有的读写请求必须提供用户名和密码,否则会被拒绝
6. 权限验证测试
etcdctl --endpoints=${ENDPOINTS} --user=user0:123 put foo bar
# 使用 user0 用户(密码是 123)写入 key "foo",写入成功(因为具有读写权限)etcdctl --endpoints=${ENDPOINTS} get foo
# 不带用户名直接访问 key "foo",失败(因为认证开启了,默认匿名用户无权限)etcdctl --endpoints=${ENDPOINTS} --user=user0:123 get foo
# 使用 user0 读取 key "foo",成功(因为拥有读权限)etcdctl --endpoints=${ENDPOINTS} --user=user0:123 get foo1
# 使用 user0 读取 key "foo1",失败(权限只对 foo 有效,foo1 不在授权范围)
可以看到,user0 对 key foo1,没有读写权限。
上面这个示例建议亲手去敲一遍,有助于快速理解角色-用户模型。
6. 集群搭建
6.1. 方法一:静态集群配置(推荐用于生产)
在 /etc/etcd 目录下创建三个配置文件(这里就用一台机器上不同端口来模拟集群)
etcd-node1.yaml
name: node1
data-dir: /tmp/etcd1listen-peer-urls: http://127.0.0.1:2380
listen-client-urls: http://127.0.0.1:2379initial-advertise-peer-urls: http://127.0.0.1:2380
advertise-client-urls: http://127.0.0.1:2379initial-cluster: node1=http://127.0.0.1:2380,node2=http://127.0.0.1:3380,node3=http://127.0.0.1:4380
initial-cluster-token: local-etcd-cluster
initial-cluster-state: new
etcd-node2.yaml
name: node2
data-dir: /tmp/etcd2listen-peer-urls: http://127.0.0.1:3380
listen-client-urls: http://127.0.0.1:3379initial-advertise-peer-urls: http://127.0.0.1:3380
advertise-client-urls: http://127.0.0.1:3379initial-cluster: node1=http://127.0.0.1:2380,node2=http://127.0.0.1:3380,node3=http://127.0.0.1:4380
initial-cluster-token: local-etcd-cluster
initial-cluster-state: new
etcd-node3.yaml
name: node3
data-dir: /tmp/etcd3listen-peer-urls: http://127.0.0.1:4380
listen-client-urls: http://127.0.0.1:4379initial-advertise-peer-urls: http://127.0.0.1:4380
advertise-client-urls: http://127.0.0.1:4379initial-cluster: node1=http://127.0.0.1:2380,node2=http://127.0.0.1:3380,node3=http://127.0.0.1:4380
initial-cluster-token: local-etcd-cluster
initial-cluster-state: new
集群启动
etcd --config-file=/etc/etcd/etcd-node1.yaml
在每个终端分别执行命令。
参数说明:
1. initial-cluster-token: local-etcd-cluster
etcd 集群的初始化 token,用于标识一个新的集群。所有节点必须使用相同的 token 才能加入该集群。
2. initial-cluster-state: new
指定 etcd 是启动一个新的集群(new),还是加入一个已有的集群(existing)。
new:表示初始化新的 etcd 集群(初次部署)
existing:表示已有集群的成员(用于节点重启或扩容)
3. name: node3
分别为三个 etcd 节点指定唯一的名称,后面用于集群识别。
4. data-dir: /tmp/etcd3
数据存储目录,etcd会在此目录保存所有数据
# 监听peer通信的URL(集群节点间内部通信)
listen-peer-urls: http://127.0.0.1:4380# 监听客户端请求的URL(应用程序连接etcd的地址)
listen-client-urls: http://127.0.0.1:4379# 向集群其他节点宣告的peer通信地址
# 必须可从其他节点访问,通常与listen-peer-urls一致
initial-advertise-peer-urls: http://127.0.0.1:4380# 向客户端宣告的服务地址
# 必须可从客户端访问,通常与listen-client-urls一致
advertise-client-urls: http://127.0.0.1:4379# 初始集群配置(集群所有节点的通信地址)
initial-cluster: node1=http://127.0.0.1:2380,node2=http://127.0.0.1:3380,node3=http://127.0.0.1:4380
6.2 方法二:基于 discovery token 的动态发现(适合临时集群)
简单来说,方法二适合部署时不提前知道所有成员 IP 的情况。
这里的 discovery
URL 每次使用都要新生成(不可复用)!
具体可以参考官方文档,这里不多演示。
How to Set Up a Demo etcd Cluster | etcd
curl https://discovery.etcd.io/new?size=3
https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92# grab this token
TOKEN=token-01
CLUSTER_STATE=new
NAME_1=machine-1
NAME_2=machine-2
NAME_3=machine-3
HOST_1=10.240.0.17
HOST_2=10.240.0.18
HOST_3=10.240.0.19
DISCOVERY=https://discovery.etcd.io/a81b5818e67a6ea83e9d4daea5ecbc92THIS_NAME=${NAME_1}
THIS_IP=${HOST_1}
etcd --data-dir=data.etcd --name ${THIS_NAME} \--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \--discovery ${DISCOVERY} \--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}THIS_NAME=${NAME_2}
THIS_IP=${HOST_2}
etcd --data-dir=data.etcd --name ${THIS_NAME} \--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \--discovery ${DISCOVERY} \--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}THIS_NAME=${NAME_3}
THIS_IP=${HOST_3}
etcd --data-dir=data.etcd --name ${THIS_NAME} \--initial-advertise-peer-urls http://${THIS_IP}:2380 --listen-peer-urls http://${THIS_IP}:2380 \--advertise-client-urls http://${THIS_IP}:2379 --listen-client-urls http://${THIS_IP}:2379 \--discovery ${DISCOVERY} \--initial-cluster-state ${CLUSTER_STATE} --initial-cluster-token ${TOKEN}
现在 etcd 启动了,开始测试吧!
export ETCDCTL_API=3
ENDPOINTS=127.0.0.1:2379,127.0.0.1:3379,127.0.0.1:4379etcdctl --endpoints=$ENDPOINTS endpoint status --write-out=table
可以看到,测试成功了,列出了所有节点。node1 是 leader,node2和node3是 follower。
6.3. 集群模式:Raft 共识算法
etcd 是一个 强一致性(strongly consistent) 的分布式键值存储系统,但它不是传统意义上的主从结构,而是基于 Raft 共识算法 构建的。
etcd 集群基于 Raft 算法,每个节点(member)在某一时刻的角色可能是以下之一:
角色 | 含义 |
---|---|
Leader | 当前集群的主节点,负责接收客户端写请求,并将数据复制给 Follower。整个集群 只能有一个 Leader。 |
Follower | 普通成员节点,响应来自 Leader 的日志复制和心跳,不能单独处理写入。 |
Candidate | 候选者,用于发起 Leader 选举的过渡角色。 |
这个就解决了传统主从集群模式下主节点宕机了,整个集群就不可用的问题。etcd 集群可以自动进行故障恢复,自动投票选出新的 master 节点。
etcd 的一致性模型是:
线性一致性(Linearizability):所有客户端看到的顺序与 Raft 日志顺序一致。
也就是说:
-
所有写操作必须通过 Leader;
-
Leader 会将写请求复制到多数派(超过半数)节点,才会被“提交”(解决了集群脑裂)
etcd(Raft) | 传统主从 | |
---|---|---|
节点角色 | Leader + Follower + Candidate | Master + Slave |
容错方式 | 多数节点存活即可服务(如 3 节点需 ≥2 存活) | Master 挂了要人工或脚本切换 |
数据复制 | 同步复制,多数派一致后才提交 | 主写从读,通常异步复制 |
一致性 | 强一致性(线性一致) | 最终一致性(可能存在延迟) |
再看下之前的图:
字段名称 | 说明 | 示例值 | 重要性 |
---|---|---|---|
ID | 节点的唯一标识符(16进制格式) | 84724583e7fe06d8 | 用于识别集群中的特定节点 |
VERSION | etcd 服务器版本号 | 3.5.21 | 集群所有节点版本应一致 |
DB SIZE | 后端数据库的物理大小 | 20 kB | 监控存储增长的关键指标 |
IS LEADER | 当前是否为领导者节点 | true /false | 集群健康关键指标(必须有且只有一个leader) |
IS LEARNER | 是否为学习者节点(非投票成员) | false | 学习者节点不参与选举,用于灾备或读扩展 |
RAFT TERM | 当前任期号(每次选举递增) | 2 | 数值越大表示集群经历的领导选举次数越多 |
RAFT INDEX | 当前最高日志条目索引号 | 9 | 表示写入操作的序列号 |
RAFT APPLIED INDEX | 已应用到状态机的最高日志索引 | 9 | 应与RAFT INDEX接近,差值大表示有未应用的操作 |
ERRORS | 节点错误信息(为空表示正常) | `` | 出现内容时表示节点异常 |
7. 其他功能特性
7.1 prefix 前缀
1. 通过 prefix 前缀查找 key
2. 通过 --preifx 删除 key
etcdctl --endpoints=$ENDPOINTS put k1 value1
etcdctl --endpoints=$ENDPOINTS put k2 value2
etcdctl --endpoints=$ENDPOINTS del k --prefix
7.2 etcd 事务
因为 etcd 本身就是 raft 强一致性的中间件,保证了 CAP 理论里的 CP。etcd 无论单机还是集群都支持事务,它内部使用了 MVCC 多版本并发控制机制。
Redis 保证了 CAP 理论里的 AP,属于最终一致性。
Redis 通过 multi + watch 命令并 不能保障事务,Redis 官方文档里说 Redis 设计就是为了高性能,一致性保障并不是最主要的。
MULTI
+WATCH
的缺陷
- 仅提供乐观锁(CAS),若
WATCH
的键被其他客户端修改,事务会失败。- 无回滚机制,失败后需手动重试。
所以 Redis 里实现事务只能通过 Lua 脚本 来实现。
Lua 脚本的原子性
- 虽然单线程执行 Lua 能保证原子性,但:不支持跨键事务(所有操作必须在同一个脚本中)
- 也就是说 cluster 分片集群模式下,lua 脚本可能无法保障事务。
场景举例:
你要同时更新一个用户的邮箱和手机号
# 插入数据
etcdctl put /users/12345/email "old.address@johndoe.com"
etcdctl put /users/12345/phone "123-456-7890"
# 开启事务
etcdctl txn --interactivecompares:
# 这里除了用value(),也可以使用version()版本号机制
value("/users/12345/email") = "old.address@johndoe.com"success requests (get, put, delete):
put /users/12345/email "new.address@johndoe.com"
put /users/12345/phone "098-765-4321"failure requests (get, put, delete):
get /users/12345/email
测试:
1. Atomicity(原子性)
事务是原子的,要么所有操作都成功,要么一个都不执行。
只有 value("/users/12345/email") 等于 "old.address@johndoe.com"时,邮箱和手机号才会一起更新。 如果不是,则不会更新任何内容。
这保证了不会出现只更新了一半字段的尴尬情况。
2. Consistency(一致性)
3. 注意点:避免在同一事务中对同一个 key 多次 put
❌ 错误示例:
# compares:
value(counter) = "1"# success requests:
put counter 2
put counter 3 # 同一个事务里又对 counter 执行一次 put,会导致冲突或不可预期结果
这种情况虽然可能不会报错,但有时你不知道最终会落哪个值。etcd 没法保证顺序处理多次对同一个 key 的写入。
✅ 正确方式:
每个事务里对同一个 key 最多只能 put 一次。
7.3. watch 实时监听 key
etcdctl watch stock1
7.4. 设置 lease 租约
类似 redis 的 TTL 机制,但更强大(支持一个 leaseID 多键绑定、续租等)。
# 1. 创建一个 TTL=300秒 的租约(返回 Lease ID)
etcdctl lease grant 300
→ lease 2be7547fbc6a5afa granted with TTL(300s)# 2. 将键值对 "sample=value" 绑定到这个租约
etcdctl put sample value --lease=2be7547fbc6a5afa# 3. 获取键值对(此时可以正常读取)
etcdctl get sample
→ sample
→ value# 4. 手动续租(保持 Lease 存活,即重置为300秒)
etcdctl lease keep-alive 2be7547fbc6a5afa# 5. 主动撤销租约(立即删除所有关联的键)
etcdctl lease revoke 2be7547fbc6a5afa# 6. 等待 300秒后(或撤销后),键值对自动消失
etcdctl get sample
→ (无输出,键已被删除)
7.5. 分布式锁
# 客户端1
etcdctl lock lock1# 客户端2,此时会阻塞,直到客户端1释放锁
etcdctl lock lock1
7.6. 健康检测
etcdctl endpoint status (--endpoints=$ENDPOINTS|--cluster)etcdctl endpoint health (--endpoints=$ENDPOINTS|--cluster)
关于etcd的分享就到这里了。
我是此林,关注我吧,带你看不一样的世界!