
简介《etcd实战及其原理分析》是一份面向分布式系统开发与运维人员的PDF文档适合需要掌握一致性键值存储、服务发现与集群协调的初中级工程师查漏补缺。内容以etcd v3.5为基线先介绍应用场景与分层架构再落到单实例、单机多实例和多节点集群部署以及etcdctl常用命令与权限管理。原理部分沿读写请求链路展开涉及KVServer、拦截器、串行读与线性读、ReadIndex、Quota、WAL、Apply等模块并专章梳理Raft的Leader选举、日志复制与安全性以及MVCC的treeIndex、更新、查询和删除机制便于把命令操作与底层实现对应起来。资源包仅含1个PDF文件约4.55MB按介绍、实战、原理三部分编排目录清晰适合通读和按主题检索。目前已有450人学习可作为etcd入门与复盘的参考材料。1. 从一次读到旧值说起这份 etcd 资料补的是哪块短板三节点 etcd 集群客户端 put 返回成功紧接着 get 却读回旧值——第一次碰到这个现象的人多半以为集群坏了重启一遍又好了然后不再深究。真正的原因通常不在 etcd 本身而在于这次读走的是串行读还是线性读、连的是 Leader 还是落后节点、那个节点的 Apply 进度落在了哪里。这份《etcd实战及其原理分析.pdf》讲的正是这一段v3.5 的三种部署形态、etcdctl 的键与租约语义、Auth 权限体系以及 Range 与写请求的完整链路、Raft 和 MVCC 的实现细节。适合两类人正要把 etcd 接进配置中心、服务注册或分布式锁的业务开发和想把读写路径讲清楚、经得起追问的中高级候选人。2. 把 etcd v3.5 跑起来单实例、单机多实例、三节点集群三种起法环境搭建这一节看着像流水账实际是全篇的地基。绝大多数etcd 用不起来的抱怨本质上都是部署形态选错或者参数含义没吃透单实例当生产用、单机多实例被 2380 端口撞死、多节点集群 initial-cluster 写错一个地址就永远凑不齐 Quorum。先把这三种形态的边界摆清楚再动手。2.1 先选形态单点调试、单机多实例验证、多节点才谈生产单实例适合本地开发联调一个进程、2379 一个客户端口写完配置就能跑但它没有 Quorum 概念进程一挂服务就完全不可用任何有可用性要求的场景都不能这么上。单机多实例的价值在于用一台机器验证集群行为Leader 选举、Follower 落后、member 增删这些操作在单机上就能演练省机器也省心代价是三个进程共享同一块磁盘的 IO测出来的性能数字没有参考价值。多节点集群才是生产形态。etcd 用仲裁机制成员数为 n 时需要 (n1)/2 个节点达成一致操作才算成功也就是三节点容忍一个节点故障、五节点容忍两个、七节点容忍三个。节点数不是越多越好每次写都要把日志复制到多数派并落盘节点越多写延迟越高、吞吐越低。社区给的常见建议是 3、5、7 这三个奇数档七节点对绝大多数业务已经足够再往上加更多是为了数据冗余而不是性能。注意偶数节点不会提升容错能力四节点的容错能力和三节点一样都是容忍一个却要多同步一份数据纯属浪费。还有一种容易被忽略的形态是 Proxy 模式它不参与 Raft、不存数据只做反向代理把请求转发到集群。这种模式适合客户端不方便维护多个 endpoint 的场景但代理本身也是单点现在更常见的做法是让客户端直接拿全量 endpoint 列表做轮询。2.2 单实例二进制包解压即用先认清 2379 和 2380单实例部署是最快能看到数据的一次。从 etcd 官方 release 页面取对应版本的 linux-amd64 压缩包解压后目录里就是 etcd 服务端和 etcdctl 客户端两个二进制不需要额外依赖。# 解压官方 release 页面下载的对应版本压缩包版本号按实际替换 tar -zxvf etcd-v3.5.5-linux-amd64.tar.gz cd etcd-v3.5.5-linux-amd64/ # 前台启动先看日志里打印的 name / listen-client-urls / ready to serve ./etcd # 测试环境常用后台启动日志固定落到一个文件便于 tail nohup ./etcd /tmp/etcd.log 21 # 确认版本以及 2379客户端和 2380节点间是否都在监听 ./etcd --version ss -lntp | grep -E 2379|2380 # 把两个二进制丢进 PATH后面就不用带 ./ 了 cp etcd etcdctl /usr/local/bin/这段里最关键的信息是端口分工2379 面向客户端客户端 put/get/watch 都打这个口2380 面向集群内其他成员承载 Raft 的日志复制和心跳。单实例时 2380 也在监听只是没有别的节点来连它很多人看到 2380 开着会误以为已经集群了其实只是一个节点在自言自语。进程起来之后用 etcdctl 做第一次读写。v3.5 的 etcdctl 默认已经是 v3 API从老脚本迁过来的人习惯性加export ETCDCTL_API3留着也无害但要知道它不是必须的。# 写入一个键成功会返回 OK etcdctl --endpointshttp://127.0.0.1:2379 put username fox # 读回来 etcdctl --endpointshttp://127.0.0.1:2379 get username # 用 json 格式输出能看到 revision / create_revision / mod_revision / version etcdctl --endpointshttp://127.0.0.1:2379 get username -w json--endpoints这个参数建议从一开始就显式写别依赖默认值。生产环境里要配全量地址列表客户端会做故障转移某个节点挂了自动换下一个这个习惯从单机阶段就养成后面迁到集群不用改代码。2.3 单机多实例把 2379/2380 错开用 goreman 一次拉起单机多实例的第一道坎是端口。三个 etcd 进程如果都用默认的 2379/2380第二个进程直接启动失败。常见做法是把三组端口错开成 12379/12380、22379/22380、32379/32380客户端口和节点间口成对偏移看日志时不容易混。手动开三个终端挨个起进程太痛苦goreman 这类进程管理工具可以按一个配置文件批量拉起、单独停止、单独重启。它依赖 Go 环境装完就能用。# 安装 goreman需要本地有 Go 环境 go install github.com/mattn/goremanlatest # 验证安装 goreman help然后写一个集群描述文件每行一个进程格式是进程名: 启动命令。三个节点的 name、data-dir、两组监听地址必须各不相同而--initial-cluster和--initial-cluster-token必须完全一致——这两处不一致是最常见的启动失败原因。# local-cluster-profile三节点本地集群 etcd1: etcd --name infra1 \ --listen-client-urls http://127.0.0.1:12379 \ --advertise-client-urls http://127.0.0.1:12379 \ --listen-peer-urls http://127.0.0.1:12380 \ --initial-advertise-peer-urls http://127.0.0.1:12380 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra1http://127.0.0.1:12380,infra2http://127.0.0.1:22380,infra3http://127.0.0.1:32380 \ --initial-cluster-state new etcd2: etcd --name infra2 \ --listen-client-urls http://127.0.0.1:22379 \ --advertise-client-urls http://127.0.0.1:22379 \ --listen-peer-urls http://127.0.0.1:22380 \ --initial-advertise-peer-urls http://127.0.0.1:22380 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra1http://127.0.0.1:12380,infra2http://127.0.0.1:22380,infra3http://127.0.0.1:32380 \ --initial-cluster-state new etcd3: etcd --name infra3 \ --listen-client-urls http://127.0.0.1:32379 \ --advertise-client-urls http://127.0.0.1:32379 \ --listen-peer-urls http://127.0.0.1:32380 \ --initial-advertise-peer-urls http://127.0.0.1:32380 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra1http://127.0.0.1:12380,infra2http://127.0.0.1:22380,infra3http://127.0.0.1:32380 \ --initial-cluster-state new# 按配置文件拉起全部进程 goreman -f ./local-cluster-profile start # 任意连一个节点看成员列表三个 member 都应该在且恰好一个 isLeader etcdctl --endpointshttp://127.0.0.1:12379 member list # 单独停止、单独重启某个节点用来演练故障场景 goreman run stop etcd2 goreman run restart etcd2集群跑起来之后往非 Leader 节点写一次数据观察客户端行为# 指定 etcd2 的客户端口写入etcd 会自动转发到 Leader 处理 etcdctl --endpointshttp://127.0.0.1:22379 put user fox这几个参数的语义值得单独记一遍后面上生产改配置全靠它们参数作用常见错误--name节点在本集群内的唯一名字三个节点写成同一个member 加不进去--initial-cluster集群成员名字与 peer 地址的完整列表写成客户端口、漏写某个成员、名字与--name不一致--initial-cluster-statenew表示新建集群existing表示加入已有集群新集群误写existing一直等不到成员--listen-client-urls客户端请求的监听地址只绑 127.0.0.1容器外或跨机访问不通--advertise-client-urls告诉客户端用这个地址访问我与 listen 不一致又没打通网络客户端连接超时--listen-peer-urls节点间通信的监听地址与客户端口混用导致 Raft 消息错乱--initial-advertise-peer-urls告诉其他成员用这个地址找我与 initial-cluster 中的地址写法不一致--initial-cluster-state这个参数是区分场景的关键新建集群三个节点都写new如果是往已有集群里加节点新节点要写existing并且--initial-cluster里要同时包含老成员和新成员。这一点在扩容时最容易翻车。2.4 三节点集群静态配置写死地址动态发现靠一次性 token真正上生产时多节点部署有三种引导方式静态配置、etcd 动态发现、DNS 动态发现。它们的区别只在于节点启动时怎么知道其他成员是谁。静态配置最简单前提是搭建之前各节点的 IP 已经确定。三台机器分别启动每台的--name、两组监听地址、--initial-advertise-peer-urls各不相同但--initial-cluster和--initial-cluster-token三台完全一致。下面以 10.0.0.11、10.0.0.12、10.0.0.13 为例第一台的启动命令长这样# 节点一其余两台把 name、IP、端口按各自实际值替换 etcd --name infra1 \ --data-dir /data/etcd \ --listen-client-urls http://10.0.0.11:2379 \ --advertise-client-urls http://10.0.0.11:2379 \ --listen-peer-urls http://10.0.0.11:2380 \ --initial-advertise-peer-urls http://10.0.0.11:2380 \ --initial-cluster-token etcd-cluster-1 \ --initial-cluster infra1http://10.0.0.11:2380,infra2http://10.0.0.12:2380,infra3http://10.0.0.13:2380 \ --initial-cluster-state new这里每个节点都要有独立的--data-dir不能共用目录否则 WAL 和 boltdb 会被互相覆盖症状是集群反复重启、member 信息错乱。三台都起来之后随便连一台执行member list应该看到三个 member 和一个 Leader。当各节点 IP 事先不确定比如弹性伸缩、容器编排场景静态配置就不适用了这时用动态发现先通过发现服务申请一个 token所有新成员带着同一个 token 启动由发现服务在引导阶段协调出成员列表。# 申请一个 size3 的发现 token返回一个 URL curl https://discovery.etcd.io/new?size3 # 每个节点用同一个 token 启动其余参数按实际填写 etcd --name etcd1 \ --data-dir /data/etcd \ --initial-advertise-peer-urls http://10.0.0.11:2380 \ --listen-peer-urls http://10.0.0.11:2380 \ --listen-client-urls http://10.0.0.11:2379 \ --advertise-client-urls http://10.0.0.11:2379 \ --discovery https://discovery.etcd.io/申请到的token注意发现 token 是一次性的一个 token 只对应一个集群引导失败也不能拿去引导第二个集群必须重新申请。而且它只在引导阶段起作用运行期的扩缩容不能靠它。三种方式选哪种取决于你能不能在建集群前确定成员地址能确定就用静态配置最可控不能确定又不想依赖 DNS 就用发现服务已经有成熟的 DNS 体系可以考虑 DNS 发现。无论哪种集群起来后第一件事都是member list确认成员数量和 Leader 状态。3. etcdctl 的键、租约与权限revision、version、auth 三条容易搞混的线命令行工具是验证一切判断的手段。这一节把三类最常用的操作拆开键的区间语义、版本计数器的含义、以及 Auth 开启后的权限模型。前两个决定了你读到的数据对不对第三个决定了集群开不开得了机——auth enable 的顺序搞错锁死自己是血泪经验里出现频率最高的一种。3.1 put/get/del 的区间语义默认单键前缀和范围要显式开etcdctl 的 get 默认只查一个键要查一批必须显式加参数。这和其他 KV 工具不太一样新手容易写出以为查了前缀其实只查了一个 key的代码。# 单键读写 etcdctl --endpointshttp://127.0.0.1:12379 put /test/name fox etcdctl --endpointshttp://127.0.0.1:12379 get /test/name # 十六进制输出排查二进制 value 或不可见字符时用 etcdctl --endpointshttp://127.0.0.1:12379 get /test/name --hex # 范围查询左闭右开[name, name3) 包含 name、name2不包含 name3 etcdctl --endpointshttp://127.0.0.1:12379 get /test/name /test/name3 # 前缀查询配合 --limit 控制返回条数避免一次拉回几十万 key etcdctl --endpointshttp://127.0.0.1:12379 get --prefix /test etcdctl --endpointshttp://127.0.0.1:12379 get --prefix /test --limit 3 # 从某个 key 开始按字典序读不设结束位置表示读到末尾 etcdctl --endpointshttp://127.0.0.1:12379 get --from-key /test/name2 # 删除单个键、删除一个区间 etcdctl --endpointshttp://127.0.0.1:12379 del /test/name etcdctl --endpointshttp://127.0.0.1:12379 del /test/name /test/name3 # 删除时把旧值带回来做审计或补偿逻辑时有用 etcdctl --endpointshttp://127.0.0.1:12379 del --prev-kv /test/name范围查询的左闭右开语义要记牢get a c拿到的是a和b不含c。前缀查询本质上是把前缀转成[prefix, prefix1)这个区间。--limit在生产排障时非常有用键空间里几十万个 key 的情况下不加限制直接拉前缀客户端可能先被内存撑爆。删除不是物理删除这一点在第 4 章的 MVCC 里会展开但排障时要知道删完 key 之后用--rev依然能读到删除前的历史版本磁盘上的数据也不会立刻减少得靠 compaction 清理。3.2 revision 与 version两个计数器各管什么别拿来互相校验etcd 有全局的 revision 和单 key 的 version 两个计数器语义完全不同。revision 是集群范围的 64 位计数器任何一次 put 或 delete 都会让它自增 1跟是哪个 key 无关version 是单个 key 自己的写次数从 1 开始key 被删除后重新创建会从 1 重新计。把这两个混为一谈写出来的重试、幂等逻辑就会出问题。# 读取当前 key 的完整元信息 etcdctl --endpointshttp://127.0.0.1:12379 get /test/name -w json # 读取第 8 个全局版本时的值用来回看历史 etcdctl --endpointshttp://127.0.0.1:12379 get --rev8 /test/name返回的 json 里几个字段值得逐一看字段含义使用场景cluster_id集群唯一 ID确认客户端连的是不是同一个集群member_id处理本次请求的节点 ID排障时确认请求落到了哪个 memberrevision服务端当前全局版本号做 watch 断点续传、增量同步raft_term当前 Leader 的任期号判断是否发生过重新选举create_revisionkey 创建时的全局版本判断这个 key 是新建还是覆盖mod_revisionkey 最后一次修改的全局版本做乐观锁的 CAS 比较versionkey 自身的写次数判断这个 key 被改过几次revision 有一个细节1 是保留版本号用户的第一个 key 从 2 开始算。所以看到 create_revision 是 2说明这是集群里第一个写入的 key不是异常。--rev这个参数是 etcd 相比很多 KV 存储的差异点它让读历史快照变成一次普通的读请求做数据变更审计、事故回溯时不用额外搭一套 binlog。前提是那个 revision 还没被 compaction 掉压缩之后历史版本就读不到了会返回版本已压缩的错误。3.3 watch 与 lease一次订阅还是持续触发租约到期谁负责删 keywatch 是 etcd 的招牌能力之一。单次 watch 在键发生变化时输出新值加--rev可以从指定版本开始补事件这在客户端断线重连时非常关键——重连后不用全量拉一遍从上次处理到的 revision1 继续 watch 就行中间漏掉的事件会被补发。# 另开一个终端先挂上 watch etcdctl --endpointshttp://127.0.0.1:12379 watch /test/name # 在原终端改一次值watch 那边会立刻输出事件并带 revision etcdctl --endpointshttp://127.0.0.1:12379 put /test/name wolf # 从指定历史版本开始补事件用于断线重连场景 etcdctl --endpointshttp://127.0.0.1:12379 watch --rev8 /test/nameetcd 的 watch 是稳定的长连接订阅不是一次性的单次触发某个 key 删掉重建、前缀下新增 key都能持续收到事件。做服务注册、配置热更新靠的就是这条链路。lease 承担的是存活周期控制的角色。客户端申请一个租约、拿到 lease ID把 key 绑到这个租约上之后要么周期性续约要么就等着到期被自动删除。# 授予一个 TTL 30 秒的租约返回 lease ID etcdctl --endpointshttp://127.0.0.1:12379 lease grant 30 # 把 key 绑到租约上写法和普通 put 一样只是多一个 --lease etcdctl --endpointshttp://127.0.0.1:12379 put --leaseleaseID /test/name fox # 手动续约进程活着就定期刷 TTL etcdctl --endpointshttp://127.0.0.1:12379 lease keep-alive leaseID # 查询租约剩余时间加 --keys 能一并列出绑在上面的 key etcdctl --endpointshttp://127.0.0.1:12379 lease timetolive leaseID etcdctl --endpointshttp://127.0.0.1:12379 lease timetolive --keys leaseID # 主动撤销租约绑定的 key 会被一并删除 etcdctl --endpointshttp://127.0.0.1:12379 lease revoke leaseID服务注册就是这套用法的典型服务启动时申请一个较长 TTL 的租约把自己的地址写到/services/name/instanceID并绑定租约后台起一个 keep-alive 循环进程崩溃、网络中断导致续约失败租约到期后 etcd 自动把那个 key 删掉其他服务 watch 到删除事件就知道这个实例下线了。相比临时节点那种方案租约的语义更明确续约失败就是失败不会出现节点还在但状态不明的情况。注意TTL 不要设得太短。网络抖动一次就可能续约失败导致 key 被误删也不要设太长实例已经死了很久才被发现服务发现就失去了意义。实践中按心跳间隔的 3 到 5 倍取值比较稳。3.4 Auth 权限user、role、permission 三层顺序错一步就锁死etcd 的权限模型是三层结构用户user绑定角色role角色绑定权限permission权限是某个 key 范围上的 read/write/readwrite。开启认证之前必须先把 root 用户建出来否则 auth enable 执行成功的那一刻所有没带用户名密码的请求都会被拒绝包括你自己接下来想创建用户的那条命令。# 创建用户、改密码、查列表、删除 etcdctl --endpointshttp://127.0.0.1:12379 user add fox etcdctl --endpointshttp://127.0.0.1:12379 user passwd fox etcdctl --endpointshttp://127.0.0.1:12379 user list etcdctl --endpointshttp://127.0.0.1:12379 user del fox # 创建角色、给角色赋权、收回权限、删除角色、查看角色 etcdctl --endpointshttp://127.0.0.1:12379 role add test etcdctl --endpointshttp://127.0.0.1:12379 role grant-permission test readwrite /test/name etcdctl --endpointshttp://127.0.0.1:12379 role revoke-permission test /test/name etcdctl --endpointshttp://127.0.0.1:12379 role del test etcdctl --endpointshttp://127.0.0.1:12379 role list etcdctl --endpointshttp://127.0.0.1:12379 role get test # 用户绑定角色、解绑 etcdctl --endpointshttp://127.0.0.1:12379 user grant-role fox test etcdctl --endpointshttp://127.0.0.1:12379 user revoke-role fox test # 开启认证前必须先建 rootroot 自带全部权限 etcdctl --endpointshttp://127.0.0.1:12379 user add root etcdctl --endpointshttp://127.0.0.1:12379 auth enable # 之后所有操作都要带凭据否则返回认证失败 etcdctl --userroot --password你的密码 --endpointshttp://127.0.0.1:12379 get /test/name集群场景下的顺序更要小心对三个节点分别建 root、然后带 root 凭据执行 auth enable。切到认证模式之后普通用户的完整流程是先建用户、再建角色、给角色赋权、最后把用户绑到角色上四步缺一不可权限范围只认前缀和区间/test和/test/name是两个不同的权限范围。注意一旦开启 auth 而 root 密码又丢了恢复手段相当麻烦常规做法是停掉所有节点、在一个节点上带--force-new-cluster单节点拉起重建用户后再恢复集群。上线前把 root 凭据放进配置管理比事后补救省事得多。4. 一次 get 背后发生了什么Range 链路、WAL 写入与 MVCC 的 treeIndex命令行玩熟之后遇到读到的数据不对写成功但另一台机器读不到这类问题光会敲命令就不够了。这一章按读、写两条路径拆开再把 Raft 和 MVCC 单独拎出来讲清楚是这份资料里最有含金量的部分。4.1 读请求Client 轮询、拦截器、串行读与线性读的分岔etcd 是典型的读多写少存储实际业务里读请求通常占到三分之二以上所以读路径的设计直接决定整体表现。一次etcdctl get hello --endpoints...从敲下回车到看到结果大致经过这些环节。Client 层先解析参数get对应 KVServer 的 Range 接口hello是 key--endpoints是后端地址列表。然后用 clientv3 库创建一个客户端对象负载均衡算法是 Round-robin也就是对每个请求按轮询顺序从 endpoint 列表里挑一个保持长连接让各节点负载尽量均衡。生产环境一定要配多个 endpoint某个节点故障时客户端能自动重连到其他节点单个 endpoint 的配置等于把 etcd 的高可用废掉一半。请求进到 server 端先经过一串 gRPC 拦截器这是 etcd 比较巧妙的设计用非侵入的方式把 metrics 采集、日志、请求耗时、错误码、来源 IP 这些通用能力挂上去还基于拦截器实现了两个重要特性——执行操作前检查集群是否有 Leader以及请求耗时超过阈值时打印带来源 IP 的慢查询日志。排障时这条慢查询日志非常好用能直接定位到是哪个客户端在拖慢整个集群。进了 KVServer 的 Range 处理逻辑真正的分岔点出现了这次读是串行读还是线性读。串行读Serializable直接读本地状态机不跟 Leader 确认延迟低、吞吐高但可能读到旧数据。原因很直白一个写请求要经过 Leader 落 WAL、广播给 Follower、多数派确认、提交、再异步 Apply 到状态机。如果客户端连的那个 Follower 磁盘 IO 刚好有波动它的 Apply 进度落后此时读到的就是写之前的旧值。线性读Linearizable走 ReadIndex 机制节点收到读请求后先向 Leader 确认当前已提交的最新日志索引拿到之后在本地等待自己的 Apply 进度追上这个索引再读状态机。这样读到的数据一定不旧于请求发起时刻已提交的数据。etcd 的默认读是线性读就是为了让客户端在任意节点上都能读到一致的数据。注意对一致性要求不高的场景比如读一份几乎不变的静态配置显式降级成串行读能明显降低读延迟和 Leader 压力但涉及分布式锁、选主、配置切换这类必须看到最新值的逻辑不要图省事改成串行读。线性读的成本在于多了一次与 Leader 的往返确认所以它比串行读慢。这个代价换来的是任意节点都能读到一致数据这个性质值不值取决于业务。4.2 写请求从 Quota 检查到 WAL 落盘再到 Apply 进 MVCC写路径比读路径长得多环节也更多。一次 put 大致经过 Quota、KVServer、WAL、Apply、MVCC 五个模块。Quota 模块负责后端数据库的体积管控。etcd 的 boltdb 文件大小有上限--quota-backend-bytes控制默认在 2 GiB 量级接近上限时会先发 NOSPACE 告警超过之后集群整体转为只读所有写请求被拒绝。这个设计是为了防止磁盘被悄悄写满导致更严重的故障但代价是如果没人处理整个集群就写不进去了。所以监控 dbSize 和 NOSPACE 告警是运维的必修课。KVServer 模块负责请求校验和鉴权检查 key 和 value 的长度是否超过限制、当前用户对该 key 范围是否有写权限、请求体是否超过--max-request-bytes。校验通过之后写请求被包装成一个 Proposal 交给 Raft 模块带上 prevKV、lease 之类的附加信息。Raft 层收到 Proposal 后只有 Leader 能处理先把日志条目追加到自己的 WAL 并落盘然后并行广播给所有 FollowerFollower 落盘后回 ACK当超过半数节点确认落盘这个条目被标记为已提交。注意这里是落盘后确认而不是收到就确认这也是 etcd 写入延迟主要来自 fsync 的原因。已提交的日志条目由 etcdserver 模块异步取出交给 Apply 模块应用到状态机。Apply 里做的就是 MVCC 的活写一个新的 revision更新内存里的 treeIndex然后把 key、revision、value 写进 boltdb 的一个事务里。这一步做完客户端那边才收到 put 成功的响应。整个链路里最容易被忽视的是 Apply 是异步的日志提交和状态机可见之间存在一个窗口。这解释了为什么写成功后立刻从另一个节点读偶尔会读不到——如果那个节点 Apply 落后而客户端又用了串行读就会看到旧状态。线性读正是为了消除这个窗口而存在的。4.3 RaftLeader 选举、日志复制、Term 与 Index 定位Raft 是 etcd 保证多节点一致的基石三个核心机制值得掰开选举、复制、安全性。选举靠心跳维持。Leader 周期性向 Follower 发心跳etcd 默认在百毫秒量级Follower 超过选举超时默认在秒级且每个节点随机化没收到心跳就认为 Leader 失联把自己的任期 term 加一、转为 Candidate、向其他节点拉票。拿到超过半数选票的节点成为新 Leader。随机化的选举超时避免了所有 Follower 同时竞选、反复僵持。日志复制是 Leader 收到写请求后的动作追加到本地日志、广播 AppendEntries、收集 ACK、多数派确认后提交然后通知所有节点可以应用。Follower 的日志如果和 Leader 不一致比如上次 Leader 只写了一半就挂了Leader 会通过回退索引的方式找到最后一个共同点用自己较新的日志覆盖对方。这套机制保证所有节点的日志最终一致。安全性约束有两条很关键。第一条是选举限制只有日志不落后于其他节点的候选人才可能当选具体比较方式是先比最后一条日志的 term、term 相同再比 index更大者更新。这条规则保证了已经提交的日志不会因为选举而丢失。第二条是提交规则Leader 只能提交当前 term 的日志不能靠数副本数来提交之前 term 的日志必须等当前 term 有一条新日志提交之后之前的日志才被间接提交。Term 和 Index 是 Raft 里定位日志的两个坐标term 表示这是第几任 Leader 任期内的日志index 是在整个日志序列里的位置。排查集群问题时endpoint status输出的 raftIndex 和 raftTerm 就是这两个值节点之间 raftIndex 差距过大通常意味着某个节点 Apply 严重落后需要重点看它的磁盘。节点数对 Raft 的影响在这里体现得最直接写请求要等多数派落盘节点越多需要的 ACK 越多长尾延迟越明显。这也是七节点之后不建议继续扩的原因。4.4 MVCCtreeIndex 在内存里做索引boltdb 存真正的版本数据MVCC 是 etcd 支持无锁读、历史版本查询的基础它由两部分组成内存里的 treeIndex 和磁盘上的 boltdb。treeIndex 是一棵内存 B 树实际实现是 btreekey 是用户键value 是这个键对应的所有 revision 组成的数组。它不存数据本身只存这个 key 有哪些版本这份索引。因为全部在内存里查找是一条 O(log n) 的路径。boltdb 里存的是另一套映射revision 到 key/value 的对应关系。每个 revision 对应一条记录key 是 revision 编码后的字节value 里包含用户键和实际数据。这种设计把按 key 找版本和按版本取数据两件事拆到了两个结构上各自都能做到高效。更新 key 的流程是分配一个新 revision全局计数器加一把新数据写进 boltdb然后在 treeIndex 里把这个 revision 追加到对应 key 的版本数组末尾。注意写入 boltdb 和更新 treeIndex 是在同一个事务性流程里完成的否则崩溃后会出现索引和数据对不上。查询 key 的流程反过来先在 treeIndex 里找到这个 key 的版本数组根据请求的 revision默认是最新二分查找出小于等于目标 revision 的最大版本号再拿这个版本号去 boltdb 里读实际数据。这就是为什么--rev能读到历史值——历史版本并没有被覆盖一直躺在 boltdb 里只是索引里能查到而已。删除 key 不是物理删除而是写入一条删除标记分配新 revisionvalue 为空标记为 tombstone同时在 treeIndex 里追加这个 revision。查询时如果最新版本是 tombstone就返回 key 不存在但如果指定了更早的 revision依然能读到删除前的值。这套机制带来一个副作用历史版本会无限增长磁盘只增不减。那就需要压缩Compaction把某个 revision 之前的所有历史版本丢掉只保留每个 key 最新的那个版本。etcd 支持手动etcdctl compact rev和自动压缩--auto-compaction-retention按小时或按 revision 数保留。压缩只释放 boltdb 内部的空闲页文件大小不会自动缩小还需要etcdctl defrag做一次碎片整理才能真正把磁盘空间还给操作系统。5. 五个高频故障的排查顺序端口冲突、Quorum 丢失、NOSPACE、auth 锁死与租约蒸发部署和原理过一遍剩下的就是踩坑。下面五条是我在测试环境和类生产环境里反复遇到的每条按现象、原因、解决三段写排障时可以直接对照。5.1 集群起不来日志一直刷cannot fetch cluster information现象三个进程都在跑但member list只能看到一个成员日志里反复出现拉取集群信息失败或者等待其他成员超时集群始终没有 Leader。原因--initial-cluster与实际 peer 地址对不上或者端口写成了客户端口。还有一种情况是--initial-cluster-token三个节点不一致每个节点都认为自己在加入不同的集群互相不认。第三种是--name与 initial-cluster 里的名字不一致节点看见列表里有自己但按名字匹配不上。解决三个节点的--initial-cluster、--initial-cluster-token、--initial-cluster-state必须逐字符一致只有--name和各自的地址不同。确认 initial-cluster 里写的全是 2380 这类 peer 端口而不是 2379。如果已经用错误参数启动过、产生了脏数据必须清空--data-dir重新来光改参数重启是没用的旧数据目录里的成员信息会一直干扰。5.2 第二个节点起不来报 address already in use现象单机多实例场景下第一个节点启动正常第二个节点直接退出日志里是端口被占用。原因etcd 默认客户端口 2379、节点间口 2380同一台机器上多个实例不做偏移必然冲突。很多人只改了客户端口忘了 peer 端口或者反过来。解决每个实例的客户端口和 peer 口都要成对偏移比如 12379/12380、22379/22380、32379/32380并且--listen-*-urls与--initial-advertise-peer-urls、--initial-cluster里出现的地址必须完全对应。改完端口之后--data-dir也要跟着用不同目录否则旧实例的成员信息会混进来。5.3 集群突然写不进去客户端报 no space现象所有写请求被拒绝返回etcdserver: mvcc: database space exceeded读请求还正常监控里 dbSize 一路涨到接近上限。原因历史版本没有压缩boltdb 文件持续增长触发了 quota 限制。etcd 一旦进入 NOSPACE 状态会整体转只读这是保护机制不是 bug。常见诱因是某些 key 被高频更新又没有配置自动压缩。解决分三步走。先etcdctl alarm list确认 NOSPACE 告警存在再对当前 revision 做一次压缩etcdctl compact 当前revision然后逐个节点执行etcdctl defrag注意 defrag 期间该节点会阻塞要一个一个来别同时做最后etcdctl alarm disarm解除告警写操作才恢复。长期方案是把--auto-compaction-retention打开并定期检查 dbSize 趋势。5.4 开启 auth 之后所有命令都返回 authentication failed现象执行auth enable成功紧接着任何 etcdctl 命令都报认证失败连user list也看不了整个集群等于锁死。原因开启认证之前没有创建 root 用户或者创建了但密码没记住。auth enable 生效后没有凭据的请求一律被拒绝包括管理类命令。解决如果 root 用户存在只是密码丢了先带--userroot --password试几个可能的密码确实找不回来常规恢复手段是停掉所有节点挑一个节点带--force-new-cluster单节点强制拉起用这个单节点重建用户体系再让其他节点重新加入。这个操作有丢数据风险只在确认无法恢复时使用。预防手段很简单建 root → 验证 root 能读写 → 再 enable且把凭据存进配置管理。5.5 服务注册表里的实例莫名其妙消失现象服务注册用的 key 过一段时间就没了watch 那边收到删除事件但服务进程明明还活着。原因租约的 keep-alive 断了。可能是客户端到 etcd 的网络抖动、也可能是 keep-alive 循环所在的 goroutine 被阻塞、还可能是 TTL 设得太短而客户端续约间隔太长。租约一到期etcd 不关心业务进程是否存活绑定在上面的 key 一律删除。解决先确认 TTL 和续约间隔的比例续约间隔不要超过 TTL 的三分之一再检查客户端有没有处理 keep-alive 返回的 channel 关闭事件很多实现只起了一个续约循环却从不监听这个 channel连接断了也不知道等到 key 消失才发现。另外 watch 断线重连时要带上次处理到的 revision1否则断开期间的事件会永久丢失表现为某个实例其实早就下线了但本地状态一直没更新。6. 用 endpoint status 和 check perf 给集群做体检一套能落地的自检顺序功能跑通不等于可以上线。我现在的习惯是任何一次参数变更、版本升级、扩容缩容之后先跑一遍固定顺序的体检确认集群处于健康状态再谈业务验证。第一步看成员和健康度# 三个节点是否都在是否恰好一个 Leader有没有 learner etcdctl --endpointshttp://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 member list # 每个 endpoint 的健康检查返回 healthy 表示能正常处理请求 etcdctl --endpointshttp://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint health # 更详细的状态dbSize、raftIndex、raftTerm、leader、isLearner etcdctl --endpointshttp://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint status -w p a hrefhttps://download.csdn.net/download/weixin_43109587/87442251 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p