跳到主要内容

问题定位与实战经验

混合云项目最容易失败的地方不在某一条命令,而在层与层之间的假设。排障时不要直接猜 Cilium 或 Terraform,先确认问题停在哪一层。

分层排障模型

案例 1:Terraform plan 通过但 apply 失败

项目内容
现象terraform plan 能生成计划,apply 时云 API 报权限、库存或配额错误
定位命令make aliyun-planmake tencent-plan、云控制台资源创建记录
判断依据plan 主要验证配置结构和一部分读 API,创建类权限与库存要到 apply 才暴露
修复动作检查 RAM/CAM 权限、实例规格库存、地域可用区、账号配额,必要时换规格或可用区

经验:不要把 plan 通过当作“部署一定成功”。IaC 的静态计划和云厂商运行时约束是两类风险,需要分别验证。

案例 2:节点创建成功但 SSH 不通

项目内容
现象控制台看到 ECS/CVM 已运行,但本地无法 SSH
定位命令terraform output、云控制台安全组、ssh -vvv <user>@<ip>
判断依据公网 IP、默认用户、SSH key、安全组来源 CIDR 任一不匹配都会失败
修复动作确认 ssh_user 与镜像一致,admin_cidrs 包含当前公网出口,公钥路径正确

经验:先确认“身份、入口、路由、安全组”四件事,不要直接重建节点。

案例 3:WireGuard underlay 不通

项目内容
现象节点能 SSH,但跨云内网地址不通
定位命令make check-underlay、节点上的 WireGuard 状态检查、云安全组规则
判断依据UDP 51820 是否允许、peer 地址是否对应、路由是否指向 WireGuard 接口
修复动作修正安全组来源、重新渲染配置、重启 WireGuard,再逐节点验证

经验:underlay 不通时不要先看 Cilium。Cilium 依赖底层节点连通,底层没通,上层只会产生噪声。

案例 4:Kubernetes 节点 NotReady

项目内容
现象kubeadm 初始化后节点没有 Ready
定位命令make check-clusterskubectl get nodes -o widejournalctl -u kubelet
判断依据kubelet、containerd、CNI 配置、Pod CIDR 任一异常都会影响 Ready
修复动作先看 kubelet 日志,再确认 Cilium 是否安装成功,最后检查 kubeadm 配置与 CIDR

经验:NotReady 是结果,不是根因。先查 kubelet,再查 CNI,再查配置输入。

案例 5:Cluster Mesh 没有 connected

项目内容
现象两个集群各自健康,但 cilium clustermesh status --wait 卡住
定位命令make check-clustermesh、两侧 L4 LB、NodePort 32379、Cilium CLI
判断依据Cluster ID、NodePort、LB 监听、安全组、跨云访问路径都必须匹配
修复动作从本集群访问对端 LB/NodePort,确认安全组允许,再检查 Cluster Mesh 配置

经验:跨集群失败时要区分“集群内 Cilium 健康”和“跨集群 API 可达”。

案例 6:state 为空但云上可能有旧资源

项目内容
现象terraform state list 为空,make aliyun-planmake tencent-plan 显示全量创建,但 generated/*/terraform-output.json 里仍有旧 IP
定位命令terraform -chdir=aliyun state listterraform -chdir=tencent state listmake aliyun-planmake tencent-plan
判断依据state、云上真实资源、generated/*/terraform-output.json 三者不一致时,不能把旧 output 当作当前事实
修复动作先决定恢复 state、销毁旧资源后重建,或接受重新创建;确认重建后再执行 make aliyun-applymake tencent-apply 并重新导出 output

经验:state 为空时不要直接跑后续 Ansible。先让 Terraform 输出重新代表真实云资源,再生成 inventory。

案例 7:kubeadm init 卡在 API Server 初始化

项目内容
现象make ansible-kubeadmkubeadm init --upload-certs 超时,kubelet 日志显示拉取 registry.k8s.io/pause 失败
定位命令journalctl -u kubeletsystemctl status containerdgrep -n "sandbox" /etc/containerd/config.toml
判断依据containerd 2.x 使用 plugins.'io.containerd.cri.v1.images'.pinned_images.sandbox,旧的 sandbox_image 替换规则不会生效
修复动作同时覆盖 containerd 1.x 的 sandbox_image 和 containerd 2.x 的 sandbox,再重新运行 make ansible-bootstrap

经验:kubeadm 超时不一定是 kubeadm 本身的问题。先看静态 Pod 是否被 containerd 拉起,再看 pause 镜像和 containerd 配置格式。

案例 8:合并 kubeconfig 后某个集群认证失败

项目内容
现象单独使用 generated/<cloud>/kubeconfig 正常,合并后访问另一个 context 报 the server has asked for the client to provide credentials
定位命令kubectl --kubeconfig generated/kubeconfig config get-contextskubectl --kubeconfig generated/kubeconfig --context <context> get nodes
判断依据两份 kubeconfig 默认用户都叫 kubernetes-admin,直接 flatten 会让用户认证信息冲突
修复动作使用 make merge-kubeconfigs 生成统一 kubeconfig,它会把用户改成 kubernetes-admin-<cluster>,context 统一为集群名

