隔离边界:只使用本机 Docker Compose 的 lab-etcd 项目,三个容器不映射宿主机端口;不要对生产 etcd 执行 stop、snapshot restore 或 --force-new-cluster。需要 Docker Compose v2,预留约 500 MB 内存。
docker version --format '{{.ServerVersion}}' && docker compose version
mkdir -p /tmp/lab-etcd
cat > /tmp/lab-etcd/docker-compose.yml <<'EOF'
name: lab-etcd
x-etcd: &etcd
image: quay.io/coreos/etcd:v3.5.15
entrypoint: /usr/local/bin/etcd
services:
etcd1:
<<: *etcd
container_name: lab-etcd1
command: ["--name=etcd1", "--data-dir=/etcd-data", "--listen-client-urls=http://0.0.0.0:2379", "--advertise-client-urls=http://etcd1:2379", "--listen-peer-urls=http://0.0.0.0:2380", "--initial-advertise-peer-urls=http://etcd1:2380", "--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380", "--initial-cluster-token=lab-etcd", "--initial-cluster-state=new"]
etcd2:
<<: *etcd
container_name: lab-etcd2
command: ["--name=etcd2", "--data-dir=/etcd-data", "--listen-client-urls=http://0.0.0.0:2379", "--advertise-client-urls=http://etcd2:2379", "--listen-peer-urls=http://0.0.0.0:2380", "--initial-advertise-peer-urls=http://etcd2:2380", "--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380", "--initial-cluster-token=lab-etcd", "--initial-cluster-state=new"]
etcd3:
<<: *etcd
container_name: lab-etcd3
command: ["--name=etcd3", "--data-dir=/etcd-data", "--listen-client-urls=http://0.0.0.0:2379", "--advertise-client-urls=http://etcd3:2379", "--listen-peer-urls=http://0.0.0.0:2380", "--initial-advertise-peer-urls=http://etcd3:2380", "--initial-cluster=etcd1=http://etcd1:2380,etcd2=http://etcd2:2380,etcd3=http://etcd3:2380", "--initial-cluster-token=lab-etcd", "--initial-cluster-state=new"]
EOF
docker compose -f /tmp/lab-etcd/docker-compose.yml up -d
sleep 8
E=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E endpoint status --write-out=table
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E put lab/quorum before
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 snapshot save /tmp/snapshot.db
docker cp lab-etcd1:/tmp/snapshot.db /tmp/lab-etcd/snapshot.db
endpoint status 应列出 3 个成员且只有一个 leader。快照保存在宿主机 /tmp/lab-etcd/snapshot.db,恢复前先确认它存在。
停掉 etcd2 和 etcd3,只留下 etcd1。三节点集群的多数派是 2,剩 1 个成员后无法提交写请求,也无法完成需要 quorum 的线性读:
docker compose -f /tmp/lab-etcd/docker-compose.yml stop etcd2 etcd3
sleep 5
docker compose -f /tmp/lab-etcd/docker-compose.yml ps
docker compose -f /tmp/lab-etcd/docker-compose.yml logs --tail=15 etcd1
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 endpoint status --write-out=table
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 endpoint health; echo "health_exit=$?"
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 put lab/quorum after; echo "put_exit=$?"
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 --consistency=s get lab/quorum; echo "serializable_exit=$?"
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=http://etcd1:2379 get lab/quorum; echo "linearizable_exit=$?"
预期 etcd1 日志出现 leader 丢失和 election timeout;endpoint status 可能报超时或显示没有 leader;endpoint health 退出码非 0;put 失败,报 etcdserver: no leader 或 context deadline exceeded,put_exit 非 0。串行读 --consistency=s 仍可能返回旧值 before,但线性读 get 失败。串行读成功只说明本地状态可读,不代表集群有 quorum。
endpoint status --write-out=table 看每个成员的 leader、raftIndex 和 raftTerm;用 member list 看成员是否仍在集群中。三节点允许坏 1 个,坏 2 个即失去多数派。--consistency=s 串行读可由本地成员返回,可能读到旧值。不要把串行读成功当成集群健康。etcdctl member list 或 endpoint status 能返回本地信息,不代表能提交写;另一个误判是看到 no leader 就立刻重启所有节点。三个节点同时重启会延长不可用时间,也不能解决数据或网络问题。lost the leader、election timeout、failed to get leader 和磁盘/网络错误。若只有部分客户端失败,先核对 endpoint 列表、DNS 和 TLS,而不是直接判定 quorum 丢失。--force-new-cluster。先恢复 etcd2,让集群重新达到 2/3 多数派;确认 leader 和写入恢复后,再恢复 etcd3:
docker compose -f /tmp/lab-etcd/docker-compose.yml start etcd2
sleep 5
E2=http://etcd1:2379,http://etcd2:2379
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E2 endpoint health
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E2 put lab/quorum recovered
docker compose -f /tmp/lab-etcd/docker-compose.yml start etcd3
sleep 5
E3=http://etcd1:2379,http://etcd2:2379,http://etcd3:2379
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E3 endpoint health
docker compose -f /tmp/lab-etcd/docker-compose.yml exec -T etcd1 /usr/local/bin/etcdctl --endpoints=$E3 get lab/quorum
预期 etcd2 启动后重新选出 leader,put 成功;etcd3 启动后三个 endpoint 都健康,读取到 recovered。如果 etcd2 无法恢复,可改用 etcd3 先恢复,但一次只启动一个成员,等 quorum 和日志追赶完成后再启动下一个。
docker compose -f /tmp/lab-etcd/docker-compose.yml down -v --remove-orphans
rm -rf /tmp/lab-etcd
只删除本实验的 Compose 项目和容器数据;不要对生产 etcd 执行 down -v 或删除数据目录。
--consistency=s 与默认读在 quorum 丢失时分别是什么行为?