Kubernetes Service与IPVS模式详解:从原理到排错实战 你有没有遇到过这种场景集群里明明副本都在 Running前端却频繁报错查了一圈发现后端 Pod 的 IP 变了而调用方还死守着旧的 Pod IP 不放当 Service 和 IPVS 这两个词同时出现时多半是 kube-proxy 把默认转发模式切换成了 IPVS目的就是要解决这个问题。这篇从 Service 的职责讲起重点拆解 IPVS 模式下的负载均衡链路带你把它完整跑通。1. 为什么要 ServicePod 的“易碎”属性决定了你绕不开它1.1 一个让我下定决心重新梳理 Service 的故障现场有次我在某个压测环境里调试应用Deployment 副本数从 2 扩到 5前端调用方却开始大面积超时。排查时我发现调用方配置的是某个 Pod 的 IP而不是服务名。结果扩缩容触发了一次 Pod 重建旧的 Pod IP 被回收新 Pod 拿到另一个 IP前端还在用旧地址自然全挂。这不是个例。在 Kubernetes 里Pod 本质上就是“易碎”的节点故障、更新版本、资源不足导致驱逐、滚动发布任何一个动作都可能让 Pod 消失并重建而重建后的 Pod IP 十有八九和原来不一样。你要是让业务直接通过 Pod IP 互相调用那整个集群就是一座随时会塌的积木塔。所以集群里必须有一个稳定的“访问入口”让调用方只认这个入口不关心背后 Pod 怎么变。同时当后端有多个副本时这个入口还要能把流量分摊到不同 Pod 上不然所有请求都打到同一个 Pod其他副本闲着这就失去了扩容的意义。1.2 Service 是那个“门口的前台”不是“具体工位”Service 是 Kubernetes 内置的一种抽象它会创建一个虚拟 IPClusterIP这个 IP 在整个集群内可用并且不会因为 Pod 变化而变化。你可以把它理解成公司前台员工工位可以换、人员可以调整但前台永远在那访客只需要找前台就行。具体到实现上Service 通过 selector 选择一组 Pod并实时维护 Endpoints 列表。Endpoints 就是当前真正在提供服务的 Pod IP 列表。只要 Pod 的标签和 Service 的 selector 匹配并且 Pod 处于 Ready 状态它就会出现在 Endpoints 里。整个过程是自动的不需要你手动改配置。当请求打到 Service 的 ClusterIP 上集群里的 kube-proxy 会根据转发规则把流量定向到 Endpoints 里的某个 Pod IP。这就是负载均衡的雏形。所以 Service 不只是“稳定 IP”它本身就承担了客户端负载均衡的职责把同一个 ClusterIP 的流量按某种策略分配到多个后端 Pod。1.3 三种 Service 类型不同阶段的“对外开放方式”Service 在不同场景下给外部提供访问入口的方式也不一样最常见的三种是 ClusterIP、NodePort、LoadBalancer。ClusterIP 默认类型只给集群内部访问比如 A 服务调 B 服务用这个就行。它提供一个虚拟 IP但如果你在集群外根本访问不到这个 IP因为它不是挂在物理网卡上的真实地址。NodePort 在 ClusterIP 基础上给每个节点额外开放一个固定端口。外部访问任意一台节点的这个端口流量就会进入集群再被转发到 ClusterIP 背后的 Pod。适合本地测试、临时环境、或者不想依赖云厂商的场景。LoadBalancer 通常在云环境下使用云厂商的控制器会创建一个外部负载均衡器把公网流量接入集群。它的底层其实是 NodePort 套了一层外壳。你可以把它理解成LoadBalancer 是“高级 NodePort”NodePort 是“高级 ClusterIP”三者是层层包含的关系。2. IPVS 和 iptableskube-proxy 的两种“流量搬运工”2.1 kube-proxy 在整条链路里到底扮演什么角色你可能听过 kube-proxy但不一定清楚它具体做什么。kube-proxy 是运行在每个节点上的代理进程负责监听集群里 Service 和 Endpoints 的变化然后把对应的转发规则写到当前节点的网络层里。这里的网络层主要是 netfilter也就是 Linux 内核里的网络包过滤框架。iptables 和 IPVS 都是操作 netfilter 的用户态工具但两者的工作方式差别很大。当请求访问 Service 的 ClusterIP 时节点收到数据包后kube-proxy 写入的规则会修改包的目的地址把它改成某个后端 Pod 的真实 IP这个过程叫 DNAT。为了让后端在响应时还能原路返回iptables 和 IPVS 都会借助 conntrack 机制来追踪连接状态确保回包能正确送回客户端。说句大白话kube-proxy 本身不处理业务流量它只负责往内核里“写规则”。真正搬运数据包的是内核里的转发机制。而选择 iptables 还是 IPVS就是在选择用什么方式来“搬运”。2.2 iptables 模式和 IPVS 模式的核心差异早期 Kubernetes 默认用 iptables 模式它的规则是链式结构的。每新增一个 Serviceiptables 规则就会在转发链上增加几条条目。数据包到达时要按顺序从头到尾匹配这些规则直到命中。想象一下一个排队窗口队伍越来越长每个人都要从队头开始问一圈才轮到自己效率自然高不了。当集群里只有几个 Service 时iptables 性能没有问题。但当 Service 数量增长到几百个每条链上的规则条目可能达到几千条匹配开销就会显著增加。在流量很大的生产环境里CPU 会消耗在无谓的规则匹配上延迟也会飘。IPVS 完全是另一种设计。它运行在内核态直接基于哈希表做查找时间复杂度是 O(1)不会随着 Service 数量增加而明显恶化。同时IPVS 本身就是一个专业的负载均衡模块支持多种调度算法比如轮询、加权轮询、最少连接等。我用一个不严谨但好懂的类比iptables 像人工柜台客户排队一个个问“你要办什么业务”IPVS 像智能叫号系统你一来就根据号码分配到对应窗口根本不用逐个问。所以在大规模 Service 场景下IPVS 是更合适的选择。对比项iptables 模式IPVS 模式查找方式链式规则逐个匹配内核哈希表 O(1) 查找规则数量影响显著Service 越多越慢很小性能平稳调度算法基本只支持随机/轮询rr、wrr、lc、wlc 等丰富算法内核依赖依赖 iptables/ipset依赖 ip_vs 系列内核模块运维工具iptables 命令ipvsadm、ipset适用规模Service 少、流量小Service 多、流量大2.3 IPVS 调度算法流量按什么规则分给后端IPVS 本身是 Linux 内核里的虚拟服务器模块设计目标就是高性能负载均衡。它支持的调度算法很多Kubernetes 里常用这几个rr轮询请求按顺序逐个分给后端 Pod默认算法简单公平。wrr加权轮询给每个后端设置权重权重高的分到更多连接。lc最少连接谁当前连接数少就分给谁适合长连接场景。wlc加权最少连接在最少连接基础上加了权重因素。sh源地址哈希同一个来源 IP 固定分给同一个后端可以用来做简单的会话保持。在 Kubernetes 中kube-proxy 会把所有 Ready 的 Pod 加入 IPVS 转发组默认权重是 1。如果你不修改 kube-proxy 的调度器配置默认就是 rr。实际上绝大多数业务用默认 rr 就够了但如果你有大量长连接或需要源 IP 保持可以考虑改成 wlc 或 sh。需要特别提醒的是这个调度器是 kube-proxy 的全局配置不是针对某一个 Service 单独指定的。你只能通过修改 kube-proxy 配置来统一调整而不是在 Service 的 YAML 里写一个字段就能改变算法。这一点很多刚接触的人容易搞混。2.4 让 kube-proxy 换成 IPVS三步切换全程记录如果你的集群还是默认的 iptables 模式切到 IPVS 并不复杂但有几个细节必须处理否则会翻车。第一步检查内核模块是否就绪。IPVS 运行需要 ip_vs 和相关调度模块。先在节点上执行modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_wlc lsmod | grep ip_vs如果输出里能看到 ip_vs 相关模块说明内核支持。如果 modprobe 报错说明内核没有编译这些模块通常是系统内核太精简导致的。另外为了防止节点重启后模块丢失我建议把模块写进开机自动加载配置cat /etc/modules-load.d/k8s-ipvs.conf EOF ip_vs ip_vs_rr ip_vs_wrr ip_vs_lc ip_vs_wlc ip_vs_sh ip_vs_dh EOF第二步安装管理工具。IPVS 的查看命令是 ipvsadm还需要 ipset 配合apt install -y ipvsadm ipset第三步修改 kube-proxy 配置并重启。用 kubeadm 部署的集群kube-proxy 的配置存放在 kube-system 命名空间下的 ConfigMap 里kubectl -n kube-system edit configmap kube-proxy把 config.conf 里的 mode 从 iptables 改成 ipvs如果有需要的调度算法可以在 ipvs 节点里加一行 scheduler。改完后滚动重启 kube-proxy 的所有 Podkubectl -n kube-system rollout restart daemonset kube-proxy重启完成后随便挑一个 kube-proxy Pod 看日志看到类似Using ipvs Proxier的日志就说明切换成功了。3. 实战部署一个后端服务把三种 Service 类型全跑一遍3.1 先用一个能区分后端身份的 Deployment理论讲再多不如实际跑一遍。我先创建一组最简单的后端 Pod每个 Pod 启动时把自己的 hostname 写入首页这样我们 curl 的时候就能从响应内容看出请求到底落在了哪个 Pod 上。新建一个 deployment.yamlapiVersion: apps/v1 kind: Deployment metadata: name: echo-deploy spec: replicas: 3 selector: matchLabels: app: echo template: metadata: labels: app: echo spec: containers: - name: echo image: nginx:1.27 command: [/bin/sh, -c] args: - echo hello from $(hostname) /usr/share/nginx/html/index.html nginx -g daemon off;这里的关键是 label 必须写对app: echo。后面 Service 的 selector 要和它保持一致否则 Endpoints 会是空的。创建并查看kubectl apply -f deployment.yaml kubectl get pods -o wide三个 Pod 都 Running 之后进入其中一个 Pod直接 curl 另外两个 Pod 的 IP会发现能访问到但响应内容都不相同分别显示各自的 hostname。这说明 Pod 之间是可以直接互访的。但你也看到了这个访问方式绑定死了 IP很不灵活。3.2 ClusterIP集群内部访问的稳定入口接着创建 ClusterIP 类型的 ServiceapiVersion: v1 kind: Service metadata: name: echo-svc spec: type: ClusterIP selector: app: echo ports: - port: 80 targetPort: 80这里 port 是 Service 对外暴露的端口targetPort 是 Pod 内容器实际监听的端口。它们可以不一样但通常保持一致少给排查添麻烦。创建后看状态kubectl apply -f service-clusterip.yaml kubectl get svc echo-svc kubectl get endpoints echo-svckubectl get svc会显示一个 ClusterIP比如 10.96.123.45。kubectl get endpoints会显示三个 Pod IP正是刚才 Deployment 那三个副本。如果 Endpoints 是空的别往下走了先检查 selector 是不是写错。接下来进入任意一个 Pod循环访问 Service 的 ClusterIPkubectl exec -it 任意Pod -- sh -c curl -s http://10.96.123.45; echo重复执行几次你会发现响应里的 hostname 在三个 Pod 之间轮换。这就说明流量已经被 Service 正确分发到了多个后端。此时你再到节点上执行ipvsadm -L -n你会看到类似这样的输出TCP 10.96.123.45:80 rr - 10.244.1.2:80 Masq 1 0 0 - 10.244.2.3:80 Masq 1 0 0 - 10.244.3.4:80 Masq 1 0 0LocalAddress 是 ClusterIP后面跟的 rr 是调度算法三个后端就是三个 Pod IP转发方式是 Masq也就是 NAT 模式。ActiveConn 和 InActConn 的数值表示当前连接数流量打过来时这里会增加。3.3 NodePort让集群外也能访问的关键一步ClusterIP 只能在集群内部访问外部是到不了的。如果你在本地电脑上想访问这个服务就需要 NodePort。创建 NodePort 类型的 ServiceapiVersion: v1 kind: Service metadata: name: echo-nodeport spec: type: NodePort selector: app: echo ports: - port: 80 targetPort: 80 nodePort: 30080nodePort 不写的话Kubernetes 会自动分配一个 30000-32767 范围内的端口。我建议显式指定方便测试和记忆。创建后通过任意一台节点的 IP 加端口访问curl http://任意节点IP:30080你会看到 Response 里同样随机返回某个 Pod 的 hostname。这就是 NodePort 的转发链路外部请求先到节点的 30080 端口然后被转发到 ClusterIP再由 IPVS 规则分发给后端 Pod。需要注意的是NodePort 是每个节点都会开放的。也就是说如果你有三台节点那么三台节点的 30080 端口都可用你可以把它们一起放到前面加一个外部负载均衡器或者通过 DNS 轮询来分发。3.4 LoadBalancer云环境里的“最后一公里”当集群部署在云厂商环境时最常用的暴露方式就是 LoadBalancer。它的 YAML 和 NodePort 几乎一样只需要改 typeapiVersion: v1 kind: Service metadata: name: echo-lb spec: type: LoadBalancer selector: app: echo ports: - port: 80 targetPort: 80在云环境下提交这个 YAML 后云厂商的负载均衡控制器会自动创建一个外部负载均衡器并分配一个公网 IP。这个公网 IP 会将流量转发到各个节点的 NodePort 上再进入 ClusterIP 的转发链路。整个过程对你来说是透明的。但在本地裸机集群或自建环境中LoadBalancer 类型会一直处于 Pending 状态因为没有云厂商的控制器去创建真正的负载均衡器。这种场景下要么继续用 NodePort要么借助开源的裸机负载均衡方案比如 MetalLB来模拟云环境的能力。搞清楚这三者的关系你就能在实际环境中做选择了集群内部服务用 ClusterIP外部临时访问用 NodePort正式对外提供服务且有云环境时用 LoadBalancer。4. Service 排错与经验从 Endpoints 到 ipvsadm 的排查链路4.1 服务不通先别碰网络先看 Endpoints我见过太多人一上来就查网络、抓包折腾半天最后发现 Service 的 selector 写错了。Service 能正常工作的大前提是 Endpoints 里有内容。排查第一步永远是看这两个输出kubectl get svc 服务名 kubectl get endpoints 服务名如果 Endpoints 是空的看看 Backend 那一列有没有显示unhealthy再检查三件事selector 的 key/value 和 Pod 的 label 是否完全一致Pod 是否 ReadytargetPort 是否写成了容器没监听的端口。有一次我把app: echo-web写成了app: echo_webPod 标签明明是 echo_webService 选择器却写成了 echo-webEndpoints 当然为空。这种问题靠肉眼看很费劲直接执行下面这条命令就能找到答案kubectl get pods --show-labels然后对比两边标签一目了然。4.2 ipvsadm 是排查 IPVS 模式的“透视镜”确认 Endpoints 正常后如果流量还是不通就去节点上看 IPVS 转发表ipvsadm -L -n正常情况下每个 ClusterIP、NodePort 都会对应一条 IPVS 转发条目。如果你在这个集群里创建了 echo-svc那么表里必然有一条 10.96.123.45:80 的记录。如果看不到说明 kube-proxy 没有成功给这个 Service 建规则可以去查 kube-proxy 的日志kubectl -n kube-system logs -l k8s-appkube-proxy --tail200另外IPVS 模式下 kube-proxy 会创建一个名叫 kube-ipvs0 的虚拟网卡接口所有 Service 的 ClusterIP 都会挂在这个接口上。可以用这个命令快速确认ip addr show kube-ipvs0列表里如果没有 Service 的 ClusterIP说明 kube-proxy 的 sync 出问题了大概率是网络插件或版本兼容问题。4.3 externalTrafficPolicyCluster 和 Local 到底怎么选NodePort 和 LoadBalancer 类型的 Service 有一个额外参数叫 externalTrafficPolicy默认是 Cluster可选 Local。Cluster 模式流量进入任意节点后可能被转发到其他节点上的 Pod。因为经过了额外一跳后端看到的客户端源 IP 不是真实的而是节点地址。这是默认行为适合大多数业务。代价是可能产生额外的一跳延迟并且极端情况下会加重节点间的网络压力。Local 模式流量只在本节点上转发如果本节点上没有这个 Service 的后端 Pod这个节点的 NodePort 会把请求直接丢掉。好处是保留了源 IP而且不会绕路。坏处是如果有些节点没有对应 Pod这几个节点就无法提供服务前端负载均衡器需要有健康检查能力否则就会出现部分节点访问失败。如果你在做四层网络隔离、安全审计、或者后端业务强依赖客户端真实 IP就选 Local。如果只是普通业务老老实实用默认的 Cluster省心很多。4.4 几个我踩过的坑和现在的排查习惯先说我踩过的坑希望能帮你避开同样的弯路。第一个坑手动直接访问 Pod IP。早期我总觉得 Service 多了一层不够直接直接把 Pod IP 写进配置里。后来 Pod 每次重建都面临重新配置的麻烦而且一旦节点有问题Pod 被调度到别处所有依赖它的服务都会断。现在我的原则很明确集群内部一律通过 Service 访问宁可多一层抽象也不要给自己埋不稳定因素。第二个坑不加选择地给所有 Service 都用 NodePort。NodePort 确实方便但端口范围有限服务一多很难管理而且每个节点都开放端口安全隐患不小。正确的做法是内部服务用 ClusterIP只有真正需要外部访问的服务才用 NodePort并且给网络策略配上访问控制。第三个坑切换 IPVS 模式后没有检查所有 kube-proxy Pod 是否都更新完成。因为 kube-proxy 是 DaemonSet它会逐个节点滚动更新。如果我在某个节点上执行ipvsadm -L -n发现规则没生效就以为切换失败其实只是那台节点的 kube-proxy 还没重启完。遇到这种情况先确认所有节点上的 kube-proxy Pod 都处于 Running 状态再看对应节点的规则。第四个坑高并发场景下遇到 conntrack 表满导致丢包。IPVS 模式同样依赖 conntrack 来做 NAT如果集群里大量短连接conntrack 表很容易被打满表现为随机丢包、连接超时。排查时看这两个数值sysctl net.netfilter.nf_conntrack_count sysctl net.netfilter.nf_conntrack_max如果 count 接近 max就要考虑调大 max或者优化应用的连接使用方式。我说说现在的排查习惯。遇到 Service 不通我固定按这个顺序来先看 Endpoints 有没有后端再看 kubectl get svc 的端口映射对不对然后到节点上看 ipvsadm -L -n 有没有对应条目最后才看 kube-proxy 日志和网络策略这个顺序从抽象层到实现层一层层往下钻大多数问题在第三步之前就能定位。Service 和 IPVS 的原理不复杂但牵扯到的链路很长有了稳定的排查思路才能避免每次都从头踩坑。