Zer0e's Blog

【架构之路13】节点与api-server网络不通后发生了什么

字数统计: 3.5k阅读时长: 12 min
2026/06/07 Share

前言

好久没有写这个系列了,首先我决定重启技术文章的更新,原因是我打算引入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 解析间歇性失败(实际上是错误的)

  1. Pod 的 DNS 请求指向 CoreDNS 的 Service ClusterIP(例如 10.96.0.10)。

  2. 节点上的 iptables/IPVS 规则(由 kube-proxy 维护)会将发往该 ClusterIP 的请求随机负载均衡到所有 CoreDNS 后端 Pod。

  3. 由于该节点与 API Server 失联,节点上的 kube-proxy 无法感知 Endpoints 的变更。即使异常 CoreDNS Pod 的 Ready 状态变为 False,并被 API Server 从正常节点的规则中剔除,本节点内的 iptables 规则仍然保持着旧的端点列表,其中依然包含这个已经不健康的本地 CoreDNS Pod。

  4. 因此,每次 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/unreachablenode.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
2
3
# 修改 /etc/kubernetes/manifests/kube-apiserver.yaml
--default-not-ready-toleration-seconds=300 # 默认为 300
--default-unreachable-toleration-seconds=300 # 默认为 300

为单个 Pod 设置个性化容忍时间

在 Pod 的 YAML 定义中设置 tolerationSeconds 字段,可以实现更精细的控制。

1
2
3
4
5
6
7
8
9
tolerations:
- key: "node.kubernetes.io/not-ready"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 120 # 这个 Pod 仅容忍 120 秒
- key: "node.kubernetes.io/unreachable"
operator: "Exists"
effect: "NoExecute"
tolerationSeconds: 120

not-ready和unreachable有什么区别

NotReady 是一个确定的已知状态,表明节点控制器确认节点自身出现了严重问题,无法运行 Pod。
Unreachable 是一个不确定的未知状态,表明节点控制器与节点失联,无法确定其真实状况,大概率是网络问题。

区分这两种状态,主要是为了让集群能够智能地处理故障,而不是盲目驱逐所有 Pod。

当节点状态为 NotReady 时,问题很可能出在节点自身(如 kubelet 崩溃)。此时,该节点上的 Pod 基本已无法正常工作,集群主动驱逐它们以触发重建,是合理的选择。

而当节点状态为 Unreachable 时,问题可能仅仅是网络中断,节点上的 kubelet 和 Pod 其实还在正常运行。如果集群立刻驱逐 Pod,可能会中断一个正在工作的服务。因此,Kubernetes 提供一段容忍期,等待网络恢复,避免因短暂的网络抖动而导致大量 Pod 重调度。

碎碎念

我觉得文章大部分让AI写其实没毛病,但有几个细节其实AI答得不好,比如CoreDNS的行为,我一是自己去搭建集群验证,二是去翻源码,导致我这篇文章本来三天前就要发的,结果验证实在是太麻烦了。最后还是没排查出来是为什么,打算先放一放,有时间慢慢再验证。

CATALOG
  1. 1. 前言
  2. 2. 正文
    1. 2.1. api-server不可用的时候,集群是否还能接收流量?
      1. 2.1.1. 通过ClusterIP访问Service
      2. 2.1.2. 通过NodePort访问Service
      3. 2.1.3. 通过LoadBalancer类型的Service(云厂商LB)
      4. 2.1.4. 通过Ingress(Nginx Ingress Controller等)
      5. 2.1.5. 通过Pod IP直接访问(绕过Service)
      6. 2.1.6. 通过 Service 名称(DNS)访问
      7. 2.1.7. 总结
      8. 2.1.8. 碎碎念
    2. 2.2. 单个节点和api-server失联,什么时候触发剔除?
      1. 2.2.1. Service 什么时候剔除该节点上的 Pod?
      2. 2.2.2. 为什么实际注入故障后,应用出现一些报错?
      3. 2.2.3. api-server什么时候驱逐节点上的 Pod?
        1. 2.2.3.1. 🎛️ 如何调整等待时间?
        2. 2.2.3.2. 为单个 Pod 设置个性化容忍时间
        3. 2.2.3.3. not-ready和unreachable有什么区别
  3. 3. 碎碎念