1. 项目概述:什么是“二进制平滑升级Kubernetes集群”?
“二进制平滑升级Kubernetes集群”——这八个字,是过去两年在某大型云原生运维团队内部高频出现的实战术语,不是PPT里的概念包装,而是每天凌晨三点值班时反复确认的 checklist。它指的是一种不中断业务、不重建节点、不触发Pod驱逐风暴的集群版本演进方式:直接替换控制平面(kube-apiserver、etcd、kube-controller-manager等)和工作节点(kubelet、kube-proxy)的二进制可执行文件,配合配置热重载与状态校验,在分钟级内完成从 v1.24 到 v1.26 或 v1.27 的跨小版本升级。它不依赖于托管服务的“一键升级”按钮,也不走 kubeadm upgrade 那套默认流程——后者在生产环境常因 etcd 版本兼容性、证书过期、CRD schema 变更等问题卡在 preflight 检查阶段。我们真正用起来的,是一套基于二进制文件原子替换 + 进程优雅重启 + 状态双轨校验的轻量级升级机制。关键词“二进制”强调操作粒度——你面对的是 /usr/bin/kube-apiserver 这个文件本身,而不是抽象的“组件”;“平滑”不是形容词,而是可量化的 SLA:API Server 不可用时间 < 30 秒,Pod 重启率 < 0.5%,StatefulSet Pod 保持 PVC 绑定不漂移。适合谁?不是刚学完 kubectl get nodes 的新手,而是已经独立维护过 50+ 节点集群、能看懂 kubelet --help 输出中 --dynamic-config-dir 和 --feature-gates 含义的中级以上 SRE 或平台工程师。如果你还在为一次升级要提前一周发变更单、协调业务方停服窗口、备份三套 etcd 快照而头疼,那这个方案就是你该抄的作业。
2. 整体设计思路与方案选型逻辑
2.1 为什么放弃 kubeadm upgrade?——来自三次失败升级的教训
kubeadm upgrade 是官方推荐路径,但我们在某次 v1.25.6 → v1.26.3 升级中连续三次失败,根本原因不是命令写错,而是它把“安全”预设得过于理想化。第一次失败在 etcd 升级环节:kubeadm 尝试将 etcd 从 3.5.4 升到 3.5.9,但集群中已有两个节点运行着 3.5.7 的 etcd 镜像(因之前手动 patch 过),kubeadm 检测到版本不一致直接 abort,报错信息只有一行 “etcd version mismatch”,没告诉你哪台节点版本异常。第二次失败在证书续期:kubeadm upgrade apply 默认会轮换所有证书,但我们的 CA 证书是外部签发的,私钥不在集群内,强制续期导致 apiserver TLS 握手失败,整个集群 API 不可用达 18 分钟。第三次失败最典型——CRD 兼容性问题:v1.26 引入了新的 admissionregistration.k8s.io/v1 API 组,而我们一个自研 Operator 的 CRD 文件仍使用 v1beta1,kubeadm 在 apply 前不做 schema 兼容性扫描,直到 controller-manager 启动后报错 “failed to convert CRD to v1”,此时升级已进行到一半,回滚成本极高。这三次失败让我们彻底转向二进制升级:它不封装逻辑,不自动决策,把每个动作暴露给你——你决定什么时候停 kube-apiserver,你确认 etcd 数据目录权限是否正确,你手动验证 CRD 是否已迁移。控制权回到人手里,而不是交给一个黑盒脚本。
2.2 为什么不选托管服务或发行版升级?——成本、可控性与学习曲线的三角权衡
有同事提议直接迁移到某公有云的托管 Kubernetes 服务,理由是“他们负责升级”。但实际评估发现三个硬伤:第一,网络延迟不可控。我们核心业务要求 API Server P99 延迟 < 80ms,而托管服务的 control plane endpoint 解析到公网 IP,实测平均增加 42ms RTT;第二,定制能力归零。我们需要在 kube-scheduler 中启用 custom plugin,而托管服务只开放有限的 feature-gates 配置;第三,成本翻倍。同等规模集群,托管服务月费是自建的 2.3 倍,且无法复用现有物理机资源。至于发行版如 RKE2 或 OpenShift,它们确实提供升级命令,但底层仍是封装好的二进制替换逻辑。我们试过 RKE2 的 rke2 server --version 升级,它在后台静默拉取新二进制、重启进程,但当某次升级后 kubelet 日志出现 “failed to list nodes: context deadline exceeded” 时,我们花了 3 小时才定位到是 rke2 自动注入的 cni-plugin 配置与我们自定义的 Calico 配置冲突——这种黑盒行为放大了排障难度。二进制升级的哲学是:你升级的不是 Kubernetes,而是你对 Kubernetes 的理解深度。当你亲手 touch /etc/kubernetes/manifests/kube-apiserver.yaml,修改 image 字段并 watch 它被 static pod controller 重新拉起,你才真正明白 control plane 的生命周期管理机制。
2.3 二进制升级的核心设计原则:三不一稳
我们最终确立四条铁律,贯穿整个方案设计:
不重建节点(No Node Rebuild):拒绝任何需要 drain node → reimage OS → reinstall kubelet 的方案。节点 OS 补丁、内核升级、容器运行时更新,全部与 Kubernetes 升级解耦。kubelet 二进制升级后,通过 --dynamic-config-dir 指向的 KubeletConfiguration 对象实现配置热更新,无需重启进程。
不中断 API(No API Downtime):利用 Kubernetes 多副本 control plane 特性。我们始终部署 3 个 master 节点,升级时严格按顺序:先停第 1 个节点的 kube-apiserver(通过 systemctl stop kube-apiserver),等待其完全退出后,再替换二进制并启动;确认该节点健康后(curl -k https://localhost:6443/healthz 返回 ok),再操作第 2 个节点。整个过程,其余两个 apiserver 实例持续提供服务,VIP(keepalived)自动剔除故障实例,业务无感。
不触发驱逐(No Pod Eviction Storm):关键在于 kubelet 升级策略。我们禁用 --eviction-hard 参数的默认值(memory.available<100Mi),改用 --eviction-minimum-reclaim="memory.available=500Mi",并在升级前手动设置节点为 unschedulable(kubectl cordon node-x),但不 drain。这样,现有 Pod 继续运行,新调度被阻止,kubelet 进程重启后,通过 --bootstrap-kubeconfig 加载初始凭证,自动向 apiserver 重新注册,所有 Pod 状态保持 Running,只是 phase 从 Running 变为 Unknown 再变回 Running,整个过程约 8~12 秒,远低于 Pod 的 liveness probe timeout。
状态稳态校验(Steady-State Verification):这是平滑的终极保障。我们不依赖 “kubectl get nodes” 显示 Ready 就认为成功。而是构建一套校验清单:etcd member list 是否全为 started 状态;kube-scheduler 和 kube-controller-manager 的 leader election 注解是否指向当前节点;所有 DaemonSet 的 desiredScheduled == currentScheduled;特别是,检查 kube-system namespace 下所有 Pod 的 restartCount,确保没有因配置错误导致的无限重启循环。这套校验脚本在每次节点升级后自动执行,只有全部通过才允许升级下一个节点。
3. 核心细节解析与实操要点
3.1 升级前必须完成的五项静态检查
二进制升级的成败,70% 取决于升级前的准备。我们固化了一个 pre-upgrade.sh 脚本,强制执行以下五项检查,任何一项失败即中止:
etcd 版本兼容性矩阵校验:Kubernetes 官方文档明确标注了各版本支持的 etcd 最小/最大版本。例如 v1.26.x 要求 etcd >= 3.5.4 且 <= 3.5.12。我们用 etcdctl version 获取当前版本,再查表比对。曾有一次,集群 etcd 是 3.5.13(因手动升级过),而 v1.26.5 的 release note 明确说 “etcd 3.5.13 is not tested”,我们立即回退 etcd 到 3.5.12,避免踩坑。
证书有效期扫描:执行 openssl x509 -in /etc/kubernetes/pki/apiserver.crt -noout -dates,提取 Not After 时间。如果距离当前时间不足 30 天,必须先 renew 证书。我们遇到过因忽略此步,升级后 kubelet 因 client certificate expired 无法连接 apiserver,所有节点显示 NotReady。
CRD Schema 兼容性预检:使用 kubectl convert 命令模拟转换。例如,对所有 v1beta1 的 CRD,运行 kubectl convert -f crd.yaml --output-version apiextensions.k8s.io/v1。若报错 “error: unable to convert... no kind 'CustomResourceDefinition' is registered for version 'apiextensions.k8s.io/v1beta1'”,说明该 CRD 已被移除,必须手动迁移到 v1。我们有个监控 CRD,v1.26 中 deprecated,必须先用 kubectl replace 替换为 v1 版本,再升级。
Feature Gates 启用状态审计:检查 /var/lib/kubelet/config.yaml 和 /etc/kubernetes/manifests/*.yaml 中的 --feature-gates 参数。v1.26 移除了诸如 CSIMigrationAzureFile、LegacyNodeRoleBehavior 等 gates。如果集群中启用了这些已废弃的 gate,升级后组件会 panic 退出。我们编写了一个 Python 脚本,遍历所有 manifest 文件,grep --feature-gates 并解析键值对,生成待清理列表。
节点磁盘空间与 inode 预留:kubelet 升级后会生成新的 containerd snapshot,需额外 2~3GB 空间。执行 df -h /var/lib/containerd 和 df -i /var/lib/containerd。曾有一次,/var/lib/containerd 的 inode 使用率达 98%,升级后 containerd 无法创建新 snapshot,Pod 启动失败,报错 “no space left on device” —— 实际是 inode 耗尽。我们此后在所有节点部署了 inotifywait 监控 /var/lib/containerd/snapshots 目录的 inode 变化。
提示:这五项检查必须在所有 master 和 worker 节点上并行执行,并汇总结果。我们用 Ansible 的 serial: 10 控制并发数,避免对 apiserver 造成压力。
3.2 二进制文件获取与校验的实操细节
获取正确的二进制是第一步,也是最容易出错的一步。我们绝不从 GitHub Release 页面直接下载 tar.gz,因为:
- 官方 release 包含所有平台(linux/amd64, linux/arm64, darwin 等),体积大,下载慢;
- 某些版本存在 build timestamp 不一致问题,导致相同版本号的二进制 md5 不同;
- 缺少针对特定 OS 的 patch(如 CentOS 7 需要 glibc 2.17+,而某些二进制链接了 2.18)。
我们的标准流程是:
从 Kubernetes 官方镜像仓库拉取:访问 https://registry.k8s.io/v2/,找到对应版本的 kube-apiserver-amd64 镜像。例如 v1.26.5 的镜像 digest 是 sha256:7a1b2e...。我们用 skopeo copy docker://registry.k8s.io/kube-apiserver-amd64:v1.26.5 dir:/tmp/kube-apiserver-v1.26.5 提取镜像层。
解压并提取二进制:进入 /tmp/kube-apiserver-v1.26.5,找到 layer.tar,用 tar -xf layer.tar 解压,二进制文件在 /bin/kube-apiserver。将其复制到 /tmp/kube-apiserver-v1.26.5-bin。
强制静态链接与 strip:执行 file /tmp/kube-apiserver-v1.26.5-bin,确认输出包含 “statically linked”。若非静态,则用 patchelf --set-rpath '' /tmp/kube-apiserver-v1.26.5-bin 强制去除动态库依赖。再执行 strip /tmp/kube-apiserver-v1.26.5-bin 减小体积。
多维度校验:
- md5 校验:对比官方 release 页面公布的 md5sum(注意:不是 github 的 checksums.txt,而是 kubernetes.io/docs/setup/release/notes/ 中的 Binary Checksums 表格);
- 签名验证:下载官方 GPG key(https://dl.k8s.io/release/$(curl -s https://dl.k8s.io/release/stable.txt)/kubernetes-release-archives.asc),用 gpg --verify kubernetes-server-linux-amd64.tar.gz.sig kubernetes-server-linux-amd64.tar.gz;
- 运行时校验:在测试节点执行 /tmp/kube-apiserver-v1.26.5-bin --version,确认输出为 Kubernetes v1.26.5,且无 panic 或 segfault。
注意:我们严禁在生产节点上直接运行未校验的二进制。所有校验必须在隔离的测试环境完成,并生成一份 signed checksum report,由两名 SRE 签字确认后,才允许分发。
3.3 控制平面组件升级的精确时序与信号捕获
master 节点升级是心脏手术,必须精确到秒。我们为每个组件定义了升级时序图(非 mermaid,纯文字描述):
kube-apiserver:这是唯一不能容忍中断的组件。我们采用滚动重启:
- systemctl stop kube-apiserver(等待进程完全退出,ps aux | grep apiserver 应无结果);
- cp /tmp/kube-apiserver-v1.26.5-bin /usr/bin/kube-apiserver;
- systemctl start kube-apiserver;
- 关键等待:执行 while ! curl -k https://localhost:6443/healthz; do sleep 1; done,直到返回 "ok";
- 状态确认:kubectl get componentstatuses(虽已 deprecated,但仍是快速判断 apiserver 是否 ready 的手段),确保 etcd、scheduler、controller-manager 状态为 Healthy。
etcd:升级 etcd 是风险最高环节。我们坚持“先备份,再升级,后验证”:
- 执行 ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key snapshot save /backup/etcd-pre-upgrade-$(date +%Y%m%d-%H%M%S).db;
- 停止 etcd:systemctl stop etcd;
- 替换二进制:cp /tmp/etcd-v3.5.12/etcd* /usr/local/bin/;
- 关键检查:etcd --version 必须输出 3.5.12,且 etcd --help | grep "enable-grpc-gateway" 存在(v3.5+ 新增);
- 启动 etcd:systemctl start etcd;
- 成员状态验证:ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 --cacert=... member list,确认所有成员 status 为 started,且 clientURLs 正确。
kube-controller-manager 与 kube-scheduler:这两个组件升级最简单,因为它们是 leader-elected,单实例宕机不影响全局。我们甚至可以同时升级所有 master 节点的这两个组件,只要确保至少一个实例在线。升级后,通过 kubectl get endpoints kube-controller-manager -n kube-system 查看 endpoints 的 ip 地址,确认 leader 已切换到新版本节点。
实操心得:我们给每个组件的 systemd service 文件增加了 RestartSec=10 和 StartLimitIntervalSec=600,防止因配置错误导致的快速重启循环。同时,在 /etc/systemd/system/kube-apiserver.service 中添加 ExecStartPre=/usr/local/bin/pre-upgrade-check.sh,强制每次启动前执行基础检查。
4. 实操过程与核心环节实现
4.1 完整升级流程:从准备到验证的 12 个关键步骤
我们把一次完整的 v1.25.12 → v1.26.5 升级拆解为 12 个原子步骤,每步都有明确输入、输出和超时阈值。这不是理论流程,而是我们线上集群的真实 runbook:
Step 1:冻结变更
- 输入:运维日历、Jira 变更系统
- 操作:在所有协作工具中发布升级公告,冻结所有非紧急变更;关闭 CI/CD 流水线的自动部署;
- 输出:变更冻结确认邮件;
- 超时:30 分钟。
Step 2:执行 pre-upgrade.sh
- 输入:Ansible inventory 文件(包含所有 master/worker 节点 IP);
- 操作:ansible-playbook pre-upgrade.yml -i inventory.ini;
- 输出:pre-upgrade-report.json,包含五项检查的 pass/fail 结果;
- 超时:15 分钟;若任一 fail,终止流程。
Step 3:备份 etcd 全量快照
- 输入:NFS 存储挂载点 /backup/etcd;
- 操作:在每台 master 节点上并行执行 etcdctl snapshot save;
- 输出:/backup/etcd/etcd-full-20231015-020000.db(带时间戳);
- 超时:20 分钟(取决于数据量,我们集群约 12GB)。
Step 4:备份静态 Pod manifests
- 输入:/etc/kubernetes/manifests/ 目录;
- 操作:tar -czf /backup/manifests-pre-upgrade-$(date +%s).tar.gz /etc/kubernetes/manifests/;
- 输出:压缩包及 md5 校验值;
- 超时:2 分钟。
Step 5:分发新二进制到所有节点
- 输入:校验通过的 /tmp/kube-apiserver-v1.26.5-bin 等文件;
- 操作:用 rsync -avz --delete /tmp/kube-bin/ user@node:/usr/bin/,并设置权限 chmod 755;
- 输出:所有节点 /usr/bin/ 下新二进制存在且可执行;
- 超时:10 分钟。
Step 6:升级第一个 master 节点的 kube-apiserver
- 输入:节点 hostname(如 master-01);
- 操作:ssh master-01 "systemctl stop kube-apiserver && cp /usr/bin/kube-apiserver-v1.26.5-bin /usr/bin/kube-apiserver && systemctl start kube-apiserver";
- 输出:curl -k https://master-01:6443/healthz 返回 ok;
- 超时:5 分钟。
Step 7:验证第一个 master 节点状态
- 输入:kubectl config 指向该节点 VIP;
- 操作:执行 kubectl get nodes, kubectl get cs, kubectl get pods -A;
- 输出:所有资源列表正常,无 CrashLoopBackOff;
- 超时:3 分钟。
Step 8:升级剩余 master 节点(滚动)
- 输入:master-02, master-03;
- 操作:重复 Step 6-7,但每次只操作一台,间隔 2 分钟;
- 输出:所有 master 节点 kube-apiserver 版本为 v1.26.5;
- 超时:15 分钟。
Step 9:升级 etcd 集群(逐个成员)
- 输入:etcd member list 输出;
- 操作:对每个 member,执行 stop → replace binary → start → etcdctl member list 验证;
- 输出:etcdctl endpoint health 返回 healthy;
- 超时:30 分钟(etcd 启动较慢)。
Step 10:升级所有 worker 节点 kubelet
- 输入:worker 节点列表;
- 操作:对每个节点,执行 kubectl cordon node-x → systemctl stop kubelet → cp new-kubelet → systemctl start kubelet → kubectl uncordon node-x;
- 输出:所有节点 status 为 Ready,且 kubelet version 为 v1.26.5;
- 超时:45 分钟(50 节点集群)。
Step 11:执行 post-upgrade 校验
- 输入:post-upgrade-check.sh 脚本;
- 操作:运行脚本,检查 etcd member status、leader election、DaemonSet 调度、Pod restartCount;
- 输出:post-upgrade-report.html,绿色 PASS 或红色 FAIL;
- 超时:10 分钟。
Step 12:解冻变更与监控观察
- 输入:Prometheus Grafana dashboard;
- 操作:恢复 CI/CD;观察 24 小时内 API Latency、etcd backend commit duration、Pod startup time 指标;
- 输出:升级成功报告,包含关键指标基线对比图;
- 超时:24 小时(观察期)。
提示:我们为每个 Step 编写了对应的 Bash 函数,封装在 upgrade-lib.sh 中,例如 function upgrade_apiserver() { ... }。整个流程可通过 ./run-upgrade.sh --from v1.25.12 --to v1.26.5 自动驱动,但所有关键步骤(如 etcd backup、cordon node)都 require human confirmation,输入 yes 才继续。
4.2 配置文件热更新的关键技巧:KubeletConfiguration 的落地实践
kubelet 升级后,旧配置可能不兼容新版本。例如 v1.26 移除了 --cadvisor-port,新增了 --container-runtime-endpoint。我们不再编辑 /var/lib/kubelet/kubeadm-flags.env,而是全面转向 KubeletConfiguration API:
生成初始配置:在升级前,用 kubeadm init phase upload-config kubelet --config kubeadm-config.yaml 生成 base config,保存为 /var/lib/kubelet/config.yaml。
启用动态配置:在所有 worker 节点的 /var/lib/kubelet/kubeadm-flags.env 中添加 --dynamic-config-dir=/var/lib/kubelet/dynamic-config。
创建 ConfigMap:将修改后的 KubeletConfiguration 写入 ConfigMap:
cat > kubelet-config-1.26.yaml << 'EOF' apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration serverTLSBootstrap: true rotateCertificates: true cgroupDriver: systemd containerRuntimeEndpoint: unix:///run/containerd/containerd.sock EOF kubectl create configmap kubelet-config-1.26 --from-file=kubelet-config-1.26.yaml -n kube-system- 触发热重载:kubelet 会自动监听 /var/lib/kubelet/dynamic-config 目录,当 ConfigMap 挂载后,它会在 30 秒内 reload 配置。我们通过 journalctl -u kubelet -n 100 | grep "Loaded configuration from" 确认。
实操心得:我们发现 --dynamic-config-dir 在 v1.25+ 才稳定支持。早期版本(v1.23)存在 race condition,kubelet 可能在 configmap 挂载前就启动,导致配置不生效。因此,我们强制在升级脚本中加入等待逻辑:while ! ls /var/lib/kubelet/dynamic-config/* 2>/dev/null; do sleep 2; done。
4.3 网络插件与 CNI 配置的平滑过渡方案
CNI 插件(如 Calico、Cilium)是升级中最易被忽视的雷区。v1.26 对 Pod CIDR 分配逻辑做了调整,如果 CNI 插件版本过旧,可能导致新 Pod 无法分配 IP。
我们的处理方案是:CNI 升级与 Kubernetes 升级解耦,但时间上紧邻。
Calico 升级:我们使用 tigera-operator 方式部署。升级时,先更新 operator 的 Deployment 镜像为 v3.25.0(对应 k8s v1.26),operator 会自动 rollout calico-node DaemonSet。关键点是:operator 的升级 yaml 中,spec.install.imagePullPolicy 设为 IfNotPresent,避免升级时拉取镜像阻塞。
Cilium 升级:使用 helm upgrade。我们预先准备好 helm values.yaml,其中 cilium.operator.replicas = 2(保证高可用),且启用 --set upgradeCompatibility=v1.26。Cilium 的 upgradeCompatibility 参数会自动处理 v1.26 的 API 变更,如 NetworkPolicy 的 policyTypes 字段校验。
验证方法:在升级后,创建一个 test-nginx Pod,然后 exec 进入,ping 集群内其他 Pod 的 ClusterIP。如果通,则 CNI 工作正常;如果不通,立即检查 cilium status 或 calicoctl node status。
注意:我们严禁在升级过程中修改 CNI 的 IP Pool 或 ASN 配置。所有网络拓扑变更必须在升级前或升级后 48 小时外进行。
5. 常见问题与排查技巧实录
5.1 典型问题速查表:从现象到根因的 7 个高频场景
| 现象 | 可能根因 | 排查命令 | 解决方案 |
|---|---|---|---|
| kubectl get nodes 返回 No resources found | kube-apiserver 未启动或 TLS 证书错误 | systemctl status kube-apiserver; journalctl -u kube-apiserver -n 50 | 检查 /etc/kubernetes/pki/apiserver.crt 是否过期;确认 --advertise-address 与 --bind-address 一致 |
| etcd member list 显示某个 member 为 unstarted | etcd 数据目录权限错误或磁盘满 | ls -ld /var/lib/etcd; df -h /var/lib/etcd | chown -R etcd:etcd /var/lib/etcd;清理磁盘后,执行 etcdctl member remove 再 add |
| kubelet 启动后,节点状态为 NotReady | CNI 插件未就绪或 kubelet 无法连接 apiserver | journalctl -u kubelet -n 100 | grep -E "(notready|failed)" | 检查 /etc/cni/net.d/ 下是否有 valid config;确认 kubeconfig 文件路径正确 |
| Pod 一直处于 ContainerCreating 状态 | containerd snapshotter 初始化失败 | crictl ps -a; ctr -n k8s.io containers list | ctr -n k8s.io i pull registry.k8s.io/pause:3.9;重启 containerd |
| kube-scheduler 报错 "leaderelection lost" | 多个 scheduler 实例同时认为自己是 leader | kubectl get endpoints kube-scheduler -n kube-system -o wide | 检查所有 master 节点的 kube-scheduler manifest,确认 --leader-elect=true 且 --lock-object-name 唯一 |
| 升级后,Ingress Controller 503 错误 | Ingress Controller 的 ServiceAccount token 无效 | kubectl describe pod -n ingress-nginx nginx-ingress-controller | 删除 /var/run/secrets/kubernetes.io/serviceaccount/token 文件,重启 pod |
| metrics-server 无法收集指标,报 "x509: certificate signed by unknown authority" | metrics-server 的 CA 证书未更新 | kubectl get secret -n kube-system metrics-server-certs -o yaml | 重新生成 certs,或更新 metrics-server deployment 的 --kubelet-insecure-tls 参数 |
5.2 一次真实故障的完整复盘:etcd 启动卡在 "waiting for initial advertise peer URLs"
这是我们在 v1.25 → v1.26 升级中遇到的最棘手问题。现象是:etcd 进程启动后,journalctl 日志停在 “waiting for initial advertise peer URLs”,不再前进,且 etcdctl endpoint health 无响应。
排查过程:
- 第一步:确认配置文件。检查 /etc/etcd/etcd.conf,advertise-peer-urls 设置为 https://10.0.1.101:2380,与节点实际 IP 一致。
- 第二步:检查端口占用。netstat -tuln | grep 2380,发现端口空闲。
- 第三步:检查证书。openssl x509 -in /etc/kubernetes/pki/etcd/peer.crt -text | grep "IP Address",发现证书中只包含了 127.0.0.1,未包含节点 IP 10.0.1.101!
- 根因定位:原来,我们集群的 etcd 证书是用 kubeadm init 时自动生成的,当时 kubeadm-config.yaml 中的 localAPIEndpoint.advertiseAddress 写错了,导致所有 etcd peer 证书都缺少节点 IP SAN。
解决方案:
- 生成新的 etcd peer 证书:用 cfssl 工具,基于原有 CA,为每个节点生成包含其 IP 的 peer.crt;
- 替换证书:cp new-peer.crt /etc/kubernetes/pki/etcd/peer.crt;cp new-peer.key /etc/kubernetes/pki/etcd/peer.key;
- 重启 etcd:systemctl restart etcd;
- 验证:etcdctl member list 显示所有成员为 started。
教训:etcd 证书的 SAN 列表必须包含所有可能的 advertise-peer-urls,这是二进制升级前 pre-check 的第六项,我们后来把它加进了 pre-upgrade.sh。
5.3 避坑经验:那些文档里不会写的 5 条血泪教训
不要相信 “stable” 标签:kubeadm config images list --kubernetes-version stable 输出的版本,可能是 v1.26.0,但 v1.26.0 有已知 bug(如 kube-proxy iptables 规则丢失)。我们坚持用 kubectl version --short 获取当前集群版本,再查 Kubernetes Changelog,选择最新 patch 版本(如 v1.26.5)。
etcd 快照备份必须离线存储:我们曾把 etcd 快照备份到同一台 NFS 服务器,结果 NFS 服务器宕机,备份全丢。现在,快照同时推送到对象存储(S3 兼容)和本地另一台独立服务器。
kubelet 的 --root-dir 参数不能改:v1.26 文档说可以修改 --root-dir,但我们实测,如果从 /var/lib/kubelet 改到 /data/kubelet,会导致 containerd 无法找到 snapshot,Pod 启动失败。坚持用默认路径。
升级后立即删除旧二进制:/usr/bin/ 下保留 kube-apiserver-v1.25.12 和 kube-apiserver-v1.26.5 两个文件,看似方便回滚,实则危险。systemd 服务文件中 hardcode 了 /usr/bin/kube-apiserver,如果误操作 cp v1.25.12 over v1.26.5,灾难发生。我们升级后立即 rm /usr/bin/kube-apiserver-v1.25.12。
监控告警必须临时静音:升级期间,大量指标(如 kube_node_status_phase、etcd_disk_wal_fsync_duration_seconds)会触发告警。我们提前在 Prometheus Alertmanager 中创建一个 maintenance silence,覆盖所有相关告警,升级完成后手动解除。
6. 后续扩展与能力延伸
二进制平滑升级不是终点,而是平台工程能力的起点。我们正基于此构建三层延伸能力:
第一层:自动化编排:将上述 12 步流程封装为 GitOps 工作流。当 Argo CD 检测到 kubernetes-version ConfigMap 更新时,自动触发升级流水线,每个 Step 作为独立 Job 运行,失败时自动暂停并通知 Slack channel。
第二层:灰度升级:不再全量升级,而是按节点标签(node-role.kubernetes.io/upgrade=canary)选择 5% 的 worker 节点作为灰度组。灰度组升级后,运行 2 小时混沌测试(如随机 kill Pod、网络延迟注入),全部通过才放行全量。
第三层:版本治理:建立集群版本矩阵。定义 “LTS 版本”(如 v1.24、v1.26)和 “Current 版本”(如 v1.27),所有新集群默认部署 LTS,老集群每季度评估是否升级到新 LTS。避免陷入 “永远追最新版” 的陷阱。
我个人在实际操作中的体会是:二进制升级教会我的,不是如何敲命令,而是如何阅读 Kubernetes 的源码注释。比如,当你看到 kube-apiserver 的 --etcd-servers 参数,去翻它的 cmd/kube-apiserver/app/server.go,会发现它最终调用 etcd/client/v3.New 客户端,而这个客户端的 dial timeout 是 3 秒——这就