第 4 章

etcd 备份与集群升级

·约 36 分钟

集群跑起来之后,迟早要升级。升级前先做一件事:备份 etcd。这两件事在 CKA 里几乎必考。

实验在自己的 VMware 集群上完成:K8s v1.31.14,etcd 3.5.24,最后升到 v1.32.13。新版 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 起就是默认值,不用再加。


集群升级

把集群从 v1.31.14 升到 v1.32.13,一共三台:控制平面 ubuntu,worker ubuntu1、ubuntu2。

全程照官方文档做,用到三页:

文档讲什么
Upgrading kubeadm clusters主流程、控制平面怎么升、失败了怎么办
Changing the Kubernetes package repository换 apt 源
Upgrading Linux nodesworker 怎么升

路径是 Docs → Tasks → Administer a Cluster → Administration with kubeadm,和 etcd 那页在同一个大目录下。文档写的是最新版本(1.36 → 1.37),老版本的流程基本一样,把命令里的 1.37.x 换成自己的版本就行。

两条规则

一次只升一个小版本:1.31 → 1.32 可以,1.31 → 1.33 不行,要分两次。

先升控制平面,再升 worker:Version Skew Policy 规定 kubelet 可以比 API Server 旧,但不能比它新。先升 worker 的话,就成了 kubelet 比 API Server 新,不允许。

一个节点上的两层

升级要换两样东西,分两步换:

ubuntu(控制平面节点)
├─ kubelet                 ← apt 装的系统服务,自己 apt install 换
│   ├─ Static Pod:etcd / apiserver / controller-manager / scheduler   ← kubeadm upgrade 换
│   ├─ DaemonSet Pod:calico-node / kube-proxy
│   └─ 普通 Pod:CoreDNS、应用……
  • kubeadm upgrade 改的是 /etc/kubernetes/manifests/ 里的清单,kubelet 看到清单变了,重建 Static Pod
  • kubelet 本身是个普通程序,kubeadm 不管,要用 apt 换包、重启服务
  • kubectl get nodes 的 VERSION 列显示的是每个节点 kubelet 的版本,控制平面的版本要看 kubectl version 的 Server Version

升级前:备份

文档只说要备份重要的东西,没列清单。判断标准是丢了能不能重新生成:

东西丢了会怎样备不备
etcd 数据集群里所有对象都没了必须
/etc/kubernetes/pki/ 证书整台机器重建时,没有原来的 CA,所有组件和节点都要重新签证书必须
kubeadm 配置重建时要用同样的参数(Pod 网段、API Server 地址等)建议
Static Pod 清单、kubeconfigkubeadm 用配置和证书能重新生成不用

快照按上面「存快照」的方法存好,放进 ~/backup,再备份证书和配置:

sudo cp -r /etc/kubernetes/pki ~/backup/
kubectl -n kube-system get cm kubeadm-config -o yaml > ~/backup/kubeadm-config.yaml

kubeadm 配置不是一个现成的文件。我 init 时直接在命令行传参数,/root 下没有 kubeadm-config.yaml。kubeadm 把配置存进了集群:kube-system 里的 ConfigMap kubeadm-config,升级时读的也是它:

[upgrade/config] Reading configuration from the "kubeadm-config" ConfigMap in namespace "kube-system"...

导出来能看到 init 时定下的参数:

    controlPlaneEndpoint: 10.177.16.45:6443
    kubernetesVersion: v1.31.14
    networking:
      podSubnet: 192.168.0.0/16
      serviceSubnet: 10.96.0.0/12

kubernetesVersion 就是 kubeadm 认定的集群版本,升级成功后会变成 v1.32.13。

换软件源

pkgs.k8s.io 每个小版本一个仓库,源地址里写死了版本号:

$ cat /etc/apt/sources.list.d/kubernetes.list
deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /

