这段时间在给某个基础服务团队搭独立的高可用 etcd,先说结论:如果还在手工初始化 etcd 三节点,再用一堆脚本去扩容和备份,在 Kubernetes 里真的有点浪费调度能力。我最后用的是 etcd-operator 这套方案,把 etcd 集群当成普通应用来做声明式管理。这篇文章会从 operator 部署、EtcdCluster 资源设计、扩容备份、故障恢复一直写到常见问题排查,顺带把踩过的坑交代清楚,希望后面接手的人少走弯路。
先说清楚适用范围:这套方案管理的是承载业务数据的独立 etcd 集群,比如服务发现、分布式锁、配置存储这类场景。Kubernetes 控制面自己的 etcd 不在这套方案的管理范围内,也不建议用 etcd-operator 去碰,那个应该由集群安装工具或者静态 Pod 方式负责。
1. 部署前必须想清楚的三件事
1.1 这套 etcd 集群到底是给谁用的
很多人一看到“在 Kubernetes 部署 HA etcd”就直接开始写 YAML,但最关键的问题没先回答:这个 etcd 是给谁用的。我在实际落地时,遇到的第一个需求是给一套跨节点的服务发现中间件做底层存储,要求数据不能丢,节点挂了要自动剔除,机器故障要能容忍。这种业务型 etcd 放在 Kubernetes 里非常合适,因为 Kubernetes 本身能帮它处理“进程挂了重启”和“节点故障重新调度”的问题。
但如果你打算用 etcd-operator 去管理 Kubernetes 控制面的 etcd,那就是给自己挖坑。控制面 etcd 的启动参数、证书、静态 Pod 清单都绑在 kubelet 和集群引导流程里,operator 管理的 Pod 和依赖关系跟它完全不一样,强行切换大概率会把整个集群搞挂。所以第一步就是把边界画清楚:etcd-operator 只负责业务侧独立 etcd,Kubernetes 自己那份不碰。
1.2 为什么选 Operator,而不是 StatefulSet 加手工初始化
这里多说几句选型逻辑。如果只是把 etcd 跑成一个三节点 StatefulSet,事情其实不难,难的是它根本没有“集群成员管理”的概念。StatefulSet 扩容时只会多出一个 Pod,它不知道要调用 etcd 的成员接口把这个新实例加进 Raft 集群;缩容时更是危险,如果直接删掉一个正在担任 leader 的成员,整个集群可能立刻就失去 quorum。
etcd-operator 的核心价值就在于它把 etcd 的运维逻辑做成了控制器。它会持续监听 EtcdCluster 自定义资源,发现实际成员数和期望的 size 不一致时,自动去 etcd 里执行 member add 或 member remove,再配合 Kubernetes API 创建或删除 Pod。这个过程里,每个成员加入集群的节点名、客户端地址、peer 地址都由 operator 统一安排,不用人肉去查 member list 再写脚本。
对我来说,选择 operator 还有一个更实际的原因:它把“集群拓扑”变成了一份可审计的 YAML。比如我要从三节点扩到五节点,不需要登录任何一台 etcd 机器执行命令,只要把 EtcdCluster 的 size 改成 5 提交即可。后续谁动过什么,Git 里都有记录。
1.3 高可用设计目标:先定故障容忍度
在创建集群之前,需要明确一个数字:你要能容忍几个节点同时故障。etcd 使用 Raft 协议,N 节点集群的 quorum 是 floor(N/2)+1。三节点集群的 quorum 是 2,表示可以容忍 1 个节点故障;五节点集群的 quorum 是 3,表示可以容忍 2 个节点故障。
三节点是生产环境最常见的起步配置,因为它在成本、性能和故障容忍之间比较均衡。如果你只有 2 个节点,严格意义上不算 HA,因为 Raft 没有多数派;如果节点数到了 4,故障容忍度也只有 1,而且奇数节点能避免脑裂,所以没必要用偶数。我在这次实践中用的是三节点,同时要求 Pod 必须反亲和,保证三个副本尽量落在不同物理节点上。节点数少于三个的话,再好的调度策略也救不了你。
2. 环境检查与 operator 安装
2.1 版本组合先确认,否则后面全是泪
etcd-operator 的版本和 Kubernetes 版本之间的兼容性,是最容易踩的坑。我这次使用的组合是 etcd-operator v0.9.4 配合 etcd v3.4.27,Kubernetes 集群是 1.20 版本。这个组合跑下来比较稳,CRD、RBAC、operator 都能正常工作。
如果你手里的 Kubernetes 版本比较高,比如 1.22 以上,就要特别注意 CRD 的 apiVersion。老的 etcd-operator 部署清单里很多 CRD 用的是 apiextensions.k8s.io/v1beta1,而 v1beta1 在高版本 Kubernetes 里已经被移除了。遇到这种情况,不要硬套老清单,要么手动把 CRD 定义转换成 v1 再提交,要么换一个还在维护的部署方式。这个点在 5.4 节还会再展开。
版本组合建议参考下面这个表格,是我个人实测下来相对稳妥的组合:
| 组件 | 版本 | 备注 |
|---|---|---|
| Kubernetes | 1.20 或更低 | 高版本需要处理 CRD v1beta1 兼容问题 |
| etcd-operator | 0.9.4 | 稳定版本,功能足够用 |
| etcd | 3.4.27 | 和 operator 兼容性较好,不建议直接上 3.5 未验证版本 |
| etcdctl | 3.4.x | API 版本用 v3,别再用 v2 |
2.2 准备命名空间与 RBAC
因为 operator 要创建和删除 Pod、Service、PVC,还要读取节点信息,它必须有足够的 RBAC 权限。我习惯把业务 etcd 放在独立命名空间里,避免跟其他应用混在一起权限混乱。这里用一个etcd命名空间,后续 EtcdCluster、备份 CronJob、监控 ServiceMonitor 都放在里面。
操作流程分三份清单来准备:
- 第一份是 RBAC,定义 ServiceAccount、ClusterRole、ClusterRoleBinding;
- 第二份是 CRD,就是 EtcdCluster 等自定义资源定义;
- 第三份是 operator 的 Deployment,里面指定镜像、启动参数、命名空间。
我这边没有直接使用仓库里的原始 YAML,而是下载到本地后做了两处修改:一是把镜像地址替换成私有仓库里的镜像,二是把 Deployment 的 namespace 改成etcd。生产环境里镜像来源一定要固定,不要再依赖公网仓库,否则镜像拉取失败的时候排查起来非常痛苦。
示例安装命令如下:
kubectl create namespace etcd kubectl apply -f manifests/rbac.yaml kubectl apply -f manifests/crd.yaml kubectl apply -f manifests/operator.yaml2.3 验证 operator 是否真正启动成功
清单 apply 完不等于 operator 就能用了,一定要确认 controller 起来了,并且和 Kubernetes API 的连接是正常的。我一般会看两样东西:Pod 状态和日志。
kubectl -n etcd get pod -l name=etcd-operator kubectl -n etcd logs -l name=etcd-operator --tail=50正常启动时,日志里会出现类似 “etcd-operator started” 的信息,之后会进入等待新增 EtcdCluster 资源的 watch 循环。如果 Pod 一直 CrashLoopBackOff,先看 RBAC 有没有配好,最常见的问题是 ClusterRoleBinding 没有绑定到正确的 ServiceAccount。
3. 创建 HA etcd 集群的核心 YAML 与参数解析
3.1 一份能直接落地的 EtcdCluster 资源
operator 安装好之后,就可以定义业务侧的 etcd 集群。下面这份 YAML 是我在项目中实际用过的模板,去掉了内部敏感信息,只保留核心配置:
apiVersion: etcd.database.coreos.com/v1beta2 kind: EtcdCluster metadata: name: business-etcd namespace: etcd spec: size: 3 version: 3.4.27 pod: antiAffinity: true resources: requests: cpu: 500m memory: 512Mi limits: cpu: "2" memory: 4Gi etcdEnv: - name: ETCD_AUTO_COMPACTION_RETENTION value: "1h" - name: ETCD_QUOTA_BACKEND_BYTES value: "8589934592" storage: volumeClaimTemplate: metadata: name: data spec: accessModes: - ReadWriteOnce resources: requests: storage: 20Gi这里先说几个容易被忽略的点。version字段会决定 operator 去拉哪个 etcd 镜像,如果镜像同步没做好,集群创建会卡在 ImagePullBackOff。antiAffinity: true是 HA 的关键,etcd 三个副本会尽量调度到不同节点,避免一台物理机故障带走两个 etcd 成员。但反亲和不是强制执行,节点不够的时候 Pod 依然可能落到同一台机器,所以底层节点数量一定要大于副本数。
3.2 关键参数为什么这么设置
| 参数 | 推荐值 | 设计原因 |
|---|---|---|
| size | 3 | 奇数节点,quorum=2,容忍单节点故障 |
| storage | 20Gi 起 | etcd 数据量增长比预期快,预留空间至少是当前数据量的两倍 |
| antiAffinity | true | 保证三个 Pod 分散到不同节点 |
| resources.limits.cpu | 2 | 没有 CPU 限制时,突发流量可能把节点打爆;限制太小又会拖慢 fsync |
| ETCD_AUTO_COMPACTION_RETENTION | 1h | 历史版本数据自动压缩,避免无限增长 |
| ETCD_QUOTA_BACKEND_BYTES | 8Gi | 控制 etcd 数据库大小,超过后会触发告警和写保护 |
CPU 限制这里尤其要注意。etcd 对磁盘 fsync 和网络延迟极其敏感,如果把 CPU limit 设成 100m 这种小值,遇到业务高峰可能因为 CPU 被限流导致心跳超时,进而引发频繁选主,这是我在测试环境踩过一次的教训。内存方面,etcd 进程本身不算特别吃内存,但 Linux 内核的 page cache 会帮助提升读性能,所以至少要给 4Gi 的 limit,不要抠门。
存储那块默认是走 Kubernetes 动态供给的 PVC。生产环境建议优先使用 SSD 存储类,因为 etcd 的每次写入都要等 WAL 落盘,机械硬盘的 fsync 延迟在这种场景下很难看。如果你当前的 StorageClass 是慢盘,建议先建一个专用的高性能 StorageClass 再创建集群。
3.3 部署之后如何确认集群健康
提交 YAML 后,operator 会陆续创建三个 etcd Pod。这时不要只看 Pod 状态为 Running 就以为完事了,要确认 etcd 内部真的形成了集群,并且有 leader。
kubectl -n etcd apply -f etcdcluster.yaml kubectl -n etcd get etcdclusters kubectl -n etcd get pod -l app=etcd,etcd_cluster=business-etcd然后找一个能访问集群内 Service 的位置,用 etcdctl 做 endpoint 健康检查。如果集群内部还没有现成的 etcdctl 客户端,可以用一个临时 Pod,或者直接在任意一个 etcd Pod 里执行:
kubectl -n etcd exec business-etcd-xxxxx -- env ETCDCTL_API=3 etcdctl \ --endpoints=http://localhost:2379 endpoint healthoperator 通常会自动创建客户端访问用的 Service,实际名称以kubectl -n etcd get svc看到的为准。我第一次部署的时候就是没等 operator 把 Service 建好就急着写客户端连接地址,结果连接超时,回头一看 Service 还没 Ready,属于自己着急了。
4. 备份、恢复与日常运维
4.1 operator 自带的备份能力没那么省心
etcd-operator 官方文档里确实提过备份和恢复相关的 CRD,但在实际生产里我没有完全依赖它。原因很简单:备份组件本身也是一段需要长期维护的代码,如果它跟 operator 主版本绑得太死,operator 一旦归档或停止更新,备份链路也会跟着失效。对我来说,备份这种事越简单越可靠。
所以我选择的方式是:定时用 etcdctl 做 snapshot save,把快照文件放到一个独立的 PVC 里,然后再同步到对象存储。整套逻辑用一个 CronJob 就能实现,不依赖额外的备份控制器,出了问题也容易排查。
4.2 用 CronJob 定时快照
下面是一个简化版的备份 CronJob,每天凌晨两点执行一次。它从业务 etcd 的客户端 Service 拉快照,写到挂载的备份 PVC 里。注意 etcdctl 的 API 版本必须指定为 v3,否则命令行为完全不一样。
apiVersion: batch/v1 kind: CronJob metadata: name: etcd-backup namespace: etcd spec: schedule: "0 2 * * *" jobTemplate: spec: template: spec: restartPolicy: OnFailure containers: - name: etcdctl image: registry.example.com/etcd:3.4.27 command: - /bin/sh - -ec - | ETCDCTL_API=3 etcdctl \ --endpoints=http://business-etcd-client.etcd.svc:2379 \ snapshot save /backup/snapshot-$(date +%Y%m%d-%H%M).db ls -lh /backup env: - name: ETCDCTL_API value: "3" volumeMounts: - name: backup mountPath: /backup volumes: - name: backup persistentVolumeClaim: claimName: etcd-backup-pvc这个 CronJob 跑起来之后,一定要手动触发一次测试,确认快照文件真的能生成,而不是等到数据出问题时才发现备份早就失败了。我见过不少团队把备份任务设置好就不管了,结果 PVC 满了之后快照文件写入失败,恢复时才发现一份可用备份都没有。
4.3 监控与告警配置
etcd 自带 Prometheus metrics 端口,客户端端口 2379 上直接暴露/metrics。如果集群里已经部署了 Prometheus,可以创建一个 ServiceMonitor 或者直接通过 Pod 注解让采集器发现目标。
需要重点盯的四个指标:
etcd_server_is_leader:看当前成员是不是 leader,变化本身不一定是问题,但频繁变化要警惕;etcd_server_leader_changes_seen_total:leader 切换次数突然上涨,通常说明节点间网络或磁盘有抖动;etcd_disk_wal_fsync_duration_seconds:WAL fsync 耗时偏高,大概率是磁盘性能扛不住;etcd_server_failed_proposals_total:如果持续增长,说明集群内部已经无法正常达成共识。
告警规则里,我最看重的是“当前存活成员数小于 quorum”。三节点集群里只要这个值小于 2,应用层写入就会全部失败,必须立刻介入。这类告警建议直接配置成 PagerDuty 级别的紧急告警,不要跟普通日志告警混在一起。
4.4 版本升级的注意点
etcd-operator 支持通过修改 EtcdCluster 的 version 字段来升级集群。它会逐个重建成员,确保在任何时刻集群都保持多数派存活。但升级前永远要做两件事:先备份,再检查版本兼容性。etcd 的跨大版本升级通常要求先升到中间版本,比如 3.4 升 3.5,尽量不要跨两个大版本直接跳。
升级过程中我会持续观察 Pod 变化和 leader 切换次数。如果某个成员升级后一直起不来,先不要急着把整个 CR 回滚,先看这个成员的日志,确认是数据目录格式不兼容还是启动参数问题。临时把 version 改回去可能让已经升级的成员和旧成员之间产生版本不一致,这种状态比不升级更危险。
5. 故障场景与排查实录
5.1 Pod 一直 Pending,怎么看都像调度不出去
第一个常见故障是 Pod 卡在 Pending。如果设置了antiAffinity: true,但集群里可调度的节点数小于 3,Pod 会一直等待。另一种可能是 PVC 无法绑定,比如没有匹配的 StorageClass 或者存储配额不足。
排查命令很简单:
kubectl -n etcd describe pod business-etcd-xxxxx kubectl -n etcd get pvc如果事件里写的是 “0/3 nodes are available” 并且带了 PodAntiAffinity 关键字,那基本就是节点数不够。解决办法是加节点,或者临时关闭反亲和。但我建议生产环境别为了省节点就关闭反亲和,那等于把一个 HA 集群的核心保护拆掉了。
5.2 启动后 CrashLoopBackOff,数据目录冲突
创建同名集群或者复用 PVC 时,容易遇到一个隐蔽问题:PVC 里残留了上一次集群的 etcd 数据。新 Pod 启动后,etcd 发现数据目录里的成员 ID 和当前集群期望的成员 ID 对不上,直接拒绝启动,表现就是 CrashLoopBackOff。
这种场景下,先确认这个 PVC 是否还需要保留。如果里面有旧集群数据但确认不需要恢复,最省事的处理方式是把对应的 PVC 删除再重建,让 operator 为每个成员重新申请一块干净存储。不要在一个已经有旧数据的目录上反复折腾,etcd 对数据目录一致性非常敏感。
5.3 三节点挂了两台,quorum 彻底丢了
这是最严重的故障。三节点集群挂掉两台,剩下一个节点虽然进程活着,但无法满足 quorum=2,所以读写在应用层全部失败。此时 operator 能做的工作极其有限,它无法帮你凭空恢复多数派,需要人工介入。
我的处理思路分两步:
第一步,确认剩下的那台节点数据是否完整,能不能抢救。如果能,保留它的数据目录,然后找到原集群最后一次成功的快照,用 snapshot restore 在干净环境恢复一个临时集群。
第二步,如果快照也没有,那只能接受丢失一部分最新数据的现实。把能够抢救出来的成员数据目录做冷备份,再基于最后一次可靠的快照重建新集群。这个过程很痛苦,所以备份的重要性无论如何强调都不过分。
5.4 高版本 Kubernetes 下 CRD 安装失败
老版本 etcd-operator 的部署清单里常出现apiextensions.k8s.io/v1beta1,在 Kubernetes 1.22 以上直接 apply 会报错。这时候需要把 CRD 定义转换成 v1 格式再提交。CRD 本身只是声明资源结构,改成 v1 不影响 operator 的业务逻辑。
如果你不想维护这套兼容层,更稳妥的做法是换一个还在活跃维护的 etcd 部署方案。我后面会单独说这一点。总之,CRD apiVersion 这类问题在等保要求高的环境里很常见,不要在报错面前硬扛,先确认版本矩阵。
5.5 常见问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| Pod Pending | 节点数不足 / PVC 无法绑定 | 查看 describe pod,增加节点或检查 StorageClass |
| CrashLoopBackOff | 数据目录冲突 / 镜像拉取失败 | 确认 PVC 是否残留旧数据,检查镜像可用性 |
| endpoint health 报 unhealthy | 集群成员间网络不通 / 防火墙限制 | 检查 2379/2380 端口连通性 |
| 频繁 leader 切换 | CPU limit 太小 / 磁盘 fsync 慢 | 调大资源 limit,确认存储类型 |
| 扩容卡住 | 新成员一直没加入 Raft | 查看 operator 日志,确认 member add 是否完成 |
| CRD apply 报错 | apiVersion 和 K8s 版本不兼容 | 转换 CRD 定义或者升级部署方案 |
6. 关于 etcd-operator 的现状与后续演进
6.1 官方归档不代表不能用于存量生产
需要说明的是,etcd-operator 这个项目目前已经处于归档状态,也就是说不会有新的特性、安全修复和版本发布。存量业务如果已经跑了一段时间,并且版本组合稳定,短期内继续使用问题不大。我在这套环境里运行了大半年,除了正常的节点维护,没有遇到过 operator 本身崩溃的情况。
但归档状态意味着它跟不上 Kubernetes 版本的快速变化。未来如果集群升级到某个新版本,operator 用到的旧 API 被移除,业务 etcd 就可能面临无法声明式管理的局面。所以凡是新启动的项目,我不会推荐再选它作为主方案。
6.2 如果今天重新选型,我会怎么考虑
如果是新项目,我更倾向用社区还在维护的 etcd Helm Chart,或者基于 StatefulSet 加初始化脚本的成熟方案。这些方案没有那么智能的成员管理,但胜在代码有人持续维护,遇到 Kubernetes 升级时能跟着适配。操作上多写两个初始化 Job 不是不能接受,关键是整个链路可控。
当然,这并不意味这篇文章白写了。etcd-operator 的使用经验里,关于 etcd 集群本身的设计原则、资源限制、备份恢复、故障排查思路,换到任何部署方式都一样适用。你只要理解了 Raft 的多数派原理、理解了 etcd 对磁盘和网络的敏感度,哪怕后面把 operator 换掉,也能很快上手新方案。
最后分享一个个人经验:无论你用哪套方案,都要把 deployment 的镜像版本、EtcdCluster 的 YAML、备份 CronJob 全部放进 Git 管理。etcd 这种存储组件最怕的不是技术选型,而是某个凌晨突发故障时,发现没人知道当前版本是什么、备份有没有成功。运维文档里写一百句话,都不如一条能复现的 Git commit 有用。