第 4 章
etcd 备份与集群升级
集群跑起来之后,迟早要升级。升级前先做一件事:备份 etcd。这两件事在 CKA 里几乎必考。
实验在自己的 VMware 集群上完成:K8s v1.31.14,etcd 3.5.24。新版 etcd 3.6 有几处命令变了,不一样的地方会单独标出来。
etcd 存着什么
K8s 的所有资源——Deployment、Service、ConfigMap、Secret、RBAC、节点信息——都存在 etcd 里。其他控制平面组件基本不存数据,挂了重启就行;etcd 丢了,集群就"失忆"了。所以备份集群状态就是备份 etcd。
每个资源在 etcd 里是一个 key,路径格式是 /registry/<资源类型>/<namespace>/<名字>(节选):
/registry/apiextensions.k8s.io/customresourcedefinitions/ciliumcidrgroups.cilium.io
/registry/deployments/default/nginx
/registry/services/specs/default/nginx
为什么不能直接复制数据目录
etcd 一直在写(心跳、租约续期……),cp -r /var/lib/etcd 复制到一半数据就可能变了,得到一份前后不一致的坏文件——和不能在 MySQL 运行时复制数据目录一个道理。
etcdctl snapshot save 是让 etcd 自己导出某一时刻的完整数据:前后一致、单个文件、带校验值。
快照里有所有 Secret
K8s 默认不加密 Secret,只做 base64 编码。拿到快照就能还原出集群里所有密码和密钥,备份文件要按敏感数据保管。
找到 etcd 的配置
etcd.yaml 是 Static Pod 清单
etcd 的配置在 /etc/kubernetes/manifests/etcd.yaml。这个目录由 kubelet 直接监视:放了什么 YAML,就在本节点跑什么 Pod,不经过 API Server。
$ sudo ls /etc/kubernetes/manifests/
etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml
控制平面组件没法通过 API Server 创建自己——API Server 还没起来时,谁来创建 API Server?所以 kubeadm 把它们写成静态文件交给 kubelet。改了这里的文件,kubelet 会自动重建 Pod,以后改 API Server 参数、恢复 etcd 都在这里操作。
文件只有 root 可读,普通 cat 会报 Permission denied,要加 sudo。
用 grep 查参数
sudo grep -E 'data-dir|crt|key' /etc/kubernetes/manifests/etcd.yaml
- --cert-file=/etc/kubernetes/pki/etcd/server.crt
- --data-dir=/var/lib/etcd
- --key-file=/etc/kubernetes/pki/etcd/server.key
- --peer-cert-file=/etc/kubernetes/pki/etcd/peer.crt
- --peer-key-file=/etc/kubernetes/pki/etcd/peer.key
- --peer-trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
- --trusted-ca-file=/etc/kubernetes/pki/etcd/ca.crt
7 行分三组:
| 用途 | 参数 | 用不用 |
|---|---|---|
| 数据目录 | --data-dir | 快照存这里 |
| 对客户端(2379 端口) | --trusted-ca-file / --cert-file / --key-file | ✅ 填进 etcdctl |
| 成员之间(2380 端口) | --peer- 开头的三个 | ❌ |
ca.crt 两组共用:同一个 CA 签发了 server 和 peer 两套证书。
踩坑:一开始写的
grep 'data-dir|trusted-ca'没有任何输出。grep 默认是基础正则(BRE),|只是普通字符,要用grep -E 'A|B'(扩展正则)或者grep 'A\|B'。
调用 etcdctl
etcdctl 在容器里
etcd 镜像(registry.k8s.io/etcd)打包了三个程序:
| 程序 | 作用 |
|---|---|
etcd | 数据库本身 |
etcdctl | 客户端:查询、备份 |
etcdutl | 离线工具:直接操作数据文件,比如恢复快照 |
kubeadm 不会在节点上装 etcdctl,所以要借 kubectl exec 进容器执行:
kubectl -n kube-system exec etcd-ubuntu -- etcdctl -h
- Pod 名
etcd-ubuntu:etcd.yaml 里写的是name: etcd,Static Pod 会自动加上节点名 --之后是在容器里执行的命令
exec 不会新建容器,只是在正在运行 etcd 的那个容器里临时起一个 etcdctl 进程,跑完就退出。两个进程在同一个容器里,所以 etcdctl 连 127.0.0.1:2379 就能找到 etcd。
etcd 镜像是 distroless 精简镜像,连 shell 都没有:
$ kubectl -n kube-system exec -it etcd-ubuntu -- sh
error: Internal error occurred: ... OCI runtime exec failed: exec failed: unable to start container process: exec: "sh": executable file not found in $PATH
所以只能每次 exec 一条具体命令进去。这段报错从左到右正好是 exec 请求经过的各层:API Server → kubelet → containerd → runc,排障时看最后一句。
三个证书:双向 TLS
etcd.yaml 里有 --client-cert-auth=true,意思是 etcd 要求客户端也出示证书(mTLS),连接时双方互验身份:
| etcdctl 参数 | 对应 etcd.yaml | 作用 |
|---|---|---|
--cacert | --trusted-ca-file → ca.crt | 验证对方确实是 etcd |
--cert | --cert-file → server.crt | 出示我的证书 |
--key | --key-file → server.key | 证明这张证书是我的 |
路径不用背,看 etcd.yaml 填 etcdctl 就行。
每条命令都带这三个参数太长了,做实验时设一个 alias:
alias e='kubectl -n kube-system exec etcd-ubuntu -- etcdctl --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key'
之后 e endpoint health、e member list 就够了。
hostPath:容器和节点共享目录
etcd.yaml 里的挂载声明:
volumeMounts:
- mountPath: /var/lib/etcd
name: etcd-data
- mountPath: /etc/kubernetes/pki/etcd
name: etcd-certs
volumes:
- hostPath:
path: /etc/kubernetes/pki/etcd
name: etcd-certs
- hostPath:
path: /var/lib/etcd
name: etcd-data
容器里的 /var/lib/etcd 和节点上的 /var/lib/etcd 是同一个目录,证书目录也一样。所以容器里的 etcdctl 能用节点上的证书路径,容器里存的快照在节点上也能看到。
挂载不是手动做的:kubeadm init 生成 etcd.yaml 时写好了声明,之后每次容器启动,由 kubelet 通知 containerd 执行挂载。目录的创建时间就是 init 那天:
$ sudo stat /var/lib/etcd
...
Birth: 2026-08-29 06:56:11.547867945 +0000
用 crictl 从容器运行时这一层看实际的挂载:
sudo crictl inspect $(sudo crictl ps --name etcd -q) | grep -A2 '"containerPath"'
| 容器路径 | 节点路径 | 谁声明的 |
|---|---|---|
/var/lib/etcd | /var/lib/etcd | etcd.yaml |
/etc/kubernetes/pki/etcd | /etc/kubernetes/pki/etcd | etcd.yaml |
/etc/hosts | /var/lib/kubelet/pods/<Pod UID>/etc-hosts | kubelet 自动加 |
/dev/termination-log | /var/lib/kubelet/pods/<Pod UID>/containers/… | kubelet 自动加 |
为什么非要挂载:容器的文件系统是临时的,删了重建就没了。crictl ps 显示 etcd 容器的 ATTEMPT 是 2,重启过,但数据一点没丢,因为数据一直在节点磁盘上。
crictl 直接问 containerd 本节点跑着哪些容器,不依赖 API Server。API Server 挂了 kubectl 就什么都查不到,这时靠
crictl ps -a和crictl logs排障。
查看 etcd 状态
endpoint 是什么
endpoint 就是一个访问地址(协议 + IP + 端口)。etcdctl 不去任何地方"发现"地址,给它什么就连什么,不给就用默认的 127.0.0.1:2379。
etcd 在哪些地址上监听,etcd.yaml 里写着:
- --listen-client-urls=https://127.0.0.1:2379,https://10.177.16.45:2379
- --listen-metrics-urls=http://127.0.0.1:2381
- --listen-peer-urls=https://10.177.16.45:2380
| 端口 | 给谁用 | 说明 |
|---|---|---|
| 2379 | 客户端(API Server、etcdctl) | 本机和其他机器都能连,一个 member 开了两个 endpoint |
| 2380 | 其他 etcd 成员 | 成员都在别的机器上,所以不监听 127.0.0.1 |
| 2381 | 健康检查、监控指标 | 只对本机开放,所以用 HTTP 不加密;livenessProbe 和 readinessProbe 打的就是这个端口 |
验证"给什么连什么":
$ e --endpoints=https://10.177.16.45:2379 endpoint health
https://10.177.16.45:2379 is healthy: successfully committed proposal: took = 8.833634ms
$ e --endpoints=https://10.177.16.45:9999 endpoint health
... dial tcp 10.177.16.45:9999: connect: connection refused ...
https://10.177.16.45:9999 is unhealthy: failed to commit proposal: context deadline exceeded
Error: unhealthy cluster
context deadline exceeded 只是"重试到超时",真正的原因要往上找:
| 底层报错 | 含义 | 常见原因 |
|---|---|---|
connection refused | 机器通,端口没人监听 | 端口写错、etcd 没起来 |
i/o timeout | 机器都连不上 | IP 写错、防火墙 |
certificate / tls 相关 | 连上了,身份验证失败 | 证书路径错、用错证书 |
和 K8s 的 Endpoints 不是一回事
kubectl get ep 里找不到 etcd。K8s 的 Endpoints 是 Service 背后的 Pod 地址列表,只有 Service 才有。etcd 没有 Service:Service 本身要靠 API Server 和 etcd 才能运转,etcd 不能反过来依赖它。
API Server 是直接连 etcd 地址的,写在 kube-apiserver.yaml 里(kubeadm 默认配置):
--etcd-servers=https://127.0.0.1:2379
--etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
--etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
--etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
API Server 连 etcd 也要三个证书,用的是它专用的客户端证书 apiserver-etcd-client。
| etcd 的 endpoint | K8s 的 Endpoints | |
|---|---|---|
| 是什么 | 一个访问地址 | 一种资源对象 |
| 在哪看 | etcdctl 的 --endpoints、API Server 的 --etcd-servers | kubectl get ep |
| 谁有 | etcd 成员 | Service |
endpoint health
$ e endpoint health
127.0.0.1:2379 is healthy: successfully committed proposal: took = 15.833875ms
健康检查不是 ping:它会真的提交一次测试写入,确认写入成功才算 healthy,15.8ms 是这次写入的耗时。
member list
$ e member list -w table
+------------------+---------+--------+---------------------------+---------------------------+------------+
| ID | STATUS | NAME | PEER ADDRS | CLIENT ADDRS | IS LEARNER |
+------------------+---------+--------+---------------------------+---------------------------+------------+
| c3210e4aa6db4a6e | started | ubuntu | https://10.177.16.45:2380 | https://10.177.16.45:2379 | false |
+------------------+---------+--------+---------------------------+---------------------------+------------+
- CLIENT ADDRS 只有一个:member list 显示的是
--advertise-client-urls(告诉别人用哪个地址连我),不是--listen-client-urls(实际监听哪些地址)。127.0.0.1 告诉别的机器也没用 - IS LEARNER:新成员先当 learner,只同步数据不投票,追上进度后用
member promote转正,避免拖慢集群写入 - STATUS 是
started只说明它加入过集群,不代表现在还活着。成员宕机了 member list 照样显示 started,判断健康要用endpoint health --cluster
member 和 endpoint 的区别:
member | endpoint | |
|---|---|---|
| 查什么 | 集群的成员名单 | 指定地址上的 etcd 状态 |
| 常用子命令 | list / add / remove / promote | health / status / hashkv |
| 范围 | 连任意一个成员都返回完整名单 | 只查 --endpoints 给的地址,加 --cluster 查全部成员 |
quorum:为什么是 3 或 5 个成员
etcd 用 Raft 共识:每个成员存一份完整数据,写入要超过半数成员确认才算成功。
| 成员数 | 需要确认 | 最多能坏 |
|---|---|---|
| 1 | 1 | 0 |
| 2 | 2 | 0 |
| 3 | 2 | 1 |
| 4 | 3 | 1 |
| 5 | 3 | 2 |
2 个和 1 个一样一个都不能坏,4 个和 3 个一样只能坏 1 个,所以成员数取奇数。实验集群只有 1 个成员,没有任何冗余,更要备份;HA 集群坏了一个成员直接替换就行,不需要从快照恢复。
endpoint status
$ e endpoint status -w table
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 127.0.0.1:2379 | c3210e4aa6db4a6e | 3.5.24 | 3.4 MB | true | false | 4 | 661112 | 661112 | |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
| 列 | 值 | 解读 |
|---|---|---|
| ID | c3210e4aa6db4a6e | 和 member list 一致,这个 endpoint 连的就是这个 member |
| DB SIZE | 3.4 MB | 默认上限 2GB,写满会触发 NOSPACE 告警,etcd 变成只读 |
| IS LEADER | true | 只有一个成员,它自己就是 leader |
| RAFT TERM | 4 | 选过 4 次 leader,单成员 etcd 每次重启都会重新选举 |
| RAFT INDEX / APPLIED INDEX | 661112 / 661112 | 确认的写入次数 / 已落进数据库的次数,相等说明没有积压 |
66 万次写入大部分是心跳:kubelet 定期续节点租约,controller-manager 和 scheduler 续自己的 leader 租约。集群什么都不干,也一直在写 etcd。
存快照
snapshot save
$ e snapshot save /var/lib/etcd/snapshot.db
{"level":"info",...,"msg":"created temporary db file","path":"/var/lib/etcd/snapshot.db.part"}
...
{"level":"info",...,"msg":"fetched snapshot","endpoint":"127.0.0.1:2379","size":"3.4 MB","took":"now"}
{"level":"info",...,"msg":"saved","path":"/var/lib/etcd/snapshot.db"}
Snapshot saved at /var/lib/etcd/snapshot.db
路径必须在挂载目录下。命令在容器里执行,路径是容器里的路径;存到 /tmp 之类的地方,节点上看不到,容器一重建就丢了。etcd 容器只挂了 /var/lib/etcd 和证书目录,所以存 /var/lib/etcd。
Tab 补全的陷阱:输入
e snapshot save /var/按 Tab,补全出来的是节点上的目录。补全由本机的 shell 完成,它不知道这个参数最后会传进容器。
直接在节点上运行 etcdctl 时(考试一般是这样),路径就是节点上的路径,按题目要求存。
在节点上确认:
$ sudo ls -lh /var/lib/etcd/
total 3.3M
drwx------ 4 root root 4.0K Sep 26 05:25 member
-rw------- 1 root root 3.3M Sep 26 06:55 snapshot.db
日志说 3.4 MB、ls -h 说 3.3M,其实是同一个大小:etcd 按 1000 进位,ls -h 按 1024 进位。
snapshot status:验证快照
$ e snapshot status /var/lib/etcd/snapshot.db -w table
Deprecated: Use `etcdutl snapshot status` instead.
+----------+----------+------------+------------+
| HASH | REVISION | TOTAL KEYS | TOTAL SIZE |
+----------+----------+------------+------------+
| d587d57d | 582270 | 783 | 3.4 MB |
+----------+----------+------------+------------+
| 列 | 含义 |
|---|---|
| HASH | 校验值,复制到别处后可以对比,确认文件没坏 |
| REVISION | etcd 的数据版本号,每修改一次 key 加 1 |
| TOTAL KEYS | 数据库里 key 的总数,包括还没压缩掉的历史版本和 etcd 内部数据 |
| TOTAL SIZE | 和 save 时一致 |
REVISION(582270)比 RAFT INDEX(661112)小,因为 Raft 日志里还有租约、压缩这类不直接改 key 的操作。
版本差异:etcd 3.6 起,snapshot status 和 snapshot restore 从 etcdctl 移到了 etcdutl。3.5 里还能用,但会提示 Deprecated;到了 3.6.5,etcdctl -h 里只剩 snapshot save。
复制到备份目录
mkdir -p ~/backup
sudo cp /var/lib/etcd/snapshot.db ~/backup/snapshot.db-$(date +%F)
sudo cp -r /etc/kubernetes/pki/etcd ~/backup/
- 快照和 etcd 数据在同一块磁盘上,盘坏了会一起丢,所以要复制出来;生产环境还要再复制到别的机器
- 证书也要备份:节点坏了要在新机器上重建 etcd 时,需要同一套 CA 证书,其他组件才能继续和它通信
$(date +%F)生成2026-09-26这样的日期,按文件名排序就是时间顺序sudo cp出来的文件属于 root、权限是600,保持这样就好,快照里有所有 Secret
小结:etcd 备份做了什么
| # | 步骤 | 说明 |
|---|---|---|
| 1 | 查 etcd.yaml | 找数据目录和三个客户端证书的路径 |
| 2 | endpoint health | 确认 etcd 能正常写入 |
| 3 | member list / endpoint status | 确认成员数、leader、数据库大小 |
| 4 | snapshot save | 存到挂载目录,节点上也能看到 |
| 5 | snapshot status | 验证快照完整 |
| 6 | 复制快照和证书 | 离开 etcd 的数据盘,按敏感数据保管 |
恢复(snapshot restore)一旦出错,集群就不可用了,所以放到最后再单独练。
考试里怎么做
- 考试环境一般在节点上直接装好了 etcdctl,不走
kubectl exec;证书只有 root 能读,要加sudo - 题目通常会给出证书路径;没给就
sudo grepetcd.yaml - 命令模板在官方文档 Operating etcd clusters for Kubernetes,考试时可以查
- 同一台机器上要跑多条 etcdctl,可以用环境变量代替参数:
sudo -i
export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key
etcdctl snapshot save /opt/etcd-backup.db
sudo 默认不会带上普通用户的环境变量,所以先 sudo -i 切到 root 再 export。老教程里的 ETCDCTL_API=3 从 etcd 3.4 起就是默认值,不用再加。
集群升级
实操完成后补充。
命令速查表
etcdctl 的命令都省略了三个证书参数。
| 命令 | 用途 |
|---|---|
sudo grep -E 'data-dir|crt|key' /etc/kubernetes/manifests/etcd.yaml | 查数据目录和证书路径 |
kubectl -n kube-system exec etcd-<节点名> -- etcdctl <子命令> | 在 etcd 容器里执行 etcdctl |
etcdctl endpoint health | 健康检查(会真实写入一次) |
etcdctl endpoint status -w table | 版本、leader、数据库大小、Raft 状态 |
etcdctl member list -w table | 成员名单 |
etcdctl snapshot save <文件> | 存快照 |
etcdctl snapshot status <文件> -w table | 验证快照(3.6 起改用 etcdutl) |
sudo crictl ps --name etcd | 从容器运行时看 etcd 容器 |
sudo crictl inspect <容器ID> | 看容器的挂载等详情 |
grep 常用法
| 用法 | 场景 |
|---|---|
grep -E 'A|B' | 匹配 A 或 B |
grep -v Running | 反向匹配:k get po -A | grep -v Running 找出不正常的 Pod |
grep -i error | 忽略大小写 |
grep -A 10 Events | 显示匹配行及之后 10 行:k describe po x | grep -A 10 Events |