不换源的话,apt-cache madison kubeadm 只能列出 1.31.0 到 1.31.14,根本没有 1.32。文档的主页面只用一句话提了这件事,然后链到另一页,很容易漏看;漏了也有兜底,madison 那一节写着:看不到想要的版本,就回去检查仓库。

sudo sed -i 's/v1.31/v1.32/' /etc/apt/sources.list.d/kubernetes.list
sudo apt update
sudo apt-cache madison kubeadm
   kubeadm | 1.32.13-1.1 | https://pkgs.k8s.io/core:/stable:/v1.32/deb  Packages
   kubeadm | 1.32.12-1.1 | https://pkgs.k8s.io/core:/stable:/v1.32/deb  Packages
   ...
   kubeadm | 1.32.0-1.1 | https://pkgs.k8s.io/core:/stable:/v1.32/deb  Packages

文档建议升到最新的补丁版本,选 1.32.13。

  • sed 's/找什么/换成什么/',三个 / 一个都不能少,少了最后一个会报 unterminated 's' command
  • 先不加 -i 跑一遍,屏幕上看结果对了再加 -i 改文件
  • 每台节点的 apt 源是各自的,三台都要换

控制平面:ubuntu

第一步:升级 kubeadm

sudo apt-mark unhold kubeadm && \
sudo apt-get update && sudo apt-get install -y kubeadm='1.32.13-1.1' && \
sudo apt-mark hold kubeadm
$ kubeadm version
kubeadm version: &version.Info{Major:"1", Minor:"32", GitVersion:"v1.32.13", ...}

hold 是锁住版本,平时 apt upgrade 不会顺手把 K8s 的包升上去。K8s 升级要按顺序来,不能让 apt 自己决定。所以每次都是先 unhold、装好、再 hold 回去。

踩坑:第一次写成 kubeadm='1.32.13',报 Version '1.32.13' for 'kubeadm' was not found。apt 的版本号要和 madison 第二列一字不差,后面的 -1.1 是打包修订号。文档写成 '1.37.x-*',那个 -* 就是通配这一段的。

失败后再跑一次,提示 kubeadm was already not on hold:第一次的 && 链在 install 失败后就停了,最后的 hold 没执行。

第二步:upgrade plan

$ sudo kubeadm upgrade plan
[upgrade/versions] Cluster version: 1.31.14
[upgrade/versions] kubeadm version: v1.32.13
W1009 ... could not fetch a Kubernetes version from the internet: unable to get URL "https://dl.k8s.io/release/stable.txt": ... tls: handshake failure
W1009 ... falling back to the local client version: v1.32.13

Components that must be upgraded manually after you have upgraded the control plane with 'kubeadm upgrade apply':
COMPONENT   NODE      CURRENT    TARGET
kubelet     ubuntu    v1.31.14   v1.32.13
kubelet     ubuntu1   v1.31.14   v1.32.13
kubelet     ubuntu2   v1.31.14   v1.32.13

COMPONENT                 NODE      CURRENT    TARGET
kube-apiserver            ubuntu    v1.31.14   v1.32.13
kube-controller-manager   ubuntu    v1.31.14   v1.32.13
kube-scheduler            ubuntu    v1.31.14   v1.32.13
kube-proxy                          1.31.14    v1.32.13
CoreDNS                             v1.11.3    v1.11.3
etcd                      ubuntu    3.5.24-0   3.5.24-0

You can now apply the upgrade by executing the following command:

        kubeadm upgrade apply v1.32.13

plan 只检查、不改动,两张表分别是:

表内容说明
第一张三台的 kubeletkubeadm 不管,apply 之后要自己 apt 换
第二张控制平面组件 + addonapply 自动换
  • kube-proxy 的 NODE 列是空的:它是 DaemonSet,更新一次所有节点都生效
  • CoreDNS 和 etcd 这次版本不变,所以文档里升级 etcd 前的注意事项(先停 apiserver 再升级)用不上
  • TLS 警告:plan 会去 dl.k8s.io 查最新版本,我的网络走代理,没连上,就退回用 kubeadm 自己的版本当目标。不影响升级,apply 时明确写出版本号就行

