前言
好久没有写这个系列了,首先我决定重启技术文章的更新,原因是我打算引入AI的编写内容,想了很久,技术文章的某个点深入后,AI也许懂得比我还多,总之先尝试下吧。
回到标题,起因是去到客户现场,客户想做整个K8S集群的不可用故障注入,问我有没有什么好的办法。我给了一些方案:
- 将所有节点进行关机或者重启
- 将api-server副本数缩0(适用于kubeadm等部署方式)或者将master节点搞挂
- 将所有节点与api-server的通信拦截掉
刚开始我其实只想到前两种办法,第一种也是最容易想到的,客户现在也是这么做的,但是痛点就是重启的时间太长了,没法快恢。
第二种方案是其他客户模拟过,将api-server搞挂掉,所有节点无法连接到控制面,等待DNS缓存开始过期后,整个集群开始不可用。但这种方式因为云上的api-server都是托管的,如果需要不可用需要走很复杂的授权和变更流程。
最后一种是因为客户提到了他们经历的一次故障,由于将service的Loadbanlance外部端口绑定到了api-server的LB上,导致所有节点和api-server的通信断掉,引起了整个集群不可用。这我在内部还写了一篇文章,下次有机会讲讲。总之,客户不经意的提醒,让我想起了第三种方案。
第三种方案只需要将节点和api-server的网络通信断掉,就可以模拟集群“挂掉”的场景。但我写这篇文章不是为了讲怎么注入故障,而是当节点和api-server失联、或者api-server不可用后整个集群挂掉的具体表现是什么,也算是重新再复习一下。
声明:下面有些是deepseek写的。
正文
api-server不可用的时候,集群是否还能接收流量?
通过ClusterIP访问Service
能接收流量,原理:kube-proxy(通常基于iptables/IPVS模式)将Service规则直接下发到节点的内核转发链中。这些规则是静态存在于每个节点内存里的,不依赖apiserver实时查询。
影响:
已存在的Service → Pod的转发完全正常。
新建的Service或Pod(例如滚动更新、扩容)→ 新规则无法写入节点,新Pod无法被负载均衡。
如果后端Pod因健康检查失败被kubelet杀掉,旧的转发规则仍会指向不存在的Pod IP,导致502或连接拒绝。
通过NodePort访问Service
能,情况同ClusterIP,原理:NodePort本质上是ClusterIP规则在每个节点iptables/IPVS中的额外入口(DNAT链)。
影响:与ClusterIP完全一致。已映射的端口转发继续生效;新部署的服务无法通过NodePort访问。
通过LoadBalancer类型的Service(云厂商LB)
部分能,存在“掉后端”风险。原理:云控制器管理器(CCM)需要监听apiserver中的Node和Service事件,并向云负载均衡器(如AWS NLB、阿里云SLB)同步后端节点IP和Pod就绪状态。
影响:
如果apiserver刚挂,LB上已配置的后端节点列表不会立即消失,流量仍可到达节点,再由kube-proxy转发到Pod。短期内能接收。
但如果节点故障或Pod漂移,CCM无法更新LB后端,LB会将流量继续发给已死的节点,导致故障。
当LB后端健康检查(检查节点kubelet或Pod端口)失败后,云LB会主动剔除该节点,导致流量中断。
通过Ingress(Nginx Ingress Controller等)
能,但只能路由已有规则。原理:Ingress Controller通过List-Watch监听apiserver的Ingress/Service/Endpoint资源,将配置转换为Nginx配置并reload。
影响:
apiserver挂掉后,Controller内存中的路由表不会丢失。现有入站流量(example.com/api → Service)继续正常转发。
无法感知新加入的Ingress规则或后端Pod变化。如果后端Pod全死,Controller无法摘除,仍会尝试将流量转发到无效Pod IP。
某些高级功能(如自动签发证书的cert-manager)完全不可用。
通过Pod IP直接访问(绕过Service)
能,且最稳定。原理:Pod IP直接由CNI(如Calico、Flannel)下发到节点路由表,不经过kube-proxy,也不依赖apiserver进行实时规则同步。
影响:只要Pod和节点网络正常,直接访问Pod IP的流量完全无感知。但你需要通过其他方式(如服务发现、DNS)获取Pod IP列表——这些获取动作依赖apiserver。
通过 Service 名称(DNS)访问
短期内(TTL 内):能接收流量,因为缓存 DNS 记录 + 已有的 iptables 规则都有效。
超过 TTL 或需要解析新 Service 时:无法访问。DNS 解析失败,客户端拿不到 IP,流量根本发不出去。
原理:依赖集群内的 CoreDNS(或 kube-dns)。CoreDNS 组件通过 watch apiserver 来获取 Service 和 Pod 记录。apiserver 不可用时:CoreDNS 无法收到任何 Service 的创建、删除、更新事件。
总结
已有的业务流量(数据平面)基本能正常接收,但所有依赖动态配置或管理操作的流量(控制平面)会立刻或逐渐失败。
能正常接收的流量(短期或长期可用)
- 直接访问 Service ClusterIP(不经过 DNS)
- 访问 NodePort
- 直连 Pod IP
- 通过 Ingress 访问已有路由规则
- 通过 LoadBalancer 访问(短期内,云 LB 后端列表未失效前)
原因:这些流量的转发规则(iptables/IPVS、节点路由表、Ingress Controller 内存配置)在 api-server 故障前已下发到各节点,运行时不依赖实时查询 api-server。
不能或逐渐不能接收的流量
- 通过 Service 名称(DNS) 访问:依赖 CoreDNS 从 api-server 同步记录,缓存过期后(默认30s)解析失败
- 新建 Service / 新部署 Pod 的流量:新规则无法写入节点
- 后端 Pod 已变化的已有 Service:规则陈旧,可能转发到已销毁的 Pod
- 所有 kubectl 管理命令:port-forward、exec、logs、apply、delete 等直接调用 api-server
- 依赖 动态准入控制、自动扩缩容、服务发现注册 的组件
碎碎念
总而言之,api-server挂掉后,集群不会立即不可用,除非你全部用的coreDNS做服务发现,因此也建议生产环境用注册中心一类的东西,将数据面与管控面解耦。
其次,如果一定要依赖coreDNS,一定要对coreDNS做好容量规划、ttl和cache。
单个节点和api-server失联,什么时候触发剔除?
当一个节点与 API Server 通信中断时,需要区分 Service 剔除 Pod 和 API Server 驱逐 Pod 这两个动作,它们的触发时机和依赖机制不同。
Service 什么时候剔除该节点上的 Pod?
Service 通过 Endpoints(或 EndpointSlice)选择后端 Pod。Pod 要从 Service 中剔除,必须满足以下条件之一:
- Pod 的 Ready 条件变为 False
- Pod 被标记为 Terminating(即被删除/驱逐)
但实际上,节点失联后,Pod Ready 状态为何不会立即变化,原因是: - Pod 的 Ready 状态是由 kubelet 主动上报给 API Server 的。
- 节点与 API Server 失联后,kubelet 无法上报状态,API Server 中该 Pod 的 Ready 条件将停留在最后一次上报的值(若之前是 True,就一直保持 True)。
- 即使 kubelet 本地仍在运行健康检查,也不会触发 Ready 变为 False,因为没有通信链路。
因此,失联初期,Service 仍将这些 Pod 视为 Ready,流量依然会被转发过去,如果节点只是与控制平面网络隔离(仅和 API Server 失联),但数据平面(Pod 网络、与其它节点的通信)仍然正常,那么 Service 转发到该节点 Pod 的流量并不会形成黑洞——流量能正常到达 Pod 并被处理,服务仍然可用。
为什么实际注入故障后,应用出现一些报错?
至于演练时,阻断单节点和api-server通信后,遇到的应用访问失败现象,AI推测很可能是 DNS 解析间歇性失败(实际上是错误的):
Pod 的 DNS 请求指向 CoreDNS 的 Service ClusterIP(例如 10.96.0.10)。
节点上的 iptables/IPVS 规则(由 kube-proxy 维护)会将发往该 ClusterIP 的请求随机负载均衡到所有 CoreDNS 后端 Pod。
由于该节点与 API Server 失联,节点上的 kube-proxy 无法感知 Endpoints 的变更。即使异常 CoreDNS Pod 的 Ready 状态变为 False,并被 API Server 从正常节点的规则中剔除,本节点内的 iptables 规则仍然保持着旧的端点列表,其中依然包含这个已经不健康的本地 CoreDNS Pod。
因此,每次 DNS 请求都有一定概率(取决于后端的数量)被转发到这个无法解析集群内域名的本地 CoreDNS 副本,从而导致解析失败。
经过我的验证,AI的说法完全错误,当某个节点和api-server断联,节点上的coreDNS容器依旧可以解析集群内域名。通过对源码的研究,可以发现CoreDNS实际上是对集群内域名做了一次性List,再加后续的watch,将集群内域名写入到本地缓存中,而这个缓存是没有过期时间的。也就是当你获取一个集群内域名时,如果api-server已经失联,依旧可以返回一个解析结果。
那么问题来了,如果CoreDNS没问题,那究竟是为什么应用有报错呢?这个我也没搞懂了。还需要持续的测试才行。
api-server什么时候驱逐节点上的 Pod?
当节点与 API Server 断联后,kube-controller-manager 并不会立即驱逐 Pod,通常会等待大约 5 分 40 秒(约 340 秒)。
这是一项保护机制,旨在防止因网络短暂抖动而引发大规模Pod重调度。整个阶段主要是 Node Lifecycle Controller 通过基于污点(Taint)的驱逐逻辑来处理的。
整个过程各阶段和相关默认时间如下:
当节点与 API Server 断联后,kube-controller-manager 并不会立即驱逐 Pod,通常会等待大约 5 分 40 秒(约 340 秒)。
这是一项保护机制,旨在防止因网络短暂抖动而引发大规模 Pod 重调度。整个过程主要是 Node Lifecycle Controller 通过基于污点(Taint)的驱逐逻辑来处理的。
整个过程各阶段和相关默认时间如下:
| 阶段 | 耗时 / 关键参数 | 主要操作 |
|---|---|---|
| 1️⃣ 节点状态更新 | 每 10 秒(--node-status-update-frequency) |
节点上的 kubelet 每 10 秒向 API Server 上报一次心跳。 |
| 2️⃣ Controller 探测 | 每 5 秒(--node-monitor-period) |
kube-controller-manager 中的 node-lifecycle-controller 每 5 秒检查一次节点状态。 |
| 3️⃣ 标记节点不健康 | 等待约 40 秒(--node-monitor-grace-period) |
若连续 40 秒未收到心跳,控制器将该节点标记为 NotReady。同时为其添加 node.kubernetes.io/unreachable 和 node.kubernetes.io/not-ready 污点,调度器也立即停止向该节点调度新 Pod。 |
| 4️⃣ Pod 容忍期 | 默认 300 秒(5 分钟)(tolerationSeconds) |
标记为不健康后,节点控制器会等待 Pod 预设的容忍时间(tolerationSeconds),默认 5 分钟。 |
| 5️⃣ 执行驱逐操作 | 容忍期后立即执行 | 容忍期(如 5 分钟)结束后,节点控制器才开始驱逐 Pod。 |
这个 5 分钟默认计时也是从 Kubernetes 1.20 及之后版本开始,完全采用基于污点的驱逐(TaintBasedEvictions)机制后的默认行为。更早的版本中也有类似的逻辑,但实现方式有所不同。
🎛️ 如何调整等待时间?
你可以通过修改默认容忍时间,让驱逐更快或更慢发生,具体方法如下:
修改 API Server 的启动参数,对所有新建 Pod 生效。
1 | # 修改 /etc/kubernetes/manifests/kube-apiserver.yaml |
为单个 Pod 设置个性化容忍时间
在 Pod 的 YAML 定义中设置 tolerationSeconds 字段,可以实现更精细的控制。
1 | tolerations: |
not-ready和unreachable有什么区别
NotReady 是一个确定的已知状态,表明节点控制器确认节点自身出现了严重问题,无法运行 Pod。
Unreachable 是一个不确定的未知状态,表明节点控制器与节点失联,无法确定其真实状况,大概率是网络问题。
区分这两种状态,主要是为了让集群能够智能地处理故障,而不是盲目驱逐所有 Pod。
当节点状态为 NotReady 时,问题很可能出在节点自身(如 kubelet 崩溃)。此时,该节点上的 Pod 基本已无法正常工作,集群主动驱逐它们以触发重建,是合理的选择。
而当节点状态为 Unreachable 时,问题可能仅仅是网络中断,节点上的 kubelet 和 Pod 其实还在正常运行。如果集群立刻驱逐 Pod,可能会中断一个正在工作的服务。因此,Kubernetes 提供一段容忍期,等待网络恢复,避免因短暂的网络抖动而导致大量 Pod 重调度。
碎碎念
我觉得文章大部分让AI写其实没毛病,但有几个细节其实AI答得不好,比如CoreDNS的行为,我一是自己去搭建集群验证,二是去翻源码,导致我这篇文章本来三天前就要发的,结果验证实在是太麻烦了。最后还是没排查出来是为什么,打算先放一放,有时间慢慢再验证。