我第一次把生产流量切到一套自己用 KubeKey 拉起来的三控制面集群上,是在一个周末的凌晨。那套集群跑起来只花了不到四十分钟,但从"能跑"到"敢让它扛业务",中间隔了将近三周。问题不在 KubeKey 本身——它的定位很克制,把 kubeadm 的复杂度收敛成一个二进制加一份 YAML,真正把我绊住的,是那些配置文件里看起来无关紧要、实际上决定了故障时系统能不能自愈的字段:VIP 漂移、apiserver 后端健康检查、etcd 的 fsync 延迟、Pod 网段和宿主机网段撞车。所以这篇不打算写成一份安装手册,那种东西官方文档已经有。我想聊的是高可用 Kubernetes 集群在落地过程中,哪些决策必须提前定死、哪些参数写错会在半年后才爆发、以及排查时应该按什么顺序看。适合已经能跑通单机集群、准备把环境推进到多节点、甚至要交付给别人的朋友参考,也适合正在被"三个 master 却仍然会挂"这个问题折磨的人对照着找原因。
1. 高可用的成败不在 kubeadm,在部署前的地址规划
1.1 三个 master 不等于高可用:先数清楚有几个单点
很多人对高可用的理解停留在"控制面节点数量 ≥ 3"。这个认知不能说错,但远远不够。我习惯在动手之前先在纸上把所有可能的单点列出来,逐个问一句"它挂了会发生什么"。通常一张表写下来,你会发现三节点控制面只解决了其中一小半问题。
| 组件/资源 | 单点时的后果 | 常见的补齐方式 |
|---|---|---|
| kube-apiserver | kubectl 无法连接,控制器停摆,但已运行的 Pod 不受影响 | 多副本 + 统一入口 |
| etcd | 失去集群状态存储,整集群雪崩 | 奇数节点集群,3 或 5 个 |
| 入口地址 | 客户端配置写死在某个 IP,换节点就得改 kubeconfig | VIP 或域名 + 负载均衡 |
| 容器镜像仓库 | 节点重建时拉不到镜像,扩容直接失败 | 内网仓库 + 镜像预拉取 |
| 存储后端 | 有状态业务全部不可用 | 分布式存储或外置存储服务 |
| 时间源 | 证书校验、日志对齐、etcd 选主都可能异常 | 统一内网 NTP |
这张表最有价值的地方不是列全了,而是让你意识到:Kubernetes 的高可用是分层的高可用,控制面、网络、存储、镜像、时间,任何一层单点,都足以让整套集群在关键时刻失效。我在一个项目里就吃过亏——三台控制面做得很标准,但镜像仓库只有一台,某次仓库所在的机器磁盘满了,节点滚动重启时全部卡在拉镜像,业务侧的扩容拖了四个小时。
从这张表出发,还要再问一层:每个组件的故障恢复时间是多少。kube-apiserver 挂掉一个副本,客户端如果配了负载均衡,切换通常在秒级;etcd 挂掉一个节点,集群会自动选主,但如果挂掉之后磁盘 IO 跟不上,恢复可能拖到分钟级。这个差异决定了你在设计超时、重试、探针参数时的取值。我一般会把关键客户端的连接超时设得比控制面故障切换时间稍长一点,避免切换瞬间的抖动直接变成业务报错。
另一个容易被忽略的点是控制面节点本身的地域/机架分布。如果三台控制面都在同一台物理宿主机上,或者挂在同一个交换机下,那"三节点"只是形式上的冗余。虚拟化环境下尤其要注意,很多人是在同一台大宿主机上开三台虚拟机,宿主机一挂,三个 etcd 同时失联,集群照样瘫痪。有条件的话,控制面尽量跨物理机甚至跨机架。
1.2 KubeKey 把 kubeadm 的哪些活接管了
要理解 KubeKey 的边界,得先理解它下面那层发生了什么。kubeadm 负责的是单个节点上的引导工作:初始化控制面、生成证书、拉起静态 Pod、生成 join 命令。它天然假设"你已经解决了入口统一和节点互通的问题"。所以在纯 kubeadm 的世界里,高可用方案通常是自己搭一套负载均衡 + VIP,然后告诉 kubeadm 那个统一的入口地址是什么。
KubeKey 做的事情是在 kubeadm 之上加了一层编排,把"装系统依赖、分发二进制、生成配置、按顺序执行、装插件"这些步骤串起来,并且用一份声明式配置描述整个集群。它接管的部分包括:容器运行时安装与配置、kubelet/kubeadm/kubectl 的二进制分发、证书生成、网络插件部署、以及最关键的——内置的负载均衡与 VIP 组件。
这个"内置"是我最欣赏的设计。它默认会在每个控制面节点上部署一套轻量的代理和地址漂移组件,让每个节点自己就能把发往统一入口的请求转发到当前存活的所有 apiserver 上。好处是不依赖外部负载均衡器,一台裸金属机器、几台虚拟机就能起飞;代价是这套内置组件的行为需要你理解,否则排查时会一头雾水。
还有一层是制品管理。KubeKey 可以把所有需要的镜像、二进制、系统依赖打包成一个离线制品包,在网络受限的环境里用这个包完成部署。这个能力在真实交付中价值极高,因为多数企业内网没法直连公共镜像源。我第一次做离线部署时低估了这件事,以为只是把镜像 load 进去,结果发现容器运行时、CNI 插件、metrics 组件全都有自己的镜像,漏一个就卡在某个环节,日志还未必直白。
1.3 内置 localhost 负载均衡和外置 LB 的取舍
选内置还是外置,这个问题我在不同项目里给过不同答案,判断依据其实就三条:环境里有没有现成的负载均衡设施、团队能不能维护那套设施、以及未来是否需要把入口暴露到集群外。
内置方案的优势是自包含。集群安装完,入口就通了,不需要跟网络团队协调 VIP 申请、不需要等负载均衡配置变更。故障时是集群自己处理漂移,运维链路短。缺点是这套组件跑在控制面节点上,会占用一点资源,而且它的配置不像专业负载均衡器那样可以通过控制台可视化管理。
外置方案的优势是职责分离。如果环境里已经有硬件负载均衡或者统一的软件负载均衡平台,把 apiserver 入口交给它,天然获得健康检查、连接数统计、访问日志这些能力,而且升级集群时不会牵连入口组件。缺点是依赖外部团队配合,出问题时定位链路变长——你没法确定是集群侧的问题还是负载均衡侧的问题。
我自己的经验是:中小规模、团队人少、快速交付,选内置;规模较大、有多套集群、已有统一网络设施,选外置。还有一个折中做法:先内置跑起来,等集群稳定、业务上量之后,再平滑切到外置入口。切换本身不复杂,改一下配置文件里的入口地址,重新执行一次配置下发即可,但前提是你一开始就别把入口地址硬编码到各种脚本和 kubeconfig 里。
这里有个细节要提醒:如果选了内置方案,控制面节点数量建议是奇数,并且不要少于三个。两个控制面节点的内置方案在某些版本里能装,但地址漂移组件的选主在双节点下容易出现双方都认为自己该持有地址的情况,虽然有一系列机制去缓解,但排查成本远高于直接上三节点。
2. 装系统时的每一个"默认值"都是后面排查的导火索
2.1 主机名、内网地址与 hosts 解析的三重约束
我在帮别人看集群问题时,第一件事永远是问三个问题:主机名是什么、内网地址是什么、机器之间怎么解析。这三个问题能覆盖我遇到过的将近四成"莫名其妙"的故障。
主机名必须唯一且稳定。Kubernetes 用节点名作为节点对象的唯一标识,如果两台机器同名,注册时会互相覆盖,表现出来就是节点数量对不上、Pod 调度到"不存在的节点"、kubelet 日志里反复报注册失败。更隐蔽的情况是机器克隆——从一台模板机复制出来的虚拟机,主机名没改,装完之后两台机器的 kubelet 抢同一个节点对象,你删掉节点它又自己回来,看起来像是"节点删不掉"。
内网地址必须在配置文件里明确写出来,不要指望工具自动探测。多网卡机器上,自动探测选中的往往是管理网卡而不是业务网卡,或者是某个虚拟网桥的地址。表现出来是集群装完了,但节点之间通信走错了路径,跨节点 Pod 访问时通时不通。KubeKey 的配置里对每个主机都有地址字段和内网地址字段,前者用于 SSH 连接,后者用于集群内部通信,两个都填清楚,能省掉大量麻烦。
hosts 解析这块,我的做法是要么全部走 DNS,要么全部写 hosts 文件,绝不允许一半一半。混用最典型的问题是:安装时用的是 IP,后来有人加了域名解析,某些组件缓存了旧结果,导致部分请求走域名部分走 IP,看起来是随机失败。如果集群规模不大,直接在每台机器的 hosts 文件里写死映射反而最稳,改的时候批量下发一次就行。
还有一个实际踩过的坑:主机名里不要带下划线和大写字母。域名规范上不允许下划线,某些组件在校验时直接报错;大写字母在某些解析路径下会被转成小写,导致实际解析结果和你写的不一致。统一用全小写、连字符分隔,是最省心的做法。
2.2 内核模块与 sysctl:不配置会在哪个阶段炸
容器网络依赖几个内核能力,缺了它们,集群可能装得上但网络不通,或者装到一半就失败。这部分内容网上资料很多,但我想说清楚的是每个配置项对应哪个阶段、缺了会看到什么现象,这样排查时你才知道该往哪看。
# 加载网桥相关模块,容器网络的基础 modprobe br_netfilter modprobe overlay # 持久化,避免重启后失效 cat > /etc/modules-load.d/k8s.conf <<'EOF' br_netfilter overlay EOF # 关键内核参数 cat > /etc/sysctl.d/k8s.conf <<'EOF' net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --systemnet.ipv4.ip_forward没开,表现出来是跨节点 Pod 完全不通,同节点内正常,这时候你会怀疑 CNI 插件,其实根因在转发没开。bridge-nf-call-iptables没开,表现更隐蔽:一部分 Service 访问正常,一部分超时,因为 NAT 规则没有对网桥流量生效。overlay模块没加载,如果用的是 overlay 类型的存储驱动,容器起不来,kubelet 日志里报驱动初始化失败。
另外两个经常被漏掉的系统层面配置:一是文件句柄上限,默认值在几千的量级,节点上跑几十个 Pod 之后,容器运行时会开始报 too many open files;二是交换分区必须关闭,kubelet 启动时会检查,开着就直接拒绝启动,报错信息还算直白,但如果你是在模板机里就开着 swap,批量克隆之后每台都要处理一遍。
还有个细节是防火墙。分布式场景下,地址漂移依赖的协议、以及 etcd 节点之间的通信端口,都需要放行。如果选择直接关掉防火墙,记得确认环境的安全策略允许这么做;如果不允许,就必须把端口清单列全,包括控制面的入口端口、etcd 的对等通信端口、kubelet 的通信端口、以及 CNI 插件自己用的封装协议。我见过最典型的失败是:防火墙只放行了 TCP,地址漂移用的协议是独立的网络层协议,被直接丢掉,导致 VIP 在节点间来回跳。
2.3 时间同步与证书签发时间的隐藏关联
时间不同步这件事,危害程度和它被重视的程度严重不成比例。我在三个不同项目里遇到过时间漂移引发的故障,每一次的排查时间都超过半天,因为现象完全不像时间问题。
最直接的影响是证书校验。集群里所有组件之间都走双向证书认证,证书有效期校验依赖本地时间。如果某台节点时间快了一天,而集群刚签发的证书开始时间设的是稍早一点,那这台节点在极端情况下会认为证书"还未生效";反过来,时间慢的节点会认为证书已经过期。表现出来是某个节点的 kubelet 突然连不上 apiserver,日志里全是 TLS 握手失败,重启 kubelet 也没用,你会花大量时间怀疑证书本身。
第二层影响是 etcd 的一致性。etcd 的租约机制依赖时间,节点间时间偏差过大时,租约可能提前过期或者迟迟不过期,表现出来是 Pod 的续约异常、节点被误判为失联。
第三层是日志分析。三台节点日志时间不一致,你想还原一次故障的时间线,得先在脑子里做时区换算,效率极低。
处理方式很直接:在配置文件里指定统一的内网时间源,让工具在部署时自动完成同步配置,同时把定时同步服务设成开机自启。不要依赖手动同步,人一定会忘。如果内网没有可用的时间源,用公共时间源也行,但要确认网络策略允许。同步完成后,用简单命令核对各节点时间偏差,控制在秒级以内再往下走。
如果节点重启后时间没有自动同步,很可能是定时任务没设成开机启动,或者同步服务在容器运行时启动之后就再没跑过。部署完成后专门检查一次,能省掉未来某次深夜排障。
3. config-sample.yaml 逐段拆解:字段写错就是一次事故
3.1 hosts 与 roleGroups:角色划分的边界
KubeKey 的配置文件是整个部署过程唯一的真相来源,我建议把它当成一份需要评审的代码来对待,而不是随手改改的运行参数。先看主机和角色这两段,它们的核心是把"哪些机器"和"每台机器干什么"分开描述。
apiVersion: kubekey.kubesphere.io/v1alpha2 kind: Cluster metadata: name: sample-cluster spec: hosts: - {name: node-a, address: 10.0.0.11, internalAddress: 10.0.0.11, user: ops, password: "***"} - {name: node-b, address: 10.0.0.12, internalAddress: 10.0.0.12, user: ops, password: "***"} - {name: node-c, address: 10.0.0.13, internalAddress: 10.0.0.13, user: ops, password: "***"} - {name: node-d, address: 10.0.0.14, internalAddress: 10.0.0.14, user: ops, password: "***"} roleGroups: etcd: - node-a - node-b - node-c control-plane: - node-a - node-b - node-c worker: - node-dhosts里每台机器用名字做标识,后面的角色分配全部引用名字,这样地址变更时只改一处。这里最容易出错的是名字和实际主机名不一致。我的习惯是让配置里的name和机器上执行hostname的结果完全一致,避免中间多一层映射。曾经有一次配置里写的是别名,实际主机名是另一套,装完之后节点对象的名字全是别名,和运维台账对不上,后续所有排查都要在脑子里做一次转换。
roleGroups的划分有几条经验。etcd 和 control-plane 可以共节点,但要注意资源竞争。小规模集群里共节点是最省机器的方案,三台机器同时承担两个角色,但 etcd 对磁盘延迟极度敏感,控制面上的组件(尤其是 apiserver 的审计日志)会持续写盘,两者抢同一块盘时,etcd 的写延迟会被拉高。上线前用简单的压测工具测一下磁盘顺序写和随机写的性能,如果随机写延迟在毫秒级以上,考虑给 etcd 单独挂一块盘,把数据目录指过去。
worker 节点不要和控制面混部,除非集群很小且你对干扰有心理准备。工作负载的突发 IO 和内存占用会直接影响控制面稳定性,尤其是那种会短时间吃掉大量内存的批处理任务,触发系统 OOM 时最先被杀的很可能是控制面进程。
还有一个实操建议:第一台 worker 可以晚一点加。先用三台控制面把集群骨架跑通、验证入口和调度都正常,再横向扩容 worker。这样出问题时变量少,定位快。我见过有人在配置文件里一次性写十几台机器,第一次执行就失败,然后花很久才发现是某一台机器的 SSH 密钥有问题。
3.2 controlPlaneEndpoint:VIP、端口和漂移行为
这一段是高可用配置的心脏,值得单独拿出来讲。
controlPlaneEndpoint: domain: lb.kubesphere.local address: "10.0.0.100" port: 6443address是那个"永不变化的入口地址",通常是一个没有被任何机器占用的内网 IP。所有节点的 kubeconfig、所有组件的连接配置,最终都指向这个地址。它能在节点间漂移,漂到哪台机器,哪台机器就承接入口流量。
这里的关键认知是:VIP 不是负载均衡,它是入口的稳定性保证。真正的流量分发靠的是每个控制面节点上那套本地代理组件——它监听 VIP 上的端口,把请求转发到后端所有健康的 apiserver。所以实际链路是:客户端连 VIP,VIP 当前在某台节点上,那台节点上的代理根据健康检查结果挑一个 apiserver 转发过去。
这就解释了两个常见现象。第一,当持有 VIP 的节点宕机,客户端会出现短暂连接中断。这是地址漂移本身需要的时间,通常在一到三秒之间。如果你的业务客户端对这么长的中断敏感,就得考虑在外层再加一层重试,或者干脆用外置负载均衡方案。第二,某个 apiserver 健康检查判定异常时,代理会把它从后端摘掉,这时候集群整体还能工作,只是容量下降。如果三个 apiserver 里有两个被判定异常,剩下的那个会承担全部流量,可能出现连接排队。
端口的选择也有讲究。默认的入口端口和 apiserver 实际监听的端口可以不同,代理会做转发。我见过有人把入口端口改成 443,图个"看起来正规",结果某些组件内部写死了默认端口导致连接失败。除非有明确的端口规范要求,否则保持默认,减少不必要的变量。
还有一个配置项常被忽略:如果环境里已经有别的地方占用了同一个 VIP,或者 VIP 所在的网段和 Pod 网段重叠,漂移会失败或者出现路由混乱。部署前用地址解析工具扫一遍,确认这个 IP 没有任何机器响应,是最基本的检查。
3.3 network 段落:Pod 网段冲突是最高频的失败原因
网络配置看起来简单,但它是新手部署失败的第一大来源,而且失败现象往往具有欺骗性——集群装成功了,节点都是 Ready,但 Pod 起不来或者互相访问不通。
network: plugin: calico kubePodsCIDR: 10.233.64.0/18 kubeServiceCIDR: 10.233.0.0/18kubePodsCIDR是 Pod 地址段,kubeServiceCIDR是 Service 地址段,两者不能重叠,也不能和宿主机所在的网段重叠。最后这条是最容易踩的:如果你的机器在10.233.0.0/16这个网段里,而 Pod 网段也选了10.233.x.x,那么集群内部访问 Pod 地址时,路由会认为这是本地网段直接投递,永远到不了 CNI 插件那里。表现出来就是 Pod 之间 ping 不通,但你在节点上抓包会发现包根本没出网卡。
我的习惯是选一个显式的私有网段规划表,在动手前就写清楚:
| 用途 | 建议规划 | 说明 |
|---|---|---|
| 节点管理网 | 按现网规划 | 不要随意改 |
| Pod 网段 | 独立私有段,与现网不重叠 | 建议 /16 或更大,为扩容留空间 |
| Service 网段 | 另一个独立私有段 | 与 Pod 段不重叠 |
| 集群入口 VIP | 管理网内的空闲地址 | 用前必须确认未被占用 |
选段大小的时候要算一下:/18大约能容纳一万六千个地址,对于几百个 Pod 的规模绰绰有余;但如果未来打算跑上万 Pod,就得用/16。改这个值在集群建好之后非常麻烦,需要重建网络插件,所以一开始就按最大预期来规划。
网络插件的选择也值得说两句。常见选择包括功能全面的方案和轻量简洁的方案,前者策略能力强、可观测性好,但组件更多、资源占用更高;后者部署简单、性能开销小,但策略表达能力和排查工具相对有限。我的判断标准是:如果未来需要做 Pod 级别的网络隔离策略,直接选功能全面的方案;如果只是内网服务互联且规模可控,轻量方案反而更省心。切换插件在集群建好之后要清理一堆残留资源,别指望一条命令搞定。
3.4 registry 与 addons:离线部署的两块拼图
内网环境几乎一定需要私有镜像仓库。配置里这一段的作用是告诉集群"镜像从哪里拉",以及"要不要在集群里顺手装一个仓库"。
registry: privateRegistry: "registry.internal:5000" namespaceOverride: "k8s" registryMirrors: [] insecureRegistries: - "registry.internal:5000"如果环境里已经有仓库,直接指向它,同时把地址加到非安全仓库列表里——内网仓库多半没有正规证书,不加这一项,节点拉镜像会报 TLS 校验失败,而错误信息通常埋在容器运行时的日志里,不翻不知道。
如果环境里没有仓库,可以在部署时让工具顺手装一个,代价是占用一台节点的一点存储空间,好处是自包含。我最开始倾向于不用内置仓库,觉得多一个组件多一份维护负担,后来发现内置仓库在节点重建场景下特别方便——节点重装之后,镜像直接从集群内拉,不用依赖外部网络。
addons段落是安装集群插件的地方,可以配本地存储、监控组件、入口控制器等。这里的经验是:第一批只装最必要的,别贪多。集群刚建好时,先把网络、DNS、基础监控装上去,验证一遍;其他的按需再加。一次性装一堆插件,某个失败了你很难判断是集群问题还是插件配置问题,而且首次部署失败重来的成本不低。
本地存储插件特别需要留意。很多集群在没有持久化存储的情况下能跑得很好,一旦要部署有状态服务就开始抓瞎。如果打算用本地盘做存储,务必确认数据目录挂在一个有足够空间的独立分区上,不要和系统盘共享。我见过本地存储插件把系统盘写满,导致节点上的容器运行时无法写入,kubelet 直接罢工,节点变 NotReady。
4. 从 create cluster 到第一个 Pod 跑起来
4.1 执行前的十分钟自查清单
在敲下那条创建命令之前,我会花十分钟把所有前置条件过一遍。这十分钟能省掉后面几小时的返工。
# 1. 所有节点时间偏差 for h in node-a node-b node-c node-d; do ssh $h "date +'%F %T'" done # 2. 所有节点主机名唯一且已配置解析 for h in node-a node-b node-c node-d; do ssh $h "hostname" done # 3. VIP 未被占用 ping -c 2 10.0.0.100 || echo "VIP 空闲,可用" # 4. 关键端口未被占用 ss -lntp | grep -E '6443|2379|2380|10250' || echo "端口空闲" # 5. 内核参数已生效 sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptables清单里最容易被跳过的是 VIP 占用检查。上一套集群遗留下来的地址漂移组件如果没清理干净,可能还占着这个 IP,新集群装的时候两边打架,表现出来是"入口时通时不通",而且日志里没有任何明确报错。
还有一项检查是磁盘空间。控制面节点的系统盘至少留出二十个 G 的空闲,容器运行时的数据目录、kubelet 的临时目录、日志目录都在上面。空间不足时,最先出问题的是容器启动,报错信息通常和空间无关,绕半天才发现是盘满了。
如果用的是密码登录,还要确认密码里的特殊字符在配置文件里被正确转义。我遇到过一次密码里带$符号,在 YAML 里被当成变量展开,导致 SSH 认证失败,报错信息是"认证失败",排查方向完全跑偏,最后逐字对比才发现。稳妥的做法是用密钥认证,配置里写私钥路径,既避免转义问题,也省掉密码管理的麻烦。
4.2 部署日志的阶段性解读
创建命令执行起来之后,日志是唯一的进展指示。理解日志的阶段划分,能让你快速判断卡在哪。
第一个阶段是环境准备。工具通过 SSH 连到每台机器,检查系统信息、安装容器运行时、分发二进制文件、配置系统参数。这个阶段的日志会反复出现主机名,你能看到它在逐台推进。卡在这里通常是 SSH 不通、权限不足、或者系统版本不受支持。特别提醒权限问题:如果用的是普通用户,必须确保该用户能免密执行特权命令,否则会在某个环节静默卡住。
第二个阶段是控制面初始化。第一台控制面节点会执行 kubeadm 的初始化,生成证书、拉起静态 Pod、写入集群配置。这个过程日志量不大,但耗时较长,因为要等各个组件就绪。如果卡在这里,最常见的原因是镜像拉不到或者网络插件初始化失败。看日志里的镜像地址,确认仓库可达。
第三个阶段是其余节点加入。其他控制面节点和 worker 节点依次加入集群,这个阶段日志最多,也最有可能失败。卡在这里要重点看两件事:加入命令是否过期(加入凭证有有效期,大规模部署时如果中途卡太久,后面的节点会因为凭证过期失败),以及网络插件是否在所有节点上正常启动。
第四个阶段是插件安装。按配置装网络插件、DNS、监控等。这个阶段的失败往往和镜像、存储挂载有关。
我个人的习惯是把完整的部署日志重定向到文件保存,同时在另一个终端盯关键节点的 kubelet 日志。前者用于事后回溯,后者能实时看到问题。第一次部署失败时,光看终端滚动的输出很容易漏掉关键行。
4.3 集群起来后的四层验证法
集群装完、命令返回成功,不等于可用。我按四层依次验证,每层都有明确的判断标准,任何一层不过就不往下走。
第一层是组件状态。看节点是否全部就绪、系统组件是否全部运行。这一层不过,说明集群骨架有问题。检查时注意系统组件的就绪副本数,有时候显示运行中但就绪数为零,说明健康检查没过,实际不可用。
kubectl get nodes -o wide kubectl get pods -n kube-system kubectl get --raw='/readyz?verbose'/readyz这个接口比看 Pod 状态更准确,它会把每个控制面组件、每个 etcd 节点的健康检查结果逐条列出来,哪一项失败一目了然。我习惯把它作为"集群是否可以交付"的硬性门槛。
第二层是网络连通性。在两个不同节点上各起一个临时 Pod,互相访问,同时访问一个 Service。这一层不过,说明网络插件没正常工作。检查时先看网络插件的 Pod 日志,再看节点的路由表和转发规则。
第三层是入口可用性。用 VIP 地址去访问 apiserver,模拟客户端的真实路径;然后故意停掉一个控制面节点,观察客户端是否还能正常请求。这一步很多人不做,但它恰恰是高可用配置是否真正生效的唯一验证方式。停掉节点之后,如果你发现客户端连不上了,说明漂移没生效或者代理配置有问题。
第四层是调度与存储。创建一个带持久化卷声明的有状态服务,确认能正常调度、能挂载存储、能读写数据。这一层不过,集群只能跑无状态业务。
四层都过之后,我会再做一次破坏性验证:随机停掉一台控制面节点,确认集群各项操作仍然正常,然后再恢复。这个验证看起来多余,但它是从"装好了"到"敢用了"之间的分界线。
5. 四类真实翻车场景的排查链路
5.1 etcd 三节点仍然写不进去
现象:三节点 etcd 全部显示健康,但创建资源时偶尔超时,压测时大量请求失败。
这类问题的排查链路我一般这样走。先看 etcd 的写延迟指标,重点看落盘操作的耗时分布。如果 P99 延迟在几十毫秒以上,问题基本锁定在磁盘。三节点都在同一台宿主机上、或者共用同一个存储卷,就会出现"看起来三个节点,实际上共享同一个 IO 瓶颈"的情况。
第二步看网络延迟。etcd 节点之间的对等通信对延迟敏感,如果三台机器跨了机房、或者网络路径上有明显的拥塞,选主和日志复制都会变慢。用简单的网络测试工具测一下节点两两之间的往返时延,正常情况下应该在同一数量级。
第三步看资源竞争。控制面组件跑在同一批机器上时,apiserver 的审计日志、容器运行时的写操作会和 etcd 抢磁盘。我曾经在一个环境里发现,审计日志被配置成记录所有请求的完整内容,每秒写几十兆,直接把 etcd 的落盘延迟拉到几百毫秒。把审计级别从最详细降到元数据级别之后,问题立刻消失。
排查过程中还有一个容易忽略的点:etcd 的数据目录是否用了单独的磁盘。如果和数据目录挤在一起的是系统盘,那系统的日志写入、包管理操作的临时文件都会影响 etcd。确认方式是看数据目录所在分区的 IO 情况,把 etcd 目录单独挂到一块盘上,通常能直接解决大半问题。
最后补充一个容易误判的情况:集群只有两个 etcd 节点。这种配置下,任何一台挂掉都会失去多数派,集群直接变成只读甚至不可用。有人觉得"两台总比一台强",实际上两台是最差的配置,可靠性还不如单台。etcd 节点数必须保持奇数。
5.2 某个 apiserver 挂掉后 kubectl 连接卡住 30 秒
现象:停掉一台控制面节点,kubectl 命令没有立即报错,而是卡住半分钟才返回结果,偶发成功偶发失败。
这个现象几乎可以确定是健康检查的判定时间太长。本地代理组件定期检查后端每个 apiserver 的健康状态,检查间隔加上失败重试次数,决定了"故障被发现"需要多久。如果间隔设得太长,故障节点会在这段时间里持续被转发请求,客户端就会卡住。
排查的时候先确认代理组件的配置,看健康检查的间隔、超时、重试次数。这三个值决定了故障检测时间,粗略估算方式是间隔乘以重试次数。默认值通常在秒级,如果你发现要等十几秒,说明参数被改过或者检查本身很慢。
另一个可能的原因是连接的复用。客户端和 apiserver 之间的长连接如果落在那台挂掉的节点上,不会立即失效,要等 TCP 超时才会重连。这时候即使代理已经把这个后端摘掉了,已有的连接仍然卡着。解决办法是在客户端侧配置合理的连接超时和重试,或者用短连接模式(代价是性能下降)。
还有一种情况是VIP 漂移失败。如果持有 VIP 的节点恰好是被停掉的那台,VIP 应该漂到别的节点上。漂移依赖地址解析协议或者类似的机制,如果网络设备做了防护、或者节点之间的二层通信被限制,漂移就不会发生。表现出来是整个入口地址完全不可达,而不只是某个后端被摘掉。这个和上一个现象的区别很明显:前者是"慢",后者是"完全不通"。
排查漂移问题,最直接的方式是在停掉节点之后,从另一台机器上查这个 VIP 对应哪个物理地址,看是否发生了变更。如果没有变更,去检查地址漂移组件的日志和状态,确认它是否认为自己对端还活着。
5.3 扩容节点后集群忽然变成不可调度
现象:往配置文件里加了几台 worker,重新执行之后,新节点是 Ready 的,但 Pod 调度不上去,或者老节点上的 Pod 被驱逐。
这个问题的根因往往在配置文件被改动的位置不对。重新执行部署命令时,工具会对比现有集群和目标状态,做增量调整。如果你改了网络段、改了节点角色、或者不小心删掉了某个角色分组里已有的节点,它会尝试"修正"现有集群,结果可能超出预期。
我第一次遇到这个问题时,是因为把一台原本属于控制面分组的节点从列表里删掉了,本意是"不想让它再当控制面",结果工具执行了移除操作,顺带把它上面的 etcd 成员也删了,集群失去多数派,短暂不可用。这件事给我的教训是:扩容和缩容是两件完全不同的事,扩容基本安全,缩容需要单独设计流程。
扩容的正确做法是只往列表里加,不动已有的条目。加完之后执行时,工具会做节点发现、安装依赖、加加入集群,过程相对安全。执行完检查新节点的标签和污点是否符合预期,有时候新节点会带上不可调度的污点,需要手动处理或者通过配置指定。
还有一种调度不上去的原因是资源请求。新节点的可分配资源被系统组件预留了一部分,实际可用量比你算的少。如果 Pod 的资源请求设得比较大,节点看起来空着却调度不上去。用节点描述命令看可分配资源和已分配资源,对比一下就清楚了。
如果发现节点上的 Pod 被驱逐,要去查驱逐原因。常见的是磁盘压力或者内存压力触发,节点上的临时存储用量超了阈值。新节点刚加入时,镜像拉取会占用大量临时空间,如果盘不大,很快就触发驱逐。给数据目录留足空间,或者调整驱逐阈值,都能缓解。
5.4 证书到期前的静默失效
集群证书有有效期,这几乎是所有自建集群的定时炸弹。默认有效期通常是一年,一年之后的某个时间点,集群突然不可用,所有组件报证书过期。
这个问题的可怕之处在于它是静默的。没有告警,没有渐变,到了那个时间点直接就坏了。而且坏的方式很迷惑:kubectl 连不上,但节点上的容器还在正常运行,业务可能完全没感知,直到你需要操作集群时才发现。
排查思路很直接:用证书检查命令看每个证书的剩余有效期,把快过期的列出来。我建议把这件事做成例行巡检的一部分,而不是等出问题再查。
# 检查控制面证书剩余有效期 kubeadm certs check-expiration # 集群内置的 CA 证书有效期通常更长,但也要确认 openssl x509 -in /etc/kubernetes/pki/ca.crt -noout -dates处理方式有两类。一类是定期手动续期,简单直接,但要记得住。另一类是部署时就把证书有效期拉长,代价是需要重新签发,且部分组件的证书不推荐设太久。我的做法是两者结合:部署时把控制面证书有效期设成较长的值,同时把续期检查纳入月度巡检,双保险。
续期操作本身不算复杂,对每个控制面节点执行续期命令,然后重启相关组件。但要注意顺序和影响面:续期会导致 apiserver 短暂重启,如果同时对所有节点操作,集群会有短暂不可用窗口。稳妥做法是逐个节点进行,每台操作完等组件完全就绪再下一台。
还有一类证书容易被忽略:聚合层证书和前端代理证书。这些证书不在常规检查的输出里,但过期后会影响某些扩展 API 的访问。检查方式是遍历证书目录下的所有文件,逐个看有效期,虽然麻烦,但比出事之后再查强。
6. 交付之后的长期维护节奏
6.1 etcd 快照与集群配置的双备份
集群能不能扛住故障,一半看架构,一半看备份。我见过太多环境,架构做得漂漂亮亮,但从来没有验证过恢复流程,真出事的时候发现备份文件是空的。
备份分两层。第一层是etcd 快照,这是集群状态的唯一权威来源,包含了所有的资源对象定义。快照要定期做,并且必须验证可恢复性。我建议的做法是每周做一次全量快照并保存到集群之外的存储,每月做一次恢复演练——在一个隔离环境里用快照起一个临时集群,确认数据完整。演练这一步最容易被省略,但也最重要,因为快照命令返回成功不代表文件可用。
# 在控制面节点上执行快照 ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%F).db \ --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 # 校验快照完整性 etcdctl snapshot status /backup/etcd-$(date +%F).db --write-out=table快照文件的大小和集群里对象数量相关,通常不大,但要注意保存位置不要和 etcd 数据目录在同一块盘上。同一块盘坏了,数据和备份一起没。
第二层是集群配置文件和相关脚本的版本管理。把部署用的配置文件、定制化的插件配置、巡检脚本全部放进版本控制,每次变更都留记录。这个习惯的价值在故障时体现得最明显:你需要知道"上一次能正常工作时的配置长什么样",没有版本记录就只能靠记忆。
还有一个实践是把配置文件和实际集群做定期比对。集群运行久了,总会有人手动改点东西,配置文件和实际状态逐渐偏离。下次重新执行部署命令时,工具会按文件里的定义"纠正"集群,那些手动改的东西就没了。定期比对能提前发现这种偏离。
6.2 版本升级的路径选择与回退预案
升级这件事,我现在的态度比刚入行时保守得多。早期觉得"新版本有新特性,早点升",现在更倾向于"没有明确收益就不升,要升就做好回退准备"。
升级路径上,有几个原则。不要跨太多小版本,版本跨度太大时,中间版本引入的变更会被一次性叠加,出问题时很难定位是哪次变更导致的。稳妥做法是逐个小版本推进,每升一次跑一遍验证,确认没问题再继续。
先升控制面,再升工作节点,这个顺序不能反。工作节点上的 kubelet 版本不能高于控制面版本,反过来通常允许一定范围的偏差。
升级前必须做一次完整备份,包括 etcd 快照和配置文件。同时准备好回退方案:如果升级后发现严重问题,能不能退回旧版本?控制面组件的回退相对可行,etcd 数据格式的降级通常不可逆,所以升级前的快照是唯一的退路。
升级过程中的验证节奏,我的做法是每完成一个阶段就做一次四层验证的简化版:组件就绪、网络连通、入口可用。任何一项不过就停下来,不要抱着"升完再说"的心态继续。
还有一个很实际的经验:升级窗口要留够。小规模集群升级可能半小时搞定,但节点多、或者采用逐个节点滚动的方式,时间会拉长很多。如果窗口不够,中途停下来会留下一个半新半旧的状态,比不升级更麻烦。
6.3 一份能直接抄的日常巡检清单
最后分享一份我自己在用的巡检清单,不涉及任何平台的专有功能,纯命令行就能跑。频率上,核心项每天看,次要项每周看。
| 检查项 | 命令要点 | 关注阈值 |
|---|---|---|
| 节点状态 | 查看所有节点是否就绪 | 出现 NotReady 立即处理 |
| 系统组件 | 查看系统命名空间 Pod 状态 | 就绪副本数不足要查原因 |
| 控制面健康 | 访问就绪检查接口 | 任一子项失败即异常 |
| etcd 延迟 | 查看落盘耗时指标 | P99 持续偏高要查磁盘 |
| 磁盘空间 | 查看各节点分区使用率 | 超过 80% 开始干预 |
| 证书有效期 | 检查各证书剩余时间 | 剩余不足 60 天安排续期 |
| 事件 | 查看近期异常事件 | 关注频繁出现的同类事件 |
| 备份 | 确认最新快照存在且可校验 | 缺失或损坏立即补做 |
清单里的"事件"这一项经常被低估。事件里能看到很多还没演变成故障的苗头,比如某个 Pod 反复重启、某个卷挂载超时、某个节点频繁失联又恢复。这些信号单独看都不严重,但同一类事件在短时间内反复出现,基本预示着一个即将到来的问题。
巡检结果我习惯用一个简单的表格记录,每天一行,异常项标出来。时间长了能看出趋势,比如某个节点的磁盘使用率每周涨两个百分点,那你就能提前算出什么时候需要扩容,而不是等它满了才手忙脚乱。
我个人在实际操作中的体会是,高可用集群真正的门槛不在搭建阶段,而在搭建完之后的那几个月——那些配置文件里看起来无害的默认值、那些"先这样以后再优化"的临时决定,会在某个深夜一起找上门来。所以与其追求一次装得多漂亮,不如把巡检、备份、验证这几件枯燥的事变成固定动作,让集群的问题在变成事故之前就被发现。