第三步:upgrade apply

sudo kubeadm upgrade apply v1.32.13

问一次 [y/N],输入 y,几分钟后:

[upgrade] SUCCESS! A control plane node of your cluster was upgraded to "v1.32.13".

日志就是 apply 的工作流程:

顺序日志做了什么
1Pulling images先拉新镜像
2etcd → kube-apiserver → kube-controller-manager → kube-scheduler一个一个换,每个都等 kubelet 重启好才换下一个
3Renewing ... certificate顺手续签证书
4backing up old manifest to "/etc/kubernetes/tmp/kubeadm-backup-manifests-..."旧清单自动备份
5Storing the configuration used in ConfigMap "kubeadm-config"更新集群里记录的版本
6Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"kubelet 的配置换成新版,程序还是旧的
7Applied essential addon: CoreDNS / kube-proxy更新 addon

etcd 版本没变,也显示 upgraded successfully:证书续签了,要重启一次才能加载新证书。

还有一条警告:

detected that the sandbox image "" of the container runtime is inconsistent with that used by kubeadm. It is recommended to use "registry.k8s.io/pause:3.10" as the CRI sandbox image.

kubeadm 没读到 containerd 配的 pause 镜像,不影响升级,containerd 会用它自己的默认值。

apply 完,版本对不上:

$ kubectl get nodes
NAME      STATUS   ROLES           AGE   VERSION
ubuntu    Ready    control-plane   41d   v1.31.14
...
$ kubectl version
Client Version: v1.31.14
Server Version: v1.32.13

Server 已经是 1.32.13,VERSION 列还是 1.31.14,因为 kubelet 还没换。这时 API Server 1.32、kubelet 1.31,正是版本规则允许的状态。

第四步:drain

下一步要换 kubelet。新版 kubelet 接手后会把这台节点上的容器全部重启(文档:container spec hash 变了),所以先把能挪走的 Pod 挪到别的节点:

$ kubectl drain ubuntu --ignore-daemonsets
node/ubuntu cordoned
Warning: ignoring DaemonSet-managed Pods: kube-system/calico-node-6cpps, kube-system/kube-proxy-ns9nk
node/ubuntu drained
动作作用
cordon标记 SchedulingDisabled,不再往这里派新 Pod
evict驱逐现有 Pod,让它们去别的节点重建

两类 Pod 挪不走:Static Pod 和节点绑死了,drain 不碰;DaemonSet Pod 每个节点一份,赶走了会立刻再建回来,所以要加 --ignore-daemonsets 跳过。

这次一个 Pod 都没驱逐,输出里没有 evicting pod。ubuntu 上本来就只有这两类:

$ kubectl get pods -A -o wide --field-selector spec.nodeName=ubuntu
NAMESPACE     NAME                             READY   STATUS    RESTARTS      AGE
kube-system   calico-node-6cpps                1/1     Running   8 (14h ago)   41d
kube-system   etcd-ubuntu                      1/1     Running   9 (20m ago)   41d
kube-system   kube-apiserver-ubuntu            1/1     Running   0             19m
kube-system   kube-controller-manager-ubuntu   1/1     Running   0             19m
kube-system   kube-proxy-ns9nk                 1/1     Running   0             18m
kube-system   kube-scheduler-ubuntu            1/1     Running   0             18m

AGE 和 RESTARTS 两列正好反映 apply 做了什么:

Pod变化原因
apiserver / controller-manager / schedulerAGE 归零清单换了新镜像,Pod 被替换
kube-proxyAGE 归零,名字也换了DaemonSet 更新,Pod 被替换
etcdAGE 还是 41d,重启次数 +1清单没实质变化,只是容器重启加载新证书
calico-node没变kubeadm 不管 CNI

第五步:升级 kubelet 和 kubectl

