Docker Compose搭建etcd集群:参数详解、健康检查与故障演练实战 如果你跟我一样过去习惯用裸进程或者脚本去拉 etcd 集群第一次看到完整的三节点配置时一定会觉得有点乱每个节点要同时处理 name、IP、客户端端口和节点间通信端口还有一串看起来像“身份证号”的 initial-cluster 参数。这篇文章就围绕 docker-compose 搭建 etcd 集群这件事把我最近的完整实操过程梳理一遍包含可以直接复制的 compose 文件、每个启动参数背后的作用、启动后的健康检查、故障演练以及平时不容易查到的排查经验。适合刚接触 etcd、打算把单点改成集群的朋友也适合已经搭过但还没彻底弄懂参数细节的运维同路人。1. 项目拆解etcd 集群到底在解决什么问题1.1 单点部署是所有事故的源头很多团队最先接触 etcd都是因为某个配置中心、服务注册表或者分布式锁需要一致性存储。开发环境里跑一个单节点看起来没什么问题等流量上来或者机器重启一下就露馅了。单节点 etcd 意味着没有副本数据只有一份。磁盘故障、误删数据目录、机器无法启动任何一个意外都可能导致配置数据永久丢失。更麻烦的是raft 算法需要多数派才能完成日志提交单节点虽然自身可以工作但没有“容错”可言它不是受保护的状态而是全量风险。所以搭建集群的第一个动机就是把“一个节点挂了全部不可用”变成“两个节点挂了我还在线”。etcd 的复制模型是 raft三节点集群最多容忍一台机器故障五节点集群最多容忍两台。这也是为什么更多人愿意在 etcd 上多花几个节点数而不是单点裸奔。1.2 为什么选择 docker-compose 而不是裸机或 k8s裸机部署 etcd 并不难下载二进制、写 systemd 单元、启动三个进程但真正麻烦的是后续维护日志轮转、进程守护、多节点环境差异、网络地址变更这些都得自己处理。docker-compose 把这些配套问题打包成一份 YAML数据卷、网络、健康检查、重启策略都能声明式管理对中小规模集群和本地开发环境非常友好。在另一头Kubernetes 自带 etcd 管理能力但如果你的业务只用 etcd 做配置中心或分布式锁完全不值得为此引入整套容器编排平台。docker-compose 的定位刚好卡在中间比裸脚本更规范比 k8s 更轻量。尤其是本地起三节点验证 raft 行为、测试客户端故障切换时一条命令就能完成整个环境的搭建和销毁性价比非常高。1.3 环境和前置条件我建议使用较新的 Docker Compose v2也就是docker compose子命令。老版本的docker-compose也能运行但在命令写法上有细微差别。Docker 引擎建议 20.10 以上避免旧版本在网络自定义方面的一些已知问题。理论上三节点可以全部跑在一台宿主机上通过容器内固定 IP 模拟三个独立节点。这只适合学习验证不适合生产。生产环境尽量让每个 etcd 节点落在不同宿主机否则一台物理机挂掉三个容器同时消失集群照样瘫痪。后面演示基于单台宿主机做三容器集群方便你复现。2. 动手之前集群拓扑与关键参数设计2.1 选几个节点奇偶数的真相有人会觉得四节点比三节点多一个应该更稳。实际上从 raft 的容错公式看四节点的多数派是 3和五节点的多数派也是 3但四节点和五节点容错的节点数不一样这里容易记错让我直接说结论一个 N 节点 etcd 集群最多容忍(N-1)/2向下取整的节点故障。三节点容忍 1 个四节点也是容忍 1 个五节点才升到 2 个。所以说四节点集群比三节点多花一份资源容错能力却没有提升反而增加边缘网络和成员管理的复杂度。这就是 etcd 集群推荐 3、5、7 这种奇数的原因。三节点是绝大多数业务的最佳起点对配置中心来说足够五节点通常给关键基础设施或需要跨可用区场景使用七节点以上很少见。2.2 静态发现 vs 动态发现etcd 集群节点互相发现的机制有静态配置和 DNS 动态发现两类。静态配置就是在启动参数里把三个节点的地址和名字写死例如etcd1http://172.20.0.11:2380...。这种方式的优势很直接所有成员都清晰可见配置即事实任何节点启动时都知道应该联系谁。DNS 动态发现则依赖 SRV 记录节点变化时只需要调整 DNS不需要挨个改配置。听起来优雅但坑也更多DNS 缓存策略、解析顺序、服务记录格式一旦排查起来非常绕。对于 docker-compose 管理的固定节点静态配置完全是更好的选择。只有当你做动态扩缩容平台、节点频繁上下线时才值得认真考虑 discovery 方案。2.3 端口和网段规划etcd 有两个重要端口2379 是客户端端口面向业务服务2380 是 peer 端口用于节点间 raft 通信和心跳。部署集群前要先规划网段我下面统一用 172.20.0.0/24 示例网段三个节点分别取 11、12、13。宿主机端口映射也需要提前定。容器间通过自定义网络内的固定 IP 通信所以 peer 端口 2380 不一定暴露到宿主机。客户端端口 2379 一般需要暴露给应用三个节点可以分别映射成宿主机 23791、23792、23793。如果你只有一套应用需要访问 etcd甚至可以只映射第一个节点的 2379但为了故障切换方便建议三个节点的客户端端口都暴露出来客户端配置里写三个地址做负载。下面是这次部署的地址规划节点容器内固定 IP客户端端口peer 端口宿主机映射etcd1172.20.0.112379238023791:2379etcd2172.20.0.122379238023792:2379etcd3172.20.0.132379238023793:23793. 完整配置与参数逐个拆解3.1 一个可以直接使用的 docker-compose.yml我使用的 etcd 镜像来自公共镜像仓库的官方发布版本版本号锁定为 v3.5.4。如果你在部署时发现版本较旧可以换成同系列更新的 patch 版本但三个节点最好保持一致。version: 3.9 services: etcd1: image: quay.io/coreos/etcd:v3.5.4 container_name: etcd1 hostname: etcd1 restart: unless-stopped networks: etcd-net: ipv4_address: 172.20.0.11 ports: - 23791:2379 command: - etcd - --nameetcd1 - --data-dir/etcd-data - --listen-client-urlshttp://0.0.0.0:2379 - --listen-peer-urlshttp://0.0.0.0:2380 - --advertise-client-urlshttp://172.20.0.11:2379 - --initial-advertise-peer-urlshttp://172.20.0.11:2380 - --initial-clusteretcd1http://172.20.0.11:2380,etcd2http://172.20.0.12:2380,etcd3http://172.20.0.13:2380 - --initial-cluster-statenew - --initial-cluster-tokenetcd-cluster-01 - --loggerzap - --log-levelinfo volumes: - etcd1_data:/etcd-data healthcheck: test: [CMD, etcdctl, endpoint, health, --endpointshttp://127.0.0.1:2379] interval: 30s timeout: 10s retries: 5 start_period: 20s etcd2: image: quay.io/coreos/etcd:v3.5.4 container_name: etcd2 hostname: etcd2 restart: unless-stopped networks: etcd-net: ipv4_address: 172.20.0.12 ports: - 23792:2379 command: - etcd - --nameetcd2 - --data-dir/etcd-data - --listen-client-urlshttp://0.0.0.0:2379 - --listen-peer-urlshttp://0.0.0.0:2380 - --advertise-client-urlshttp://172.20.0.12:2379 - --initial-advertise-peer-urlshttp://172.20.0.12:2380 - --initial-clusteretcd1http://172.20.0.11:2380,etcd2http://172.20.0.12:2380,etcd3http://172.20.0.13:2380 - --initial-cluster-statenew - --initial-cluster-tokenetcd-cluster-01 - --loggerzap - --log-levelinfo volumes: - etcd2_data:/etcd-data healthcheck: test: [CMD, etcdctl, endpoint, health, --endpointshttp://127.0.0.1:2379] interval: 30s timeout: 10s retries: 5 start_period: 20s etcd3: image: quay.io/coreos/etcd:v3.5.4 container_name: etcd3 hostname: etcd3 restart: unless-stopped networks: etcd-net: ipv4_address: 172.20.0.13 ports: - 23793:2379 command: - etcd - --nameetcd3 - --data-dir/etcd-data - --listen-client-urlshttp://0.0.0.0:2379 - --listen-peer-urlshttp://0.0.0.0:2380 - --advertise-client-urlshttp://172.20.0.13:2379 - --initial-advertise-peer-urlshttp://172.20.0.13:2380 - --initial-clusteretcd1http://172.20.0.11:2380,etcd2http://172.20.0.12:2380,etcd3http://172.20.0.13:2380 - --initial-cluster-statenew - --initial-cluster-tokenetcd-cluster-01 - --loggerzap - --log-levelinfo volumes: - etcd3_data:/etcd-data healthcheck: test: [CMD, etcdctl, endpoint, health, --endpointshttp://127.0.0.1:2379] interval: 30s timeout: 10s retries: 5 start_period: 20s volumes: etcd1_data: etcd2_data: etcd3_data: networks: etcd-net: ipam: driver: default config: - subnet: 172.20.0.0/24这个文件省去了很多人的重复劳动但直接抄之前你需要理解一件事为什么每个节点都要重复写一遍完整的 initial-cluster。因为 etcd 启动时节点本身并不知道集群里有多少成员它只能从自己拿到的配置列表里发现其他节点。三份配置内容相同但每个节点的 announce 地址不同这样任何一个节点先启动它都会去尝试联系另外两个地址等待多数派出现后选主。3.2 每个启动参数都在干什么先从最容易误解的--listen-*和--advertise-*说起。listen是进程监听在哪个地址容器里一般写0.0.0.0表示容器内所有网卡都提供服务。advertise是告诉其他节点和客户端“你来访问我时用哪个地址”这个必须写成实际可达地址不能写0.0.0.0。以 etcd2 为例容器内固定 IP 是 172.20.0.12所以--advertise-client-urlshttp://172.20.0.12:2379。如果你错误地写成http://etcd2:2379依赖 DNS 解析虽然容器名在 compose 网络里可解析但容易引入不确定因素。直接写固定 IP 最稳。--initial-cluster-statenew表示这是一次全新创建。如果后面某个节点数据目录崩溃、需要重新加入旧集群就要用existing模式否则新旧集群 ID 对不上会产生冲突。--initial-cluster-token相当于给这个集群打了一个唯一标签所有节点必须一致。它的作用是防止某个节点误加入一个数据集不同但地址相同的集群。恢复集群、跨环境迁移时这个 token 也要跟着走。--loggerzap是我强烈建议开启的。etcd 默认日志是 capnslog字段格式和可读性远不如 zap排查问题时不方便。换成 zap 后会输出结构化 JSON 日志日志里能直接看到 raft term、节点 ID、提案信息等。3.3 数据卷隐藏的坑每个节点背后的数据卷我特意用了 named volume 而不是 bind mount。原因很直接etcd 容器内部的进程往往以非 root 用户运行bind mount 宿主机目录时经常遇到目录权限不匹配的问题表现为permission denied或者failed to lock file。named volume 由 Docker 管理初始权限能自动与容器镜像期望对齐踩坑概率低很多。不过 named volume 也有麻烦数据卷不直观备份时要通过docker cp或者卷快照工具导出。我的习惯是日常测试无所谓跑起来就行如果是最小化生产环境还是用 bind mount 并在宿主机手动调整目录属主保证可审计性。还有一点必须强调etcd 数据目录不要放在 Docker 容器层。容器重建后数据丢失这谁都懂但真正容易忽略的是 Docker 默认的 storage driver 写性能和 fsync 开销。etcd 对磁盘延迟敏感留给它一个独立数据卷别和其他日志文件混在同一个 volume。4. 实操记录启动、验证、故障演练4.1 启动集群与健康检查配置文件准备好后先在同一目录执行docker compose up -d docker compose ps第一次启动时三个容器会同时进入启动流程。日志里可能出现某些节点短暂地“联系不到其他成员”这是正常的etcd 启动时会带有重试逻辑等多数派出现后自动收敛。我习惯先等十秒左右再看一次健康状态docker compose exec etcd1 sh -c etcdctl member list --write-outtable docker compose exec etcd1 sh -c etcdctl endpoint health --cluster --write-outtable如果看到三行healthy集群已经进入正常状态。这里要注意member list只能看到成员信息不代表 leader 已经选好endpoint health --cluster才是真正逐个检查可用性的命令。有一个很容易踩的坑直接在新版镜像里跑etcdctl可能因为环境变量ETCDCTL_API未设置而进入 v2 模式只要你有任何输出异常可以先显式指定docker compose exec etcd1 sh -c ETCDCTL_API3 etcdctl endpoint health --cluster4.2 写入读取验证集群健康不代表读写链路正常我习惯做一次最小验证docker compose exec etcd1 sh -c etcdctl put /demo hello docker compose exec etcd2 sh -c etcdctl get /demo如果从 etcd2 能读到 etcd1 写入的值说明 raft 日志同步、提交和本地 apply 都在正常工作。这个环节我见过太多人跳过结果等真正接入应用时才发现客户端配置里少了某个地址或者证书没配对。4.3 故障演练停掉一个节点三节点集群的核心价值是容忍单节点故障所以一定要亲自做一次故障演练。模拟故障最简单的方式是docker compose stop etcd2现在集群还剩 etcd1 和 etcd3它们仍然能组成多数派。再执行docker compose exec etcd1 sh -c etcdctl endpoint health --cluster --write-outtable你会看到 etcd2 显示unreachable但 as long as 另外两个端点是 healthy集群依然可读可写。这就是 raft 的多数派机制在起作用。实际业务客户端如果配了三个地址遇到 etcd2 不可达时会自动转而请求其他节点。恢复节点同样直接docker compose start etcd2 docker compose exec etcd2 sh -c etcdctl endpoint health --cluster --write-outtableetcd2 重新加入后会主动从 leader 那里追赶日志最终回到 healthy 状态。这个过程不需要人工干预也不用改配置前提是 etcd2 的数据目录还保留着原来的数据。如果你把数据目录删空再恢复就会变成一个新节点反而需要用 member add 的方式加回集群。4.4 如果必须扩容怎么做虽然静态配置模式下的扩容有点绕但方法并不复杂。核心步骤是先通过etcdctl member add把新节点注册进现有集群再去启动新容器。假设要加 etcd4先执行docker compose exec etcd1 sh -c etcdctl member add etcd4 --peer-urlshttp://172.20.0.14:2380命令会返回一段提示告诉你新节点需要使用的启动参数。之后在 compose 文件里新增 etcd4 服务注意initial-cluster要包含所有四个节点的地址并且--initial-cluster-stateexisting因为是对已有集群做成员变更不是重新初始化。启动新节点后用etcdctl member list确认 leader 跟上、日志追上。缩容则相反先etcdctl member remove node-id再从 compose 里移除对应服务并停止容器。顺序乱掉会出问题反过来操作可能导致旧节点重新连接僵尸成员。4.5 备份、恢复和灾难演练再稳的集群也可能遇到双节点同时故障。三节点集群一次损失两个节点就无法形成多数派此时理论上是“不可用但数据未丢”恢复也很麻烦。提前做备份就显得特别重要。备份用 etcd 自带的 snapshot 功能docker compose exec etcd1 sh -c etcdctl snapshot save /etcd-data/snapshot.db docker cp etcd1:/etcd-data/snapshot.db ./etcd-snapshot.dbsnapshot 保存的是某一时刻的完整数据快照。恢复演练时最好在一套干净的目录里进行etcdctl snapshot restore ./etcd-snapshot.db \ --name etcd-restored \ --data-dir /tmp/etcd-restored \ --initial-cluster etcd-restoredhttp://127.0.0.1:2380 \ --initial-cluster-token restored-cluster然后启动一个单节点验证数据确认后再考虑是否接入原集群。这里有一个我踩过的坑恢复的新节点和旧成员会持有完全不同的集群 ID如果直接把恢复节点混入原集群会看到各种成员冲突和日志报错。正确思路是恢复出来的节点先自己单独运行验证无误后通过 member add 的方式重新把其他节点拉进来或者直接把原集群所有数据目录清理掉用恢复节点作为唯一起点重建。5. 常见问题与排查技巧5.1 问题速查表下面这张表可以贴在运维手册里遇到问题先按行对号入座。现象可能原因处理办法容器启动后一直 restart数据卷权限错误或端口冲突查看日志清理数据卷检查端口映射etcdctl member list报 TLS 错误客户端和服务端证书配置不一致检查或先关闭证书认证排查基础链路集群节点间反复选举日志出现no leader网络分区或 quorum 不足检查容器间网络连通性确认节点数写入耗时严重日志出现apply request took too long磁盘 fsync 慢或存储驱动问题换 SSD给 etcd 单独数据卷节点重启后提示member xxx is already a member数据目录里有旧成员信息清空该节点数据目录后重新加入从宿主机访问 2379 超时防火墙或安全组未放行放行对应端口确认映射地址从客户端看不到 leader客户端连接了落后节点且集群不稳定检查集群整体健康用 endpoint health 逐个确认5.2 时间不同步带来的麻烦etcd 的 raft 选举对节点间时钟差非常敏感么准确说raft 本身不要求强时钟同步但网络延迟和时钟偏移会让心跳超时和选举超时乱掉。常见症状是日志里频繁出现 leader 切换、请求超时严重时客户端会拿到不一致的状态。容器通常继承宿主机的时钟所以主要保证宿主机 NTP 正常。一次部署前我遇到两个节点时间差了十几秒结果集群一直选不出稳定的 leader每次请求都要等超时。配置好 NTP 后问题立刻消失。现在我做任何集群前都会先跑一条命令确认所有节点时间误差在 100ms 以内。5.3 节点起不来的三块绊脚石第一块是数据目录残留。如果你之前用initial-cluster-statenew启动过集群后来觉得不对想重新初始化但没清空容器数据卷就会出现成员ID冲突。解决办法是清除对应节点的数据目录让它以一个“全新节点”的身份加入。第二块是端口没放行。容器内互访一般没问题但从宿主机或外部访问时2380 和 2379 都要在防火墙和安全组放行。特别要注意如果容器是在云主机上安全组规则和宿主机防火墙是两个独立层次只改一处不会生效。第三块是镜像版本不一致。三个节点如果分别用了 3.4 和 3.5有可能在协议细节上有差异导致一个节点始终无法追日志。同一集群严格锁定相同版本升级时先升级 follower 再滚动到 leader避免跨大版本。5.4 生产环境必须养成的几个习惯第一不要把 2379 直接暴露到公网。etcd 默认没有任何加密认证裸奔就意味着任何人可以读取甚至覆盖数据。要么限制网络访问要么启用 TLS两者都要做。第二开启监控。etcd 暴露的/metrics接口可以接到监控系统重点看etcd_server_has_leader、etcd_server_leader_changes_seen_total和etcd_server_proposals_failed_total这几个指标。磁盘 IO 和内存也要盯etcd 对资源抖动非常敏感。第三变更之前先备份。无论是改成员、升级版本还是会话参数都先把 snapshot 保存下来。etcd 不像普通数据库可以直接复制数据目录最简单可靠的兜底就是 snapshot。6. 最后分享一点个人经验搭建 docker-compose 版的 etcd 集群真正的难点不是敲命令而是理解集群行为。我在做迁移演练时反复验证过同一件事节点宕机后的自动恢复不是魔法而是多数派、日志复制、心跳超时这些机制协同工作的结果。只有亲手停掉一个节点、观察日志、确认读写仍正常再把它恢复回来才能真正理解 etcd 为什么说自己是“为可靠而设计”。建议你拿到这份 compose 文件后别急着投进生产先在本地把故障演练完整走几遍特别是停节点、恢复、加成员、备份恢复这四个动作。我开始也会觉得这些动作多余直到一次真的遇到双节点同时离线脑子里没有预案的时候才意识到演练的价值。多踩几次坑你对 etcd 的信任感和掌控感会完全不一样。