etcd v3 Endpoint、Watch、Lease 与 RBAC 工具手册
从“配置已经发布,进程却没有更新”开始
一个订单服务把开关写进了键值存储。发布平台显示写入成功,部分实例却继续使用旧值;重启后恢复,过一阵又复现。此时真正需要回答的不是“etcd 进程是否存活”,而是下面这条链路在哪里断了:
客户端连接正确集群
-> 读取前缀并保存响应 revision
-> 从 revision + 1 建立 watch
-> 处理 PUT / DELETE
-> 断线后续接,历史被压缩时重新全量读取etcd 是面向关键元数据的强一致键值存储。它适合保存体积小、需要一致读写或协调语义的配置、选主状态和服务租约,不适合承载大对象、日志、业务流水,也不是消息队列。它的价值不在“能存字符串”,而在 MVCC revision、线性一致读、事务比较、watch 和 lease 可以组合成可证明的状态机。
当前稳定发布版本是 v3.6.11,正文命令以 v3.6 API 和同版本 etcdctl、etcdutl 为基线。etcd 采用 Apache License 2.0。官方支持矩阵把 Linux AMD64 与 ARM64 列为 Tier 1;macOS、Windows 等平台可用于开发验证,但属于较低支持层级。生产选型前应重新查看 v3.6 支持平台 和 发布页,不要把“能下载二进制”误当成同等级生产支持。
第一次操作只需要 Docker Compose、宿主机空闲的 2379 端口和独立测试目录。2379 是客户端 gRPC/HTTP 入口,2380 是成员间 Raft 通信入口;后者不应暴露给应用。实验必须使用独立实例,绝不能连接 Kubernetes 控制面的 etcd。
先把单节点开发实例跑起来
官方提供预编译二进制、Homebrew 与容器镜像等入口,并提醒操作系统发行版仓库中的版本可能明显滞后。跨平台开发更容易复现的入口是官方发布镜像。下面的 Compose 文件固定补丁版本,并只把客户端端口绑定到本机:
services:
etcd:
image: gcr.io/etcd-development/etcd:v3.6.11
container_name: te-etcd
command:
- /usr/local/bin/etcd
- --name=dev-1
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://127.0.0.1:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://dev-1:2380
- --initial-cluster=dev-1=http://dev-1:2380
- --initial-cluster-state=new
ports:
- "127.0.0.1:2379:2379"
volumes:
- etcd-data:/etcd-data
volumes:
etcd-data:listen-client-urls 决定进程在哪里接收请求,advertise-client-urls 告诉客户端和其他成员“应该怎样找到我”。宿主机访问的单节点实验可以公布 127.0.0.1:2379;如果客户端也在 Compose 网络中,应公布容器 DNS 名,例如 http://etcd:2379。把不可达的 localhost 或容器内 IP 公布出去,会出现“容器内健康,应用连接失败”的典型假象。
启动并检查三个工具是否同版本:
docker compose up -d etcd
docker exec te-etcd etcd --version
docker exec te-etcd etcdctl version
docker exec te-etcd etcdutl version
docker exec te-etcd etcdctl endpoint health
docker exec te-etcd etcdctl endpoint status -w table预期能看到 v3.6.11,健康检查包含 is healthy,状态表给出 endpoint、member ID、leader、Raft term、Raft index 和数据库大小。若 endpoint health 成功而应用失败,先比较应用实际 endpoint 与 advertise 地址;若状态表里没有 leader,再查启动参数、data-dir 身份和 peer 通信,不要继续做业务写入。
删除这个开发实例时先停容器,再显式删除自己的卷:
docker compose down
docker volume ls --filter name=etcd
docker compose down -v最后一条会永久删除实验数据,只能用于确认归属的本地卷。共享实例、Kubernetes 持久卷和生产 data-dir 必须走备份、审批和成员变更流程。
这只是本地环境清理,不是业务配置回滚。配置回滚应读取目标键当前 mod_revision,用 Txn 比较它仍等于待撤销版本,再写回经过审查的旧值;若比较失败,说明期间已有新发布,必须停止并重新评估。删除卷、复制旧 data-dir 或恢复整集群快照都不应承担单个配置项回滚。
一次写入如何变成可追踪的 revision
先写入一个项目专属前缀,避免实验落到根键空间:
docker exec te-etcd etcdctl put /te/order/dev/config/payment-timeout 1500
docker exec te-etcd etcdctl get /te/order/dev/ --prefix -w jsonJSON 响应中的 header.revision 是整个键空间在该响应时刻的修订号。每个键还有三组容易混淆的字段:
create_revision:这个键当前生命周期的创建修订号;删除后重建会变化。mod_revision:最近一次修改这个键的全局修订号,可做比较交换。version:当前生命周期内修改次数;首次创建通常为 1。
etcd 的 v3 API 由 gRPC 服务组成:KV 负责 Range/Put/Delete/Txn,Watch 提供双向流,Lease 处理租约,Auth、Cluster 和 Maintenance 管理安全、成员和快照。etcdctl 是同一套 gRPC API 的命令行客户端。没有合适 gRPC 客户端的语言可使用 JSON gRPC gateway;v3.5 及以后路径是 /v3/*,其中键和值按 protobuf 的 bytes 映射为 Base64:
curl -sS http://127.0.0.1:2379/v3/kv/range \
-X POST \
-H 'content-type: application/json' \
-d '{"key":"L3RlL29yZGVyL2Rldi9jb25maWcvcGF5bWVudC10aW1lb3V0"}'预期响应中的 kvs[0].value 也是 Base64。HTTP 网关适合兼容接入,不会改变一致性语义;生产应用优先使用维护活跃、版本兼容的官方或成熟 gRPC 客户端,并设置请求超时、TLS 和身份认证。
默认 Range 是线性一致读,需要确认当前集群共识;--consistency=s 对应 serializable 本地读,延迟和可用性更好,但可能返回旧值。配置发布、锁和选主判断不要为了省一次共识往返而偷偷改成串行化读。
正向实验:从快照读取无缝切到 watch
只执行 watch /prefix --prefix 有一个竞态窗口:应用完成初次读取之后、watch 建立之前发生的修改可能漏掉。正确做法是记录全量读取的响应 revision,然后从 revision + 1 监听。
对整个项目前缀执行一次线性一致 Range,保存键值快照并记下 header.revision,假设它是 R。不能只读取某一个键后就监听整个前缀,否则其他键在初始状态里本来就是缺失的。
docker exec te-etcd etcdctl get /te/order/dev/ --prefix -w json > etcd-prefix-snapshot.json
R=$(jq -r '.header.revision' etcd-prefix-snapshot.json)在终端 A 从下一修订号开始监听:
docker exec -it te-etcd etcdctl watch /te/order/dev/ \
--prefix --rev=$((R + 1)) --prev-kv在终端 B 连续修改和删除:
docker exec te-etcd etcdctl put /te/order/dev/config/payment-timeout 2000
docker exec te-etcd etcdctl del /te/order/dev/config/payment-timeout预期终端 A 依次看到 PUT 与 DELETE,事件携带新的 mod_revision;--prev-kv 还会返回变更前的键值。revision 是全局顺序,不是单个键的版本,因此同一事务里的多项修改共享一个 revision,而不同事务按 revision 排序。
客户端应用应把“当前配置 + 最后处理 revision”作为一个恢复单元。收到事件后先校验 key 前缀和事件类型,再更新内存状态,最后推进 checkpoint;重连时从 checkpoint + 1 继续。watch 是状态变更流,不是持久消费队列,不能替代需要独立消费进度和重放保留策略的 Kafka、Pulsar 等消息系统。
反向实验:压缩历史后旧 watch 必须失败
这个实验故意制造可解释的失败。先连续写入几次并查看当前 revision:
docker exec te-etcd etcdctl put /te/order/dev/lab/value v1
docker exec te-etcd etcdctl put /te/order/dev/lab/value v2
docker exec te-etcd etcdctl endpoint status -w json从状态输出取得当前 revision C 后压缩到该点,再尝试从明显更早的 revision 监听:
docker exec te-etcd etcdctl compact C
docker exec te-etcd etcdctl watch /te/order/dev/ --prefix --rev=1预期第二条命令返回类似 required revision has been compacted,watch 响应会携带可用的 compact_revision 并取消该 watcher。这不是网络重试能修复的问题。恢复算法必须重新执行 prefix Range,原子替换本地状态,取得新的响应 revision,再从下一 revision 建立 watch。
压缩只丢弃旧 MVCC 历史,不会自动把后端数据库文件缩小;碎片回收由 etcdctl defrag 完成。自动压缩保留窗口、watch 最长离线时间、后端配额和 defrag 维护窗口必须一起设计,否则要么历史无限增长,要么客户端频繁落入全量重建。
Lease 负责存活,不负责永久配置
lease 的 TTL 是服务端授予值。一个键最多绑定一个 lease;lease 到期或被撤销时,所有绑定键被删除,并产生 DELETE watch 事件。先申请租约:
docker exec te-etcd etcdctl lease grant 30把输出中的 lease ID 赋给 shell 变量 LEASE_ID,再绑定临时实例。命令中的 $ 不能省略;字面量 LEASE_ID 不是合法租约:
LEASE_ID=<上一条命令返回的十六进制 lease ID>
docker exec te-etcd etcdctl put /te/order/dev/instances/worker-001 online --lease="$LEASE_ID"
docker exec te-etcd etcdctl lease timetolive "$LEASE_ID" --keys
docker exec -it te-etcd etcdctl lease keep-alive "$LEASE_ID"keep-alive 会持续占用终端并刷新租约。停止它,等待服务端判定超时,或主动执行:
docker exec te-etcd etcdctl lease revoke "$LEASE_ID"
docker exec te-etcd etcdctl get /te/order/dev/instances/worker-001预期最后一次读取没有键。反例是把 /config/payment-timeout 绑到短租约:一次客户端停顿就会把持久配置一起删除。服务租约必须有明确 owner、重建逻辑、TTL 与 keepalive 失败指标;永久配置不要绑定短 lease。
Txn 用比较条件关闭并发覆盖窗口
两个发布者都先 get 再 put,后写入者会静默覆盖先写入者。Txn 把 Compare 和后续操作放进一次原子执行:比较可针对 value、version、create revision、mod revision 或 lease。
先读取目标键并记下 mod_revision=M,然后进入交互事务:
docker exec -it te-etcd etcdctl txn依次输入下面三段,段间保留空行:
mod("/te/order/dev/config/payment-timeout") = "M"
put /te/order/dev/config/payment-timeout 2500
get /te/order/dev/config/payment-timeout没有并发修改时预期得到 SUCCESS。再次使用旧的 M 执行同一事务,预期走 failure 分支并输出当前值。这个失败是乐观锁证据,调用方应返回冲突、重新读取并让上层决定是否重试,不能无上限自动覆盖。
事务内允许的操作数受 --max-txn-ops 限制,单次请求受 --max-request-bytes 限制。把大量配置塞进一次事务会同时增加 Raft 复制、序列化、watch 扇出和尾延迟;需要大批量原子更新时,先重新审视数据模型,而不是直接放大限制。
把它接进真实项目
项目连接配置至少区分 endpoint、命名空间、认证和 TLS 文件,不把密码写入仓库:
ETCD_ENDPOINTS=https://etcd-1.example.invalid:2379,https://etcd-2.example.invalid:2379,https://etcd-3.example.invalid:2379
ETCD_KEY_PREFIX=/te/order/prod/
ETCD_USERNAME=te-order-prod
ETCD_PASSWORD_FILE=/run/secrets/etcd-password
ETCD_CA_FILE=/run/secrets/etcd-ca.crt
ETCD_CERT_FILE=/run/secrets/etcd-client.crt
ETCD_KEY_FILE=/run/secrets/etcd-client.key客户端启动顺序应当固定:
校验 endpoint 与 cluster_id
-> 带 deadline 执行线性一致 prefix Range
-> 构建不可变配置快照并记录 revision
-> 从 revision + 1 建立 watch
-> 收到事件后校验、应用、推进 checkpoint
-> ErrCompacted 时回到全量 Range日志可以记录 cluster ID、member ID、prefix、revision、重连次数和错误码,不能记录密码、私钥和敏感配置值。指标至少覆盖请求延迟与错误率、watch 重建/压缩恢复次数、lease keepalive 失败、当前已应用 revision 与集群 revision 差距。差距持续扩大比“连接成功”更能说明实例正在变陈旧。
RBAC 正反验证:允许自己的前缀,拒绝别人的前缀
本地实验可先创建 root 用户,再启用认证。user add 会交互读取密码,避免把密码落入 shell 历史:
docker exec -it te-etcd etcdctl user add root
docker exec te-etcd etcdctl auth enable
docker exec -it te-etcd etcdctl --user root role add te-order-role
docker exec -it te-etcd etcdctl --user root role grant-permission \
te-order-role readwrite /te/order/dev/ --prefix=true
docker exec -it te-etcd etcdctl --user root user add te-order-dev
docker exec -it te-etcd etcdctl --user root user grant-role te-order-dev te-order-role正向验证:
docker exec -it te-etcd etcdctl --user te-order-dev put /te/order/dev/verify ok
docker exec -it te-etcd etcdctl --user te-order-dev get /te/order/dev/verify反向验证:
docker exec -it te-etcd etcdctl --user te-order-dev get /te/other/prod/secret
docker exec -it te-etcd etcdctl --user te-order-dev del /te/ --prefix预期两条反向命令都返回 permission denied。若成功,说明前缀范围过宽或应用拿到了管理员身份。RBAC 只解决 API 授权;客户端和 peer 通信仍要使用 TLS/mTLS、网络策略和独立监听地址。/metrics、/health 的暴露也要单独收口,不能因为启用了 auth 就默认安全。
mTLS 分别封住 client 与 peer 两条链路
生产成员需要两组用途明确的证书:client listener 的 server 证书用于应用到 etcd,peer 证书用于成员间 Raft 通信。证书 SAN 必须覆盖实际 advertise 地址;每个成员使用自己的私钥,不能把一份通配私钥复制到所有节点。成员启动参数至少包含:
--listen-client-urls=https://0.0.0.0:2379
--advertise-client-urls=https://etcd-1.example.invalid:2379
--cert-file=/etc/etcd/pki/server.crt
--key-file=/etc/etcd/pki/server.key
--trusted-ca-file=/etc/etcd/pki/ca.crt
--client-cert-auth=true
--listen-peer-urls=https://0.0.0.0:2380
--initial-advertise-peer-urls=https://etcd-1.example.invalid:2380
--peer-cert-file=/etc/etcd/pki/peer.crt
--peer-key-file=/etc/etcd/pki/peer.key
--peer-trusted-ca-file=/etc/etcd/pki/peer-ca.crt
--peer-client-cert-auth=true
--tls-min-version=TLS1.2client-cert-auth 要求客户端证书由受信 CA 签发;peer-client-cert-auth 对成员连接执行同样验证。只把 URL 改成 https 而不启用这两个开关,只获得加密,不获得对端身份约束。自动生成的 auto-tls 与 peer-auto-tls 适合短期实验,不应替代生产 CA、证书轮换和吊销流程。
ETCDCTL_API=3 etcdctl \
--endpoints=https://etcd-1.example.invalid:2379 \
--cacert=/run/secrets/etcd-ca.crt \
--cert=/run/secrets/etcd-client.crt \
--key=/run/secrets/etcd-client.key \
endpoint health
ETCDCTL_API=3 etcdctl \
--endpoints=https://etcd-1.example.invalid:2379 \
--cacert=/run/secrets/etcd-ca.crt \
endpoint health第一条应返回健康,第二条应因缺少客户端证书而在认证阶段失败。还要用错误 CA、错误 SAN 和已撤销应用身份分别做失败验证。启用 RBAC 后,证书 Common Name 可以映射为 etcd 用户;证书认证成功仍不代表有键空间权限,最终允许范围由 user/role 决定。监控端点使用独立 handler,不受 v3 RBAC 自动保护,应绑定私网 listen-metrics-urls、使用 mTLS 或由网络策略收口。
三节点多数派为什么是生产常见起点
etcd 用 Raft 复制日志。写请求由 leader 排序,日志复制到多数派后才提交,再由各成员应用到 MVCC 后端。三成员需要 2 票,可容忍 1 个成员故障;五成员需要 3 票,可容忍 2 个。四成员仍只能容忍 1 个故障,却多一份复制开销,所以通常选择奇数投票成员。
多数派不是“容器数”。三个容器若在同一宿主机、同一磁盘或同一故障域上,宿主机故障仍会一起消失。成员需要稳定、低延迟的磁盘,跨故障域部署,并监控 WAL fsync、backend commit、leader 变化、proposal pending、数据库配额和网络 RTT。写延迟取决于 leader 到多数派的慢路径;盲目跨远距离地域拉开成员会放大每次提交延迟。
增加成员不会提升写吞吐,因为每次写要复制给更多投票者。学习者 learner 不投票,可先追赶日志再晋升,降低直接加入落后投票者对 quorum 的风险。成员变更应一次一个,并保持 strict-reconfig-check;失败成员长期不移除会吃掉故障余量。
只有多数派存活时集群才能继续提交更新。失去多数派后不要通过复制旧 data-dir、同时强启多个“新集群”来抢救,这会制造身份和历史分叉。应保护现场、确认最后可信快照和成员清单,再执行恢复流程。
Snapshot 是恢复材料,不是复制卷的同义词
在线快照由 Maintenance API 获取:
docker exec te-etcd etcdctl snapshot save /tmp/etcd-snapshot.db
docker cp te-etcd:/tmp/etcd-snapshot.db ./etcd-snapshot.db
docker run --rm -v "$PWD:/backup" gcr.io/etcd-development/etcd:v3.6.11 \
etcdutl snapshot status /backup/etcd-snapshot.db -w table预期状态表包含 hash、revision、key 数和大小。直接复制 member/snap/db 可能漏掉仍在 WAL 中但尚未进入后端快照的数据,因此在线备份优先使用 etcdctl snapshot save。
恢复使用 etcdutl snapshot restore 创建新的 data-dir 和新的集群逻辑身份,不是把文件覆盖回正在运行的成员:
etcdutl snapshot restore etcd-snapshot.db \
--name=restore-1 \
--data-dir=restore-1.etcd \
--initial-cluster=restore-1=http://127.0.0.1:12380 \
--initial-advertise-peer-urls=http://127.0.0.1:12380恢复前要隔离客户端流量,恢复后检查 endpoint、cluster ID、快照 revision、关键前缀和 watch 客户端重建。旧 revision 观察者可能把恢复后的较小 revision 当成“没有新事件”。对依赖长期 watch 的系统,尤其 Kubernetes informer,v3.6 应按快照年龄和历史写入峰值计算足以覆盖缺口的 revision bump,并把这段历史标记为已压缩:
etcdutl snapshot restore etcd-snapshot.db \
--name=restore-1 \
--data-dir=restore-1.etcd \
--initial-cluster=restore-1=http://127.0.0.1:12380 \
--initial-advertise-peer-urls=http://127.0.0.1:12380 \
--bump-revision=1000000000 \
--mark-compacted1000000000 是官方用于说明“一周旧快照且每秒低于 1500 次写入”的示例,不是所有集群的固定答案。计算值必须覆盖快照之后可能发生的最大写入数;mark-compacted 会让旧 watcher 失败并触发全量重建,避免缓存把恢复后的状态误判为陈旧事件。恢复实例启动后应使用新的 endpoint 执行 endpoint status --cluster、前缀 Range、RBAC 允许/拒绝和新 watch 验证。备份只有经过隔离恢复演练并验证关键键空间,才算可用恢复能力。
Kubernetes 的 etcd 边界
Kubernetes 把集群对象状态存入自己的 etcd,kube-apiserver 才是受支持的 API 边界。应用直接连接控制面 etcd 会绕过准入、鉴权、审计和对象语义,还会让误删、压测、compaction、defrag 或大 watch 影响整个控制面。
即使平台管理员能提供 endpoint 和证书,也不应把控制面 etcd 当共享配置中心。应用需要 etcd 时创建独立集群、独立证书、独立容量预算和备份策略。把独立 etcd 运行在 Kubernetes StatefulSet 中,与“复用 Kubernetes 自己的 etcd”是两回事;前者仍要处理持久卷、调度故障域、Pod 中断预算和多数派可用性。
容量、成本与长期治理
etcd 的成本主要来自高质量低延迟磁盘、跨故障域网络、三或五份副本、备份存储和维护值班,而不是授权费。容量治理应从工作负载推导:键和值大小、写入频率、watch 数和扇出、lease 数、历史保留窗口、事务大小以及恢复时间目标。
持续观测这些趋势:
backend 大小与 quota-backend-bytes 余量,NOSPACE 告警出现前就要处理增长源。dbSize 与 dbSizeInUse 的差距,判断压缩后是否需要受控 defrag。WAL fsync、backend commit 和请求延迟分位数,定位磁盘或多数派慢节点。
leader 变更、无 leader、proposal failed/pending 和 peer RTT,识别 Raft 不稳定。watch 数、慢 watcher、compaction 恢复率和应用 revision lag,识别客户端欠债。lease 数、到期删除量与 keepalive 失败,识别租约风暴。
团队登记的最小对象不是“有一套 etcd”,而是 cluster、endpoint、prefix、user/role、watch、lease、备份与 owner。每次升级先阅读对应版本升级指南,逐成员滚动,确认客户端兼容与降级条件;版本、证书、快照恢复和多数派故障演练都要有周期。退出 etcd 时先停止新写入,导出并校验键空间,让消费者切到新系统,观察一个完整业务窗口,再撤销凭证和清理独立资源。
当团队能证明下面这些事实,etcd 才从“能启动”变成了可治理的工程能力:应用知道自己连接的是哪个 cluster;全量读取与 watch 之间没有窗口;压缩后能重建状态;租约删除不会带走持久配置;并发更新会暴露冲突;越权请求确实被拒绝;多数派故障与快照恢复都能在隔离环境演练;控制面 etcd 从未被应用借用。