sudo apt-mark unhold kubelet kubectl && \
sudo apt-get update && sudo apt-get install -y kubelet='1.32.13-1.1' kubectl='1.32.13-1.1' && \
sudo apt-mark hold kubelet kubectl
sudo systemctl daemon-reload
sudo systemctl restart kubelet

apt 的输出里有一段:

Restarting services...
 systemctl restart kubelet.service

这是 Ubuntu 自带的 needrestart,发现 kubelet 的程序被换了,自动重启了一次。文档还是写了 daemon-reload + restart:不是每个系统都有 needrestart;新包可能改了 kubelet 的 systemd 配置,要 daemon-reload 才会重新读取。

$ kubectl get nodes
NAME      STATUS                     ROLES           AGE   VERSION
ubuntu    Ready,SchedulingDisabled   control-plane   41d   v1.32.13
ubuntu1   Ready                      <none>          41d   v1.31.14
ubuntu2   Ready                      <none>          41d   v1.31.14

第六步:uncordon

$ kubectl uncordon ubuntu
node/ubuntu uncordoned

再看 ubuntu 上的 Pod,4 个 Static Pod 的 AGE 全部变成了 96s:新 kubelet 接手后把它们重建了一遍。这就是 drain 的原因。

Worker:ubuntu1、ubuntu2

和控制平面的区别:

控制平面worker
先装什么kubeadmkubeadm + kubectl
kubeadm 命令upgrade applyupgrade node
drain / uncordon本机回控制平面执行
后装什么kubelet + kubectlkubelet

一台升完、确认 Ready,再升下一台。两台同时 drain,Pod 就没地方去了。

#在哪做什么
1worker换源,装 kubeadm 和 kubectl 1.32.13-1.1
2workersudo kubeadm upgrade node
3ubuntukubectl drain <节点> --ignore-daemonsets
4worker装 kubelet 1.32.13-1.1,daemon-reload,restart kubelet
5ubuntukubectl uncordon <节点>

upgrade node 在 worker 上几乎全是跳过,只做一件事:

[upgrade/preflight] Skipping prepull. Not a control plane node.
[upgrade/control-plane] Skipping phase. Not a control plane node.
[upgrade/kubeconfig] Skipping phase. Not a control plane node.
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[upgrade/kubelet-config] The kubelet configuration for this node was successfully upgraded!
[upgrade/addon] Skipping the addon/coredns phase. Not a control plane node.
[upgrade/addon] Skipping the addon/kube-proxy phase. Not a control plane node.

它从集群拉 kubelet 的配置写到本地。新配置来自 ConfigMap kubelet-config,apply 时已经更新过了。

踩坑一:在 worker 上 drain

yustia@ubuntu1:~$ kubectl drain ubuntu1 --ignore-daemonsets
E1009 ... couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?

worker 上没有 ~/.kube/config:join 时只拿到 kubelet 自己用的凭证,没有管理员的 kubeconfig。kubectl 找不到配置文件,就去连默认地址 localhost:8080,那里什么都没有。

看到 localhost:8080,就是 kubectl 没找到 kubeconfig,先看自己在哪台机器、用的哪个用户。文档里 drain 和 uncordon 的注释都写着 execute this command on a control plane node。

踩坑二:emptyDir 挡住 drain

$ kubectl drain ubuntu2 --ignore-daemonsets
node/ubuntu2 cordoned
error: unable to drain node "ubuntu2" due to error: cannot delete Pods with local storage (use --delete-emptydir-data to override): kube-system/metrics-server-75969676f4-xbfws, continuing command...

emptyDir 的数据存在节点本地,Pod 一删就没了。drain 不知道数据重不重要,就停下来等你确认。metrics-server 的 emptyDir 里只有启动时生成的临时文件,在别的节点重建时会重新生成,可以删:

kubectl drain ubuntu2 --ignore-daemonsets --delete-emptydir-data

判断标准还是能不能重新生成:缓存和临时文件可以删;有人把真实数据放进 emptyDir,就不该加这个参数,那个 Pod 应该改用 PV。

