Service 偶发连不上,我才把 Kubernetes 网络基础串明白:Pod、Service、DNS、策略一条链排查
那天下午服务刚开始滚动发布,告警群就弹出一串错误:order 服务调用 payment 服务,日志里时不时冒出 connection refused,偶尔还有 3 秒超时。
我先确认影响范围。报错并不是一直有,基本集中在发布那十几分钟里。错误率不高,可支付链路的业务方已经开始问了。
最麻烦的是,每个人怀疑的地方都不一样。有人说是 DNS,有人说是 CNI 插件,还有人张口就要重启 CoreDNS。真正让我静下心来的,是把 Kubernetes 网络从头到尾捋成一条链:请求从哪个 Pod 出发,经过哪个名字、哪个 IP、哪条规则,最后落到哪个后端。
这篇文章就沿着这条链,把 Kubernetes 网络最该懂的基础知识讲清楚。每一层我都给出能直接执行的只读检查命令,也讲怎样根据输出判断下一步。最后用这次报错,完整走一遍证据链。
请求离开 Pod 之前,先得知道自己站在哪
Kubernetes 的网络模型有三条底层约定:
- 每个 Pod 有自己的 IP。
- 所有 Pod 之间不经过 NAT 就能互通,跨节点也一样。
- 节点上的组件(例如 kubelet)可以访问本节点上的 Pod。
Kubernetes 自己不负责实现这些约定,具体工作交给 CNI 插件,比如 Calico、Cilium、Flannel。同一个 Pod 里的多个容器共享同一个网络命名空间。所以 sidecar 容器访问 127.0.0.1 就能连到业务容器。这也意味着同一个 Pod 里的两个容器不能监听同一个端口。
动手排查之前,先确认上下文和版本。很多“诡异现象”最后发现只是连错了集群:
# 只读:确认当前集群、版本和节点网络信息
kubectl config current-context
kubectl version
kubectl get nodes -o wide
kubectl -n kube-system get pods -o wide在 kubectl get nodes -o wide 的输出里,重点看 INTERNAL-IP 和 KERNEL-VERSION。kube-system 里跑着哪些 DaemonSet,通常能直接看出用的是哪种 CNI,比如 calico-node、cilium 或 kube-flannel-ds。如果你有节点权限,也可以登录节点看 /etc/cni/net.d/ 下的配置文件,以那里为准。
接着看出问题的 Pod 本身:
# 只读:Pod IP、所在节点、重启次数
kubectl -n get pods -o wide -l app=正常情况下,每个 Pod 都有一个属于 Pod 网段的 IP,READY 是 1/1 这样的满值。如果某个 Pod 的 IP 一直是 ,问题通常出在 CNI 给 Pod 分配地址的环节。这时用 kubectl describe pod 看 Events,里面常会出现 FailedCreatePodSandBox 一类的报错,下一步就该去查 CNI 插件的日志。
Service 只是一份转发约定,真正决定流量去哪的是 EndpointSlice
这篇文章最想让你记住的一句话:Service 的 IP 永远在,能不能连通取决于 EndpointSlice 里那几个 ready 的地址。
ClusterIP 是一个虚拟 IP,没有任何网卡真正持有它。每个节点上的 kube-proxy(或者替代它的 CNI 组件,比如 Cilium 的 kube-proxy replacement)会监听 Service 和 EndpointSlice 的变化,然后把访问 ClusterIP 的流量改写,转发到某个后端 Pod 的 IP 上。
常见的 Service 类型有这几种:
- ClusterIP:集群内部访问,默认类型。
- NodePort:在每个节点上开放一个端口。
- LoadBalancer:对接云厂商的负载均衡器。
- Headless:
clusterIP: None,DNS 直接返回各个 Pod 的 IP,常用于 StatefulSet。 - ExternalName:只返回一条 CNAME 记录,不做任何转发。
Service 通过 selector 选中 Pod。只有处于 Ready 状态的 Pod,才会被当作可接流量的后端写进 EndpointSlice。较新版本的官方文档已经把旧的 Endpoints API 标记为弃用,排查时请直接看 EndpointSlice:
# 只读:Service 定义与后端列表
kubectl -n get svc -o wide
kubectl -n get endpointslices -l kubernetes.io/service-name=
kubectl -n get endpointslices -l kubernetes.io/service-name= -o yaml在 YAML 输出里,每个 endpoint 下面的 conditions 有三个字段值得逐一看:
# 示例输出(脱敏)
endpoints:
- addresses: ["10.244.2.41"]
conditions:
ready: true
serving: true
terminating: false
targetRef:
kind: Pod
name: payment-6d9f7c-abcde判断方法如下:
ready: true:kube-proxy 会把流量发给这个后端。terminating: true:Pod 正在退出。- 后端列表为空:先核对 Service 的
selector和 Pod 的 labels 是否一致,再核对targetPort和容器实际监听的端口是否一致。这两处写错是最常见的低级错误。 - 地址都在且是 ready:问题不在 Service 定义这一层,继续往下查 kube-proxy 规则和应用本身。
kube-proxy 把规则写在哪,决定了你该用什么工具看
kube-proxy 在 Linux 上常见的模式有 iptables、ipvs 和 nftables,不配置时默认是 iptables。各模式在你所用版本里的成熟度和推荐程度,以对应版本的官方文档为准。
先确认当前用的是哪种模式(需要节点权限):
# 只读,在节点上执行;默认 metrics 监听地址为 127.0.0.1:10249
curl -s http://127.0.0.1:10249/proxyMode
# 只读,kubeadm 部署的集群可以看 ConfigMap;mode 为空表示使用默认值
kubectl -n kube-system get cm kube-proxy -o yaml | grep -n "mode:"然后按模式看规则:
# iptables 模式:规则注释里带有 namespace/service 名称
iptables-save -t nat | grep "/"
# ipvs 模式:需要安装 ipvsadm
ipvsadm -Ln -t :
# nftables 模式
nft list table ip kube-proxy | grep -n "/"在 iptables 模式下,正常的规则链是 KUBE-SERVICES → KUBE-SVC-xxx → 多条 KUBE-SEP-xxx,每条 KUBE-SEP 对应一个后端 IP。把这些后端 IP 和 EndpointSlice 里的地址对一下:
- 两边一致:规则同步正常。
- 节点上的规则比 EndpointSlice 旧:去看这个节点上 kube-proxy 的日志,查是否有同步失败或 API 连接异常。
这一步要注意一点:在规则很多的大集群里,完整的 iptables-save 输出可能有几万行。所以一定要带 grep 过滤,也不要在业务高峰期反复全量导出。另外,如果集群已经用 Cilium 等 eBPF 方案替代了 kube-proxy,上面这些命令可能什么都看不到。这时应该改用该 CNI 自带的命令行工具查看 Service 映射。
DNS 不是黑盒:search 域和 ndots 决定了一次解析要问几次
Pod 访问 payment 这样的短名字时,靠的是 kubelet 写进 Pod 里的 /etc/resolv.conf。我更推荐用临时调试容器进去看,因为很多业务镜像里没有任何网络工具:
# 会创建一个临时容器(ephemeral container):生产环境请使用内部仓库里经过审核的调试镜像
kubectl -n debug -it --image=/netshoot: --target= -- sh
# 以下命令在调试容器内执行,都是只读操作
cat /etc/resolv.conf
nslookup ..svc.cluster.local典型的 resolv.conf 内容:
nameserver 10.96.0.10
search .svc.cluster.local svc.cluster.local cluster.local
options ndots:5ndots:5 的意思是:名字里的点少于 5 个时,解析器会先把 search 域逐个拼上去尝试,最后才查原始名字。所以在 Pod 里访问 api.example.com 这样的外部域名,可能要先失败好几次查询,才能拿到正确结果。外部域名多、QPS 高的服务,会因此给 CoreDNS 增加明显的负担。缓解办法有两种:在外部域名末尾加一个点,写成完全限定域名;或者通过 Pod 的 dnsConfig 调低 ndots。改之前要先确认业务里没有依赖短名字解析,否则会把原本能用的调用改坏。
如果怀疑 CoreDNS 本身有问题,先做只读检查:
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=100Pod 状态正常、日志里没有大量 SERVFAIL 或连接上游超时,就不要重启 CoreDNS。重启会让所有业务的解析同时受到影响,而且会抹掉现场。
NetworkPolicy:默认全放行,一旦被选中就变成白名单
NetworkPolicy 的语义经常被误解,我把它压缩成三句话:
- 没有任何策略选中某个 Pod 时,它的流量全部放行。
- 只要有一条策略选中了这个 Pod,并且在相应方向(Ingress 或 Egress)生效,这个方向就只允许策略里明确写出的流量。
- 策略能不能真正生效,取决于 CNI 插件是否支持。如果插件不实现 NetworkPolicy,策略对象照样能创建成功,但实际没有任何效果。
# 只读:查看命名空间内的策略及其选中范围
kubectl -n get networkpolicy
kubectl -n describe networkpolicy 最常见的坑是:给命名空间加了一条默认拒绝 Egress 的策略,却忘了放行 DNS。结果所有短名字都解析失败,看起来就像网络整体断了。如果加 Egress 策略,务必显式放行到 kube-dns 的 UDP 53 和 TCP 53 端口。
节点层的两个隐形坑:MTU 和 conntrack
前面几层都查过没问题,但仍然出现“小请求正常、大响应卡住”,就该怀疑 MTU。VXLAN 这类 overlay 封装会额外占用大约 50 字节。如果 Pod 网卡的 MTU 没有相应调小,大包可能被丢掉。可以在调试容器里用禁止分片的 ping 做验证(只读):
ip link show eth0
# 1422 字节载荷 + 28 字节 ICMP/IP 头 = 1450;按你的实际 MTU 调整这个数
ping -M do -s 1422 -c 3 <对端PodIP>如果 1422 字节不通、更小的包能通,基本就能锁定 MTU 问题。修复要改 CNI 的 MTU 配置,影响整个集群。所以要先在测试集群验证,再按节点分批滚动生效。
另一个坑是 conntrack 表打满。这时候的表现是偶发超时,而且没有任何规律。在节点上执行以下命令(只读,conntrack 命令需要安装 conntrack-tools):
cat /proc/sys/net/netfilter/nf_conntrack_count /proc/sys/net/netfilter/nf_conntrack_max
conntrack -S
dmesg -T | grep -i "nf_conntrack: table full"判断标准:
count长期接近max,或者 dmesg 里出现table full, dropping packet:说明表确实满了。conntrack -S输出中的insert_failed、drop计数持续增长:也指向这一层的问题。
调大 nf_conntrack_max 属于内核参数变更,会增加内存占用。应该先在一个节点上改,观察一个业务周期,确认没有副作用后再扩大范围。原值要提前记录下来,方便回滚。
回到那次发布:证据链是怎么闭合的
下面用一个脱敏后的典型场景说明,文中所有数值都是示例。
现象与影响:14:02 开始滚动发布 payment(共 3 个副本)。在 14:03 到 14:15 之间,order 调用 payment 的错误率从接近 0 上升到约 2%(示例数据)。错误类型以 connection refused 为主,夹杂少量 3 秒超时。其他服务没有出现异常。
初判与排除:
- 怀疑 DNS。在 order 的 Pod 里循环执行
nslookup,每次都秒级返回正确的 ClusterIP。CoreDNS 日志干净。排除 DNS。 - 怀疑 CNI 或 MTU。用 curl 直接访问已经运行很久的旧 Pod IP,全部成功,大小包都正常。排除底层网络。
- 关键证据出现在 EndpointSlice。发布期间用
kubectl get endpointslices -w观察,新 Pod 启动后不到 2 秒就以ready: true出现在后端列表里。但 payment 的应用日志显示,它要大约 30 秒后才打印Listening on 8080。查看 Deployment,发现根本没有配置 readinessProbe。容器一启动,Pod 就被视为 Ready,流量随即进来。这正好解释了connection refused。 - 3 秒超时的来源:旧 Pod 收到 SIGTERM 后立刻退出,而各节点 kube-proxy 更新转发规则存在短暂延迟。少量请求被转发到了已经退出的后端。
根因:缺少 readinessProbe,加上没有优雅退出窗口,滚动发布期间流量打到了尚未就绪或已经退出的 Pod 上。
临时止损:暂停当次发布,等所有新 Pod 完成启动,错误率回落到基线。整个过程没有重启任何网络组件。
最终修复如下。修改前先导出当前配置留作备份:kubectl -n get deploy payment -o yaml > payment-backup.yaml。正式改动走 Git 声明式配置:
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 5
failureThreshold: 3
lifecycle:
preStop:
exec: { command: ["sleep", "10"] } # 需要镜像里有 sleep 命令
terminationGracePeriodSeconds: 40这里有三个前提要满足:
- preStop 的等待时间加上应用自身的优雅退出时间,必须小于
terminationGracePeriodSeconds,否则进程会被强制杀掉。 /healthz接口必须在应用真正能处理业务之后才返回成功。- 健康检查要轻量,不能依赖下游服务。否则下游一抖动,所有 Pod 会同时被摘掉,问题反而更大。
灰度与验证:先在测试环境演练一轮完整的滚动发布。生产环境选低峰期发布,用 kubectl -n rollout status deploy/payment 盯着进度,同时观察调用错误率和 P99 耗时。
停止条件:错误率超过发布前基线,或者有新 Pod 长时间不能进入 Ready。触发后立即执行 kubectl -n rollout undo deploy/payment,或者在 Git 中回滚配置。
修复后的再次发布中,EndpointSlice 显示新 Pod 大约 30 秒后才变为 ready,错误率保持在基线(示例数据)。
防复发:在变更门禁里检查每个 Deployment 是否配置了 readinessProbe 和 preStop,并为“Service 可用后端数”和“调用方错误率”配置告警。
收藏这份清单,下次排查直接对照
- 务必先确认 context 和 namespace。 在错误的集群上排查,得出的结论全部作废;在错误的集群上变更,会制造新故障。
- 不要先重启 CoreDNS 或 kube-proxy。 重启会抹掉现场,还可能把局部问题放大成全局问题。
- 千万别只看 Service 存在就认为后端正常。 ClusterIP 一直都在,要看 EndpointSlice 里有没有 ready 的地址。
- 务必核对 selector、labels 和 targetPort。 这三处写错,后端列表会直接为空。
- 不要省略 readinessProbe。 省略后,发布期间流量会打到没有就绪的 Pod 上。
- 加 Egress 策略时务必放行 DNS。 漏掉这一条,所有短名字都会解析失败,看起来像整体断网。
- 务必确认 CNI 支持 NetworkPolicy。 不支持的话,策略对象能创建成功但实际不生效,造成虚假的安全感。
- 抓包和全量导出规则一定要限定范围和时长。 这些操作有性能开销和数据泄露风险。
- 调 MTU、调 conntrack 都要逐个节点灰度,并提前记录原值。 两者都是集群级或内核级的变更,回滚必须有依据。
写在最后
Kubernetes 网络的基础知识可以连成一条链:Pod IP 和 CNI 负责互通,Service 和 EndpointSlice 负责决定流量去哪,kube-proxy 负责把这个决定落成规则,DNS 负责把名字变成 IP,NetworkPolicy 负责限制谁能访问谁,MTU 和 conntrack 是节点层最容易被忽略的两个坑。排查时沿着这条链逐层用只读命令采证,比凭感觉重启组件靠谱得多。
今天就可以做一件事:挑一个核心服务,执行 kubectl get endpointslices -l kubernetes.io/service-name= -o yaml,再看看它的 Deployment 有没有配置 readinessProbe。
如果这篇对你有帮助,欢迎关注、点赞、点在看,也转发给正在被 Kubernetes 网络问题折磨的同事。
公众号:耕云躬行录
个人博客:躬行笔记