ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

etcd v3.5.5 部署与排障:从单机到三节点集群

etcd v3.5.5 部署与排障:从单机到三节点集群

简介:《etcd实战及其原理分析.pdf》面向希望系统掌握分布式键值存储的开发者、运维与架构人员,围绕 etcd 的部署实践与核心机制展开,适合具备一定 Go 与分布式基础、需要落地服务发现、分布式锁、主备选举等场景的读者。资源包仅含 1 个 PDF 文件,大小约 4.55MB,正文按 etcd 介绍、实战操作、核心原理三部分组织。内容从单实例、单机多实例到多节点集群搭建,结合客户端命令与权限管理说明常见操作;原理部分覆盖读请求与写请求执行流程、Raft 选举与日志复制、MVCC 的 treeIndex 及增删改查机制,并梳理 WAL、Snapshot、boltdb 等存储模块。已有 448 人学习,可作为理解 etcd 架构与排错思路的参考。

1. etcd v3.5 装之前先想清楚:单机、伪集群还是真集群

帮人看一个 etcd 起不来的现场,现象很简单:bind: address already in use,端口 2380 被上一轮没退干净的进程占着,而他把>cd /tmp # 下载 v3.5.5 的 linux-amd64 包,包名里的架构要和机器对上 wget https://github.com/etcd-io/etcd/releases/download/v3.5.5/etcd-v3.5.5-linux-amd64.tar.gz # 解压后目录里同时带 etcd 和 etcdctl 两个可执行文件 tar -zxvf etcd-v3.5.5-linux-amd64.tar.gz cd etcd-v3.5.5-linux-amd64 # 装之前先确认二进制本身没问题 ./etcd --version ./etcdctl version

--version这一步别省。曾经遇到过压缩包传输中断,解压没报错但二进制是截断的,直接执行会报cannot execute binary file,早期发现能省掉后面一堆怀疑人生的时间。输出里应该能看到etcd Version: 3.5.5和API Version: 3.5两行,如果etcdctl version显示的是 v2 API,说明拿错了老包。

# 复制到系统 PATH,后续不用再带路径 cp etcd etcdctl /usr/local/bin/ # 建三个目录:数据、日志、配置 mkdir -p /data/etcd /var/log/etcd /etc/etcd # 用独立用户跑,别用 root 裸奔 useradd -r -s /sbin/nologin etcd chown -R etcd:etcd /data/etcd /var/log/etcd

目录规划这件事看起来无关紧要,但 etcd 的数据目录一旦有残留,重启时行为会很不一样。/data/etcd放 member 的 WAL 和 boltdb 快照,/var/log/etcd放日志,/etc/etcd放配置文件,三者分离,后面换盘、清数据、做备份都不会互相牵连。

2.2 用配置文件代替一长串命令行参数

etcd 支持把命令行参数全部写进 YAML,用--config-file加载。生产环境我一直用配置文件,原因很实在:命令行长到一定程度,改错一个字符肉眼看不出来,而 YAML 至少能对齐。

# /etc/etcd/etcd.conf.yml name: etcd-1># /etc/systemd/system/etcd.service [Unit] Description=etcd v3.5.5 After=network-online.target Wants=network-online.target [Service] Type=notify User=etcd Group=etcd ExecStart=/usr/local/bin/etcd --config-file=/etc/etcd/etcd.conf.yml Restart=on-failure RestartSec=5 # etcd 每个客户端连接会占文件描述符,限制太小会莫名拒绝连接 LimitNOFILE=65536 [Install] WantedBy=multi-user.target

Type=notify的意思是 etcd 启动完成会主动通知 systemd,systemctl start返回时服务已经真的可用了,不会出现"启动命令成功了但端口还没监听"的假象。Restart=on-failure配合RestartSec=5,避免进程崩溃后疯狂重启把日志刷爆。

systemctl daemon-reload systemctl enable --now etcd # 看状态,active (running) 才算起来 systemctl status etcd --no-pager journalctl -u etcd -n 50 --no-pager # 端到端验证:健康检查 + 状态查询 etcdctl --endpoints=http://127.0.0.1:2379 endpoint health etcdctl --endpoints=http://127.0.0.1:2379 endpoint status -w table # 真正落一条数据 etcdctl --endpoints=http://127.0.0.1:2379 put username fox etcdctl --endpoints=http://127.0.0.1:2379 get username