注意第一行的 node/ubuntu2 cordoned:cordon 做成了,驱逐失败了,节点处在不接新 Pod、旧 Pod 还在跑的状态。

踩坑三:重启了 kubelet,版本还是旧的

ubuntu2 上跑了 upgrade node,装了 kubeadm 和 kubectl,重启了 kubelet,uncordon 之后:

NAME      STATUS   ROLES           AGE   VERSION
ubuntu    Ready    control-plane   41d   v1.32.13
ubuntu1   Ready    <none>          41d   v1.32.13
ubuntu2   Ready    <none>          41d   v1.31.14

漏了装 kubelet 那一步。upgrade node 只换配置,不换程序,重启的还是 1.31.14 的 kubelet。重新 drain、装 kubelet、重启、uncordon,三台就都是 v1.32.13 了:

NAME      STATUS   ROLES           AGE   VERSION
ubuntu    Ready    control-plane   41d   v1.32.13
ubuntu1   Ready    <none>          41d   v1.32.13
ubuntu2   Ready    <none>          41d   v1.32.13

kubeadm upgrade node 在 ubuntu2 上跑了两次,结果都一样:kubeadm 升级命令重复跑是安全的。

升级失败怎么办

这次没失败,但文档 Recovering from a failure state 那一节值得知道。apply 跑到一半断电、断网,控制平面卡在一半新一半旧:

  1. 再跑一次 kubeadm upgrade apply:它会对比现状和目标,没做完的补上
  2. kubeadm upgrade apply --force:不改版本,强制再走一遍
  3. 手动从备份恢复:kubeadm 升级时自动备份到 /etc/kubernetes/tmp/
$ sudo ls /etc/kubernetes/tmp/
kubeadm-backup-etcd-2026-10-09-21-27-33       kubeadm-kubelet-config855925081
kubeadm-backup-manifests-2026-10-09-21-27-33
目录恢复到
kubeadm-backup-etcd-*/var/lib/etcd
kubeadm-backup-manifests-*/etc/kubernetes/manifests
kubeadm-kubelet-config*/var/lib/kubelet/config.yaml

这个目录不会自动清理,确认集群正常后要自己删。

小结:集群升级做了什么

#步骤说明
1备份etcd 快照、证书、kubeadm-config
2换源每台节点都要换,每个小版本一个仓库
3控制平面:kubeadm → plan → apply换 Static Pod 和 addon
4控制平面:drain → kubelet + kubectl → uncordon换 kubelet 程序
5worker 逐台:kubeadm + kubectl → upgrade node → drain → kubelet → uncordondrain 和 uncordon 回控制平面执行
6检查kubectl get nodes 三台都是新版本

命令速查表

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>看容器的挂载等详情

升级相关,版本号以 1.32.13 为例:

命令用途
sudo sed -i 's/v1.31/v1.32/' /etc/apt/sources.list.d/kubernetes.list换 apt 源到新的小版本
sudo apt-cache madison kubeadm列出可装的版本
sudo apt-mark unhold kubeadm / hold kubeadm解锁 / 锁住包版本
sudo apt-get install -y kubeadm='1.32.13-1.1'装指定版本,版本号要带 -1.1
sudo kubeadm upgrade plan检查能不能升、会换什么
sudo kubeadm upgrade apply v1.32.13升级第一台控制平面
sudo kubeadm upgrade node升级其他节点(worker 上只更新 kubelet 配置)
kubectl drain <节点> --ignore-daemonsets腾空节点;有 emptyDir 再加 --delete-emptydir-data
kubectl uncordon <节点>恢复调度
sudo systemctl daemon-reload && sudo systemctl restart kubelet换完 kubelet 后重启
kubectl version看 API Server 版本(Server Version)
kubectl get pods -A -o wide --field-selector spec.nodeName=<节点>看某个节点上跑着哪些 Pod

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