
这几天处理一套多 master 节点的 k8s 集群往控制面里加第二个节点时kubeadm join命令先是弹出一串端口被占用的 preflight 错误随后进程直接卡在wait-control-plane阶段不动了。第一次碰到这种报错之后不退出、反而一直挂起的情况确实有点懵。后来把 kubelet 日志、容器状态和残留进程串起来看才发现表面是端口冲突底层是上一次初始化留下的控制面残留状态没有清干净再加上对控制面 join 的流程理解不到位才导致命令卡死。这篇就把完整的分析过程、排查链路和修复操作整理出来给正在搭多 master 高可用集群、或者准备往现有集群里加控制面节点的同学一个参考。1. 控制面节点加入的完整流程卡点其实藏在阶段里1.1 控制面 join 和 worker join 的本质差别很多人在给 k8s 集群加节点时会把kubeadm join当成一个提交就完事的简单命令实际上控制面节点加入和普通 worker 节点加入走的完全是两套逻辑。普通 worker 节点 join 时kubeadm 要做的事情相对单纯通过 bootstrap token 完成发现和认证从集群里拉取 kubelet 所需的 kubeconfig然后启动 kubeletnode 对象注册进集群流程就结束了。整个过程中节点自身不会启动任何控制面组件也不需要对 etcd 做任何操作。控制面节点 join 就完全不一样了。kubeadm join --control-plane不仅要完成上面的 worker 注册流程还要在本地准备一套完整的控制面组件清单kube-apiserver、kube-controller-manager、kube-scheduler以及一套和现有集群对齐的 etcd 实例。它需要先从第一个 master 节点拉取证书、上传证书然后生成 static pod manifest 放到/etc/kubernetes/manifests目录下最后等 kubelet 把这一整套容器拉起来并通过健康检查。问题就在于这一大堆动作里只要有一个端口被占用、一个证书对不上、或者一个 static pod 写失败整个流程就会卡在某个阶段。而端口被占用恰恰是最容易在 preflight 阶段暴露又最容易在后续阶段引发连锁反应的问题。1.2 preflight 端口检查为什么不彻底kubeadm 在执行 join 时第一阶段就是 preflight 检查。这个阶段会检查很多项端口是否被占用、所需文件是否已存在、swap 是否关闭、cgroup 驱动是否匹配、kubelet 是否健康等等。正常情况下如果端口检查失败kubeadm 会直接以 fatal error 退出根本不会进入后续阶段也就谈不上卡住。但有两个例外会让端口检查看起来过了、实际上没过去第一个例外是--ignore-preflight-errors参数。只要你在 join 命令里加了类似--ignore-preflight-errorsPort-10259的选项即使检测到端口有冲突kubeadm 也会把错误降级为 warning继续往下走。很多人在遇到端口占用报错时第一反应就是我要跳过它结果就把一个本来应该中止的问题硬生生拖进了后续流程。第二个例外更隐蔽preflight 检查的端口清单和实际 static pod 启动时的绑定行为并不完全一致。preflight 主要检查认证端口和安全端口比如 6443、10257、10259。但 kube-apiserver 启动时还会用到一系列服务和 etcd 通信的动态端口etcd 实例启动时除了 2379 客户端端口还有 2380 peer 端口。这些端口在 preflight 里检查得并不严格一旦被其他进程占用等 kubelet 去启动容器时才会暴露而那时候 join 进程已经进入等待阶段了。1.3 卡住的准确位置wait-control-plane 在等什么回到卡住这个问题本身。kubeadm join 控制面节点的过程在生成完 static pod 清单之后会进入wait-control-plane阶段。这个阶段 kubeadm 主进程会反复轮询本地 kube-apiserver 的健康检查接口/healthz等待 apiserver 起来、连上 etcd、汇报健康。默认情况下这个等待是有超时时间的通常是 4 到 5 分钟。超时之后 kubeadm 才会报错退出。但如果你命令行里挂着看日志看到的场景就是命令停在某一行输出不动然后过几分钟啪一下抛出一个超时错误。很多人说卡住了其实准确描述应该是它在反复等待一个永远不可能变健康的服务。为什么会永远不健康因为端口被占用kubelet 反复拉起 kube-apiserver 容器都失败容器一直 CrashLoopBackOff健康检查接口自然永远不通。于是 join 主进程就在那个循环里干等直到超时。整个卡住现象的背后就是这样一个机制端口冲突 → 容器起不来 → 健康检查不过 → 等待超时。2. 端口被占用的三种典型来源我这次算是遇了个遍2.1 重复执行 join/init 留下的控制面残留这次遇到问题的服务器之前曾经尝试过用kubeadm init初始化单节点集群后来因为规划调整机器被分配到多 master 集群里作为第二个控制面节点。虽然之前执行过kubeadm reset但 reset 并没有把所有东西都清干净。/var/lib/etcd目录里残留了旧的 etcd 数据文件/var/lib/kubelet里残留了之前 kubelet 的配置和证书甚至系统中还挂着几个未完全退出的 etcd 相关进程。这些残留进程直接占住了 2379、2380 端口导致新的 etcd 实例根本没有办法绑定。这种残留问题非常典型。kubeadm reset 确实会停掉 kubelet 容器并清掉部分目录但对于直接以进程方式跑起来的组件而非容器方式以及 kubelet 数据目录reset 的清理并不彻底。一旦这些进程占住端口后面再跑 joinpreflight 就会报错。2.2 主机上既有业务与 k8s 端口的硬撞第二类来源和 k8s 本身没关系纯粹是机器上跑着其他业务占用了端口。我见过有人的机器上用 nginx 监听了 6443 端口做反向代理结果 kube-apiserver 死活起不来也见过 node_exporter 或自定义 agent 监听在 10255 这类端口上恰好和 kubelet 的只读端口冲突。这类问题在挑选控制面节点时特别容易被忽略。大家往往只关心机器配置、网络连通性很少会提前对端口占用做一轮体检。但控制面组件对端口的要求非常刚性etcd 的 2379/2380、apiserver 的 6443、controller-manager 的 10257、scheduler 的 10259哪一个被占了对应组件就起不来。2.3 手贱加 --ignore-preflight-errors 之后还有一部分卡住完全是操作层面人为造成的。有些人看到 preflight 报端口冲突不去排查是谁占用了端口而是直接给 join 命令追加--ignore-preflight-errors参数想绕过检查。我这次排查的时候发现机器上有上一次 join 失败后残留的 kubelet 服务还在运行。这个 kubelet 本身就监听了 10250 端口同时它的 static pod 目录/etc/kubernetes/manifests里还留着旧的 apiserver manifest。此时再次执行 joinpreflight 会提示 manifest 文件已存在但如果你无视这个提示、强制跳过kubelet 会拿着旧的、配置不对的 manifest 去启动容器结果必然是端口又被占、容器又起不来、流程又卡住。这种绕过检查的做法在单节点 init 时偶尔能救场在控制面 join 时最好别碰。3. 完整排查链路从一句端口报错到定位残留进程3.1 先看 kubeadm 输出停在哪行再看报错前缀排查的第一步不是急着看日志而是先回到 kubeadm join 的输出上看清楚它究竟报了什么、停在了哪一步。我当时看到的输出大致是这样的[preflight] Running pre-flight checks [preflight] Pulling images required for setting up a Kubernetes cluster [preflight] This might take a minute or two, please wait ... error execution phase preflight: [preflight] Some fatal errors detected: [ERROR Port-6443]: Port 6443 is in use [ERROR Port-10259]: Port 10259 is in use注意这里有个关键点报错信息是fatal errorskubeadm 理论上应该直接退出。但实际情况是这个报错出现之后命令并没有立即结束而是继续往下走了几步然后才卡住。这说明什么说明真正的触发点不完全在 preflight而在于 preflight 检查的端口清单里漏掉了 etcd 的 2379/2380 这类关键端口。换句话说6443 和 10259 的冲突只是先头部队真正让 join 卡死的是 2379 和 2380 的占用而后者在 preflight 阶段没有报 fatal error被静默放行了。所以排查时不要把目光只锁定在报错信息列出的端口上要顺着控制面组件的完整端口清单逐个确认。3.2 用一边挂着的终端观察journalctl 和 crictl 是最快突破口因为 join 命令卡住我另开了一个终端直接看 kubelet 日志。这是排查这类问题最快、最直接的手段journalctl -u kubelet --since 10 minutes ago -f日志里能看到 kubelet 反复尝试启动 kube-apiserver 静态 pod但都失败了。接着看容器状态crictl ps -a | grep -E kube-apiserver|etcd|kube-controller|kube-scheduler这一步能把问题定位到容器根本没起来还是容器起来了但健康检查失败。我这次的情况是etcd 容器连创建都没成功报错信息直接指向端口 bind 失败failed to listen on 2379: listen tcp :2379: bind: address already in use到这里问题的性质已经清楚了不是配置问题不是镜像问题就是单纯的端口被占用。3.3 端口、进程、文件三向定位确认占用者身份下一步是找到占用端口的元凶。用 ss 命令把相关端口全部捋一遍ss -lntp | grep -E (:6443|:2379|:2380|:10257|:10259)\b如果输出为空说明这些端口当前没被监听那问题可能出在别的层面比如防火墙拦截、IPv6 和 IPv4 绑定差异。如果输出了结果重点关注进程的 PID再通过 PID 反查进程详情ps -ef | grep -E kube-apiserver|etcd|kube-controller-manager|kube-scheduler | grep -v grep我这次看到的结果是2379 和 2380 被两个进程占着进程路径指向/var/lib/etcd下的旧数据目录显然是之前 init 时残留的 etcd 进程。拿到 PID 之后再顺手确认一下进程的执行路径、启动时间基本就能判断出它是上一次集群残留还是当前集群组件。3.4 排查时最容易漏掉的隐藏占用kubelet 自身和 systemd 残留服务排查端口占用时还有一个非常坑的地方kubelet 自身可能也在捣乱。如果上一次 join 失败时kubelet 服务并没有被正确停止很多场景下 systemd 里 kubelet 是 enable 状态它会一直跑着监听 10250 端口并且持续尝试加载/etc/kubernetes/manifests目录下的静态 Pod。此时你直接重新执行 joinpreflight 的端口检查未必会暴露 10250 的冲突但 kubelet 和 kubeadm 之间对集群状态的认知已经不一致了。遇到这种情况除了前面 ss 定位端口之外还要检查 systemd 服务状态systemctl status kubelet systemctl list-units | grep kube另外/etc/kubernetes目录下残留的 manifest 文件也要检查。这些文件是 kubelet 的圣旨只要文件还在kubelet 就会一直尝试拉起对应的容器。我曾经见过有人删掉了/etc/kubernetes/manifests下的 apiserver manifest但没删干净 etcd 的结果 kubelet 一直尝试启动 etcd把 2379 端口占得死死的。所以排查时对 kubelet、systemd、manifest 这三个位置的确认缺一不可。4. 修复操作reset 之外的彻底清理然后重新 join4.1 标准 kubeadm reset 为什么不彻底排查出残留进程之后修复思路就清晰了把旧状态清理干净再重新执行 join。很多人会直接执行kubeadm reset -f以为这一步能把节点恢复到出厂状态。实际上 reset 做的事情是停掉 kubelet、删除当前节点上的 static pod manifest、清理部分 k8s 相关的 systemd 服务文件。但下面这几项它通常不会碰/var/lib/etcd数据目录/var/lib/kubelet数据目录/etc/kubernetes下的部分配置文件残留已经跑在进程里的 etcd 或其他控制面组件进程所以执行 reset 之后端口冲突的根因很可能还留在原地。我这次就是这样reset 跑完2379 端口照样被旧 etcd 进程占着。必须手动补齐清理动作。4.2 手动清理残留目录与服务再补一次端口确认先停掉 kubelet 服务防止它在中途又把 static pod 拉起来systemctl stop kubelet然后手动清除 kubelet 可能会读取的所有残留目录rm -rf /etc/kubernetes rm -rf /var/lib/kubelet rm -rf /var/lib/etcd rm -rf /var/lib/cni rm -rf /etc/cni/net.d这里我建议连/etc/kubernetes一起清掉因为旧 manifest 里的 apiserver、etcd 配置如果和新集群不一致即使端口不冲突后续也会出现证书、Service CIDR 对不上的问题。清完目录之后再杀一遍残留进程pkill -9 -f etcd|kube-apiserver|kube-controller-manager|kube-scheduler最后再确认一遍端口池ss -lntp | grep -E (:6443|:2379|:2380|:10250|:10257|:10259)\b如果这一轮输出是空的说明端口已经全部释放可以开始重新 join 了。这一步千万别省很多时候卡住就是因为这里没清干净。4.3 重新加入控制面的完整命令与最终验证重新 join 之前还要确保手上有三样东西bootstrap token、discovery-token-ca-cert-hash、certificate-key。第三个是控制面节点 join 特有的如果第一次 init 时没加--upload-certs参数可以用下面的命令重新生成证书上传kubeadm init phase upload-certs --upload-certs kubeadm token create --print-join-command然后到第二个 master 节点上执行 join注意--control-plane参数必须有kubeadm join 192.168.1.10:6443 \ --token xxxx \ --discovery-token-ca-cert-hash sha256:xxxx \ --control-plane \ --certificate-key xxxx执行完 join 后在第一个 master 节点上验证控制面状态kubectl get nodes kubectl get pods -n kube-system -o wide | grep -E kube-apiserver|etcd|kube-controller|kube-scheduler两个 master 节点的 apiserver、etcd、controller-manager、scheduler 都应该是 Running 状态。我这次重新 join 后第二个节点的wait-control-plane阶段大概等了不到一分钟就通过了说明真正的卡点确实就是端口冲突和残留状态。5. 控制面端口全景与多 master 维护的预防建议5.1 控制面组件端口占用全景先背下来再动手要彻底理解这次问题得把控制面组件涉及的端口烂熟于心。下面这张表基本覆盖了多 master 部署时最核心的端口建议保存组件端口协议用途通常由谁监听kube-apiserver6443TCPHTTPS API 服务kube-apiserver 容器kube-apiserver8080TCP非安全端口新版默认关闭存在时容易和业务端口冲突etcd2379TCPetcd 客户端通信etcd 容器etcd2380TCPetcd peer 集群通信etcd 容器kube-controller-manager10257TCP安全端口/健康检查controller-manager 容器kube-scheduler10259TCP安全端口/健康检查scheduler 容器kubelet10250TCPkubelet 主端口kubelet 进程kubelet10255TCP只读端口默认不强绑某些旧版本或自定义配置kube-proxy随机TCP/UDP随机监听视 Service/NodePort 而定注意etcd 的 2380 端口在多 master 场景下尤其关键。集群里每个控制面节点都有自己的 etcd 实例它们之间需要通过 2380 进行 peer 通信这个端口一旦被占用etcd 集群根本无法建立。而 preflight 对 2380 的检查很多时候并不充分所以卡住往往就卡在这个端口上。5.2 新节点加入控制面前三分钟的端口体检清单既然吃过亏后面再做控制面扩容时我都会提前做一轮体检五分钟搞定能免掉后面一大半排错时间检查控制面核心端口是否被监听ss -lntp | grep -E (:6443|:2379|:2380|:10250|:10257|:10259)\b检查是否有 k8s 相关残留进程ps -ef | grep -E kube-apiserver|etcd|kube-controller|kube-scheduler | grep -v grep确认 manifest 目录是空的ls -la /etc/kubernetes/manifests/确认 kubelet 服务状态是 stopped 或 inactivesystemctl status kubelet任何一项有异常先在加入前解决再执行 join。这套体检脚本在多 master 扩容时我已经固定下来了。5.3 多 master 日常运维的几条纪律最后说几句控制面节点运维的经验之谈第一kubeadm reset之后一定要手动确认端口释放不要相信 reset 的输出。reset 输出里写着removed但删的是它认为该删的不是所有该删的。我对这句话的体会这次是刻骨铭心。第二控制面节点加入失败后不要在同一个节点上反复重试join。每一次失败的 join 都会留下新的残留状态叠加起来会把问题变得越来越复杂。正确做法是失败之后先 reset再清理再体检最后重新 join。第三多 master 集群里每个控制面节点的主机名、/etc/hosts 解析、系统时钟必须提前对齐。etcd 对时钟偏移和主机名解析非常敏感这两个问题虽然不直接表现为端口占用但也会让控制面 join 在等待阶段卡住表现和这次端口问题几乎一样。排查时如果端口全空下一步就该检查这两个地方。第四任何时候都不要在控制面 join 命令上盲目加--ignore-preflight-errors。遇到端口冲突去解决冲突本身而不是绕过检查。除非你能百分之百确认哪个端口是安全的、可以通过参数忽略否则这个参数带来的麻烦远比它解决的问题多。多 master 控制面搭建本来就没有太多容错空间每一步操作都最好严谨一点。把端口、残留状态这些基本功做扎实了整个集群的稳定性自然就上来了。