Kubernetes 集群的网络部分,是所有运维同学绕不过去的一道坎。标题里的 ipvs、NodePort、Headless、LoadBalancer、ExternalName 这几个词,看起来像五座大山,实际上它们全都围绕同一个对象:Service。Service 是 k8s 里把“动不动就重启换 IP 的 Pod”变成“稳定访问入口”的核心抽象,也是我在平时排查问题的时候碰到最多的一层。这篇文章我从一个正在运行的三节点集群出发,把 kube-proxy 怎么切到 ipvs 模式、五种 Service 类型怎么创建和验证、实际会遇到哪些坑,完整走一遍。无论你是在搭测试环境了解原理,还是正在现网排查 Service 访问不通,这篇都可以直接照着操作。
1. 先搞懂 Service 的底层:ClusterIP、Endpoints、转发规则
1.1 没有 Service 的时候,集群里到底乱在哪
我先讲个场景:你部署了一个三副本的 nginx,Deployment 滚动更新之后,三个 Pod 的 IP 全变了。客户端如果写死了某一个 Pod IP,服务直接挂。就算不写死,客户端要怎么知道当前这三个 Pod 的 IP 呢?总不能让运维每次变更之后都手工通知一遍所有调用方。
另一个场景是扩缩容:从 3 个副本扩到 10 个,新增的 Pod IP 客户端不知道;缩到 2 个,请求打到已销毁的 Pod 上直接超时。这些都是没有 Service 时的典型痛点。
Service 做的事情总结下来就一句话:提供一个稳定不变的 VIP(虚拟 IP),客户端只认这个 VIP,VIP 后面对应的 Pod 列表由 k8s 自动维护。无论后端 Pod 怎么变,客户端的访问入口不用动。这也是 Service 在 k8s 体系里地位这么高的原因——它是连接“动态 Pod”和“稳定调用方”之间的那个缓冲层。
1.2 Service 的三个关键部件
一个 Service 正常工作要依赖三个东西。
第一,ClusterIP / 访问入口。默认类型 ClusterIP 会从 Service CIDR 中分配一个虚拟 IP。这个 IP 不是绑定在某个具体网卡上,而是由 kube-proxy 在节点上生成的转发规则“模拟”出来的。你 ping 不通 ClusterIP,因为内核没有实际接口绑定它,只有数据平面的转发规则在“接应”。
第二,Endpoints。当 Service 写好选择器(selector),controller-manager 里的 endpoints 控制器会自动找到匹配的 Pod,把它们的 IP 和端口记录到 Endpoints 对象里。如果 selector 没匹配到任何 Pod,Endpoints 列表就是空的,这时候 Service 就是个空壳。
第三,转发规则。kube-proxy 监听 Service 和 Endpoints 的变化,在每台节点上生成规则,把访问 ClusterIP 的流量转发到后端的 Pod IP。转发规则放在谁身上,谁就是流量入口——这也是后面讨论 iptables 和 ipvs 两种模式的核心。
1.3 Service 和 DNS 是怎么配合的
集群里的 CoreDNS 会自动监听 Service 的创建和删除,生成对应的 DNS 记录。每个 Service 会得到一条 A 记录:<service>.<namespace>.svc.cluster.local,解析结果通常是 Service 的 ClusterIP。这个特性意味着,你在 Pod 里访问一个内部服务,可以不写 IP,直接写服务名,比如在默认命名空间里访问web-demo-svc,完整域名是web-demo-svc.default.svc.cluster.local,但实际使用中直接写web-demo-svc就行。
这块搞清楚了,后面理解 Headless 和 ExternalName 就会非常顺,因为这两种类型就是通过“改变 DNS 记录的类型和内容”来实现功能的。
提示:Service 一旦创建,DNS 记录的生成是自动的,不需要你手工配置 CoreDNS。如果你发现某个服务名解析不了,先别急着改 CoreDNS,回到 Service 本身查问题,大概率是 Service 没有正常创建。
2. 到底选 iptables 还是 ipvs?我把切换步骤完整踩了一遍
2.1 iptables 模式是怎么工作的
kube-proxy 默认使用 iptables 模式。每个 Service 会对应一组 iptables 规则链,比如KUBE-SVC-XXXX,链里再挂具体的KUBE-SEP-XXXX规则,指向每个后端 Pod。规则是线性匹配,本质上是链表逻辑。数据包到达节点时,要逐条匹配规则,直到命中。Service 数量少的时候没问题,规则翻倍之后,匹配开销会明显上升。
iptables 模式的另一个特点是:它的负载均衡是随机选择,并不支持加权、最少连接这种更精细的策略。对于大部分小型集群来说够用,但到了几百上千个 Service,或者流量核心在集群内部大量流转的时候,iptables 的规则膨胀和高并发下的性能问题就会浮现出来。
2.2 ipvs 模式的优势在哪里
ipvs 是 Linux 内核自带的四层负载均衡模块,工作在 netfilter 的 hook 点,但它内部使用哈希表来组织规则,查找效率远高于链表匹配。它天然支持多种调度算法:
| 调度算法 | 说明 | 适用场景 |
|---|---|---|
| rr | 轮询,默认算法 | 后端能力均等的大多数场景 |
| wrr | 加权轮询 | 后端机器配置不均衡 |
| lc | 最少连接 | 长连接较多的场景 |
| wlc | 加权最少连接 | 后端能力不均衡的长连接场景 |
| sh | 源地址哈希 | 需要来源 IP 稳定的会话场景 |
| dh | 目标地址哈希 | 目标地址固定的场景 |
同样的集群规模,ipvs 模式下节点上的规则数量和更新频率都更低,性能更好,也更适合大规模的服务暴露。另外 ipvs 模式天然支持连接跟踪,对长连接场景更友好。
提示:ipvs 模式和 iptables 模式并不是互相替代的关系。kube-proxy 在 ipvs 模式下,仍然会保留少量 iptables 规则用于处理特殊情况,比如 kube-proxy 自身的逻辑、CIDR 限制、负载均衡器健康检查等。不要看到节点上还有 iptables 规则就以为切换失败。
2.3 把 kube-proxy 切到 ipvs 模式的完整步骤
我以 kubeadm 部署的集群为例。
第一步,确认节点内核支持并加载模块。在每台节点上执行:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_lc modprobe ip_vs_wlc modprobe ip_vs_sh modprobe ip_vs_dh检查是否加载成功:
lsmod | grep ip_vs为了让重启后依然生效,把这些模块写进系统加载配置:
cat > /etc/modules-load.d/ipvs.conf <<'EOF' ip_vs ip_vs_rr ip_vs_wrr ip_vs_lc ip_vs_wlc ip_vs_sh ip_vs_dh nf_conntrack EOF systemctl restart systemd-modules-load注意:新版内核里 IPv4 连接跟踪模块统一叫 nf_conntrack,老内核叫 nf_conntrack_ipv4。写配置的时候按当前内核版本选择。
第二步,修改 kube-proxy 的 ConfigMap:
kubectl -n kube-system edit configmap kube-proxy把 config.conf 里的mode: ""改成mode: "ipvs":
apiVersion: kubeproxy.config.k8s.io/v1alpha1 kind: KubeProxyConfiguration mode: "ipvs"第三步,重启 kube-proxy:
kubectl -n kube-system rollout restart daemonset kube-proxy第四步,验证切换成功。看日志确认启动模式:
kubectl -n kube-system logs daemonset/kube-proxy --tail=50 | grep -i proxier如果看到类似Using ipvs Proxier的输出,说明切换成功。然后在节点上安装 ipvsadm 查看实际规则:
yum install -y ipvsadm # CentOS/RHEL apt install -y ipvsadm # Ubuntu/Debian ipvsadm -Ln2.4 一次典型的 ipvs 规则长什么样
在一个三节点的 kubeadm 集群里,执行 ipvsadm -Ln 的输出大致是:
IP Virtual Server version 1.2.1 (size=4096) Prot LocalAddress:Port Scheduler Flags -> RemoteAddress:Port Forward Weight ActiveConn InActConn TCP 10.96.0.1:443 rr -> 192.168.1.11:6443 Masq 1 0 0 TCP 10.96.0.10:53 rr -> 10.244.0.5:53 Masq 1 0 0 -> 10.244.0.6:53 Masq 1 0 0 TCP 10.96.0.10:9153 rr -> 10.244.0.5:9153 Masq 1 0 0 -> 10.244.0.6:9153 Masq 1 0 0第一列是本机监听的虚拟地址,也就是 Service 的 ClusterIP;第二列是调度算法;下面跟的,是这条虚拟服务对应的真实后端 Pod IP。看到这张表,你就知道 kube-proxy 已经把 Service 翻译成了内核级的负载均衡规则,后续流量分发直接由内核完成,不需要再把每个数据包都交给用户态进程处理。
3. 五种 Service 类型逐个拆解,别再傻傻分不清
3.1 ClusterIP:集群内部的默认访问方式
ClusterIP 是默认类型。只分配一个集群内部的虚拟 IP,Pod 能访问,集群外不能直接访问。配置示例:
apiVersion: v1 kind: Service metadata: name: web-demo-svc spec: selector: app: web-demo ports: - port: 80 targetPort: 80 protocol: TCP创建后kubectl get svc看到的 ClusterIP 就是 VIP。访问它的域名就是web-demo-svc.default.svc.cluster.local。这种类型适合服务之间互相调用,比如前端 Pod 调用后端 API,中间件的访问,全部走 ClusterIP 或服务名,不暴露到集群外。
3.2 NodePort:从集群外直接访问的关键
NodePort 在 ClusterIP 基础上,把服务端口映射到每台节点的固定端口上。端口范围默认 30000-32767,由 kube-apiserver 的--service-node-port-range控制。
apiVersion: v1 kind: Service metadata: name: web-demo-nodeport spec: type: NodePort selector: app: web-demo ports: - port: 80 targetPort: 80 nodePort: 30080创建后,访问任意节点的 30080 都能到达后端。它仍然保留 ClusterIP,所以集群内访问也不受影响。适合测试环境或者没有负载均衡器的裸金属环境临时暴露服务。
这里有个细节容易忽略:不写 nodePort 的时候,系统会自动从 30000 到 32767 之间分配一个,但你无法提前知道是哪个端口。写死 nodePort 可以看到固定端口,但也要注意端口冲突,如果某个节点上已经有进程占用该端口,Service 创建后状态会异常。
3.3 LoadBalancer:交给云平台的负载均衡
LoadBalancer 适合公有云环境。它的实现原理是在 NodePort 前面再加一层云厂商的负载均衡器,用户在云控制台会看到自动创建出来一个 LB 实例,流量先进 LB,再分发到各节点的 NodePort,再转发到 Pod。
apiVersion: v1 kind: Service metadata: name: web-demo-lb spec: type: LoadBalancer selector: app: web-demo ports: - port: 80 targetPort: 80在公有云环境,创建之后 EXTERNAL-IP 会变成负载均衡器的地址。在裸金属或本地测试环境,没有云控制器(cloud-controller-manager),EXTERNAL-IP 会一直停在<pending>,直到你部署像 MetalLB 这样的开源负载均衡组件,或者干脆不用它。
3.4 Headless Service:不分配 ClusterIP 的直连方式
Headless Service 不分配 ClusterIP,配置方式是把 clusterIP 设为 None:
apiVersion: v1 kind: Service metadata: name: web-demo-headless spec: clusterIP: None selector: app: web-demo ports: - port: 80 targetPort: 80它的特殊之处在于 DNS 行为:普通 Service 的 DNS 解析返回一个 VIP,而 Headless Service 的 DNS 直接返回后端所有 Pod 的 IP。也就是说,客户端拿到的是真正的 Pod IP 列表,而不是经过负载均衡的虚拟 IP。
Headless 最常见的用途有两个:一个是 StatefulSet 场景,配合有状态应用,每个 Pod 需要稳定的网络标识,Pod 的 DNS 为podName.serviceName.namespace.svc.cluster.local;另一个是数据库、消息队列这类需要客户端感知到每个副本的场景,比如应用自己要做读写分离,优先访问某个副本,用 Headless + 客户端解析拿到所有实例地址再自己选。
3.5 ExternalName:纯 DNS 层面的映射
ExternalName 是一个纯 DNS 层面的映射,它没有 selector,也不分配 ClusterIP。你只需要指定一个 externalName,集群 DNS 在解析该 Service 域名时,返回的是一条 CNAME 记录,指向外部域名。
apiVersion: v1 kind: Service metadata: name: external-svc spec: type: ExternalName externalName: www.example.com这个类型最大的价值是解耦。比如早期系统依赖一个外部 API,你可以在集群内部先用 ExternalName 把它映射成一个内部服务名,代码里统一访问内部域名。以后服务迁回集群内,只需要把 Service 类型改回 ClusterIP,代码不用改。
需要注意的是,ExternalName 不能指定端口。CNAME 本身不带端口信息,所以如果外部服务的端口不是 80/443,访问时还是要在连接串里显式带端口。
3.6 五种类型的对比速查
| 类型 | 分配 ClusterIP | 选择器 | DNS 行为 | 访问入口 | 典型场景 |
|---|---|---|---|---|---|
| ClusterIP | 是 | 一般有 | A 记录指向 VIP | 集群内 VIP | 服务间内部调用 |
| NodePort | 是 | 一般有 | A 记录指向 VIP | 节点 IP:映射端口 | 测试环境暴露服务 |
| LoadBalancer | 是 | 一般有 | A 记录指向 VIP | 云 LB 地址 | 生产环境对外暴露 |
| Headless | 否 | 一般有 | A 记录返回多个 Pod IP | 直接访问 Pod | 有状态应用、直连副本 |
| ExternalName | 否 | 无 | CNAME 指向外部域名 | 外部域名入口 | 外部服务映射、迁移过渡 |
4. 实操演示:从 YAML 到验证一次跑通五种 Service
4.1 准备测试应用:一个三副本的 nginx
先创建一个 Deployment,三副本 nginx,标签 app=web-demo。
apiVersion: apps/v1 kind: Deployment metadata: name: web-demo spec: replicas: 3 selector: matchLabels: app: web-demo template: metadata: labels: app: web-demo spec: containers: - name: nginx image: nginx:1.25 ports: - containerPort: 80创建并确认:
kubectl apply -f web-demo-deploy.yaml kubectl get pods -o wide -l app=web-demo你会看到三个 Pod,IP 分布在不同的节点网段,比如 10.244.1.x、10.244.2.x、10.244.3.x。
4.2 ClusterIP 的创建和验证
创建 3.1 节里的 web-demo-svc,然后:
kubectl get svc web-demo-svc拿到 ClusterIP,比如 10.96.98.11。进一个测试 Pod 访问:
kubectl run -it --rm probe --image=busybox:1.36 --restart=Never -- sh wget -qO- http://web-demo-svc.default.svc.cluster.local能拿到 nginx 的欢迎页,说明 ClusterIP 生效。这里我用的域名是完整域名,实际上在同一个 namespace 里直接写http://web-demo-svc也能解析到同一个地址。
4.3 NodePort 的创建和验证
再把上面的 Service 改成 NodePort 并指定 30080:
kubectl apply -f - <<'EOF' apiVersion: v1 kind: Service metadata: name: web-demo-nodeport spec: type: NodePort selector: app: web-demo ports: - port: 80 targetPort: 80 nodePort: 30080 EOF在集群外,或者集群的节点上:
curl http://192.168.1.11:30080任意节点的 30080 都会转发到后端的三个 nginx。如果你用的是公有云 VM,记得在安全组里放行这个端口,不然 curl 会一直卡住。
4.4 Headless 的创建和 DNS 验证
创建 headless service,然后用 DNS 工具验证:
kubectl apply -f - <<'EOF' apiVersion: v1 kind: Service metadata: name: web-demo-headless spec: clusterIP: None selector: app: web-demo ports: - port: 80 targetPort: 80 EOF kubectl run -it --rm dns-test \ --image=busybox:1.36 \ --restart=Never \ -- nslookup web-demo-headless.default.svc.cluster.local输出会是三个真实 Pod IP:
Server: 10.96.0.10 Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local Name: web-demo-headless.default.svc.cluster.local Address 1: 10.244.1.6 web-demo-headless.default.svc.cluster.local Address 2: 10.244.2.8 web-demo-headless.default.svc.cluster.local Address 3: 10.244.3.7 web-demo-headless.default.svc.cluster.local看到这里你就能直观理解 Headless 和普通 Service 的区别:普通 Service 只会返回一个 VIP,而 Headless 把所有后端的真实 IP 全部暴露给客户端。
如果你配合 StatefulSet 用,Pod 的稳定域名是web-0.web-demo-headless.default.svc.cluster.local这样的格式,解析出来是固定 Pod 的 IP。
4.5 ExternalName 的创建和验证
创建 external service:
kubectl apply -f - <<'EOF' apiVersion: v1 kind: Service metadata: name: external-svc spec: type: ExternalName externalName: www.example.com EOF然后查询它的 DNS:
kubectl run -it --rm dns-test \ --image=busybox:1.36 \ --restart=Never \ -- nslookup external-svc.default.svc.cluster.local重点看输出的别名部分,它会显示解析 external-svc 时最终走到了 www.example.com,返回给你的是一条 CNAME 记录。这意味着 Pod 访问http://external-svc时,流量实际会打到 externalName 指向的外部域名上。
4.6 LoadBalancer 在本地为什么一直是 pending
本地裸金属集群创建 LoadBalancer 类型 Service:
kubectl apply -f - <<'EOF' apiVersion: v1 kind: Service metadata: name: web-demo-lb spec: type: LoadBalancer selector: app: web-demo ports: - port: 80 targetPort: 80 EOF执行 kubectl get svc 会看到 EXTERNAL-IP 那列是<pending>。这很正常,因为集群里没有云厂商的 controller-manager 去调用 API 创建真正的负载均衡器。裸金属环境想用这个类型,可以装 MetalLB 这类开源组件,它会给 Service 分配一个外部可达的 IP,并且基于 BGP 或二层协议把流量导入集群。
5. 高频故障与避坑清单,给你一份速查表
5.1 Service 有 ClusterIP,但访问不通
排查思路从下往上:先看 Endpoints 是否为空:
kubectl get endpoints web-demo-svc如果 ENDPOINTS 列为空,说明 selector 没匹配上任何 Pod。最常见的原因是指标签写错了,比如 Deployment 里打的是app: webDemo,而 Service 里写的是app: web-demo,或者 namespace 不一致。
如果 Endpoints 有 IP 但还是不通,再看对应 Pod 有没有监听 targetPort。用kubectl exec进 Pod 里直接 curl 它的 Pod IP:端口,通的话基本能排除应用本身的问题,再把注意力放到 kube-proxy 上。
5.2 改了 mode: ipvs,但 ipvsadm 里看不到规则
分几步查。先看 kube-proxy 日志,确认是 ipvs 模式启动还是 fallback 到了 iptables 模式。内核模块没加载全时,kube-proxy 会回退到 iptables 模式。检查:
lsmod | grep ip_vs确认 ip_vs 等模块都在。另外,修改 ConfigMap 后一定要重启 DaemonSet,很多人改了配置不重启,当然不生效。
还要检查一个点:你执行 ipvsadm -Ln 的节点是不是跑 kube-proxy 的节点。如果某节点上 kube-proxy 被调度到别处,或者节点 NotReady,这个节点上的规则可能就是旧的。
5.3 NodePort 端口访问不通
NodePort 属于对外暴露,最容易出问题的地方往往是防火墙和安全组。先在本节点本地测:
curl http://127.0.0.1:30080本地通、外部不通,检查节点防火墙(firewalld、ufw、云安全组)。本地也不通,看 kube-proxy 有没有监听这个端口:
ss -lntp | grep 30080如果端口在监听还不行,检查 externalTrafficPolicy。默认值是 Cluster,会把流量转发给所有节点上的 Pod;改成 Local 之后只转发到本节点 Pod,如果请求打到没有 Pod 的节点上,会直接拒绝。不要在自己没有分布 Pod 的节点上测试 Local 模式的 NodePort 然后奇怪为什么不通。
5.4 Headless 解析不出 Pod IP
确认 Service 里确实写了clusterIP: None。Headless 的判断标准就是 clusterIP 是否为 None,跟 type 无关。
另外,Headless Service 必须有 selector 才能自动生成 Endpoints 和 DNS 记录。如果你创建了一个没有 selector 的 Headless Service,集群会生成一条与 Service 同名的记录,但不会自动生成 Endpoints,除非你手动创建同名 Endpoints 对象。
如果用 StatefulSet 创建了 Pod,注意 Pod 域名需要使用的 headless service 名称要和 StatefulSet 的 serviceName 一致,否则解析不到。
5.5 ExternalName 解析失败
ExternalName 最终依赖外部的 DNS 解析。测试的时候如果发现解析返回不了地址,试试直接在节点上用 dig 查一下 externalName 指向的域名能不能解析:
dig www.example.com外部域名本身解析不了,Service 当然也白搭。还有一种情况:externalName 写的是http://www.example.com这种带协议的字符串,这是错的,必须只写域名。
如果集群内 Pod 无法访问外部域名,先确认 CoreDNS 的上游 DNS 是否配置正确,或者 Pod 所在的节点有没有配置外网 DNS。和内网环境相关的问题,优先查这些。
5.6 关于 Service 设计的一些经验
最后分享几条实际运维中沉淀下来的经验。
第一,对外暴露优先用 LoadBalancer,不要裸用 NodePort。NodePort 更像是临时通道,端口范围固定、安全策略不好控制,生产环境长期使用会很难受。
第二,内部服务之间调用优先走 ClusterIP 和服务名,不要写死 Pod IP。只有需要客户端感知副本时才用 Headless。
第三,Service 命名尽量统一规范。因为 Service 域名会直接暴露给服务间调用,命名混乱的 Service 会让后续排查成本暴涨。
第四,每次创建 Service 后,养成先kubectl get endpoints再想别的习惯。大部分“访问不通”的问题,第一步就能在这解决。
我个人的习惯是,每次新建 Service 之后会顺手把类型、ClusterIP、Endpoints 状态记到表格里,尤其是跨服务调用比较多的时候,这个习惯省了很多次深夜排查。另外,在切换到 ipvs 模式之前,一定要先确认所有节点的内核模块都加载完整,否则 kube-proxy 静默回退到 iptables,排查起来特别费劲。希望这篇文章能帮你把 Service 这层一次性吃透,后面如果再遇到更典型的 Service 问题,我会单独再整理一篇排查案例出来。