搞了几年k8s,网络这块一直是新人最先懵、老手偶尔也翻车的地方。尤其当你看到项目标题里那句“IP申请如何层级同步进行”的时候,基本可以确定你已经摸到k8s网络最核心的骨架了:节点要地址、Pod要地址、Service还要地址,这三层地址各归各管、互相还有依赖,最后所有的访问请求又要在这一堆网段里按规则跳来跳去。今天我不打算给你堆概念,直接从IP分配这条线和请求路由这条线一起讲,把k8s网络通信到底是怎么转起来的彻底捋清楚。适合刚入门的运维、后端开发,也适合那些被“跨节点不通”“Service访问超时”折磨过的朋友。
1. 先理清楚:k8s里的“IP分配”到底分几层
1.1 为什么网络是k8s里最绕的一环
k8s的设计哲学是所有资源都走“声明式API”,你写个YAML说我要3个副本,控制面就帮你调度、创建、维护。但网络不一样,它没有一个统一的“网络对象”让你声明,它是由kubelet、CNI插件、kube-proxy、kube-controller-manager各管一段拼起来的。
这就导致一个问题:你想排查网络问题的时候,得先搞清楚每个组件到底管哪一段。如果一上来就到处抓包,大概率把自己绕晕。
还有一个核心约束必须记住:k8s要求Pod之间直接通过IP通信,不用经过NAT,不能在Pod之间做地址伪装。这个“扁平网络模型”是所有CNI设计的地基。为什么?因为k8s里Pod是随时生灭的,如果Pod地址还要做转换,Service的后端列表、网络策略、监控采集全部都会变得极其复杂。所以k8s的选择很简单:给每个Pod一个全网唯一、可以直接路由的IP,大家想互访就互访。
1.2 三层网络模型的边界划分
我习惯把k8s网络分成三层来看,每一层的“地址分配者”和“转发规则”完全不同:
| 网络层 | 地址类型 | 谁分配 | 谁维护转发 | 典型网段 |
|---|---|---|---|---|
| 节点网络 | Node IP(物理/虚拟机网卡地址) | 基础设施(云平台或物理网络DHCP) | 交换机/路由器 | 通常是公司内网段,比如10.10.0.0/16 |
| Pod网络 | Pod IP | CNI插件里的IPAM模块 | CNI插件 + 主机路由 | 集群初始化时规划,比如10.244.0.0/16 |
| Service网络 | ClusterIP(虚拟IP) | kube-apiserver | kube-proxy(iptables/IPVS) | 独立于Pod网段,比如10.96.0.0/12 |
这里有个最容易被忽略的点:Node IP不在k8s控制面手里,它是基础设施给的。k8s只负责把Node对象的IP地址读取出来作为路由的“下一跳”或者overlay隧道的“端点地址”。所以k8s网络故障里有一大类,根源其实在底层网络。
而Pod网络和Service网络才是k8s真正“管”的部分,也是标题里说的“层级同步”主要发生的场所。
2. IP申请与层级同步机制:从集群规划到Pod落地
2.1 集群初始化时的那几段CIDR从哪来
你得先理解一个前提:k8s自己不会凭空生成网段,Pod网段和Service网段的CIDR是在集群初始化或者网络插件配置时“人肉”指定的。比如常见的部署方式里,Pod网段就是你在CNI配置里写的那一串,Service网段是API server启动参数里的--service-cluster-ip-range。
有意思的是,Controller Manager里的--cluster-cidr和--allocate-node-cidrs这两个参数决定了节点层级的PodCIDR划分。也就是说,Pod网段被规划出来后,不是一整块直接扔给所有节点随便用,而是由控制面把这一大段再切成小块,分给每一个节点。
比例感很重要。比如集群Pod总网段是10.244.0.0/16,一共65536个地址,假设你有3个节点,控制面会把这块切成3个/24甚至更小的子网,每个节点分一段。这个切分逻辑你可以自己用子网计算器验证一下,一个/16的段切成若干个/24,每个节点最多跑254个Pod。
2.2 节点层级:PodCIDR的下发与占用
节点加入集群后,Controller Manager的NodeLifecycleController会做一件关键的事:给这个Node对象写入spec.podCIDR字段。这个过程是自动的,也是“层级同步”的第一环:控制面把大网段切分成节点子网,再通过API下发给Node对象。
你可以直接看这个字段:
kubectl get node <node-name> -o jsonpath='{.spec.podCIDR}'输出类似10.244.1.0/24,意思是这个节点上的Pod只会从这个段里拿地址。这个字段同时会被kubelet读取,并且最重要的是,CNI插件在给Pod分配IP时,依据的就是这个spec.podCIDR。
这里有个经典坑:如果你手动改了节点上的某些网络配置,却没有改掉spec.podCIDR,那CNI分配出来的IP会和实际网络环境对不上,路由直接乱套。记住,节点上的PodCIDR字段是“唯一真相”,其他一切配置都得跟它对齐。
2.3 Pod层级:CNI的IPAM每次创建都会来“领号”
真正给单个Pod分配IP的,是CNI插件里的IPAM(IP Address Management)模块。整个流程是这样的:kubelet要创建Pod时,先通过容器运行时给Pod生成网络命名空间,然后调用CNI插件(比如Calico、Flannel这些)的ADD操作。CNI插件的IPAM模块接到请求后,会干两件事:
第一,去etcd或者自己的datastore里查一下,这个节点上哪些IP已经被占用了;第二,从这个节点对应的子网里挑一个还没被占用的IP,分配出去,并且登记在册。
这个“查-挑-登记”的过程就是Pod IP申请的完整闭环。它之所以能做到“层级同步”,是因为节点子网本身是上一层控制面分配好的,IPAM只需要在子网内部做分配,不需要也不允许跨出这个子网去拿IP。
如果你用的是Calico的IPAM,可以直接用calicoctl ipam show看看每个节点上IP的占用率,你能很直观地看到“总地址池有多大、已经用掉多少、还剩多少”。排障的时候这个命令非常有用,后面我会细说。
2.4 服务层级:ClusterIP是动态但有记录的
ClusterIP的分配跟Pod IP完全是两码事。Pod IP是“分布式”的,每个节点的CNI各自分配,互不打听;ClusterIP则是“集中式”的,由kube-apiserver里的ServiceRegistry在创建Service时从--service-cluster-ip-range里挑一个空闲地址,并且写进Service对象的spec.clusterIP字段。
ClusterIP这个地址有几个特性决定了它的层级位置:它不在任何网卡上真实存在,它是一个被“声明”出来的虚拟IP;Pod网段和Service网段之间没有物理路由关系,所有转发都靠节点上的kube-proxy规则完成。
这里有个细节值得注意:Service的ClusterIP一旦分配就不会变,除非你删除Service重建。kube-proxy会被动监听Service和EndpointSlice的变化,一旦发现Service创建或后端Pod变化,就重新计算规则。这就是第三层“同步”发生的地方:上游Pod列表的变化,会通过API server的事件机制通知到所有节点的kube-proxy,然后kube-proxy把规则重建一遍。
2.5 “层级同步”到底同步了什么
把三条线连起来看,所谓的“层级同步”我理解为三层递进关系:
第一层是集群规划层,你在初始化时给定Pod网段和Service网段,这是全局约束。第二层是节点分配层,Controller Manager将Pod网段切分成节点子网写入Node对象。第三层是Pod/Service实际分配层,CNI的IPAM基于节点子网给Pod编号,API server基于Service网段给ClusterIP编号。
这三层不是同时发生的,而是“上一层先确定,下一层才能动手”的依赖关系。同步就是通过API server的watch机制层层驱动:节点对象更新触发kubelet感知,Service对象创建触发kube-proxy感知。理解了这一点,你排查网络问题时就不会到处乱找,而是先问自己:问题出在哪一层?这一层的“上层输入”对不对?
另外我还想提醒一句:ETCD是这一切同步的存储底座,所有分配记录都落在etcd里。如果你做过备份恢复或者etcd出过问题,会发现大量“IP已经被占用”或者“Service端口冲突”的诡异现象,大概率是etcd里的分配记录和实际环境不一致了。
3. Pod网络层级的访问请求路由流程:一次请求的完整旅程
3.1 同节点Pod之间的通信
先假设一个最简单的场景:两个Pod在同一个节点上,名字叫Pod A和Pod B,它们之间要通信。很多新人以为这中间会经过什么复杂路由,其实没有。
每个节点上,CNI插件都会创建一个根网桥(比如Flannel的cni0,或者Calico的转发路径虽然不同但思路类似),每个Pod创建时会被放入独立的网络命名空间,同时生成一对veth虚拟网线,一头插进Pod的eth0,另一头插进节点的根网桥。
Pod A要访问Pod B时,请求从Pod A的eth0发出,沿着veth到达节点根网桥。网桥要决定这个包往哪送,会查自己的MAC地址转发表。由于Pod B也在这个网桥上,它的MAC地址早就学到过了,网桥直接就把包转给Pod B的veth,最后送达Pod B的eth0。
这个过程中完全没有经过节点物理网卡,也没有经过任何路由决策,属于“二层直通”。所以同节点Pod间通信速度很快,延迟很低。你也可以理解为:它们住同一个小区,串门直接走过去就行,不用上马路。
3.2 跨节点Pod之间的通信:overlay与underlay两种流派
跨节点的Pod通信才是k8s网络中最复杂的部分。请求同样是Pod A出网桥,但网桥查完转发表,发现目标MAC不在本节点,怎么办呢?这里出现了两个流派。
underlay(以Calico的BGP模式为代表):节点把自己的路由直接发布到物理网络里,让底层路由器知道“10.244.1.0/24这个网段通过节点X可以到达,10.244.2.0/24这个网段通过节点Y可以到达”。这样Pod A的包出了网桥之后,进入节点主机的路由表,命中一条到对端节点子网的路由,下一跳就是对端节点的物理IP,包直接通过物理网络送过去。全程没有封包拆包,所以性能最好。但前提是你的底层网络设备要能支持那么多路由条目,公司网络太旧或者交换机太烂,这条路就走不通。
overlay(以Flannel的VXLAN模式为代表):底层网络不参与Pod路由,节点之间自己搭隧道。Pod A的包到达节点主机后,路由表告诉他“要到对端Pod子网,得钻进vxlan隧道设备”。节点把整个原始包封进UDP报文里,外层目的地址是对端节点的物理IP,然后从物理网卡发出去。对端节点收到后拆掉外层UDP,露出原始包,再按二层/三层规则送到目标Pod。
用生活化的类比:underlay是“大家把门牌号都报到市政登记处,快递直接按地址送”;overlay是“大家商量好一个暗号,所有包裹塞进一个大箱子,运到后再拆开”。
从层级同步的角度看,无论是哪种流派,节点的路由规则都会在后端动态更新。比如Calico的Felix组件会监听spec.podCIDR和Pod的创建事件,实时增删路由;Flannel后端也会监听Node对象变化,维护VXLAN的FDB表。所以你会看到,新加一个节点后,过几秒钟所有节点都自动学会了到新节点子网的路由,这就是“层级同步”在路由层面的体现。
3.3 Pod访问Service:ClusterIP背后那半张表
Pod之间通信搞清楚了,接下来就是k8s里最常用的场景:Pod通过Service访问后端。这里面藏着一个很多人想不通的问题:ClusterIP是个“假IP”,没有网卡响应它,那请求是怎么被拦下来的?
答案是:每个节点上都跑着kube-proxy,它会把Service的ClusterIP和端口翻译成具体后端Pod的IP和端口。翻译可以走iptables规则,也可以走IPVS规则,默认场景里iptables最常见。Pod发出请求,目标是ClusterIP:Port,包刚出Pod命名空间到达节点网络栈的时候,iptables的PREROUTING链里有一条规则瞄上了它,DNAT一改,目标地址变成某个后端Pod的IP。
这个“某个后端Pod”是kube-proxy从EndpointSlice里挑出来的。EndpointSlice是k8s自己维护的对象,同步了Service和Pod的关系。只要后端Pod发生变化,EndpointSlice就会更新,kube-proxy接着更新规则,整个过程不需要人为干预。
这里有个性能层面的经验:当集群里的Service数量到了几千个,iptables规则会出现明显的性能下降,因为它是线性遍历的。这种情况建议上IPVS模式,IPVS用哈希表查找,规则多的时候依旧坚挺。很多公司集群规模一大就切IPVS,就是这个原因。切换方式很简单,改kube-proxy的启动参数--proxy-mode=ipvs,但注意要确保节点上装了ipvsadm依赖。
3.4 外部流量进集群:NodePort、Ingress、LoadBalancer
Pod之间的流量走完了,再看外部进来的流量。外部流量要进集群,首先得有一个“入口”,无非是三种:NodePort、LoadBalancer、Ingress。
NodePort最朴素。Service被定义成NodePort之后,每个节点上都会开一个高位端口(默认30000-32767),外部访问任意节点的这个端口就行。请求到达节点后,同样被iptables接管,DNAT到后端Pod。你可以理解为Service在每一家住户门口都挂了个信箱,投哪个信箱都会被邮递员送到真正的收件人那里。
LoadBalancer是在NodePort之上的壳,通常由云平台或外接LB实现:LB监听公网IP,把流量转发到后端任意节点的NodePort端口。也就是:外部客户端 → LB → 节点NodePort → kube-proxy DNAT → Pod。
Ingress则更偏“业务入口”,它本身不是流量通道,而是一堆路由规则。真正干活的是Ingress Controller(比如常见的nginx-ingress),它以一个Pod的形式跑在集群里,监听80/443端口,再根据Ingress规则把请求代理到对应的Service。所以Ingress的完整链路是:外部请求 → 节点Ingress Controller Pod → Service ClusterIP → kube-proxy DNAT → 后端Pod。
从层级同步的角度看,NodePort和LoadBalancer在Service创建时就分配好了端口和地址,Ingress规则则会随着Ingress对象的创建/更新实时生效,优先级通常按路径长短匹配,这些都属于“控制面同步到数据面”的典型过程。
3.5 请求出境:Pod访问外部网络
还差最后一段:Pod主动访问外网(比如调第三方API)。Pod发出请求,目标是公网IP,节点路由表发现目标不在集群网段内,默认路由会把包交给节点物理网卡发出。此时这个包源地址是Pod IP,而公网上的服务器是不认识私有网段地址的,所以节点必须做一次SNAT,把源地址替换成节点IP。
这个SNAT行为通常由CNI插件或者kubelet配置的masquerade规则实现。比如Flannel默认会开masquerade,Calico也有-nat-outgoing参数控制。你有没有想过为什么Pod访问外网需要SNAT,但Pod之间不需要?因为公网环境无法学习到你的Pod路由,而集群内部的路由是完整的。这是“扁平网络”和“真实互联网”之间不得不做的妥协。
4. 常见问题与排查技巧实录
4.1 如何快速确认IP层级状态
遇到网络问题,我最先做的事永远是“分层确认”。如果某个Pod不通,第一眼看的是它的IP落在了哪个节点子网里:
kubectl get pod -o wide kubectl get node <node-name> -o jsonpath='{.spec.podCIDR}'目的很明确:先确认Pod IP和它所在节点的PodCIDR是否匹配。如果不匹配,问题基本就锁定在CNI的IPAM层,大概率是IP池冲突或者数据存储残留;如果匹配,再往下查路由。
这个“先看归属”的习惯,能帮你省掉大量无效抓包时间。我见过太多同行一上来就tcpdump,抓了半天发现源和目的地址都不对。
4.2 同节点通、跨节点不通
这是典型的“路由缺失”或者“overlay隧道故障”症状。同节点通信经过网桥,跨节点通信要依赖节点路由表或隧道表,后者的维护完全依赖控制面的同步。
排查第一步先看路由表到底学没学到:
ip route | grep <对端Pod网段>如果没有任何到对端Pod网段的路由,说明CNI的控制面组件没把路由下发下来,或者Node对象上的PodCIDR没被正确识别。第二步看overlay设备是否正常,比如Flannel的vxlan接口是否存在、ARP表/FDB表是否完整。第三步看节点防火墙,很多“跨节点不通”其实是宿主机安全组策略拦住了VXLAN的UDP端口,换个端口重新测试马上暴露。
4.3 Service访问不通,但后端Pod明明活着
这种问题最气人:Pod列表里全是Running,Service也有Endpoints,但访问就是超时。按照层级,这类问题先看EndpointSlice是否更新了:
kubectl get endpointslices -l kubernetes.io/service-name=<service-name> -o yaml如果你看到Endpoints里的地址和真实Pod地址对不上,说明控制面同步卡了,这种时候重启相关控制器或者等一会儿通常能恢复。如果Endpoints是对的,那问题就跑到kube-proxy的数据面了。
看iptables规则是否生成:
iptables -t nat -L KUBE-SERVICES -n | grep <ClusterIP>没有规则,说明kube-proxy没有正常监听或者sync出了差错。有规则但转发失败,就得检查后端Pod的端口是否真的在监听。记住一个通用检查模型:规则有没有、地址对不对、端口活不活,三步走完基本能定位。
还有一种隐蔽情况:ClusterIP分配时和Pod网段重叠,导致路由冲突,请求被节点当成普通的Pod寻址直接扔进CNI,永远到不了kube-proxy手里。这类问题要回到最上层的CIDR规划去解决。
4.4 网络策略不生效
开启了NetworkPolicy之后,你会发现即使路由完全正常,Pod之间还是不通。因为网络策略不是在路由层拦截,而是在CNI的数据转发路径上做过滤。
排查思路:先确认CNI是否支持NetworkPolicy,Flannel默认不支持,Calico和Cilium支持;然后确认策略对象有没有被正常选择到目标Pod,用kubectl describe networkpolicy查看Pod Selector匹配结果;最后看策略的“默认拒绝”行为,k8s里只要加了一条NetworkPolicy,未被匹配的流量就可能被丢弃,这和防火墙里“白名单优先”的思路很容易混淆。
4.5 一个好用的排查顺序清单
我整理了一份自己的排查路径,照着走基本不会迷路:
- 确认Pod IP与所在节点PodCIDR归属一致;
- 确认目标节点有到对端Pod网段的路由;
- 确认overlay隧道状态(如果在用);
- 确认kube-proxy规则与EndpointSlice一致;
- 确认目标Pod端口存活;
- 确认宿主机防火墙和云安全组没有拦截;
- 如果都查完了还是不行,看CNI组件的日志,Calico可以看calico-node容器日志,Flannel看flanneld日志。
其中第2、4步是最容易被忽略的,因为它们的“故障表象”常常被第1步的“一切正常”掩盖掉。
5. 从一次真实故障里拆解“层级同步”的威力
我印象最深的一次排障,场景是某跨平台系统在扩容新节点后,老节点上的Pod访问新节点上的Pod,一半通一半不通。当时第一反应是路由学得到不到位,看了老节点路由表发现新节点Pod子网路由确实有,但下一跳指向了错误的物理IP。
查下去才发现,新节点加入时,Controller Manager写进Node对象的InternalIP和实际物理网卡IP不一致。原因是最初安装k8s时节点用的是别名IP加入的,所有组件都把这个别名记到了心里,CNI路由也跟着错。这就是典型的“层级同步错位”:上一层写错了Node地址,下一层所有依赖同步的都跟着错。
当时处理方案有两种:一是修正Node对象的InternalIP并重启相关CNI组件,二是重新以正确IP加入节点。后来我们选择了后者,因为前者涉及的修正面太广,容易引入更多不一致。
这个事给我的启发是:k8s网络的“层级同步”不是写入就完事,它是一条依赖链。你在底层节点上手动改一个IP,控制面的记录不会自己跟着变;你在控制面改一个CIDR,节点的路由也不会自动重算。所有东西都得通过API对象和组件配合才能保持一致。所以操作任何一层的时候,一定要想清楚这一层的变化会波及下面几层的哪些依赖。
另外我还想提一个工具上的经验:同一套集群里最好别混用多种CNI,我见过有人从Flannel切到Calico但保留了旧的vxlan网卡,导致节点上同时存在两套路由规则,优先级一乱整个集群的跨节点通信直接瘫痪。切换CNI的正确姿势是:先清空Pod,清理旧CNI的配置文件和网卡残留,确认路由表干净后再装新的CNI,最后分批滚动节点验证。几个字总结就是:宁可慢,不可乱。
我个人在实际操作中的体会是,k8s网络排查最多的收获不是在“抓包看内容”上,而是养成了“分层归因”的思维习惯。接到一个网络问题,先别急着怀疑哪一个组件,先判断它发生在哪一层、这一层的上层依赖是否正常、下层数据面是否同步。把这条链路想通了,90%的问题都能快速定位,剩下的10%再交给包转发细节去抠。
这个内容后续还可以顺着两个方向展开:一是Cilium的eBPF实现,它把“层级同步”变成了更实时的透传机制;二是多集群网络下IP规划怎么不冲突。但从基础到进阶,先把今天这套三层模型吃透,后面再学什么都快。