经验:多个 kubeadm 集群的 admin 用户名经常相同。合并前要唯一化用户,否则 context 名对了也可能用错证书。

排障原则:从 HA 扩容调试中总结的教训

以下原则来自真实节点 HA 扩容过程中反复踩坑后的反思。排障的目标不是"修好",而是"不再重复修同一个问题"。

  1. 看见错误再动手。no_log 会掩盖 kubeadm/stderr 输出。生产代码可以隐藏敏感 token,但调试路径必须保留可审计的 stderr。不要根据 FAILED 状态猜测原因。

  2. 看懂错误信息再决定下一步。etcdserver: too many learner members 是一个明确的信号:etcd 集群状态有残留。不要 reset 节点重试——先检查 etcd member list、清理死成员。

  3. 每步操作后验证集群状态。kubeadm join 退出不等于失败,exit code 1 不一定意味着 cluster state 没有改变。join 可能写入了 manifests 但超时在 etcd Pod 启动检查上。下次操作前先验证 manifests 和 kubectl get nodes。

  4. 理解工具的副作用。kubeadm join --control-plane 会改变 etcd 集群成员列表(添加 learner),这个副作用不会因为 join 报错而自动回滚。kubeadm reset 清理本地但不清理远程 etcd。每次 join 操作后都要检查 etcd member list。

  5. 文档记录操作链路,不记结论。记录当时的现象、执行的命令、返回的错误、当时的判断、以及为什么判断是对的或错的。这样未来遇到同类问题时能看到完整的诊断视角,而不只是最终答案。

  6. 失败后先定状态,再决定动作。同一个错误连续出现时,停止重试,先把节点状态归类为“未 join”“已写入 manifest 但未完成”“远端 etcd 已有 learner”“已成为 voting member”。状态不明时不清理、不重跑。

  7. 自动化只做可证明安全的动作。Ansible 可以顺序 join、等待 member started、promote learner;但遇到旧 learner、dead member 或远端残留时,只输出证据和人工清理命令,不自动删除 etcd member。

HA kubeadm join 状态判断表

状态证据下一步
未 join新 master 没有 /etc/kubernetes/kubelet.conf,远端 etcdctl member list 找不到该节点 https://<private_ip>:2380可以执行 kubeadm join --control-plane
join 写入 manifest 但超时新 master 有 /etc/kubernetes/manifests/etcd.yamlkubeadm join 返回非 0,远端 member list 能看到该 peer URL不要 reset,等待 kubelet/containerd 拉起 etcd Pod,再检查 member 是否 started
etcd learner 已存在member list 里该节点最后一列为 true等待同步完成后执行 etcdctl member promote <id>
已成为 voting membermember list 里该节点最后一列为 false,节点上有 admin.conf 或 API 已看到该 master不再 join,继续下一个 master 或执行集群验收
旧 learner 或 dead member 残留本地节点未完成 join,但远端 member list 已有该 peer URL,或状态长期 unstarted先记录 member id,人工确认后 etcdctl member remove <id>,再重新运行自动化

案例 9:kubeadm join --control-plane 调试全程记录

以下按时间线记录 2026-06-11 在真实节点上执行 HA 扩容时遇到的完整排障过程。每轮记录包含操作、现象、判断和修正方向,包括正确的和错误的判断。

背景

两云已从 master_count=1 扩容到 3。Terraform apply 创建了 4 个新实例,Ansible bootstrap 与 underlay 已覆盖全部 8 个节点。接下来需要用 kubeadm join --control-plane 将新 master 加入已有集群。每个集群当前只有 1 个 master(kubeadm 初始化的单节点 etcd)。

第 1 轮:直接跑 ansible-kubeadm(失败,被 no_log 挡住)

操作:make ansible-kubeadm

输出:4 个新 master 的 join 任务全部 FAILED,错误被 no_log: true 隐藏:

fatal: [tencent-master-2]: FAILED! => {"censored": "the output has been hidden due to the fact that no_log was specified"}

当时判断:不知道错误原因。kubeadm init 被正确跳过(已有 admin.conf),upload-certs 和 token create 正常。join 本身失败——可能网络、token 或证书问题。

修正:去掉 no_log: true 重跑看真实错误。

第 2-3 轮:去掉 no_log 重新定位

操作:sed 临时注释 no_log,重跑 make ansible-kubeadm

输出:所有 join 被 skipped——因为第 1 轮失败 join 虽然报错退出,但写入了 kubelet.conf,导致幂等守卫 not kubelet_conf.stat.exists 变 false。

