问题定位与实战经验
混合云项目最容易失败的地方不在某一条命令,而在层与层之间的假设。排障时不要直接猜 Cilium 或 Terraform,先确认问题停在哪一层。
分层排障模型
案例 1:Terraform plan 通过但 apply 失败
| 项目 | 内容 |
|---|---|
| 现象 | terraform plan 能生成计划,apply 时云 API 报权限、库存或配额错误 |
| 定位命令 | make aliyun-plan、make 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-clusters、kubectl get nodes -o wide、journalctl -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-plan 或 make tencent-plan 显示全量创建,但 generated/*/terraform-output.json 里仍有旧 IP |
| 定位命令 | terraform -chdir=aliyun state list、terraform -chdir=tencent state list、make aliyun-plan、make tencent-plan |
| 判断依据 | state、云上真实资源、generated/*/terraform-output.json 三者不一致时,不能把旧 output 当作当前事实 |
| 修复动作 | 先决定恢复 state、销毁旧资源后重建,或接受重新创建;确认重建后再执行 make aliyun-apply、make tencent-apply 并重新导出 output |
经验:state 为空时不要直接跑后续 Ansible。先让 Terraform 输出重新代表真实云资源,再生成 inventory。
案例 7:kubeadm init 卡在 API Server 初始化
| 项目 | 内容 |
|---|---|
| 现象 | make ansible-kubeadm 中 kubeadm init --upload-certs 超时,kubelet 日志显示拉取 registry.k8s.io/pause 失败 |
| 定位命令 | journalctl -u kubelet、systemctl status containerd、grep -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-contexts、kubectl --kubeconfig generated/kubeconfig --context <context> get nodes |
| 判断依据 | 两份 kubeconfig 默认用户都叫 kubernetes-admin,直接 flatten 会让用户认证信息冲突 |
| 修复动作 | 使用 make merge-kubeconfigs 生成统一 kubeconfig,它会把用户改成 kubernetes-admin-<cluster>,context 统一为集群名 |
经验:多个 kubeadm 集群的 admin 用户名经常相同。合并前要唯一化用户,否则 context 名对了也可能用错证书。
排障原则:从 HA 扩容调试中总结的教训
以下原则来自真实节点 HA 扩容过程中反复踩坑后的反思。排障的目标不是"修好",而是"不再重复修同一个问题"。
-
看见错误再动手。no_log 会掩盖 kubeadm/stderr 输出。生产代码可以隐藏敏感 token,但调试路径必须保留可审计的 stderr。不要根据 FAILED 状态猜测原因。
-
看懂错误信息再决定下一步。etcdserver: too many learner members 是一个明确的信号:etcd 集群状态有残留。不要 reset 节点重试——先检查 etcd member list、清理死成员。
-
每步操作后验证集群状态。kubeadm join 退出不等于失败,exit code 1 不一定意味着 cluster state 没有改变。join 可能写入了 manifests 但超时在 etcd Pod 启动检查上。下次操作前先验证 manifests 和 kubectl get nodes。
-
理解工具的副作用。kubeadm join --control-plane 会改变 etcd 集群成员列表(添加 learner),这个副作用不会因为 join 报错而自动回滚。kubeadm reset 清理本地但不清理远程 etcd。每次 join 操作后都要检查 etcd member list。
-
文档记录操作链路,不记结论。记录当时的现象、执行的命令、返回的错误、当时的判断、以及为什么判断是对的或错的。这样未来遇到同类问题时能看到完整的诊断视角,而不只是最终答案。
-
失败后先定状态,再决定动作。同一个错误连续出现时,停止重试,先把节点状态归类为“未 join”“已写入 manifest 但未完成”“远端 etcd 已有 learner”“已成为 voting member”。状态不明时不清理、不重跑。
-
自动化只做可证明安全的动作。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.yaml,kubeadm join 返回非 0,远端 member list 能看到该 peer URL | 不要 reset,等待 kubelet/containerd 拉起 etcd Pod,再检查 member 是否 started |
| etcd learner 已存在 | member list 里该节点最后一列为 true | 等待同步完成后执行 etcdctl member promote <id> |
| 已成为 voting member | member 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 数据同步完成
终态与修复流程(已验证可行)
- etcdctl member list 确认 learner 已从 unstarted 变为 started
etcdctl member promote <id>提升 learner 为 voting- etcdctl member list 确认该成员 false(非 learner)
- 以同样流程 join master-3(阿里云同理)
事后根因分析
| 层级 | 问题 | 为何反复 |
|---|---|---|
| etcd v3.6 | learner 上限 1,并行 join 触发拒绝 | 不了解版本行为变更 |
| kubeadm | join 超时短于 Pod 启动时间,exit 1 但 manifest 已写 | 依赖 exit code 判断操作成败 |
| Ansible | no_log: true 掩盖错误;并行执行触发 learner 上限 | 安全优先于可调试性;未设计 etcd promote |
| 操作方式 | 按 exit code 立刻 reset 重试,不检查集群状态 | 急于修复而不先诊断 |
| 设计缺陷 | kubeadm 的 etcd-join 阶段应自动 promote 或提供更长超时 | 把 kubeadm 视为原子操作,忽略了多阶段副作用 |
后续设计改进
- Ansible kubeadm_cluster role 的 join 任务加 throttle: 1 保证顺序执行
- 去掉 no_log: true,改为 register + failed_when — 只隐藏 token/certificate-key 的行
- 增加 post-join promote 步骤:join 完成后自动 etcdctl member promote
- 真实的错误信息输出到文件或 Ansible 日志,不依赖 exit code 判断成败
复盘模板
每次遇到问题后,用这个模板记录:
## 问题
- 时间:
- 阶段:
- 执行命令:
- 现象:
- 原始错误:
- 影响范围:
- 当前状态判断:
- 验证命令:
- 定位证据:
- 当时判断:
- 判断是否正确:
- 根因:
- 修复动作:
- 最终验收命令:
- 后续预防:
把排障记录沉淀下来,比只记住一条命令更有价值。