第 4 章

etcd 备份与集群升级

·约 19 分钟

集群跑起来之后,迟早要升级。升级前先做一件事:备份 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/etcdetcd.yaml
/etc/kubernetes/pki/etcd/etc/kubernetes/pki/etcdetcd.yaml
/etc/hosts/var/lib/kubelet/pods/<Pod UID>/etc-hostskubelet 自动加
/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 的 endpointK8s 的 Endpoints
是什么一个访问地址一种资源对象
在哪看etcdctl 的 --endpoints、API Server 的 --etcd-serverskubectl 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 的区别:

memberendpoint
查什么集群的成员名单指定地址上的 etcd 状态
常用子命令list / add / remove / promotehealth / status / hashkv
范围连任意一个成员都返回完整名单只查 --endpoints 给的地址,加 --cluster 查全部成员

quorum:为什么是 3 或 5 个成员

etcd 用 Raft 共识:每个成员存一份完整数据,写入要超过半数成员确认才算成功。

成员数需要确认最多能坏
110
220
321
431
532

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 |        |
+----------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
列值解读
IDc3210e4aa6db4a6e和 member list 一致,这个 endpoint 连的就是这个 member
DB SIZE3.4 MB默认上限 2GB,写满会触发 NOSPACE 告警,etcd 变成只读
IS LEADERtrue只有一个成员,它自己就是 leader
RAFT TERM4选过 4 次 leader,单成员 etcd 每次重启都会重新选举
RAFT INDEX / APPLIED INDEX661112 / 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校验值,复制到别处后可以对比,确认文件没坏
REVISIONetcd 的数据版本号,每修改一次 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找数据目录和三个客户端证书的路径
2endpoint health确认 etcd 能正常写入
3member list / endpoint status确认成员数、leader、数据库大小
4snapshot save存到挂载目录,节点上也能看到
5snapshot status验证快照完整
6复制快照和证书离开 etcd 的数据盘,按敏感数据保管

恢复(snapshot restore)一旦出错,集群就不可用了,所以放到最后再单独练。


考试里怎么做

  • 考试环境一般在节点上直接装好了 etcdctl,不走 kubectl exec;证书只有 root 能读,要加 sudo
  • 题目通常会给出证书路径;没给就 sudo grep etcd.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