endpoint health返回is healthy: successfully committed proposal才算完整闭环,这句话的含义是"不仅进程在,而且能成功提交一次提案",比单纯ss -lntp看到 2379 端口有价值得多。到这一步,单实例的地基就算打完了。

3. 单机多实例与三节点集群:goreman 和静态配置两条路

单实例跑通之后,下一步是集群。这里有个分岔:本地开发验证选单机多实例(三个进程三个端口),预发和生产环境选多机三节点静态配置。两者参数结构一样,只是地址和端口不同,先把前者跑一遍,能省掉大量在真机上反复试错的时间。

3.1 用 goreman 在一台机器上拉起三个实例

单机多实例的核心矛盾只有一个:三个 etcd server 默认都想监听 2380,端口必然冲突。解决办法是给每个实例分配一套独立的客户端端口和集群端口,比如 12379/12380、22379/22380、32379/32380。

goreman 是个轻量的多进程管理工具,靠一个 Procfile 格式的文本文件批量起停进程,需要 Go 环境。

# 安装 goreman,依赖本机已有 Go go install github.com/mattn/goreman@latest # 确认命令可用 goreman help

下面这份local-cluster-profile就是三个实例的启动清单。注意每行是一条完整命令,不能换行,goreman 是按行解析的,续行符会被当成命令的一部分。

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 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --data-dir /tmp/etcd-data/infra1 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 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --data-dir /tmp/etcd-data/infra2 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 'infra1=http://127.0.0.1:12380,infra2=http://127.0.0.1:22380,infra3=http://127.0.0.1:32380' --initial-cluster-state new --data-dir /tmp/etcd-data/infra3

三个实例的--initial-cluster必须逐字一致,包括成员名和端口。这个字符串是集群引导期的"通讯录",任何一个实例写的内容不同,集群就会分裂成两个互不认识的阵营,表现出来是各自都能起,但member list只看到自己。

# 前台启动,Ctrl+C 全部停掉;加 -f 指定 profile 文件 goreman -f ./local-cluster-profile start # 另开终端,从任意一个实例查成员列表 etcdctl --endpoints=http://127.0.0.1:12379 member list -w table # 随便停一个,观察集群是否仍然可写 goreman run stop etcd3 etcdctl --endpoints=http://127.0.0.1:12379 put user fox # 再把节点拉回来,它会自动追平日志 goreman run restart etcd3

member list的输出里ISLEADER列只有一个true,其余是false;如果全是false,说明选主没完成,别急着写数据,先去看日志里的lost leader或者failed to reach之类的记录。停掉一个节点后仍然能写入,是因为 3 个节点里 2 个达成多数派就满足 Quorum;但停掉两个就彻底写不进去了,这是 Raft 的固有约束,不是配置问题。

3.2 三节点静态配置:把地址和成员名提前确定下来

生产环境的三节点集群基本都用静态配置:搭集群之前节点数量和 IP 都已知,启动时通过参数把成员列表写死。准备三台机器,假设内网地址分别是 10.0.0.11、10.0.0.12、10.0.0.13,机器名对应 infra0、infra1、infra2。

# 每个节点都要执行:建目录、放二进制、准备配置文件目录 mkdir -p /data/etcd /var/log/etcd /etc/etcd cp etcd etcdctl /usr/local/bin/

节点一的配置文件如下,另外两台只改name和所有出现 IP 的地方,initial-cluster保持完全相同。

# /etc/etcd/etcd.conf.yml (10.0.0.11 上) name: infra0># 三台分别执行 systemctl enable --now etcd # 在任意一台验证成员数量,应该是 3 个 etcdctl --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 member list -w table # 逐个体检 etcdctl --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 endpoint health --cluster

--cluster参数会把 endpoints 列表里每个成员都检查一遍,比单点检查更能暴露问题。常见的半成功状态是:member list显示三个成员,但 health 只有两个健康,那个不健康的往往是 peer 端口被防火墙拦了,日志里会出现failed to dial加目标地址。

