ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

K8s网络排查指南:从CNI到Service的层层解析

K8s网络排查指南:从CNI到Service的层层解析 接触K8s的人最先崩溃的往往是网络。我见过不少从Docker平滑转型过来的兄弟遇到的第一道坎不是YAML写错而是搞不清为什么容器有IP了还连不通、Service明明存在却访问不了、两个节点上的Pod互相ping不通。今天这篇不打算贴大段官方文档而是把这些年在K8s网络里踩过的坑串起来从网络模型到底层CNI、从Service到DNS再到典型的排障链路按实际排查思路一层层剥开。适合刚入门K8s的运维和开发也适合那些Docker很熟、K8s很懵的过渡期选手。1. 先拆一个常见误区K8s网络不是一种网络是四张网叠着用我刚开始学K8s网络时最崩溃的一点是文档里动不动说Kubernetes网络模型是扁平的可我明明看到一大堆概念ClusterIP、CNI、VXLAN、Ingress、NetworkPolicy……它们全叫网络却完全不是一回事。后来我把它们拆成四层才豁然开朗每一层解决的问题都不一样你踩的每个网络坑几乎都能定位到具体某一层。这四层分别是容器网络同一个Pod内部、集群网络Pod到Pod、服务网络Pod到Service、入口网络外部到Service。先看最小的单位——同一个Pod里的多个容器。K8s里的Pod和Docker的容器不是同一个概念尤其在网络层面Pod里所有容器共享同一个网络命名空间这才是理解K8s网络的第一块基石。1.1 同Pod内容器之间的localhost通信同一个Pod下的两个容器比如一个nginx容器和一个sidecar日志收集容器互相访问时可以直接用localhost。这个能力不是nginx或者sidecar各自带出来的而是来自Pod里那个经常被忽略的基础设施容器——pause容器也叫sandbox容器。K8s创建Pod时会先拉起pause容器它本身不干活只管创建一个network namespace并持有它Pod里的业务容器启动时通过--networkcontainer:pause容器ID加入同一个命名空间。这个机制可以类比成你在一台电脑上同时开了浏览器和终端它们共享同一张网卡、同一个回环设备、同一套端口空间。换句话说同一Pod里的容器对彼此来说就是同一台机器上的不同进程。这个设计带来两个非常实际的后果也是新手最容易踩的坑同Pod内两个容器不能监听同一个端口端口冲突就是进程打架后启动的容器会一直报Address already in use。同Pod内容器之间通信不需要任何Service或负载均衡它们天然就是直连的走localhost就行。如果你在Pod里看到类似Connection refused别急着查网络插件先看看是不是服务只监听在某个容器的独立IP上或者进程本身挂了。我见过一个案例sidecar容器想去连主容器的8080端口结果主容器只监听了0.0.0.0但进程启动失败现象却像网络没通绕了不少弯路。1.2 四类网络流量对应四个排障入口把上面的概念再放大就是K8s网络里最重要的分类方法。实际工作中我习惯把所有流量分成四类同Pod内容器之间走localhost不需要特殊组件。Pod到Pod无论两个Pod是否在同一节点都要能直接通过Pod IP互访。这是东西向流量由CNI插件负责。Pod到Service业务访问Service的ClusterIP时由kube-proxy把流量转发给某个后端Pod。这也是东西向。外部到Service用户从集群外访问业务走NodePort、LoadBalancer或Ingress这是南北向流量。举个例子你部署了一个frontend和一个backendfrontend通过Service访问backend你的排查路径应该是先确认backend的Pod本身起来了没有再确认backend的使用Pod IP能不能直接访问最后才查Service的selector和endpoint。很多同学排查问题一上来就盯着Service看其实Pod到Pod那一层还没通Service后端全是不可用的Endpoint那排查顺序一定乱。我在后面专门写了按层排查的方法论先记住这个分层思维后面才会顺。2. 为什么K8s坚持扁平IP模型和Docker单机网络的本质差异我跟很多从Docker过渡到K8s的人聊过他们最不理解的一件事是明明Docker的网络很好用容器有IP、宿主机端口映射一下就能用为什么K8s偏要搞出CNI这么一大套东西答案在于使用场景完全不同。2.1 从Docker bridge/NAT到K8s CNI的转变Docker默认的bridge网络是一个NAT网络。容器被分配172.17.0.0/16网段里的地址通过docker0网桥和宿主机相连访问外网时由宿主机做源地址转换。多个容器之间可以互通但宿主机之外的节点根本不知道这个容器的IP。要让外部访问容器得用-p 8080:80做一个端口映射容器IP对外部来说是不可路由、不可感知的。单机场景这么做毫无问题但K8s从一开始定的调子就是容器就是一台微型的、可直接寻址的主机。一个Pod可能会有多个副本被调度到不同节点如果网络模型是NAT那任何跨节点的直接通信都要做端口映射和地址翻译维护量和故障排查复杂度会迅速失控。所以K8s选择让每个Pod拥有一个集群范围内真实可见的IP这就是所谓的扁平IP模型。Docker和K8s在网络上的诉求差异可以用一个比喻说明Docker像一栋楼里的分机电话内部随便拨但外部必须打总机号码再转接K8s则要求每个工位都有一部全局唯一的直拨电话同事之间直接拨号就行不需要经过前台转接。K8s把总机和转接都拆掉了代价就是你必须自己搞定所有内部座机怎么互相连通这就是CNI存在的意义。2.2 网络模型的四条铁律K8s官方文档对网络模型的态度非常明确四条规则所有Pod与所有Pod通信不需要NAT。所有节点与所有Pod通信不需要NAT。Pod看到的自身IP与其他Pod看到的该Pod IP完全一致。跨网络实体之间的通信不需要NAT。翻译成人话任何一个Pod的IP在整个集群任何地方都是同一个ID任何人访问它都不需要翻译。这条规则的直接推论是你不能在K8s里依赖端口映射思路去暴露服务而要把每个Pod当作一台独立主机去规划网络。这个思路扭转过来之后很多概念都变得顺理成章。2.3 Pod是网络最小单元容器只是共享网络栈的房客刚才说了pause容器持有网络命名空间这里再补充完整一点。既然网络最小单元是Pod而不是单个容器那么Pod里所有容器共享同一个IP、同一张虚拟网卡、同一个主机名以及同一个端口空间。好处是你无需关心容器编排本身带来的网络复杂性坏处是排故障时你必须先问一句这个服务在Pod内监听在哪个端口的哪个地址上而不是理所当然地认为每个容器都应该有自己的IP。而且除同一Pod外所有Pod之间的流量对K8s来说都是节点间流量不管物理上是不是在同一台机器上。这就是为什么单节点集群里你也必须装好CNI没有CNI跨Pod通信根本没有通路。接下来就要说CNI这个把所有Pod串成一张网的核心机制。3. 东西向通信的引擎CNI插件与跨节点数据通路既然扁平IP模型需要每个Pod拥有集群内可达的IP谁来分配IP、谁来打通不同节点间的数据通路答案是CNIContainer Network Interface它不是K8s核心组件而是一套可以插拔的生态。理解了CNI你才算真正把K8s网络的地基踩实了。3.1 CNI插件的工作流程CNI定义的是容器运行时创建好网络命名空间之后怎么请第三方插件来把网络搭起来的一套标准接口。Kubelet在创建Pod时会先把沙箱容器pause的network namespace准备好然后调用容器运行时运行时再根据配置调用对应的CNI插件二进制。CNI插件的典型工作流程是创建一个veth pair一端插入Pod的网络命名空间通常叫eth0另一端插到主机侧通常叫calixxx或vethxxx然后分配IP地址并把它写到Pod的网卡上再添加相应的路由。不同插件在分配IP打通跨节点链路这两步的实现方式差异极大这也是Flannel、Calico、Cilium的流派之争。这里有个配置层面的细节容易被忽略CNI插件目录有多份配置文件时Kubelet会按文件名顺序读取并逐个apply。如果你同时装了多个CNI又没清理干净配置冲突会导致Pod网卡起不来状态一直卡在ContainerCreating。我现实中就见过一台机器上残留着calico配置但又装了flannel结果新Pod全卡死排了半天最后发现是/etc/cni/net.d/目录里多个conflist在打架。所以装CNI之前先清空这个目录看起来是小事能省很多事。3.2 Flannel的VXLAN/host-gw和Calico的BGPFlannel是初学者最常用的插件它有几种后端最常见的是VXLAN和host-gw。VXLAN模式是在内核里创建vxlan设备把原始数据包封装进UDP包再发到目标节点默认端口是8472/UDP代价是每个包都有封装开销性能相对弱一些。host-gw模式则更暴力直接在主机路由表里写入目标Pod网段的路由条目下一跳指向目的节点不涉及任何封装性能好很多但前提是集群节点必须在同一个二层网络能直接ARP到对方的MAC地址否则路由写不进去也发不出去。Calico走的是另一个流派。它基于BGP协议让每个节点作为BGP speaker把该节点上运行的Pod网段通告给其他节点节点收到后放进内核路由表数据包直接按三层路由转发不额外封装性能通常比Flannel的VXLAN好。在没有开启IPIP封装的情况下Calico要求底层网络能承载BGP流量TCP 179也要求跨节点三层路由可达开启IPIP封装时走UDP 4789。Cilium则是最近几年势头很猛的eBPF派。它把数据路径从iptables和overlay里解放出来直接在内核态的eBPF程序里处理转发、负载均衡、可观测性和安全策略性能优势明显但要求内核版本至少4.19推荐5.x以上对团队的内核排障能力要求也高。这三条路径的取舍很清晰想快速搭测试环境Flannel VXLAN零门槛生产环境要稳定和性能平衡Calico BGP是很多团队的选择内核够新、团队愿意折腾Cilium是面向未来的投资。3.3 真实场景怎么选CNI我给一个非常主观但实用的选型建议。如果你的集群只有三五台机器、节点在云厂商同一VPC内、主要跑内部测试和业务验证那Flannel host-gw就够了配置简单也不容易出幺蛾子。如果集群规模要从几十台往几百台走并且要用NetworkPolicy做网络隔离那直接上Calico它的BGP mesh在大规模下也更可控IPIP和VXLAN两种模式可以按需切换。如果团队已经有较强的内核能力业务对PPS或延迟敏感且希望把网络转发、监控、策略统一到一个平台Cilium值得直接上生产。这里再强调一个很多人忽略的坑就是网段规划。K8s安装时要指定--pod-network-cidr不同CNI对这个网段的处理方式不同。如果你自定义了PodCIDR却没有在CNI配置里同步或者两个网段选得太近导致路由冲突后面排查起来会非常痛苦。所以安装前先把网段规划写清楚Pod网段、Service网段、节点网络三者尽可能错开别图省事全用10.x。4. Service让飘忽不定的Pod IP变成稳定入口Pod会被调度、会崩溃、会扩容缩容IP也是随时变的。如果业务之间直接依赖Pod IP通信那Pod一重启依赖方就得跟着改配置。Service就是用来解决这个问题的抽象层它提供了一个稳定的虚拟IP并把流量负载均衡到一组Pod上。4.1 ClusterIP/NodePort/LoadBalancer/ExternalName四种类型先理清四种Service类型因为它们对应的网络入口完全不同。ClusterIP是最常用的一种也是默认类型。Service创建后K8s会从ServiceCIDR里分配一个虚拟IP这个IP只在集群内部有效。Pod里访问ClusterIP时请求被kube-proxy拦截并转发到一个健康的后端Pod。为什么它是个虚拟IP因为它不是绑定在某块网卡上的真实地址而是靠节点上的转发规则实现的。NodePort在ClusterIP之上叠加了一层K8s会在每个节点上开一个高位端口默认30000-32767把访问节点IP:NodePort的流量转发到对应的ClusterIP再转发到后端Pod。这样集群外只要知道任一节点的IP和端口就能访问服务。LoadBalancer一般是云厂商提供的它在NodePort之上再挂一个云负载均衡器把公网流量分发到各节点的NodePort端口。ExternalName最特殊它不创建ClusterIP而是在DNS层面返回一个CNAME让Service名字直接指向集群外的域名适合对接外部系统。4.2 kube-proxy的iptables和IPVS模式路由规则的两种实现Service之所以能生效核心执行者是kube-proxy这个组件。它通过API Server监听Service和EndpointSlice的变化然后在每个节点上写入转发规则。转发规则有两种主流实现方式。iptables模式是K8s的经典默认模式。它会在NAT表里创建类似KUBE-SVC-XXXX和KUBE-SEP-XXXX的规则链通过DNAT把目标为ClusterIP的流量转换到某个Pod IP。链路很长每增加一个Service就增加若干条规则。随着Service数量增多iptables规则匹配是线性遍历的规则越来越长性能会明显下降。另一个隐患是更新规则时往往全量刷新当Service规模很大时会造成瞬时的规则不一致。IPVS模式则完全不同。它利用内核的IPVS模块本质上是一张哈希转发表查找复杂度是O(1)不管你有几百个Service查表效率几乎不变。它还天然支持多种调度算法比如轮询、最少连接、哈希等。切换方式很简单把kube-proxy的配置里mode改成ipvs或者启动参数加上--proxy-modeipvs即可。切换时的坑也要说一下IPVS会用到内核模块ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh如果节点内核没加载这些模块kube-proxy会报错。所以启用前先确认模块已经加载可以用lsmod | grep ip_vs查看没有就modprobe一下否则切完你会发现服务全网不通。生产环境Service数量超过几百个时我强烈建议切到IPVS模式尤其是流量比较大的场景。4.3 Headless ServiceRedis Cluster这类有状态应用怎么直连Pod有些场景我们不需要负载均衡反而希望客户端直接拿到所有Pod的真实IP。比如Redis Cluster它的每个节点都要知道其他节点的真实IP才能构建集群ClusterIP虚拟IP对它来说没有意义。解决办法是建一个ClusterIP: None的Headless Service。Headless Service不会分配ClusterIPDNS查询时直接返回所有Pod IP列表客户端自己决定连哪个。用StatefulSet部署Redis Cluster时每个Pod都会有一个稳定的DNS名字比如redis-0.redis-headless.namespace.svc.cluster.local集群节点间通过这个稳定名字互相发现即使Pod重启IP变了名字也不会变。我在实际搭建Redis集群时踩过一次坑把Service设成了普通ClusterIP结果各节点起来后互相通过虚拟IP通信Redis Cluster拒绝了所有来自非本节点IP的握手因为虚拟IP不是任何节点的真实IP。换成Headless Service后节点直接用真实Pod IP通信问题立刻消失。这个案例很典型有状态应用通常需要Headless Service不是偶然现象而是网络模型决定了它们必须拿到真实可路由的地址。5. 集群的默认路由表DNS解析链路与集群内外流量入口Service给了稳定IP可业务总不能全靠写IP来访问服务吧K8s的做法是给集群内置一套DNS系统让服务自动获得域名。这套系统就是CoreDNS它和Ingress、NetworkPolicy一起构成了集群网络的上层建筑。5.1 CoreDNS和Service域名解析规则CoreDNS在Kubernetes里的角色是集群内DNS服务器通常以Deployment形式运行在kube-system命名空间ClusterIP固定为10.96.0.10这个地址是默认ServiceCIDR的第十个地址。Kubelet在创建Pod时会把它的/etc/resolv.conf指向CoreDNS的ClusterIP并配置好搜索域。一个Service创建后会自动获得类似service-name.namespace.svc.cluster.local的域名。Pod要访问同命名空间下的服务时直接写服务名就行比如访问myapp跨命名空间则写成myapp.prod.svc。搜索域机制让短名也能解析但如果你定义较长的域名或者涉及跨命名空间访问经常会在resolv.conf的搜索域上花时间。我排过一个特别恶心的DNS问题业务Pod里getent hosts myapp能解析出ClusterIP但curl就是超时后来逐层查才发现是CoreDNS的Pod被NetworkPolicy限制了访问某外部DNS而业务Pod里配置了自定义的上游DNS导致解析链路变得极其迂回。所以排DNS问题时先用kubectl run -it --rm debug --imagebusybox -- nslookup myapp做一个最干净的解析测试排除业务容器自身镜像里的DNS配置干扰。5.2 Ingress七层入口的具体落地Service解决了四层的负载均衡但外部访问通常讲究域名路径HTTPS证书这些七层信息这就是Ingress要管的。Ingress本身是一个API对象只定义了路由规则哪个域名哪个路径转发到哪个Service。真正执行的是Ingress Controller比如nginx ingress controller或traefik。Controller通过监听Ingress资源变化把规则转成自身的反向代理配置比如nginx的server块再通过NodePort或LoadBalancer暴露出去。如果只有一个入口你也可以直接在Service上开NodePort外面再套一层Nginx或者云负载均衡器。但集群里有几十个服务时用Ingress集中管理路由的好处就出来了一个公网入口按域名分发到不同ServiceTLS证书也统一在Ingress层终结业务Pod完全不需要关心外部怎么进来。实现上要注意IngressClass的配置社区里几套controller并存时如果没有指定IngressClass会出现我改了Ingress规则但没生效的诡异现象因为多个Controller都在争抢处理同一个Ingress对象。5.3 NetworkPolicy给东西向流量加白名单默认情况下K8s集群里任何Pod都能访问任何Pod这在公网或者多租户环境里就是裸奔。NetworkPolicy的作用是给Pod之间的东西向流量加访问控制本质是一套防火墙白名单规则。举个例子一个线上环境里有frontend、backend和db三个应用你希望frontend只能访问backendbackend只能访问dbdb不对frontend开放。用NetworkPolicy怎么做先给每个应用打上标签然后在backend的NetworkPolicy里配置ingress: from: podSelector: matchLabels: appfrontend只允许来源标签是frontend的请求进来其他来源一律拒绝。NetworkPolicy一旦作用于某个Pod默认策略就变成拒绝所有未显式允许的流量这和防火墙的默认deny逻辑是对应的很容易让人懵只写了允许规则没写Deny规则为什么流量全被拒了因为策略生效后的隐含行为就是deny all。需要特别注意的是Flannel不支持NetworkPolicyCalico和Cilium支持。如果你的业务强制要求网络隔离选型时就得先确认CNI是否实现了NetworkPolicy API而不是只看Overlay性能。6. 现场排障链从kubeadm报错到跨节点不通的完整排查思路前面讲了很多概念但读者真正需要的往往是出了问题我该怎么办。我挑了三个最常见、也最典型的K8s网络故障把排查链路完整写出来遇到类似情况可以直接照着抄。6.1 the api server is not healthy after 4m0.00747357s这类安装期网络问题很多人在kubeadm init的时候卡在apiserver健康检查这个错误上报错里最常见的一句话就是the api server is not healthy after 4m0.00747357s。这个报错本质上不是网络问题而是apiserver容器起来后无法通过健康检查但大部分深层原因又跟网络有关。我的排查顺序是这样的。第一步看kubelet日志journalctl -u kubelet -f确认kubelet有没有持续报错是不是cgroup driver不匹配比如Docker用的是cgroupfs而kubelet用的是systemd两个管理方式不一致会导致Pod状态反复。第二步crictl ps -a | grep kube-apiserver看apiserver容器是不是一直CrashLoopBackOff再用crictl logs 容器ID查apiserver自己的启动日志重点看它能不能连上etcd、有没有证书路径错误。第三步如果apiserver容器日志一切正常但健康检查还是失败回头看6443端口和防火墙ss -lntp | grep 6443很多时候是云安全组或firewalld把6443挡了kubelet的本机健康检查访问不到端口于是kubeadm认为apiserver不健康。这里有个我反复踩过的坑kubeadm init前一定要先swapoff -a并注释掉/etc/fstab里的swap行。K8s对swap的检查很严格一旦检测到swap开启组件状态会非常不稳定而它报出来的症状又常常以apiserver不健康的形式出现排查起来会绕一大圈。另外使用containerd作为CRI时要确认/etc/containerd/config.toml里sandbox_image对应的pause镜像源可以正常拉取这个镜像拉不下来所有Pod都会卡在ContainerCreatingapiserver同样可能因此不健康。如果你用Docker作为CRI但K8s版本是1.24及以上会发现docker的socket已经不被识别需要在kubeadm init时显式指定--cri-socketunix:///var/run/cri-dockerd.sock。6.2 跨节点Pod通信不通我按什么顺序查集群搭好了在两个节点上各部署一个Pod互相ping不通这是CNI层最常见的问题。我的排查顺序非常固定避免东一榔头西一棒子浪费时间。第一步看CNI组件Pod是否Running。kubectl get pods -n kube-system -o wide如果flannel或calico的Pod没有正常运行先把它们修好其他的都不用看。第二步在节点上用route -n或ip route看有没有到目标Pod网段的路由。Flannel host-gw模式会在每个节点写入类似10.244.1.0/24 via 节点IP dev eth0的路由Calico同样会在节点路由表里写入PodCIDR路由。如果这条路由不存在说明CNI控制器没有正确同步节点路由多半是CNI组件和API Server之间的连接出了问题。第三步确认底层端口放行。Flannel VXLAN需要UDP 8472Calico BGP需要TCP 179IPIP隧道需要UDP 4789这些端口在云安全组和节点防火墙里必须放行。我遇到最多的跨节点不通最后都查到安全组上。如果以上都正常但依然不通接下来在Pod内部做连通性测试同时对端节点做tcpdump抓包。在源Pod里执行tcpdump -i eth0 icmp在目标节点上执行tcpdump -i eth0 icmp看看包到底是从哪里消失的。如果目标节点抓不到封装后的包说明overlay网络的VTEP或路由有问题如果抓得到但没进入目标Pod说明目标Pod的veth或iptables规则有问题。抓包是网络排障的终极大杀器但不要一开始就上先把前面几步走完通常能解决80%的问题。6.3 NodePort外部访问失败先别急着怀疑防火墙NodePort访问失败的排障有一个很容易被带偏的信号防火墙。很多人的第一反应是去检查云安全组我当然也会查但更重要的理解是访问节点IP:NodePort链路是外部流量-节点端口-Service转发-后端Pod每一环都可能断盲目开防火墙端口可能把隐患掩盖掉。我用一个最小化的验证顺序来定位。第一步kubectl get svc确认Service类型确实是NodePort、端口范围没填错。第二步在集群内测试ClusterIPcurl http://ClusterIP:port。这一步如果不通说明问题在kube-proxy或后端Pod和防火墙无关如果通了说明Service转发逻辑是好的。第三步在节点本机上测试curl http://节点IP:NodePort注意这里绕过了部分云网络但还经过本机kube-proxy规则。第四步如果本机通但外部不通那才真正指向防火墙和安全组。第五步如果本机也不通检查kube-proxy组件是否正常运行并iptables -t nat -L -n | grep KUBE-NODEPORT确认NodePort规则存在如果规则没了就看kube-proxy日志排查它是不是因为API Server连接断开导致规则同步中断。还有一个特别常见的隐形杀手Pod readiness探针没通过或者Service的selector匹配不到任何Pod。此时Service后端一个Endpoint都没有kubectl get endpoints是空的怎么访问都不会通。这种故障有时候比防火墙还隐蔽因为你看iptables规则完全正常但所有转发都落在了一个不存在的endpoint上等于投递到空地址。我自己带过很多新人他们最有效的成长路径不是先啃完所有概念而是先把一个集群搭出来、把样例服务通起来然后故意制造几类网络故障再按层排查。K8s网络表面上复杂本质上就是我在开头说的四层结构加三个关键组件——CNI负责Pod到Podkube-proxy负责Pod到ServiceIngress和DNS负责集群对外入口。把这几个锚点钉死剩下的细节都是在这张地图上填空。最后再分享一个操作习惯每次搭建集群前我会把PodCIDR、ServiceCIDR和节点网络写在一个小表格里贴在终端上方。网络排障时先确认这个IP属于哪一层、应该由哪个组件负责再动手抓包改规则能省掉至少一半的无效排查时间。网络从来不是K8s里最简单的话题但只要有了清晰的分层视角它也不会再是过不去的坎。
返回列表