etcd 是一个强一致性的分布式键值存储,常被用作 Kubernetes 等系统的核心元数据数据库。它通过 Raft 共识算法在多个节点之间复制状态机日志,保证即使部分节点故障,集群依然能对外提供一致的读写服务。使用 Docker 启动 etcd 节点可以极大简化本地实验和测试环境的搭建,同时也能为生产部署提供一致的容器化交付方式。本文将围绕 Docker 环境下的 etcd 集群展开,说明如何配置 Raft 共识所需的参数、验证选举与日志复制行为,并给出一些生产化建议。

一、etcd 与 Raft 共识机制的核心概念
etcd 将数据以键值对形式保存在基于 Raft 实现的状态机上。Raft 将集群节点分为领导者、跟随者和候选者三种角色。领导者负责接收客户端写请求,将日志条目复制到多数派节点,然后提交并应用到状态机。跟随者只响应领导者的日志复制和心跳。当领导者故障或网络分区时,候选者发起选举,获得多数票的节点成为新领导者。由于需要多数派同意,etcd 集群的容错能力满足公式 N = 2F + 1,即 F 个节点故障时集群仍可工作。因此生产环境通常使用 3 或 5 个节点,分别允许 1 或 2 个节点故障。
共识过程不依赖 Docker 本身,但 Docker 的网络和存储配置会直接影响节点间通信和持久化能力。在容器环境中,节点之间的 peer 通信必须使用可解析的主机名或 IP 地址,否则 Raft 日志复制会失败。此外,选举超时和心跳间隔等参数也会影响故障恢复速度,后文将在部署示例中说明如何通过容器网络和参数设置来保证共识稳定。
理解 quorum 是使用 etcd 的关键。当集群中的多数节点不可达时,整个集群会进入只读或不可用状态,以防止脑裂导致的数据不一致。这一特性在容器编排环境中尤其重要,因为容器可能被调度器重启、迁移或网络隔离,必须确保 etcd 节点分布在不同宿主机或可用区,避免单点故障波及多数派。
二、使用 Docker 部署三节点 etcd 集群
首先创建一个自定义 bridge 网络,让容器之间可以通过名称互相访问。使用 docker network create etcd-net 命令即可。随后逐个启动容器,每个容器需要映射 2379 客户端端口和 2380 peer 通信端口,并挂载数据卷持久化 data-dir。下面是一个完整的 docker run 示例,用于启动第一个节点:
docker network create etcd-net docker run -d \ --name etcd-1 \ --network etcd-net \ -p 2379:2379 \ -p 2380:2380 \ -v etcd-1-data:/etcd-data \ quay.io/coreos/etcd:v3.5.9 \ /usr/local/bin/etcd \ --name etcd-1 \ --data-dir /etcd-data \ --listen-client-urls http://0.0.0.0:2379 \ --advertise-client-urls http://etcd-1:2379 \ --listen-peer-urls http://0.0.0.0:2380 \ --initial-advertise-peer-urls http://etcd-1:2380 \ --initial-cluster etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380 \ --initial-cluster-state new \ --initial-cluster-token docker-etcd-token
上述命令中,--name 设置节点名称,必须与 initial-cluster 中的名称完全一致。--listen-client-urls 和 --advertise-client-urls 分别表示客户端请求的监听地址和对外通告地址,在容器网络中通告地址需要使用容器名,这样其他客户端或节点才能通过 DNS 解析访问。--listen-peer-urls 与 --initial-advertise-peer-urls 用于 Raft 节点之间的通信,同样必须使用可解析的地址。--initial-cluster 指定初始成员列表,包含所有三个节点的名称和 peer 地址,--initial-cluster-state new 表示这是一个全新集群,--initial-cluster-token 用于防止不同集群的节点意外加入。
为了更方便地管理多节点,推荐使用 Docker Compose 一次性拉起整个集群。下面的 YAML 文件定义了三个服务,使用同一个镜像和网络,数据卷独立持久化。注意每个容器的客户端端口映射必须不同,以避免宿主机端口冲突,而容器内部的端口可以保持 2379 和 2380 不变。
version: "3.8"
services:
etcd-1:
image: quay.io/coreos/etcd:v3.5.9
container_name: etcd-1
command:
- /usr/local/bin/etcd
- --name=etcd-1
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-1:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://etcd-1:2380
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=docker-etcd-token
ports:
- "2379:2379"
- "2380:2380"
volumes:
- etcd-1-data:/etcd-data
etcd-2:
image: quay.io/coreos/etcd:v3.5.9
container_name: etcd-2
command:
- /usr/local/bin/etcd
- --name=etcd-2
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-2:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://etcd-2:2380
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=docker-etcd-token
ports:
- "23791:2379"
- "23801:2380"
volumes:
- etcd-2-data:/etcd-data
etcd-3:
image: quay.io/coreos/etcd:v3.5.9
container_name: etcd-3
command:
- /usr/local/bin/etcd
- --name=etcd-3
- --data-dir=/etcd-data
- --listen-client-urls=http://0.0.0.0:2379
- --advertise-client-urls=http://etcd-3:2379
- --listen-peer-urls=http://0.0.0.0:2380
- --initial-advertise-peer-urls=http://etcd-3:2380
- --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380
- --initial-cluster-state=new
- --initial-cluster-token=docker-etcd-token
ports:
- "23792:2379"
- "23802:2380"
volumes:
- etcd-3-data:/etcd-data
volumes:
etcd-1-data:
etcd-2-data:
etcd-3-data:
启动后可以使用 docker logs etcd-1 查看日志,当看到类似 etcdserver: setting up the initial cluster version 以及 raft: elected leader 的信息时,说明选举已经完成,集群进入正常服务状态。如果日志中反复出现选举超时或连接拒绝,通常需要检查容器网络和 initial-cluster 中的地址是否可解析。
三、验证 Raft 共识与故障转移
集群启动后,可以通过 etcdctl 工具检查成员列表和健康状态。在容器内执行 docker exec etcd-1 etcdctl member list 会列出所有节点的 ID、名称和 peer URL,而 docker exec etcd-1 etcdctl endpoint health --cluster 会显示每个节点的健康状态以及当前领导者是谁。这些命令的输出可以直观地反映 Raft 共识是否正常建立。
写入数据并验证复制是理解共识机制的重要步骤。在任一节点执行 docker exec etcd-1 etcdctl put /message "hello etcd",然后从另一个节点读取:docker exec etcd-2 etcdctl get /message --quorum。这里 --quorum 选项表示读取必须经过多数派确认,确保返回的是已提交的最新值,而不是可能滞后的本地缓存。如果没有加该选项,读取可能只走本地节点,虽然性能更高,但在网络分区时可能读到旧数据。
模拟故障转移可以验证集群容错能力。假设当前领导者是 etcd-1,执行 docker stop etcd-1 停止该容器,然后从剩余节点执行 docker exec etcd-2 etcdctl endpoint health --cluster,观察 etcd-2 或 etcd-3 是否成为新的领导者。接着执行 docker exec etcd-2 etcdctl put /after-failover "still works" 和 docker exec etcd-3 etcdctl get /after-failover --quorum,如果写入和读取都成功,说明集群在丢失一个节点后仍然满足 quorum,继续对外服务。若继续停止第二个节点,三节点集群将失去多数派,所有写操作会被拒绝,读取也可能失败,这是 Raft 保证一致性的预期行为。
四、生产环境加固与常见问题
在测试环境中使用 HTTP 明文通信可以快速验证功能,但生产环境必须启用 TLS 加密。etcd 支持为客户端和 peer 通信分别配置证书,常用的参数包括 --cert-file、--key-file、--trusted-ca-file 以及对应的 peer 版本 --peer-cert-file、--peer-key-file、--peer-trusted-ca-file。证书文件可以通过 Docker 卷挂载到容器内只读路径,并在启动参数中指定。证书中的 SAN 需要包含节点的主机名或 IP 地址,否则 TLS 握手会因主机名不匹配而失败。
数据持久化和备份同样不可忽视。使用 Docker 卷可以保留 data-dir 中的数据,但当节点损坏或需要迁移时,仅靠卷还不够。建议定期执行 docker exec etcd-1 etcdctl snapshot save /backup/etcd-snapshot.db 获取一致性快照,并将快照文件复制到集群外部存储。恢复时可以使用 etcdctl snapshot restore 命令生成新的数据目录,然后启动一个新节点加入集群,或者用快照替换故障节点的数据目录后重启。恢复过程必须谨慎处理集群成员关系,避免旧节点带着过期数据重新加入。
最后,容器资源限制和监控是保障共识稳定的基础。etcd 对磁盘延迟和 CPU 饥饿非常敏感,如果容器被宿主机限流或换页,可能导致心跳超时,引发不必要的选举甚至集群抖动。在生产环境中,应为 etcd 容器设置足够的 CPU 和内存限制,并使用独立的快速磁盘。同时开启 /metrics 端点供 Prometheus 采集,重点关注 etcd_server_leader_changes_seen_total、etcd_disk_wal_fsync_duration_seconds 等指标来判断共识健康程度。通过合理的容器化部署和监控,Docker 可以成为运行 etcd 集群的高效平台,但前提是深入理解 Raft 共识的行为边界。