3.3 关键启动参数逐条对照

参数不多,但每一个都有明确语义,混淆之后排查成本极高。下表是我自己整理的一份对照,改配置的时候会对着看。

参数作用常见误配
--name本节点在集群中的唯一名字与initial-cluster里的成员名不一致,导致握手失败
--data-dir存放 WAL 和快照的目录多实例共用同一目录,数据互相覆盖
--listen-client-urls监听客户端请求(2379)只写 127.0.0.1,跨机访问全部超时
--advertise-client-urls告知客户端用什么地址访问自己写成 0.0.0.0,客户端拿到不可达地址
--listen-peer-urls监听集群内部通信(2380)与 2379 写反,节点间永远连不上
--initial-advertise-peer-urls告知其他成员用什么地址连自己多网卡机器写错网段
--initial-cluster集群初始成员通讯录各节点内容不一致,集群分裂
--initial-cluster-token集群标识,隔离同名集群同一网络里两套集群用同一 token
--initial-cluster-statenew新建 /existing加入已有数据的目录用new重启,直接拒绝启动

--initial-cluster-token这个参数平时没人注意,但它的作用不小:同一批机器上如果同时跑着两套 etcd 集群,token 相同会导致节点互相串门,A 集群的节点跑去 B 集群注册。给它一个能区分环境的字符串,比如etcd-prod-1、etcd-test-1,成本几乎为零。

--initial-cluster-state是另一个高频雷区。new只在集群第一次引导时用,一旦># size 是集群节点数,必须提前确定,返回一个一次性 token URL curl https://discovery.etcd.io/new?size=3 # 各节点用同一个 discovery URL 启动,不再需要 initial-cluster etcd --name infra0 --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,http://127.0.0.1:2379 \ --advertise-client-urls http://10.0.0.11:2379 \ --discovery https://discovery.etcd.io/<返回的token>

这个 token 只能用于一次集群引导,集群起来之后就不能再拿它做运行时扩缩容了。扩缩容走的是member add/member remove,两套机制别混。另外--name必须唯一,重名会让发现服务直接拒绝注册。

4. etcdctl 与权限:从 put/get 到 auth enable

集群起来只是能存数据,真正接入业务之前还有两件事:把 etcdctl 的常用操作摸熟,以及把认证打开。前者决定排查效率,后者决定这套环境能不能上线。

4.1 键操作与 revision 字段怎么读

etcdctl 的键操作就那么几个,但返回值里的字段值得单独说,因为排查"数据为什么读不到"时全靠它们。

export ETCDCTL_API=3 # 3.4 以前必须显式指定,3.5 默认已是 v3,留着是为了兼容老脚本 EP="--endpoints=http://10.0.0.11:2379" # 写 etcdctl $EP put /test/name fox # 读 etcdctl $EP get /test/name # 以 JSON 输出,能看到完整元数据 etcdctl $EP get /test/name -w json # 按前缀批量读,limit 控制返回条数 etcdctl $EP get --prefix /test --limit 3 # 按字典序从某个 key 开始读 etcdctl $EP get --from-key name2 # 读历史版本,rev 是全局 revision etcdctl $EP get --rev=8 /test/name # 删除并带回被删的值 etcdctl $EP del --prev-kv /test/name

-w json的输出里有几个字段必须理解,否则会误判数据状态:

  • revision:集群全局版本号,任何一次 put 或 delete 都会让它加 1,所以它是集群级的,不是单个 key 的。
  • create_revision:这个 key 第一次被创建时的全局 revision。
  • mod_revision:这个 key 最后一次被修改时的全局 revision,和revision相等说明它就是最近一次变更。
  • version:这个 key 自己被改过几次,从 1 开始;删除后重建会重新从 1 计。
  • raft_term:当前 Leader 的任期号,节点重启或重新选主后这个值会变。

一个典型误用是拿version当全局版本号做乐观锁。version是 key 级别的,两个不同的 key 各自从 1 开始;跨 key 的并发控制必须看mod_revision。