修正:手动 kubeadm reset -f && rm -rf /etc/kubernetes/* 清理 4 个节点,重跑。结果再次全部 FAILED,仍被 no_log 挡住。

第 4 轮:手动 join 单节点,发现 kubelet.conf 自动重建

操作:从 master-1 获取 join 命令,手动在 master-2 执行

输出:

[ERROR FileAvailable--etc-kubernetes-kubelet.conf]: kubelet.conf already exists [ERROR Port-10250]: Port 10250 is in use

真正的问题:kubeadm reset 清理了 /etc/kubernetes/kubelet.conf,但 kubelet 服务本身还在运行(systemd enabled),kubelet 自动重建了 kubelet.conf 并监听 10250 端口。

修正:systemctl stop kubelet && kubeadm reset -f && rm -rf /etc/kubernetes/* /var/lib/kubelet /var/lib/etcd && kubeadm join ...

第 5 轮:并行 join 两个 master——关键转折

操作:在 tencent-master-2 和 master-3 上同时执行 reset+join

输出(master-2):

[etcd] Announced new etcd member joining to the existing etcd cluster [etcd] Creating static Pod manifest for "etcd" error: error execution phase etcd-join: etcdserver: too many learner members in cluster

输出(master-3):

error: error execution phase etcd-join: the etcd member c03f6df7fc8e28d1 is not started

信号解读(正确判断):

  • too many learner members — etcd v3.6 同时只能有 1 个 learner。并行 join 使 master-2 先成为 learner,master-3 被拒绝
  • member not started — master-3 的 join 依赖 master-2 的 etcd 成员已启动,但 Pod 还未被 kubelet 拉起
  • 两个问题叠加:并行 join 触发 learner 上限 + etcd Pod 启动延迟导致 kubeadm 超时

修正(正确方向):先清理 etcd 死成员,只 join master-2,等 Pod 启动后 promote learner,再 join master-3

第 6 轮:单台 join——被 exit code 误导

操作:etcdctl member remove <dead-member-id> → 只 join tencent-master-2

输出:

error: error execution phase etcd-join: the etcd member b0d5de0b56b9c977 is not started

错误判断:看到 exit code 1 认为 join 彻底失败,继续 reset 重试

事后验证发现:

ls /etc/kubernetes/manifests/

输出:etcd.yaml kube-apiserver.yaml kube-controller-manager.yaml kube-scheduler.yaml

ls /var/lib/kubelet/pods/*/containers/etcd

输出:5 个 etcd 容器目录——Pod 已在运行

真正根因:kubeadm 先写 manifest 到磁盘,再等 Pod 启动。kubelet 异步拉镜像、启动容器,kubeadm 的超时比 Pod 启动时间短,于是 exit 1。但 manifests 已写入,etcd 已在运行。

第 7 轮:不 reset,直接 promote(路线已明确)

操作:

kubectl --kubeconfig /etc/kubernetes/admin.conf -n kube-system exec etcd-<master-1> -- etcdctl member list
kubectl --kubeconfig /etc/kubernetes/admin.conf -n kube-system exec etcd-<master-1> -- etcdctl member promote <id>

注意事项:promote 过程中 API server 可能短暂不可达(kube-apiserver 因 etcd 拓扑变更重启);需等 learner 数据同步完成

终态与修复流程(已验证可行)

  1. etcdctl member list 确认 learner 已从 unstarted 变为 started
  2. etcdctl member promote <id> 提升 learner 为 voting
  3. etcdctl member list 确认该成员 false(非 learner)
  4. 以同样流程 join master-3(阿里云同理)

事后根因分析

层级问题为何反复
etcd v3.6learner 上限 1,并行 join 触发拒绝不了解版本行为变更
kubeadmjoin 超时短于 Pod 启动时间,exit 1 但 manifest 已写依赖 exit code 判断操作成败
Ansibleno_log: true 掩盖错误;并行执行触发 learner 上限安全优先于可调试性;未设计 etcd promote
操作方式按 exit code 立刻 reset 重试,不检查集群状态急于修复而不先诊断
设计缺陷kubeadm 的 etcd-join 阶段应自动 promote 或提供更长超时把 kubeadm 视为原子操作,忽略了多阶段副作用

后续设计改进

  1. Ansible kubeadm_cluster role 的 join 任务加 throttle: 1 保证顺序执行
  2. 去掉 no_log: true,改为 register + failed_when — 只隐藏 token/certificate-key 的行
  3. 增加 post-join promote 步骤:join 完成后自动 etcdctl member promote
  4. 真实的错误信息输出到文件或 Ansible 日志,不依赖 exit code 判断成败

复盘模板

每次遇到问题后,用这个模板记录:

## 问题

- 时间:
- 阶段:
- 执行命令:
- 现象:
- 原始错误:
- 影响范围:
- 当前状态判断:
- 验证命令:
- 定位证据:
- 当时判断:
- 判断是否正确:
- 根因:
- 修复动作:
- 最终验收命令:
- 后续预防:

把排障记录沉淀下来,比只记住一条命令更有价值。