上一篇把 Pod 之间怎么通、veth pair 怎么接、CNI 插件在节点上插了哪些网卡这些事拆完了,那还只是 K8s 网络原理的一半。真正让人半夜被叫起来处理问题的,往往不是 Pod 到 Pod,而是 Service 那层虚拟 IP 出问题、跨节点流量莫名丢包、DNS 解析慢了三秒、Ingress 502 却查不出所以然。这篇就专门啃这些硬骨头:kube-proxy 到底在节点上写了什么规则、ClusterIP 这个不存在的 IP 为什么能 ping 通(其实 ping 不通才是对的)、NodePort 的端口是怎么被抢走的、VXLAN 封装之后 MTU 该怎么算、CoreDNS 的 ndots 为什么能把一次调用拖成五次查询。适合已经把 Pod 网络跑起来、开始接触 Service 和 Ingress 的人看,也适合排障时想搞清楚"流量到底走到哪一跳"的人参考。
1. 从 Pod IP 到 Service IP:中间多绕这一层图什么
刚接触 K8s 网络原理的人常有一个困惑:Pod 明明有自己的 IP,容器之间直接通信不就完了,为什么非要再套一个 Service?我最早也有这个疑问,直到线上做了一次滚动发布,前端服务配置里写死了后端 Pod 的 IP,发布完成后所有请求全部 502,那一刻才明白 Service 存在的意义。
1.1 直接使用 Pod IP 会遇到的三个现实问题
第一个问题是 Pod IP 不固定。Pod 重建、节点漂移、扩缩容,IP 全都会变。业务代码或者配置文件里写死一个 Pod IP,本质上和在物理机时代把数据库地址写死成某台机器的内网 IP 一样脆弱。
第二个问题是 Pod IP 的存活周期和业务无关。健康检查没通过、节点资源不足被驱逐、甚至人为删掉一个 Pod 做测试,IP 就消失了。上游调用方没有任何感知机制,只能靠报错。
第三个问题是没有负载均衡。哪怕你手动维护了一份 Pod IP 列表,也需要自己在客户端做轮询、做失败重试、做连接池管理,这部分逻辑每写一遍都是重复劳动,而且每个语言、每个框架实现都不一样。
Service 解决的正是这三件事:给一组 Pod 一个稳定的虚拟 IP,由节点上的转发规则负责把流量分发到当前健康的 Pod 上。业务侧只需要认一个名字或者一个 IP,后面的变化由平台层消化。
1.2 ClusterIP 是一个"约定",不是一块网卡
这一点必须先说清楚,否则后面看规则会一直别扭。ClusterIP 分配的地址段(默认 10.96.0.0/12)在整个集群里没有任何一台机器的网卡上真正配置过。你在节点上ip addr是看不到这个地址的,在 Pod 里也看不到。
那为什么curl 10.96.0.1:443能通?因为 kube-proxy 在节点的 netfilter 或者 IPVS 里写了规则,凡是发往这个地址的包,在离开本机之前就被拦截下来,目标地址被改写成某个真实 Pod IP。这个改写动作发生在内核的网络栈里,应用完全无感。
注意:正因为 ClusterIP 是虚拟的,
ping一个 ClusterIP 通常不通,这并不是故障。ICMP 报文不会走 TCP/UDP 的 DNAT 规则链,所以没有对应的处理逻辑。判断 Service 是否正常,永远用curl或者nc -zv打具体端口,别用 ping。
1.3 一次请求在节点上到底经历了什么
把一次典型的集群内调用拆开看:客户端 Pod 发出一个包,目标地址是 Service 的 ClusterIP,源地址是客户端 Pod IP。这个包从 Pod 的 eth0 出来,经过 veth pair 到达节点侧的网桥或者路由表。在离开节点之前,它必须经过 OUTPUT 链(本节点发起的流量)或者 PREROUTING 链(从别处转发进来的流量),kube-proxy 写入的规则就挂在这些链上。
规则命中之后,包的目标地址从 ClusterIP 换成了某个后端 Pod 的 IP,端口也可能被替换。这个改写是终态的,包接着按普通路由规则走,最终到达目标 Pod。目标 Pod 看到的源地址是客户端 Pod IP(集群内调用),或者是节点 IP(经过 SNAT 的场景),这取决于具体路径。
回程包同样要经过处理,conntrack 表记录了这条连接的映射关系,返回时会被自动还原。所以排查 Service 问题的时候,conntrack命令和iptables-save/ipvsadm -Ln的输出,比看日志有用得多。
2. kube-proxy 的三种模式与规则细节
kube-proxy 是理解 Service 实现的核心组件,它本身不转发任何数据包,只负责在节点上维护转发规则。历史上它有三种工作模式,现在还在用的主要是后两种。
2.1 userspace 模式为什么被弃用
最早的实现方式是,kube-proxy 在节点上开一个用户态进程监听端口,所有 Service 流量先被 iptables 规则重定向到这个用户态进程,进程再根据配置把流量转发到后端 Pod。这条路径的问题很明显:每个包都要从内核态跑到用户态再回内核态,两次上下文切换,吞吐和延迟都不好看,并发一高就是瓶颈。这套模式在很早期的版本就已经不推荐使用,现在基本只在历史文档里能看到。
2.2 iptables 模式:随机数与规则链的配合
iptables 模式全靠内核 netfilter 完成转发,性能比 userspace 好很多,也是很长时间里的默认选择。它的规则结构大概是这样的:有一个总入口链KUBE-SERVICES,每个 Service 对应一条匹配 ClusterIP 和端口的规则,命中后跳到专属的KUBE-SVC-XXXX链;这条链里针对每个后端 Pod 有一条规则,通过statistic模块的概率匹配来分摊流量。
举个例子,一个 Service 有 3 个后端,KUBE-SVC-XXXX里会有三条规则,匹配概率分别是 1/3、1/2、1。这个设计挺巧妙:第一个包有 1/3 概率命中第一条,没命中就继续往下;剩下的流量里再按 1/2 分配;最后全部落到第三条。数学上正好是均匀的三等分。
命中之后跳到KUBE-SEP-YYYY链,里面做两件事:DNAT 改写目标地址为目标 Pod 的 IP 和端口,同时做 MASQUERADE 处理回程。整套规则的条目数量随 Service 和后端数量线性增长,这是 iptables 模式最大的隐患:几千个 Service、上万个 Endpoint 的集群里,规则总数能到十几万条,每次 Service 变更都要重新生成并同步整张表,同步期间 CPU 会明显抖动,而且规则匹配是线性遍历的,靠前的链越长,包处理开销越大。
我踩过的一个具体坑是:集群规模上来之后,某台节点上的iptables-restore执行时间从几十毫秒涨到了好几秒,期间旧规则和新规则不同步,短暂出现流量打到已经下线的 Pod 上。后来切到 IPVS 才彻底解决。
2.3 IPVS 模式:哈希表与调度算法
IPVS 是内核里的一套四层负载均衡实现,规则存在哈希表里而不是链式遍历,查找复杂度是常数级,规则规模对性能影响小得多。开启方式是在 kube-proxy 的配置里把mode设为ipvs,同时需要节点内核加载ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack这几个模块,缺一个都会启动失败。
IPVS 模式下,kube-proxy 会创建一个名为kube-ipvs0的 dummy 网卡,把所有 ClusterIP 都绑上去。这一步是必须的,因为 IPVS 只处理目标地址在本机上的流量,如果不把虚拟 IP 绑到某个网卡上,内核路由阶段就会直接判定地址不可达。这也是很多人切模式之后发现 Service 全不通的原因,多半是模块没加载全或者 dummy 网卡没起来。
验证的时候用这两条命令:
# 看虚拟 IP 有没有绑上 ip addr show kube-ipvs0 | head -20 # 看 IPVS 规则和每个后端的连接数 ipvsadm -Ln ipvsadm -Lnc | head -30IPVS 支持多种调度算法,kube-proxy 层面通过ipvs.scheduler指定,默认是rr(轮询)。实际选型时可以这样考虑:
| 调度算法 | 特点 | 适用场景 |
|---|---|---|
| rr | 严格轮询,不考虑后端负载 | 后端规格一致、请求耗时均匀 |
| wrr | 加权轮询,可给不同权重 | 新老机器混部、规格不一致 |
| lc | 最少连接数 | 长连接多、请求耗时差异大 |
| sh | 源地址哈希 | 需要会话保持但不便用 sessionAffinity |
| mh | 一致性哈希,后端变动影响小 | 有本地缓存的场景 |
个人经验是默认rr就够用,只有在后端处理耗时差异明显(比如有的请求 10ms、有的 2s)的时候,lc的效果才明显好于rr,因为轮询会把请求堆在慢机器上。
2.4 两种模式的关键参数与调优
切 IPVS 之后有几个参数值得关注。ipvs.connTimeout相关的 conntrack 超时、ipvs.minSyncPeriod和ipvs.syncPeriod影响规则同步频率,默认minSyncPeriod是 0 秒、syncPeriod是 30 秒,Service 变更频繁的集群里可以适当调小minSyncPeriod让变更更快生效,但别调得太激进,否则 kube-proxy 自身 CPU 会上去。
另外 IPVS 模式下默认还会保留一套 iptables 规则,用来处理 NodePort、LoadBalancer 这类需要 SNAT 的场景,所以iptables-save里依然能看到KUBE-SERVICES链,看到别慌,那不是没用。
还有一个容易被忽略的点:sessionAffinity在 IPVS 模式下是靠ip_vs的持久连接机制实现的,超时时间由sessionAffinityConfig.clientIP.timeoutSeconds控制,默认 10800 秒。如果客户端 IP 变化频繁(比如都在同一个 NAT 网关后面),源地址哈希和会话保持都会失效,这时候要么上七层方案,要么在应用层做会话共享。
3. 四种 Service 类型的落地与流量路径
Service 的 type 字段决定了它暴露的方式,从内到外一共四种,每种背后都是不同的规则组合。
3.1 ClusterIP:集群内部最常用的形态
默认类型,分配一个虚拟 IP,只在集群内可达。实际使用中有两个细节值得注意。
一是port和targetPort的区别。port是 Service 对外暴露的端口,targetPort是 Pod 容器实际监听的端口。很多人图省事让两者一样,但一旦容器端口改了,Service 就得跟着改。我更倾向把targetPort写成容器的命名端口(比如http),这样容器端口调整时只要改 Pod 模板,Service 不用动。
二是多端口 Service 必须给每个端口起名字。这个限制经常在写 YAML 时被忽略,报错信息也不算特别直白,其实就是name字段必填。
apiVersion: v1 kind: Service metadata: name: demo-api spec: type: ClusterIP selector: app: demo-api ports: - name: http port: 80 targetPort: http - name: metrics port: 9090 targetPort: metrics3.2 NodePort:端口范围与流量策略的取舍
NodePort 在每个节点上开一个端口(默认 30000-32767),外部访问任意节点 IP 加这个端口就能进来。它的实现是 kube-proxy 在节点上写一条规则,把0.0.0.0:端口的流量转到 ClusterIP 再转给后端 Pod。
端口范围可以改,通过 apiserver 的--service-node-port-range参数指定。改这个参数不是没有代价的:范围太宽会和宿主机上已有的服务、其他中间件的监听端口冲突,而且这个冲突往往在 Service 创建成功之后才暴露,表现为连接被别的进程抢走。我的做法是提前把节点上会用的端口段梳理一遍,避开监控 agent、日志采集这类常驻进程的端口。
externalTrafficPolicy是 NodePort 和 LoadBalancer 都需要关注的字段,取值Cluster或Local,差别很大:
| 取值 | 源 IP 是否保留 | 流量可能跨节点 | 典型问题 |
|---|---|---|---|
| Cluster | 否,会做 SNAT | 是,可能转到其他节点的 Pod | 后端看到的都是节点 IP |
| Local | 是 | 否,只转发本节点上的 Pod | 节点上没有 Pod 时连接被拒 |
选Local的动机通常是保留客户端真实 IP(做限流、审计、地域判断都要用到),但代价是流量分布不均:如果只有三个节点上有 Pod,另外两个节点的 NodePort 收到请求会直接拒绝。这时候一般要在前面挂一个负载均衡器,并且让它的健康检查也按节点做,把没有 Pod 的节点摘出去。这个联动配置漏了,表现就是间歇性连接失败,概率大概是"没有 Pod 的节点数 / 总节点数"。
3.3 LoadBalancer:云上实现与自建思路
LoadBalancer 类型是在 NodePort 基础上再向底层基础设施申请一个外部负载均衡器。在云上的实现是由 cloud-controller-manager 调用云厂商接口创建实例并绑定节点,这部分各家实现不一样,但共同点是会产生额外费用,而且创建过程是异步的,Service 创建后一段时间内EXTERNAL-IP会显示<pending>。
自建环境里没有云厂商接口,EXTERNAL-IP会一直 pending。解决办法通常是部署一个地址分配组件,从预留的地址池里给 Service 分配 IP,再通过 ARP 或者 BGP 把这个 IP 宣告到网络里。选 ARP 模式实现简单但只能二层可达,选 BGP 模式能跨网段但对网络设备有要求。
一个实际踩过的坑:地址池的网段必须和节点所在网段错开,否则分配出去的 IP 会和宿主机冲突。上线前一定要用arping在网段里扫一遍目标地址段,确认没被占用。
3.4 Headless 与 ExternalName:两个特殊形态
clusterIP: None的 Service 叫 Headless Service,它不分配虚拟 IP,DNS 查询会直接返回所有就绪 Pod 的 A 记录。这个形态在两种场景下必用:一是有状态应用的节点发现,客户端需要知道每个实例的身份;二是自己做服务发现和负载均衡,比如某些数据库客户端会拉取全部节点地址在本地做分片路由。
要注意的是 Headless Service 的 DNS 解析行为和普通 Service 不同:普通 Service 返回的是 ClusterIP,Headless 返回 Pod IP 列表,而且是经过就绪检查的。如果就绪探针配置不当,Pod 一直不 Ready,DNS 里就查不到它,表现为"Pod 明明在跑,就是解析不到"。查这个问题第一步看kubectl get endpoints(新版本用endpointslices),而不是看 Pod 状态。
ExternalName 更简单,它就是一个 DNS 别名,把 Service 名指向一个外部域名,返回 CNAME 记录,不做任何转发。适合把集群外的数据库、第三方接口包装成集群内的服务名,方便后续迁移。注意它不参与 kube-proxy 规则生成,也没有端口映射,客户端连的端口就是外部服务自己的端口。
4. 跨节点通信:Overlay 与 MTU 的那些事
Pod 在同一节点内通信走网桥,很简单。一旦跨节点,就要看 CNI 插件选的方案了,这也是"网络不通"类问题的高发区。
4.1 同节点与跨节点路径的差异
同节点两个 Pod 通信,包从 Pod A 的 eth0 出来,经 veth pair 到节点网桥,网桥查到 Pod B 的 veth 直接转过去。全程不出主机,不需要额外封装,MTU 是完整的 1500。
跨节点就不同了。节点之间必须有一条通道把 Pod 网段的路由信息传递过去,同时保证包能实际送达。方案分两类:一类是路由方案,把 Pod 网段的路由直接注入到物理网络或者节点路由表,包原样发出,不做封装;另一类是叠加方案(Overlay),把原始包整个封进一个新的包里,在外层走物理网络,到了对端节点再解封。
路由方案的性能损耗最小,因为不做封装,MTU 不减少,抓包也干净。但它要求底层网络能接受这些 Pod 网段的路由,云环境里通常做不到,所以叠加方案更通用。
4.2 VXLAN 与 IPIP 的封装开销
VXLAN 是最常见的叠加协议。它在原始以太网帧外面再套一层:外层 IP 头 20 字节、外层 UDP 头 8 字节、VXLAN 头 8 字节,加起来 36 字节;如果把内层以太网帧头 14 字节也算上,实际可用的载荷空间就少了 50 字节。
所以物理网卡 MTU 是 1500 的情况下,VXLAN 封装的 Pod 网卡 MTU 必须设成 1450。这个值不少 CNI 插件会自动算出来并写到 Pod 的网卡上,但如果你自己改过配置,或者底层网络是 PPPoE、GRE 隧道这种本身 MTU 就小于 1500 的环境,就必须手动校正。
IPIP 封装简单一些,只加一个 20 字节的外层 IP 头,所以 Pod MTU 是 1480。这也是很多人觉得 IPIP 更"轻"的原因,但它的限制是只能承载 IPv4 单播流量,跨网段场景也不如 VXLAN 灵活。
4.3 MTU 不匹配会表现成什么样
MTU 配错的症状很有迷惑性:小包一切正常,ping通,curl一个短 URL 也返回;但只要传输的数据大一点(比如返回一个几 MB 的 JSON、或者建立 TLS 握手时证书链较长),连接就会卡住不动,最后超时。原因是包需要分片,但中间设备丢弃了带 DF 标志的分片包,而对端又收不到 ICMP 的"需要分片"消息(被防火墙拦了),于是双方互相等,直到超时。
排查手段是带 DF 标志指定包大小去 ping:
# 逐步缩小 size,找到第一个不通的大小 ping -M do -s 1422 -c 3 <对端PodIP>这里1422是 1450 减去 8 字节 ICMP 头和 20 字节 IP 头之后的载荷大小。如果 1422 通、1423 不通,说明链路 MTU 就是 1450,配置没问题;如果明显小于预期,那中间某一段有问题,可能是隧道、可能是负载均衡器、也可能是安全设备。
提示:调整 MTU 之后一定要重启 Pod 或者重建网络命名空间才会生效,改配置不重启容器是白改。另外抓包时看到大量 "fragmentation needed" 或者重传,基本可以锁定是 MTU 问题。
4.4 方案选型的实际考量
我一般按这个顺序判断:底层网络能不能注入路由(能就选路由方案,省掉封装开销);不能的话看跨网段需求(需要跨网段就 VXLAN);节点规模大不大(BGP 方案在几百节点以上需要关注路由表规模和收敛时间);有没有加密需求(有的话考虑在 CNI 层面做,而不是自己套一层隧道)。
规模上了千节点之后,VXLAN 的组播和 FDB 表也会成为瓶颈,这时候一般会转向基于节点间全互联或者路由反射器的 BGP 方案。这个切换不是改个配置那么简单,涉及整个集群的网络重建,得在规划阶段就想清楚。
5. DNS 与 Ingress:应用最常碰到的两个入口
前面讲的都是基础设施层的转发,应用开发感知最强的其实是这两个:服务名能不能解析出来、外部请求能不能进来。
5.1 CoreDNS 的解析链路
集群内的 DNS 查询由 CoreDNS 处理。每个 Pod 的/etc/resolv.conf里会写入nameserver(通常是 Service 网段的第 10 个地址,比如 10.96.0.10)和一组search域,ndots默认是 5。
ndots:5的含义是:如果查询的域名里点的数量少于 5 个,就先按 search 列表依次补全再查,最后才查原始域名。这个设计是为了让demo-api这种短名字能直接解析成demo-api.default.svc.cluster.local,但副作用是,一个外部域名比如api.example.com(只有 2 个点)会先被拼成api.example.com.default.svc.cluster.local、api.example.com.svc.cluster.local等等查 4 到 5 次,全部返回 NXDOMAIN 之后才查原始域名。
后果就是每次外部调用多出几毫秒到几十毫秒的延迟,在调用链深的场景里会放大成肉眼可见的慢。优化手段有两个:一是在 Pod 的dnsConfig里把ndots调小(比如 2),二是调用时直接写完整域名并在末尾加点。前者更彻底,但要注意改小之后集群内的短名字仍然能解析(因为 search 域还在),只是查询顺序变了。
spec: dnsConfig: options: - name: ndots value: "2"还有一个常见现象是 DNS 超时。默认timeout:5、attempts:2,如果 CoreDNS 副本数少、并发高,容易触发 5 秒超时然后重试,表现出来就是偶发的接口耗时突然变成 5 秒或 10 秒。这种情况看 CoreDNS 的 QPS 和延迟指标比看业务日志更快定位。给 CoreDNS 加副本、配好反亲和性(别把多个副本调度到同一节点),再把cache插件的缓存时间调长,一般能压住。
5.2 Ingress Controller 的转发路径
Ingress 本身只是一份路由规则声明,真正干活的是 Ingress Controller。以常见的 Nginx 实现为例,它的工作方式是监听 Ingress、Service、EndpointSlice 的变化,动态生成 Nginx 配置并 reload。
关键点在于:流量从 Controller 到后端 Pod 时,很多实现是直接转发到 Pod IP,而不是绕回 Service 的 ClusterIP。这样做的目的是减少一跳、保留客户端信息、支持更细粒度的负载均衡策略。代价是 Controller 必须和 Pod 网络直通,如果 CNI 配置有问题,或者网络策略把 Controller 拦住了,就会出现 Controller 日志里一堆 "upstream timed out",但直接curlClusterIP 却是通的怪现象。
排查这类问题我一般按这个顺序走:先确认 Controller Pod 能不能直接访问后端 Pod IP(kubectl exec进去 curl),再确认 EndpointSlice 里的地址是否和实际 Pod IP 一致,最后看 Nginx 配置里的 upstream 有没有被正确更新。第三点经常被忽略,配置生成了但 reload 失败(比如语法错误、文件权限问题),Nginx 还在用旧配置跑。
5.3 EndpointSlice 与就绪探针的联动
早期版本用 Endpoints 对象存后端列表,一个 Service 的所有后端放在一个对象里,超过 1000 个就得分片,性能和扩展性都有问题。现在用 EndpointSlice,按比例切分,每个切片最多 100 个端点,更新时只同步变化的切片。
这个变化对使用者的直接影响是:查后端列表的命令要改。
kubectl get endpointslices -l kubernetes.io/service-name=demo-api -o wide更重要的是 EndpointSlice 和就绪探针的联动逻辑。只有通过readinessProbe的 Pod 才会进入ready状态的端点列表,才会被加入转发。这意味着探针配置直接决定了流量什么时候开始打进来、什么时候停止。
配置过严的探针(比如initialDelaySeconds太长)会导致 Pod 启动后很长时间收不到流量,滚动发布变慢;配置过松(比如只检查端口是否 listen)会导致应用还没初始化完就被打流量,报一堆 500。我的习惯是把探针分成两类:readinessProbe检查依赖是否就绪(数据库连接池、缓存预热完成),livenessProbe只检查进程是否卡死,两者用不同的端点,避免依赖抖动导致容器被反复重启。
6. NetworkPolicy 与故障排查实录
前面讲的都是"怎么通",这一节讲"怎么不通"和"不通了怎么查"。
6.1 NetworkPolicy 生效的前提条件
NetworkPolicy 是 K8s 里少见的"声明了不一定生效"的资源,因为它依赖 CNI 插件实现。如果 CNI 不支持网络策略,创建 NetworkPolicy 对象会成功,kubectl get也能看到,但完全没有效果,这是最坑的一点。
判断方法很简单:在目标 Pod 所在节点上看有没有对应的规则下发,或者做个对照实验,建一条deny all的策略,看流量是不是真的断了。断不了就说明 CNI 不支持,得换插件或者加一层服务网格。
策略本身的语义也容易误解。默认情况下 Pod 是非隔离的,所有流量都允许。一旦某个 Pod 被任一策略的podSelector选中,它就进入隔离状态,此时只允许策略里显式声明的流量,其余全部拒绝。注意是"任一策略选中就隔离",不是"所有策略都满足才隔离",这个语义在策略多的时候极易出错。
还有一个细节:策略里的namespaceSelector和podSelector写在同一个from元素里是"与"关系,写在不同的from元素里是"或"关系。这个区别在写复杂规则时经常搞混,建议写完用实际流量验证一遍,别只看 YAML 觉得对。
6.2 分层排查:从 DNS 到 conntrack
网络不通的排查最忌讳一上来就抓包,容易陷进去。我习惯按层往下走,每一层用最便宜的手段排除。
第一层,名字解析。在客户端 Pod 里nslookup或者getent hosts目标服务名,解析不出来就直接查 CoreDNS 和 Service 定义,后面的都不用看了。
第二层,连通性。nc -zv <ClusterIP> <port>,通了说明转发规则正常,问题在上层应用;不通再往下。
第三层,后端列表。kubectl get endpointslices看有没有 ready 的端点,为空说明选择器没匹配上或者 Pod 没就绪,这时候问题在标签或探针,跟网络无关。
第四层,节点规则。在任意节点上iptables-save | grep <ClusterIP>或者ipvsadm -Ln | grep -A5 <ClusterIP>,看规则是否存在、目标地址是否正确。规则缺失说明 kube-proxy 有问题,看它的日志和健康检查端口。
第五层,连接跟踪。conntrack -L | grep <目标IP>看有没有残留的失效连接。这一层经常出问题:后端 Pod 重启后 IP 变了,但客户端侧还有旧连接的 conntrack 条目,新连接能通、旧连接一直超时。表现是"有时候通有时候不通",非常迷惑。处理办法是清掉对应条目,或者调小 conntrack 的 TCP 超时时间。
conntrack 表本身也有容量上限,nf_conntrack_max默认值和内存相关,4GB 内存的节点大概在 6 万左右。高并发短连接场景下很容易打满,打满之后新连接直接被丢弃,日志里会出现 "nf_conntrack: table full, dropping packet"。这时候要么调大上限,要么优化连接复用(长连接、连接池),光调参数只是拖延问题。
# 查看当前使用量和上限 cat /proc/sys/net/netfilter/nf_conntrack_count cat /proc/sys/net/netfilter/nf_conntrack_max6.3 常见问题速查表
下面这张表是我这几年攒下来的,基本都是真实遇到过的现象和对策,可以直接拿去对照。
| 现象 | 可能原因 | 快速验证 |
|---|---|---|
| ClusterIP 完全不通 | kube-proxy 未运行或规则未同步 | 节点上看 iptables/ipvs 规则是否存在 |
| 偶发连接超时,重试就好 | conntrack 残留或表满 | conntrack -L看条目数、看内核日志 |
| 大响应卡住、小请求正常 | MTU 不匹配 | 带 DF 标志 ping 不同大小包 |
| 只有部分节点访问失败 | externalTrafficPolicy 为 Local 且该节点无 Pod | 对比节点上是否有后端 Pod |
| 服务名解析慢几百毫秒 | ndots 导致多次无效查询 | 抓 DNS 包看查询次数 |
| 解析不到服务名 | Pod 未就绪、选择器不匹配 | 查 EndpointSlice 的 ready 状态 |
| Ingress 502 但直连正常 | Controller 到 Pod 网络不通或配置未 reload | 进 Controller Pod 直接 curl Pod IP |
| 滚动发布期间短暂 5xx | 探针配置不当、优雅退出时间不足 | 检查 preStop 和 terminationGracePeriodSeconds |
6.4 几个我认为值得单独说的坑
第一个是优雅退出的时间。Pod 被删除时,会同时收到从 EndpointSlice 摘除和发送 SIGTERM 两个动作,但这两个动作是并行的,不保证顺序。如果应用收到 SIGTERM 立刻退出,而摘除动作还没同步到所有节点的 kube-proxy 和 Ingress Controller,就会有一小段时间流量还在往这个已经死掉的 Pod 上打,表现为发布期间零星 502。
解决办法是在容器的preStop里加一个几秒的 sleep,给规则同步留出时间,同时把terminationGracePeriodSeconds设得比应用实际退出时间更长。这个几秒的等待看起来是浪费,但能消掉绝大部分发布抖动,非常值。
lifecycle: preStop: exec: command: ["sh", "-c", "sleep 5"]第二个是 NodePort 端口冲突。Service 创建成功不代表端口能用,如果节点上已有进程占用了同一个端口,kube-proxy 的规则会加上,但流量会被本地进程截胡。排查的时候在节点上ss -lntp | grep <端口>比查 Service 快得多。
第三个是网络策略和 Ingress Controller 的相互影响。给业务命名空间加了默认拒绝策略之后,很容易忘了放行来自 Controller 命名空间的流量,结果就是集群内调用一切正常,从外部进来全部超时。加策略的时候最好同时准备好放行规则,别分两次上线。
第四个是 IPVS 模式下长连接的调度问题。默认的连接超时时间比较长,后端 Pod 扩缩容之后,旧连接还挂在已经下线的后端上,直到超时才断。这类问题在有状态服务或者数据库连接池场景下尤其明显,解决思路是在客户端配好连接的最大存活时间,主动让它定期重建,而不是依赖服务端的超时。
我个人在实际操作中的体会是,K8s 网络这块的知识,看文档只能建立起大致轮廓,真正形成判断力还是靠在具体故障里一层层往下剥。每次排查完,把当时的现象、验证命令、根因记下来,攒到一定数量之后你会发现,绝大多数问题都落在那么几个固定的点上:规则没同步、地址没匹配、包太大过不去、连接状态没清干净。把这几个点吃透,比背一堆命令有用得多。