4.2 lease 与 watch:TTL 和变更通知

租约是 etcd 做服务发现的基础。键绑定到租约上,租约到期,键自动消失,不需要业务自己写清理逻辑。

# 申请一个 TTL 30 秒的租约,返回租约 ID etcdctl $EP lease grant 30 # 写入时绑定租约 etcdctl $EP put --lease=705b8474d1acd63b /test/name fox # 续期,保持租约不过期 etcdctl $EP lease keep-alive 705b8474d1acd63b # 查剩余时间和绑定的 key etcdctl $EP lease timetolive --keys 705b8474d1acd63b # 主动撤销,绑定的 key 一起删掉 etcdctl $EP lease revoke 705b8474d1acd63b # 监听单个 key 的变化,另开终端 put 一次就能看到输出 etcdctl $EP watch /test/name

lease keep-alive是长连接,前台运行会一直挂着,实际业务里由客户端库在后台续期。手工验证时如果看到 key 莫名其妙消失,第一反应应该是租约到期而不是数据被误删,lease timetolive一查就清楚。

watch 相比轮询的价值在于事件驱动,且 etcd 的 watch 是可持续的,不会像某些实现那样触发一次就失效。做配置热更新时,业务进程只需要 watch 一个前缀,配置变更后立刻收到通知,不用重启。

4.3 用户、角色与开启认证的正确顺序

权限这块的顺序错了会把自己锁在外面。正确流程是:建 root 用户 → 建普通用户和角色 → 给角色授权 → 用户绑角色 → 最后才auth enable。

EP3="--endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379" # 第一步:必须有 root 用户,否则 auth enable 会失败 etcdctl $EP3 user add root # 第二步:开启认证,从此所有请求都要带账号 etcdctl $EP3 --user=root --password=123456 auth enable # 第三步:建普通用户和角色 etcdctl $EP3 --user=root --password=123456 user add fox etcdctl $EP3 --user=root --password=123456 role add test # 第四步:给角色授权,粒度是 key 范围加读写类型 etcdctl $EP3 --user=root --password=123456 role grant-permission test readwrite /test # 第五步:用户绑定角色 etcdctl $EP3 --user=root --password=123456 user grant-role fox test # 验证:普通用户只能操作被授权的范围 etcdctl $EP3 --user=fox --password=xxxxxx get --prefix /test etcdctl $EP3 --user=fox --password=xxxxxx get /other # 应该返回 permission denied

root 用户不需要绑定任何角色,天生拥有全部权限,这也意味着 root 密码必须是能保管住的那种。auth enable之后如果所有客户端都返回 401,先确认命令里有没有带--user和--password,再确认账号密码没打错,最后才怀疑是不是认证把集群搞坏了。

注意:auth enable之后忘记 root 密码是比较麻烦的事。恢复手段是停掉其他节点,在单个节点上带--force-new-cluster启动重建集群,然后重新配权限和数据。这属于重量级操作,执行前一定确认没有别的路可走。

角色授权的 key 范围支持前缀匹配,/test会覆盖/test及其所有子 key。如果只想放行单个 key,就写完整路径/test/name。粒度设计上建议按业务前缀划分,避免出现一条 readwrite 覆盖整个键空间的情况。

5. 排查与避坑:五条从现场带回来的记录

etcd 的报错大多集中在启动、选主、认证三个阶段,且同一句报错可能对应好几个根因。下面五条按"现象 → 原因 → 解决"整理,都是实际环境里反复出现的。

5.1 启动与端口类问题

现象一:启动即退出,日志里是bind: address already in use。原因通常不是端口真的被别人占了,而是上一轮 etcd 没退干净——kill之后进程还在收尾,或者用nohup起的实例被遗忘。etcdctl endpoint health此时还能通,让人误以为"服务正常,新实例起不来是别的原因"。 解决办法是先确认占用者:ss -lntp | grep -E '2379|2380'找到 PID,kill掉;如果确认是老实例又不想留它的数据,再清理>ss -lntp | grep -E '2379|2380' ps -ef | grep etcd | grep -v grep # 确认无用后清理,注意这一步会丢数据 rm -rf /data/etcd/*

