
我见过太多刚上手 Kubernetes 的人在排查网络问题时第一反应就是跑到 Pod 里面ping一下然后盯着Destination Host Unreachable发呆。其实 Pod 通信这件事从外面看就是一个 IP 到另一个 IP但拆开来看里面藏着四五条完全不同的链路。这篇文章我就基于一次完整的通信过程演示把 Kubernetes Pod 从自己人和自己人说话到出小区门上网的每一条路径都捋一遍顺便把那些容易踩的坑和排查思路一起放进来。这篇内容适合三类人正在准备 CKA 考试、被集群网络问题折磨的运维同学以及想在容器网络这块补补基本功的后端开发。我会尽量少讲纯理论多放可以直接执行的命令和贴近实战的现象描述。1. 先把通信主角的底细摸清Pod 凭什么在网络里是一个身份节点在讨论 Pod 之间怎么通信之前必须先弄清楚一个底层事实Pod 是整个 Kubernetes 调度和网络模型中的最小原子单元。也就是说Kubernetes 网络世界里没有容器 IP这种说法你看到的 IP 永远属于 Pod而不是某个容器。这个设计直接影响后面所有通信逻辑。1.1 Pod 的 IP 到底是谁给的通常你创建一个 Deployment 后kubelet 会负责拉起 Pod 里的容器但 Pod 的网络命名空间Network Namespace却不是 kubelet 随便创建的。kubelet 在创建 Pod 沙箱容器时会调用容器运行时的CreatePodSandbox接口这一步就会让 CNI 插件介入——由 CNI 负责分配 IP、创建 veth 对、挂载到宿主机网桥或者路由表里。# 在节点上查看 CNI 网桥 ip addr show cni0你会发现节点上多了一张 cni0 网桥。以 Flannel VXLAN 模式下常见的 bridge 插件为例每个 Pod 的 veth 设备一端在 Pod 的网络命名空间里另一端挂在 cni0 上。这个 veth 对就是 Pod 对外收发数据包的第一道关卡。如果你用kubectl get pod -o wide看到的 Pod IP 是 10.244.1.23那这个地址一定是 CNI 插件从当前节点的 PodCIDR 段里分配的不是 DHCP 随机出来的。1.2 Pod 由什么控制——控制器与 Pod IP 生命周期每当你删除一个 Pod新创建的 Pod 哪怕属于同一个 DeploymentIP 也大概率会变。因为 Pod 不是持久化的对象它背后是 ReplicaSet、StatefulSet 等控制器在管控控制器只保证副本数量不承诺 IP 不变。这一点对通信的影响非常大。如果一个应用 A 想访问另一个应用 B你不能在 A 的配置里硬编码 B 的 Pod IP而是必须通过 Service 或 Headless Service 去解耦。这也解释了为什么集群内部通信不能绕开 Service 层直接点对点打 Pod IP——你不光要面对 IP 生命周期不稳定的问题还要面临滚动更新时 Pod 被分批替换的复杂情况。另外调度器在做 Pod 调度时会考虑每个节点剩余 IP 资源是否充足。如果某个节点的 PodCIDR 耗尽就会出现 Pod 一直 Pending 的情况。关于调度器还有一条容易被忽略的现象no preemption victims found for incoming pod这行日志说明即使调度器想抢占低优先级 Pod 来腾位置却找不到合适的受害者。这种情况下 CPU、内存倒是次要的Pod 启动不了才是真正要命的问题。2. 同一个 Pod 里的多个容器共享网卡背后的通信细节Pod 内多容器通信是理解整个网络模型的基础因为它最直观地展示了共享网络命名空间这句话的含义。同一个 Pod 里的容器共享同一个网络命名空间意味着它们共享同一个 IP、同一组网卡、同一个端口空间。2.1 网络命名空间的共享机制容器引擎在创建 Pod 时会先创建 pause 容器在 containerd 下叫 sandbox这个容器只负责持有网络命名空间业务容器启动时通过--networkcontainer:pause的方式加入同一个命名空间。打个比方pause 容器就是个电话亭谁进到电话亭里都用同一个电话号码对外联系。所以同一个 Pod 里的 A 容器要访问 B 容器不需要 SVC直接用127.0.0.1:端口就能通。# 进入 Pod 里的容器 A查看网络栈 kubectl exec -it my-pod -c container-a -- ip addr # 你会看到 lo、eth0但这些接口在 container-b 里也一样存在 kubectl exec -it my-pod -c container-b -- ip addr2.2 共享网络栈下的端口冲突与 localhost 通信演示共享端口空间有一个直接的后果同 Pod 内两个容器不能监听同一个端口。典型场景是Pod 里同时跑了 Nginx监听 80和另一个也监听 80 的进程第二个进程启动时会直接抛Address already in use。实际运行中我见过不少人把日志收集容器和业务容器放同一个 Pod日志容器要监听一个端口接收业务日志。如果这个端口不是配置成 localhost 专属地址而是0.0.0.0就会和业务容器抢端口。# 同一个 Pod 内通过 localhost 通信的演示 apiVersion: v1 kind: Pod metadata: name: two-containers-demo spec: containers: - name: app image: nginx ports: - containerPort: 80 # 注意network 命名空间共享pod 内 localhost:80 可访问 - name: sidecar image: curlimages/curl command: [sleep, 3600]2.3 一个容器没起来会不会影响另一个容器——启动失败的影响边界热搜词里有一条问得很刁一个 Pod 里包含多个容器如果其中一个容器没有成功启动会影响其它容器吗这个问题要分几个层面看。首先网络上不影响已经启动的容器。因为容器只要跑起来了网络就是通的哪怕同 Pod 的兄弟容器是 CrashLoopBackOff 状态它的服务依然能正常被访问。但是如果那个没启动成功的容器是 initContainer情况就完全不同了。initContainer 必须按顺序全部成功退出主容器才会被创建。这时候任何 initContainer 失败主容器都不会启动更谈不上通信。很多人在排查 Pod 网络问题时连基础容器都没起来还一直抓包这就是白费功夫。还有一个容易忽略的间接影响如果没起来的容器占用了大量内存且触发了节点 OOMkubelet 可能会驱逐整个 Pod那所有容器都会受影响。所以准确说同一 Pod 内容器启动失败对网络通信的直接影响有限但对整个 Pod 生命周期和资源层面可能会引发连锁反应。3. Pod 与 Pod 通信的完整路径从 CNI 插件到 veth pair 转发这是整篇文章的核心环节。Pod 与 Pod 的通信路径依赖 CNI 插件建立集群范围的路由和转发规则不同 CNI 插件Calico、Flannel、Cilium、Weave实现细节有差异但逻辑链路高度一致。3.1 集群内通信的网络链路拆解以 Flannel VXLAN 模式为例同节点上的两个 Pod 通信路径是这样的Pod A - veth0Pod 内 - 宿主机的 vethXXX - cni0 网桥 - vethYYY - Pod B 的 veth0整个过程走二层转发不涉及跨节点路由延迟极低。但你抓包时会发现直接tcpdump -i eth0在 Pod 里抓包是看不到完整链路包头的——VXLAN 封装在宿主机 cni0 或 flannel.1 接口上完成。跨节点通信则复杂得多Pod A - veth0 - cni0 - 路由表查询 10.244.2.0/24 走 flannel.1- VXLAN 封装 - 对端物理网卡 - 对端 flannel.1 - 对端 cni0 - Pod B3.2 CNI 插件在 IP 分配和路由规则中扮演的角色CNI 插件做的事情可以分解成四步向容器运行时提供 IP 分配方案host-local 或基于 etcd 的 IPAM创建 veth pair把 veth 的一端放到宿主机网桥 / 路由规则在宿主机和 Pod 内写入必要的路由表项# 在节点上查看 veth 对 ip link | grep veth # 查看 Pod 内的路由表 kubectl exec -it my-pod -- ip route # 输出类似default via 10.244.1.1 dev eth010.244.0.0/16 via 10.244.1.1这里有一个常被忽略的细节Pod 里的默认网关是 cni0 的 IP在 bridge 插件下而不是宿主机物理网卡的 IP。所有非本网段流量都会先到网关再由宿主机路由表决定下一步。3.3 同节点与跨节点通信的差异跨节点之间的通信规则不是天然存在的而是 CNI 插件在安装时把每个节点上的 PodCIDR 信息同步到整个集群。如果你用 Calico 的 BGP 模式每个节点上的 BGP 会话会直接宣告自己的 PodCIDR其他节点由此知道 10.244.2.0/24 该往哪个节点发。用表格对比一下维度同节点通信跨节点通信数据路径短只经过网桥长经过隧道或路由延迟微秒级百微秒到毫秒级关键组件cni0 网桥flannel.1 / tunl0 / 物理网卡排错关注点veth 是否正常挂载节点间防火墙、VXLAN 端口是否放通这里给一个我实际踩过的坑很多公有云环境的安全组默认只放通了节点之间的 TCP/UDP 访问端口如 22、3306但 Flannel VXLAN 模式需要 UDP 8472 端口Calico VXLAN 也需要类似的 UDP 端口。安全组没放通Pod 跨节点通信就会时通时不通单看网络策略还看不出来问题。4. Service 出现后通信走向变成什么样ClusterIP 与 kube-proxy 的转发逻辑如果 Pod 之间的通信只是点到点那 Kubernetes 网络根本不值得写这么长。真正让通信变得有服务发现味道的是 Service。这也是 Pod 通信演示中最需要理解的一层抽象。4.1 ClusterIP 为什么能漂移背后是 iptables/ipvs 规则Service 的 ClusterIP 是一个虚拟 IP没有真实网卡监听它。kube-proxy 负责把访问 ClusterIP:Port 的流量转发到后端 Pod 的 IP:Port。以 iptables 模式为例kube-proxy 会创建一套链iptables -t nat -L KUBE-SERVICES -n你会发现访问 ClusterIP 的流量会经过KUBE-SVC-XXX链再跳到某个KUBE-SEP-XXXService Endpoint链。每条 SEP 链背后就是一个 Pod 地址。iptables 模式随机选择后端IPVS 模式则支持更多调度算法rr、wrr、lc 等。生产环境如果并发量大建议切换 IPVS# 查看当前 kube-proxy 模式 kubectl get configmap kube-proxy -n kube-system -o yaml | grep mode4.2 DNS 与 Service 的联动以及 ClusterIP 绕不开的一个问题——会话保持假设你创建了一个nginx-service配上 Endpoints 指向两个 Nginx Pod。当你在另一个 Pod 里执行curl http://nginx-service.default.svc.cluster.local实际解析过程是Pod 里的 DNS 配置/etc/resolv.conf指向 kube-dns / CoreDNS 的 ClusterIPCoreDNS 解析nginx-service.default.svc.cluster.local为 ClusterIP然后 curl 连接到 ClusterIP流量被 kube-proxy 规则转发到某个后端 Pod。这里有个问题默认 iptables 模式不具备会话保持能力IPVS 模式可以通过kube-proxy --ipvs-scheduler调整策略但也不是应用层会话如果业务应用对源 IP 一致性有要求比如购物车 session就必须配置service.spec.sessionAffinity: ClientIP。我的建议是Service 层做得越简单越好别在转发层面处理业务状态。需要会话保持就用 Redis/MySQL 这类外部存储状态放在应用层网络层只负责连通性。4.3 从 Service 到后端的实际流量演示来看一个演示场景。我建一个 Service 选择两个 Nginx Podkubectl create deployment nginx --imagenginx --replicas2 kubectl expose deployment nginx --port80 --target-port80 --namenginx-svc然后随便创建一个测试 Podkubectl run curl-test --imagecurlimages/curl --rm -it -- sh在测试 Pod 里访问curl nginx-svc.default.svc.cluster.local打开节点上的 iptables nat 表能看到类似这样的规则KUBE-SVC-XXXX - KUBE-SEP-YYYY指向 10.244.1.10:80 - KUBE-SEP-ZZZZ指向 10.244.2.11:80多次请求时负载均衡规则让每次请求可能落到不同后端 Pod。如果你在 Nginx 访问日志里观察到请求来源 IP 是 10.244.x.x 而不是客户端 Pod IP这是正常的因为 kube-proxy 默认做了 SNAT。5. 走出集群的通信NAT、NodePort 与 Ingress 三层出口Pod 与外部网络的互通既要允许 Pod 访问外网也要让外网能访问到 Pod。这两条方向路径完全不同也是实际运维中问题最多的区域。5.1 Pod 访问外网的 SNAT 路径Pod 访问外网如公网 API时数据包从 Pod eth0 出来经过宿主机路由后交给节点的物理网卡。由于目标 IP 不在集群网段内宿主机路由表会把包送到默认网关。这时如果源 IP 是 PodIP10.244.x.x公网网关没法回包所以必须在宿主机上做一次源地址转换SNAT。iptables -t nat -L POSTROUTING -n | grep MASQUERADE在 Flannel 默认配置里有一条规则会把来自 10.244.0.0/16 且目标不是集群网段的流量统一换成节点物理网卡 IP。这就是为什么在公网服务器上看到请求来自节点 IP 而不是 Pod IP。如果你需要让外部看到真实 Pod IP通常要靠云平台的四层 LB 直通模式或者 BGP 方案而不是默认 SNAT。5.2 外网访问 Pod 的 NodePort 和 Ingress 衔接外网访问 Pod 至少经过两层。第一层是进入集群节点第二层是节点再把流量转向 Pod。NodePort 方式下每个节点的0.0.0.0:30080这类端口会被 kube-proxy 监听流量进入节点后走KUBE-SVC-XXX和KUBE-SEP-XXX链最终转发到 Pod。注意NodePort 的默认端口范围是 30000~32767。另一种更常见的方式是 Ingress。Ingress 本身是七层反向代理常见的实现是 Nginx Ingress Controller 或 Traefik。数据路径是公网 - LB节点NodePort或云LB - Ingress Controller Pod - Service ClusterIP - 后端 PodIngress 最大的价值是按域名/路径路由。你可以在同一个 Ingress 上配置apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: multi-route spec: rules: - host: api.example.com http: paths: - path: /v1 pathType: Prefix backend: service: name: api-v1-svc port: number: 80这在通信演示里很有代表性外部流量经过一层层翻译才能到达真实处理业务的 Pod。每一层都可能成为瓶颈所以排错时最好用kubectl exec -n ingress-nginx -it ingress-pod -- /bin/bash进去看 Nginx 访问日志而不是只盯着后端 Pod 日志。6. 通信排错的实战链路从 tcpdump 到 conntrack 对照排查网络通信排错最怕的是凭感觉乱试。下面这套排查链路是我在处理过大量Pod 不通问题后沉淀下来的顺序每一步都有明确目的。6.1 容器里 tcpdump 看不到包先搞懂 nsenter一个很常见的误区kubectl exec进容器安装 tcpdump 之后发现抓不到任何包就断言网络不通。实际上很多镜像里没有 tcpdump就算有在容器内抓包看到的也只是容器网络命名空间内的包。如果想看宿主机视角的完整数据路径应该用 nsenter 进入 Pod 的网络命名空间。# 在节点上找到 Pod 的 PID PID$(kubectl get pod my-pod -o jsonpath{.status.containerStatuses[0].containerID} | sed s/containerd:\/\///) # 通过 crictl 找到沙箱 PID SANDBOX_PID$(crictl inspect $PID | jq .info.pid) # 进入该命名空间抓包 nsenter -t $SANDBOX_PID -n tcpdump -i any -nn port 80这样能看到 Pod 视角的包。在宿主机上也要抓一次如果宿主机上能看到包而 Pod 里看不到问题多半出在 veth 或网桥这一层。6.2 conntrack 表异常导致通信中断的案例我曾经遇到过一个问题某节点上的 Pod 访问集群外服务时TCP 连接时而成功时而超时重启 Pod 也没用。排查时发现在节点上执行conntrack -L | grep 10.244.3.15大量处于TIME_WAIT或ESTABLISHED但已经失效的连接条目堆积。原因是外部服务的响应包回来时conntrack 表里找不到对应的 NAT 映射导致回包被丢弃。这在一些高并发场景下很常见因为 conntrack 表条目数默认有上限超出后会自动丢弃新连接。解决方法是调大 conntrack 表sysctl -w net.netfilter.nf_conntrack_max1048576 sysctl -w net.netfilter.nf_conntrack_buckets2621446.3 从现象到根因的对照排查顺序遇到Pod 网络不通时我建议按这个顺序来确认 Pod 状态是 Running 且 Ready检查kubectl get pods -o wide在 Pod 内ping默认网关不通则问题在网络插件或 veth在 Pod 内curl目标 Service IP不通则检查 kube-proxy 规则在宿主机上tcpdump -i cni0看包是否进入网桥检查节点防火墙和云平台安全组是否放通相关端口检查目标 Pod 的 Service 的 Endpoints 是否正常kubectl get endpoints nginx-svc如果 Endpoints 列表为空说明 Service 的 selector 没有匹配到任何 Pod。这问题看起来低级但 90% 的 Service 通信异常都出在这里。7. 资源规格与并发量2c4g 的 Pod 到底能扛多少并发最后聊一个热搜词里非常务实的问题2c4g 的 Pod 支持的并发量是多少。这个问题没有标准答案但我可以从网络通信的角度给出一个合理的估算思路。7.1 并发量的真实瓶颈不是 CPU 和内存这么简单很多人以为 2c4g 的瓶颈在 CPU 或内存但从网络通信角度看真正的瓶颈往往在文件描述符数、端口范围、连接跟踪表以及应用的 IO 模型。当一个 Pod 作为 Nginx 反向代理时每个客户端连接会占用一个文件描述符。Linux 默认的ulimit -n是 1024这意味着最多同时处理 1024 个连接远远早于 CPU 瓶颈。Kubernetes 里可以给 Pod 设置pidsLimit或者在系统层调大fs.file-max/net.ipv4.ip_local_port_range。7.2 通过压测大致估算数据假设你的服务是典型的 HTTP 短连接请求单个请求耗时约 20ms在 2c4g 的 Pod 上跑 NginxPHP-FPM指标数值单核每秒可处理请求数估算约 150~300两核可承载 QPS约 300~600每连接占用内存估算约 2~4KB4GB 内存下的理论并发连接数数万级但文件描述符才是真实瓶颈这只是数量级参考。如果业务逻辑里有大量 CPU 运算、磁盘 IO 或者依赖外部数据库QPS 会大幅下降。如果你是纯静态文件服务如 Nginx 直接返回小文件2c4g 跑几千 QPS 是完全可能的。7.3 配置建议根据我在生产环境的经验针对2c4g Pod 支撑并发这件事最重要的不是你向上的峰值调优而是向下的保护策略。我强烈建议做三件事为 Pod 配置资源limits防止某个 Pod 的突发流量把整台节点拖垮配好 HPA水平自动伸缩QPS 到达阈值前自动扩展副本网络层开启 IPVS 模式减少 iptables 规则遍历带来的 CPU 开销另外如果你的服务连接数特别高可以通过kubectl exec进 Pod 执行ss -s查看当前 socket 统计快速判断连接数是否逼近上限。ss -s一旦发现大量TIME_WAIT可以调net.ipv4.tcp_tw_reuse等内核参数但注意这些参数在不同内核版本下行为有差异最好先在测试环境验证。我在实际跑压测时也发现一个很有用的思路不直接压满单 Pod而是用 Service 多副本的方式让多 Pod 分摊流量。这样单个 Pod 的任何异常比如内存泄漏都不会直接导致整个服务不可用。毕竟在 Kubernetes 里Pod 本身就是随时可以被替换的设计哲学把负载分散到多个 Pod 而不是死磕单个 Pod 的性能上限才是更符合云原生理念的选择。