用 kubeadm 装过 Kubernetes 的人都知道,默认情况下 etcd 是以静态 Pod 的形式躲在控制面节点里的,你几乎感觉不到它的存在。可一旦把集群当成正经系统来运维,就会碰到一系列别扭的事:想单独备份 etcd 却要和 apiserver 绑在一起,想升级 etcd 又怕把控制面带崩,控制面节点一换,etcd 还得跟着搬家。把 etcd 从 Kubernetes 里抽出来,单独搭一套外部集群,就能把这些包袱一次性甩掉。
这篇内容基于我自己多次搭建外部 etcd 的实操过程,从为什么选 Docker Compose、集群怎么设计,到 compose 文件怎么写、kubeadm 怎么接入,再到那些文档里不会写的坑,按真实顺序捋一遍。适合准备做高可用控制面、或者只想快速验证外部 etcd 方案的朋友参考,有三台机器起步最理想,没有三台机器,单台机器用桥接模式也能先把流程跑通。
1. 为什么要在 Kubernetes 外面单独部署 etcd
1.1 etcd 在集群里的角色
用一句话概括,etcd 就是整个 Kubernetes 的“状态数据库”。所有 API 对象——Pod、Service、Deployment、ConfigMap、Namespace——都以键值形式存在里面,apiserver 是唯一会读写它的组件,scheduler 和 controller-manager 又全部通过 apiserver 间接依赖它。Pod 真正调度到哪台节点、Service 的 selector 匹配了谁、Deployment 当前期望副本数是多少,这些元数据全部存 etcd。
我见过不少朋友对 etcd 的重视频率配不上它的地位,总觉得它是 Kubernetes 自带的小零件。实际上,整个集群里最容易“一死全死”的就是它。节点挂了可以等恢复,镜像拉不下来可以重试,但 etcd 一旦丢数据或者仲裁没了,apiserver、scheduler、controller-manager 会全员瘫痪,而且不是重启就能缓过来的那种瘫。理解了这一点,你再看“外部 etcd”这件事,本质就不是炫技,而是把集群最脆弱的核心单独管理起来。
1.2 外部 etcd 和内置 etcd 怎么选
kubeadm 默认把 etcd 跑成控制面节点上的静态 Pod,每个 master 上各一个,日志、证书、数据目录都由 kubeadm 一手管理。这种模式对大多数场景足够用,特别是只有单控制面、不想多维护一套东西的时候,别折腾。
但有几个场景我会优先考虑外部 etcd:想独立做备份恢复的时候,不想让 etcd 的运维节奏跟着 Kubernetes 控制面版本升级走;控制面节点经常变,或者用的是临时机器,想把这些机器和稳定的 etcd 解耦;需要多套集群共用一套 etcd,或者要做跨机房的 etcd 部署实验;还有测试环境控制面节点资源紧张,etcd 单独放能减少相互干扰。
其实外部 etcd 没什么高深的地方,本质上就是让 etcd 跑在 Kubernetes 之外,再通过 kubeadm 的 ClusterConfiguration 把端点指过去就行。难点不在接入,而在把 etcd 集群本身部署得稳。很多第一次搞的人倒在第一步,恰恰是因为忽略了后面要讲的设计细节。
1.3 为什么用 Docker Compose 而不是二进制或 Helm
提到部署 etcd,网上最常见的路子有两种:下载二进制后用 systemd 托管,或者套一个 Helm Chart。先说二进制方案,它对系统侵入深,每台机器都要写 systemd unit,改参数要逐台登录操作,测试环境里反复折腾非常烦。而且出了故障,systemd 日志和 etcd 日志叠在一起,排查心累。Helm Chart 方案的问题更隐蔽:它通常意味着你已经有一个 Kubernetes 集群了,那“外部 etcd”就变成了“集群里的 etcd 集群”,etcd 的高可用反而受制于它自己所在的集群,有点像套娃。
Docker Compose 的思路最朴素:用一份 YAML 把 etcd 容器的环境变量、数据卷、健康检查全定义清楚,改配置直接重开即可。它特别适合几类情况:还没建 Kubernetes 之前先把 etcd 准备好;需要快速复现和验证 etcd 故障场景;以及预发环境想要一套“能管、能拆、能重来”的外部 etcd。如果哪天觉得这套方案不合适,容器化环境也方便迁移,数据都在宿主机目录里,换部署方式不丢数据。
当然边界也要说清楚:到了大型生产规模,etcd 性能调优和运维要求更苛刻,请优先考虑专用运维方案或至少把容器编排交给更专业的工具。但绝大多数团队不会走到那一步,Compose 方案的性价比已经非常高。
2. 部署前需要想清楚的四件事
2.1 版本选型:etcd 3.5.x 与 Kubernetes 的兼容关系
版本选型直接决定后面顺不顺利。etcd 目前主要使用 3.4 和 3.5 两个大版本,Kubernetes 官方从 1.22 开始主推 3.5,到 1.27 之后 3.4 基本进入弃用状态。我的建议是,新环境一律用 3.5.x,小版本选 3.5.9 以上,那之后修过不少和数据目录、快照恢复相关的历史 bug。你可以在 Kubernetes 官方的“etcd 版本与 K8s 版本兼容矩阵”里核对,但记住大原则就行:K8s 新版永远配新 etcd,不要拿 3.4 配新版 K8s,也不要在生产环境盲目追 etcd 的最新大版本。
镜像方面,我一直用官方维护的quay.io/coreos/etcd:v3.5.13,它内置 etcdctl,方便容器内直接做健康检查。如果内网拉不到 quay.io,用docker.io/bitnami/etcd也可以,但 Bitnami 镜像的环境变量风格和官方略有差异,参数写法要对齐它们的文档。
2.2 节点数量与 RAFT 仲裁逻辑
etcd 靠 RAFT 协议做一致性,三节点是“既能容忍一台挂掉、又不至于太浪费”的最小配置。为什么硬性要求奇数台?因为 RAFT 选主和提交写请求都需要超过一半节点同意,三台允许坏一台,五台允许坏两台。四台虽然在预算上也是“坏一台不挂”,但网络分区时很容易出现双方各拿两张票、谁也赢不了的尴尬局面,等于把故障域扩大了还白花钱。两台更不用提,坏一台整个集群直接失去写能力。
所以生产环境要么不做外部 etcd,要做就老老实实三台起步。单台机器的“外部 etcd”严格说只能算功能测试,不具备任何高可用意义,kubeadm 面对它也只是“能连上”而已,谈不上集群健康。另外,节点之间的时钟必须同步,etcd 虽然不强制 NTP,但时钟漂移过大会引发选举异常,部署前记得检查宿主机的时间同步服务。
2.3 网络规划:2379 和 2380 到底干什么
这是新手最容易犯迷糊的地方。2379 是客户端端口,kube-apiserver 和 etcdctl 走它;2380 是节点间通信端口,etcd 成员之间同步数据、互相探活走它。这两个端口一旦搞反或漏开,表现很诡异:member list 能看到全部成员,但 endpoint health 一直报失败,或者集群内部能写,apiserver 却连不上。
| 端口 | 作用 | 访问方 |
|---|---|---|
| 2379 | 客户端通信 | kube-apiserver、etcdctl |
| 2380 | 节点间通信 | etcd 节点之间 |
网络规划上我的建议是:测试环境把 2379/2380 放开给控制面节点即可;生产环境把这些端口放进独立的私有网络,最好再叠加 TLS 双向认证。否则任何能访问 2379 的设备都能读写集群状态,这个风险比暴露 SSH 还高,因为 etcd 里存着集群全部元数据,删一个关键 key 都可能引发级联故障。
2.4 目录与持久化设计
Compose 的编排只管容器生命周期,数据必须落盘。docker compose down之后再up,如果数据目录被清了,你得到的就是一个全新的空集群,对 Kubernetes 来说等于直接丢数据。所以每个节点的数据目录必须单独挂载,不要三个节点共用同一个宿主机目录,也不要用/tmp这种随时可能被清理的路径。
空间也要预留够。etcd 的># 主机 10.0.0.11 上的 /opt/etcd/docker-compose.yml version: '3.8' services: etcd1: image: quay.io/coreos/etcd:v3.5.13 container_name: etcd1 hostname: etcd1 restart: always network_mode: host environment: ETCD_NAME: etcd1 ETCD_DATA_DIR: /var/lib/etcd ETCD_LISTEN_CLIENT_URLS: http://0.0.0.0:2379 ETCD_ADVERTISE_CLIENT_URLS: http://10.0.0.11:2379 ETCD_LISTEN_PEER_URLS: http://0.0.0.0:2380 ETCD_INITIAL_ADVERTISE_PEER_URLS: http://10.0.0.11:2380 ETCD_INITIAL_CLUSTER_TOKEN: k8s-external-etcd ETCD_INITIAL_CLUSTER_STATE: new ETCD_INITIAL_CLUSTER: etcd1=http://10.0.0.11:2380,etcd2=http://10.0.0.12:2380,etcd3=http://10.0.0.13:2380 ETCD_AUTO_COMPACTION_MODE: revision ETCD_AUTO_COMPACTION_RETENTION: "1000" ETCD_QUOTA_BACKEND_BYTES: "8589934592" ETCD_MAX_REQUEST_BYTES: "33554432" volumes: - /data/etcd-data:/var/lib/etcd healthcheck: test: ["CMD", "etcdctl", "--endpoints=http://127.0.0.1:2379", "endpoint", "health"] interval: 30s timeout: 3s retries: 3 start_period: 20s
第二台主机把ETCD_NAME改成etcd2、ETCD_ADVERTISE_CLIENT_URLS和ETCD_INITIAL_ADVERTISE_PEER_URLS换成 10.0.0.12,第三台同理换成 etcd3 和 10.0.0.13。注意ETCD_INITIAL_CLUSTER必须三台完全一致,列出全部成员的 peer URL,否则集群无法建立。这不是我随手写的建议,而是 etcd 本身的约束:初始化时每个节点都要用同一份“成员清单”互相确认身份。
如果没有三台机器,只想单机模拟,就把network_mode: host换成桥接模式,用 ports 映射不同端口,比如 etcd1 映射2379:2379和2380:2380,etcd2 映射127.0.0.1:23791:2379和127.0.0.1:23801:2380,etcd3 映射127.0.0.1:23792:2379和127.0.0.1:23802:2380,然后把 advertise 地址改成对应的宿主机端口。这种单机伪集群只适合学流程,别拿它验证高可用。
3.2 环境变量里最容易配错的几个参数
先说ETCD_NAME和ETCD_INITIAL_CLUSTER的关系。name 必须与 initial-cluster 清单里的键名一致,比如节点叫 etcd1,清单里就必须是etcd1=http://...:2380。如果搞混,日志会报 “name conflicts” 或 “member not found”,第一次启动就被卡住。
再看 listen 和 advertise 的区别,这是新手最容易懵的一组概念。listen 系列是“在哪块网卡上监听”,advertise 系列是“告诉别人用哪个地址来访问我”。跨主机部署时 advertise 必须写其他节点真正能访问到的 IP,不能写 127.0.0.1,否则成员之间互相找不到。listen 地址写0.0.0.0是允许的,表示监听所有接口,但对外发布的地址只能是具体 IP。
ETCD_INITIAL_CLUSTER_STATE只在首次建集群时填new,后续重启容器不需要每次都填。数据目录一旦存在 WAL,etcd 会以已有集群身份启动。我最常踩的坑是:数据目录还在,但图省事把new留在环境变量里,结果 etcd 发现自己带着旧数据又要初始化,直接拒绝启动。
最后说两个很多人不知道的生产参数。一个是ETCD_QUOTA_BACKEND_BYTES,默认只有 2GB,Kubernetes 集群对象一多很容易写满,报mvcc: database space exceeded,我建议直接调到 8GB。另一个是ETCD_AUTO_COMPACTION_MODE和ETCD_AUTO_COMPACTION_RETENTION,按 revision 压缩历史版本,能有效控制数据目录膨胀,配合配额调整效果更好。
3.3 持久化、健康检查与日志策略
volumes 配置里,宿主机/data/etcd-data和容器内/var/lib/etcd是绑定挂载。官方镜像的入口脚本会以容器内用户身份创建数据目录,如果启动时报 permission denied,先在宿主机执行chown -R <uid>:<gid> /data/etcd-data,具体 uid 看镜像说明。这个问题在根用户环境下不出现,但换了非 root 跑 Docker 就很容易触发。
健康检查用 etcdctl 的 endpoint health 子命令,30 秒一次,失败重试三次后容器会被标记为 unhealthy。日志策略我统一用 json-file 驱动并限制文件数量和单文件大小,否则 etcd 长时间运行后,Docker 日志文件能把磁盘吃满,和 etcd 数据目录抢空间。compose 里对应的写法是 logging 配置,参考下面这段:
logging: driver: json-file options: max-size: "50m" max-file: "5"restart: always让 etcd 容器在宿主机重启后自动拉起,前提是 Docker 服务本身开机自启。这一步别省,不少人和我说“机器重启后集群没了”,查到最后都是 Docker 没开自启。
4. 实操:从空目录到集群可用
4.1 初始化目录与基础校验
部署前先做三件小事:建好数据目录、确认端口空闲、检查时钟同步。命令很简单,但能省掉后面一半的排障时间。
# 每台主机执行 mkdir -p /data/etcd-data ss -lntp | grep -E ':(2379|2380)' || echo "端口空闲" timedatectl如果 2379 或 2380 已经有进程在监听,后面docker compose up -d必然报端口占用。这种冲突最常见的来源是:机器上曾经装过 kubeadm 自带 etcd,或者之前手工跑过 etcd 二进制。先把旧进程停掉再继续。
4.2 启动集群与验证成员关系
三台主机分别进入各自的 compose 目录,执行:
docker compose up -d等十秒钟左右,先看日志再下结论。启动成功的标志是日志里出现类似 “starting with 3 members” 和 “elected leader” 的语句。如果只看到 “started as a new cluster” 但迟迟没有 leader,多半是某个节点的 peer 地址不可达。
接着在任意节点上用 etcdctl 验证:
docker exec etcd1 etcdctl member list \ --endpoints=http://10.0.0.11:2379 docker exec etcd1 etcdctl endpoint health \ --cluster \ --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379 docker exec etcd1 etcdctl endpoint status \ --cluster -w table \ --endpoints=http://10.0.0.11:2379,http://10.0.0.12:2379,http://10.0.0.13:2379期望结果是:member list 显示三个成员;endpoint health 全部返回 healthy;endpoint status 表格里能看出三台节点的角色,其中一台是 leader。如果 status 表格里 Raft Term 一直不增长,说明选举异常,大概率还是网络层的问题。这一步别跳过,直接在此时把问题解决掉,比等 kubeadm 接入时再排查要轻松得多。
4.3 用 kubeadm 把 Kubernetes 控制平面接进来
etcd 集群稳定后,去控制面节点写 kubeadm 配置。关键在于etcd.external这一节,填三台 etcd 的客户端端点,这里用的是明文 http:
# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 networking: podSubnet: "10.244.0.0/16" etcd: external: endpoints: - http://10.0.0.11:2379 - http://10.0.0.12:2379 - http://10.0.0.13:2379然后执行kubeadm init --config kubeadm-config.yaml。配置了 external 之后,控制面节点上不会再生成本机的 etcd 静态 Pod,kubeadm 只会做 etcd 健康检查并把 apiserver 指向外部端点。如果 init 卡在 etcd 健康检查环节,先回到 4.2 的验证命令,确认控制面节点能访问到这三台 2379 端口。常见错误是把 endpoints 写成 127.0.0.1,那只能代表控制面本机,其他 etcd 节点根本没在监听。
初始化完成后,控制面容器里可以直接确认 apiserver 的连接情况:
kubectl get ns kubectl create deployment smoke-test --image=nginx --replicas=1 kubectl get pod -l app=smoke-test能正常创建 namespace 和 deployment,说明 apiserver 到外部 etcd 的读写链路已经通了。此时再回 etcd 里看一眼:
docker exec etcd1 etcdctl get / --prefix --keys-only \ --endpoints=http://10.0.0.11:2379 | head -30你会看到一堆以/registry/开头的 key,Kubernetes 的数据已经在 etcd 里落地了。这个瞬间最有成就感,也最容易让人放松对备份的警惕,我的建议是趁热打铁配置好快照任务,最后一节会说具体做法。
5. 常见问题与排查技巧实录
5.1 容器重启后 etcd 成员重复
这是外部 etcd 部署里出现频率最高的事故。表现是:某个节点容器被删了重建,数据目录被清掉,环境变量里的ETCD_INITIAL_CLUSTER_STATE还是new,启动后它以为自己是新成员,试图加入一个已经存在的集群,结果旧集群里还留着它的旧成员 ID,于是报 “re-add an existing member” 或类似错误。
处理思路分两步:先在存活节点上把旧成员移除,再把新节点加回来。
# 在任一生机正常的节点上执行 docker exec etcd1 etcdctl member list --endpoints=http://10.0.0.11:2379 # 用输出里的 member ID 移除故障节点 docker exec etcd1 etcdctl member remove <member-id> \ --endpoints=http://10.0.0.11:2379 # 把重新初始化的节点作为新成员加回 docker exec etcd1 etcdctl member add etcd2 \ --peer-urls=http://10.0.0.12:2380 \ --endpoints=http://10.0.0.11:2379之后在 10.0.0.12 上把ETCD_INITIAL_CLUSTER_STATE改成existing(或者从环境变量里删掉这一项),再启动容器。核心教训是:只要数据目录还在,别去动 initial-cluster-state;数据目录没了,也别直接起来,先走 member remove/add 的流程。这个规矩比任何参数调优都重要。
5.2 证书 SAN 不匹配导致的握手失败
如果生产环境启用了 TLS,最容易栽在证书 SAN 上。kube-apiserver 连接 etcd 时,会校验 etcd 服务端证书里是否包含访问它所用的 IP 或域名;etcd 节点之间互连时,也会校验对端 peer 证书。用 openssl 生成证书时如果漏了-addext subjectAltName,apiserver 日志里会反复出现 “x509: cannot validate certificate for 10.0.0.11 because it doesn't contain any IP SANs”。
给 etcd 节点签发证书时,SAN 至少要覆盖三样东西:节点自身的主机名、节点 IP、127.0.0.1。以等节点 etcd1 为例:
openssl req -newkey rsa:2048 -nodes \ -keyout etcd1.key -out etcd1.csr \ -subj "/CN=etcd1" \ -addext "subjectAltName = DNS:etcd1,IP:10.0.0.11,IP:127.0.0.1" openssl x509 -req -in etcd1.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out etcd1.crt -days 825 -copy_extensions copy然后在 compose 里挂入证书和 CA,并打开客户端证书校验:
ETCD_CERT_FILE: /etc/etcd/pki/etcd1.crt ETCD_KEY_FILE: /etc/etcd/pki/etcd1.key ETCD_TRUSTED_CA_FILE: /etc/etcd/pki/ca.crt ETCD_CLIENT_CERT_AUTH: "true" ETCD_PEER_CERT_FILE: /etc/etcd/pki/etcd1.crt ETCD_PEER_KEY_FILE: /etc/etcd/pki/etcd1.key ETCD_PEER_TRUSTED_CA_FILE: /etc/etcd/pki/ca.crtkubeadm 配置里对应要加上caFile、certFile、keyFile,并且把 endpoints 改成https://。TLS 配置一旦生效,先用 etcdctl 加--cacert、--cert、--key验证,再让 kubeadm 接入,别跳过中间步骤。
5.3 docker compose up -d 报错与容器秒退排查
群里经常有人发一条这样的报错:“cannot start docker compose application. reason: compose [start] exit status”。先明确一点,这句话本身只是 Docker Compose 告诉你“容器启动后立刻退出了”,真正的错误原因在容器日志里。正确排查路径是:
docker compose ps -a docker logs etcd1 --tail 100根据我的经验,容器秒退的三大原因按概率排序:一是 2379/2380 端口被宿主机上旧进程占用,etcd 日志里直接报bind: address already in use;二是环境变量写错了,比如ETCD_INITIAL_CLUSTER里的成员名单各节点不一致,日志里会有 “invalid configuration” 或 “http://... is not a valid URL”;三是数据目录权限不对,报 permission denied。
还有一个隐蔽问题是 Docker 版本太旧。旧版 docker-compose 对 Compose spec 的支持不完整,同样的 YAML 在新环境能跑,在老环境就报字段不支持。我的建议是统一用 Docker 自带的docker compose插件(v2 版),它随 Docker Engine 一起维护,配置行为更可预期。
5.4 仲裁丢失后的快照恢复
最坏的情况,三节点坏了两台,剩下的单节点读还能读,但所有写请求都会因为不满足仲裁而失败。这时候别想着“把剩下的一台凑凑继续干活”,正确姿势是从快照恢复出一个全新集群。
先在任何一台还在服务的节点上保存快照:
docker exec etcd1 sh -c \ "etcdctl --endpoints=http://127.0.0.1:2379 snapshot save /var/lib/etcd/snapshot.db" docker cp etcd1:/var/lib/etcd/snapshot.db ./然后对准备恢复的三台机器逐一执行 restore。关键是每台的参数必须一致,尤其--initial-cluster和--initial-cluster-token,恢复后的数据目录要以“全新集群”的身份重新组团:
etcdctl snapshot restore snapshot.db \ --name etcd1 \ --initial-cluster "etcd1=http://10.0.0.11:2380,etcd2=http://10.0.0.12:2380,etcd3=http://10.0.0.13:2380" \ --initial-cluster-token k8s-external-etcd-backup \ --initial-advertise-peer-urls http://10.0.0.11:2380 \ --data-dir /data/etcd-data-restored恢复完把容器数据目录指向恢复出来的目录,重启三个容器。恢复出的集群 cluster ID 会和原来不一样,这是正常的,别因此以为失败了。例行检查 member list 和健康状态,确认数据 key 还在,再接回 kube-apiserver。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| 2379 端口起不来 | 端口被旧进程占用 | ss -lntp排查,停掉冲突进程 |
| member list 正常但健康检查失败 | 2380 peer 端口不通或 advertise 地址错误 | 检查防火墙,确认 peer URL 可互访 |
| kubeadm init 卡在 etcd 健康检查 | endpoints 填成 127.0.0.1 | 填实际可访问 IP,确认控制面能连 2379 |
| 写入报 mvcc database space exceeded | 默认 2GB 配额已满 | 调大 ETCD_QUOTA_BACKEND_BYTES,并启用自动压缩 |
| 容器重建后报成员冲突 | 数据目录被清且仍用 new 状态启动 | member remove 后重新 add,再以 existing 启动 |
| apiserver 报 TLS 握手失败 | 证书 SAN 未包含节点 IP | 重新签发证书,SAN 覆盖主机名、IP、127.0.0.1 |
| compose start exit status | 容器启动即退出 | 看 docker logs,按端口、环境变量、权限顺序排查 |
6. 一些实操体会
最后分享几条我自己积累下来的习惯。第一,外部 etcd 建好后的第一件事不是接 Kubernetes,而是配快照定时任务,etcdctl snapshot save每天至少一次,快照文件复制到另一台物理机或独立存储上。我在本地测试时就经历过数据目录被误删,全靠一天前的快照救回来,那次之后“先备份再接控制面”就成了铁律。
第二,etcd 容器别和 Docker 数据目录放在同一块磁盘。Docker 自身的镜像层和容器日志增长很快,一旦和 etcd 抢空间,双方一起遭殃。我会单独给/data/etcd-data挂一块盘,日志另开分区,这样即使 Docker 出问题,etcd 数据也相对安全。
第三,文档里说“外部 etcd 可以明文跑”,我建议即便在测试环境也尽量用私有网络隔离端口。因为 etcd 的流量没有任何人会替你做鉴权,apiserver 连接信息一旦泄露,等于把整个集群的钥匙交了出去。TLS 的配置一次到位,后面省心得多。
这套方案我前后在测试环境和预发环境反复搭过多次,Docker Compose 版本配合外部 etcd 的做法,胜在快速、可复现、不污染宿主机。按照上面的顺序一步步来,从空目录到 kubeadm 接入,通常一个小时内就能跑完。真遇到没写进文档的怪问题,记住一条:先看 etcd 容器日志,它会把绝大部分原因直接打印出来。