现象二:改了--name或--initial-cluster后重启失败,报member ... has already been bootstrapped。原因是># 时间同步状态检查,偏差应在毫秒级 timedatectl status chronyc tracking 2>/dev/null || ntpq -p

现象五:auth enable之后,业务侧全部请求返回 401,包括原本正常的读。原因通常是三类:客户端连接没有带账号密码;密码里含特殊字符被 shell 提前解释;开启了认证但调用方还用着开启前的长连接缓存。 解决上,先去掉业务代码,用 etcdctl 手工验证一次:etcdctl --user=root --password=xxx endpoint health。如果手工能通,说明是客户端配置问题;手工也不通,再检查 root 口令是否正确。密码含$、!这类字符时用单引号包住,别用双引号。

提示:生产环境开启 auth 前,务必先在预发环境把整套授权关系跑一遍,确认每个业务方用的账号和权限范围,再在生产执行。认证一旦打开,没配好权限的客户端会立刻开始报错。

6. 装完怎么验证:一份能照着打勾的验收清单

环境装完不等于装对。我见过不少"能起来、能读写"但一压就散、一停就选不出主的集群,问题都藏在没验证的角落里。下面这份清单按从轻到重的顺序排,单机、集群都适用,建议逐条跑一遍再交付。

EP="--endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379" # 1. 三个节点都健康,且只有一个 leader etcdctl $EP endpoint health --cluster etcdctl $EP endpoint status -w table # 2. 成员数量与预期一致 etcdctl $EP member list -w table # 3. 跨节点数据可见:往 A 写,从 C 读 etcdctl --endpoints=http://10.0.0.11:2379 put /verify/key ok etcdctl --endpoints=http://10.0.0.13:2379 get /verify/key # 4. 无告警:NOSPACE、CORRUPT 都应该是空的 etcdctl $EP alarm list # 5. 数据库大小与配额占用 etcdctl $EP endpoint status -w json | grep -o '"dbSize":[0-9]*' # 6. 清理验证数据 etcdctl $EP del /verify/key

第 3 条是整个验收里最关键的一步。单节点自读自写只能证明进程活着,跨节点读写才能证明 Raft 日志确实复制到了多数派、状态机确实应用成功。第 4 条容易被忽略:alarm list返回memberID:... alarm:NOSPACE说明后端配额已经打满,此时 etcd 只接受读和删除,所有写入都会失败,而客户端看到的可能只是一句etcdserver: mvcc: database space exceeded,不查 alarm 很难定位到配额上。

故障切换也要实测一次,这是唯一能证明"高可用真的可用"的方式。

验收项操作期望结果
健康检查endpoint health --cluster全部 healthy
成员完整member list -w table数量正确,ISLEADER 只有一个 true
跨节点读A 写 C 读读到相同值
告警alarm list输出为空
停 Leader停掉 ISLEADER 为 true 的节点5 秒内剩余节点选出新 Leader,写入不中断
恢复节点启动刚停的节点自动追平日志,重新出现在 member list 中
认证开 auth 后带账号读写授权范围外返回 permission denied

停 Leader 这一步有个前提:别在只有两个节点存活的时候做。3 节点集群停 1 个还剩 2 个,满足多数派;如果先停了一个再停 Leader,剩下 1 个节点凑不够 Quorum,整个集群只读不写,新手很容易在这里以为"集群彻底坏了"。恢复方法就是把节点拉起来,等它追平日志。

还有个可选动作:etcdctl check perf可以做吞吐压测,但它会在集群里写入大量测试数据,只能在测试环境跑,生产环境慎用。真要评估性能,用endpoint status看dbSize和raftIndex的增长趋势,比压测更安全。

从那以后我每次搭完 etcd,都会强制走一遍"停 Leader 再拉回来"的流程,不管环境多小、交付多急。有次省了这一步,上线第二天主节点所在机器重启,集群选了 40 秒才选出新 Leader,原因是从节点的磁盘 I/O 太慢,日志落盘跟不上,而这个问题在任何静态检查里都看不出来。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进