K8s Service三种类型实战与排障指南
1. 从一次生产故障说起Service 到底是干什么的我在不少项目里见过这样的场景Pod 明明在跑应用却访问不通。查了半天问题不是出在容器里而是出在 Service 上。有人把 Service 当成简单的负载均衡器有人不清楚 ClusterIP、NodePort、LoadBalancer 三者的区别还有人搞不懂 Service 和 Pod 的关联方式结果排障时一头雾水。这几年 K8s 已经成为部署应用的事实标准但很多人对核心网络模型的理解还停留在会用 kubectl apply的层面。Service 作为 K8s 里连接工作负载和外部流量的枢纽恰恰是最容易踩坑的一环。我经常遇到有人问Pod 重启后 IP 变了为什么我写死的地址访问不了答案就藏在 Service 的设计动机里。先说结论Service 解决的核心问题是 Pod 的不稳定性。在 K8s 中Pod 是随时可能被销毁、重建、迁移的。Deployment 滚动更新时旧 Pod 消失、新 Pod 出现IP 地址必然发生变化。如果客户端直接访问 Pod IP一旦 Pod 重启地址就失效了。Service 提供了一层稳定的访问入口它本身有固定的标识ClusterIP 或 DNS 名字流量从这个入口进入后被转发到后端的 Pod 上。这个机制很像公司前台的电话总机——你不需要知道某个员工坐在哪个工位Pod IP只要拨打总机号码ClusterIP/DNS前台就会把电话转接到目标分机。员工换工位了号码也不用变。这篇文章我从实际实验的角度把三种 Service 类型完整跑一遍列出命令、配置、验证方法和排障经验。适合对 K8s 网络有基础了解、准备在生产环境落地 Service 的开发者或运维人员。2. 动手前的准备一套可复现的实验环境要验证 Service 的行为一套干净的环境非常重要。不用上生产级的复杂集群但你至少需要一个可用的 K8s 环境。下面是我在一年多时间里总结出的最小化部署方案以及在实际操作中最容易忽略的几个细节。2.1 实验环境版本清单先看一下我这次实验用的版本组合都是目前比较稳定的选择组件版本说明操作系统Ubuntu 22.04 LTS内核版本 5.15模块加载无压力容器运行时containerd 1.7.x比 Docker Engine 更贴合 K8s 生态K8s 集群v1.28.2kubeadm 部署的单节点集群CNI 插件Calico v3.27支持 NetworkPolicy适合学习客户端工具kubectl v1.28.2与集群版本保持一致这里要强调一个被人反复问过的问题为什么用 containerd 而不是 Docker并不是 Docker 不能用作 K8s 的运行时而是从 K8s 1.24 开始kubelet 不再直接支持 Docker Engine 的 Docker Shim 方式。虽然通过 cri-dockerd 仍然能用但性能上多了一层适配。建议新环境直接从 containerd 起步少踩一些版本匹配的坑。2.2 初始化集群的坑使用 kubeadm init 初始化时有两点需要注意第一交换空间必须关闭。如果 swap 开启kubelet 启动时会报错因为 K8s 默认不支持 swap。执行swapoff -a之后还要注释掉/etc/fstab里的 swap 挂载项否则重启后又会开启。第二需要显式指定 Pod 网段。初始化命令里加--pod-network-cidr10.244.0.0/16。我在第一次部署时忘加这个参数Calico 装好之后所有 Pod 都起不来报错信息指向 CNI 配置不一致。后来才发现是 pod 网段和 CNI 默认配置冲突了。这一步很基础但很多人栽在这里。初始化完成后还需要把 kubeconfig 拷到默认路径才能用 kubectl 访问集群mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config2.3 部署一组测试用的工作负载Service 必须绑定到真实的 Pod 上才有意义所以我提前部署一个简单的 Nginx 应用作为实验对象始终用标签选择器来锁定 Pod 集合。创建一个命名空间专门用于实验kubectl create namespace svc-lab然后准备 Deployment 配置。这里我刻意创建 3 个副本方便后面验证负载均衡行为apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy namespace: svc-lab spec: replicas: 3 selector: matchLabels: app: nginx-demo template: metadata: labels: app: nginx-demo spec: containers: - name: nginx image: nginx:1.25-alpine ports: - containerPort: 80注意ports字段只是声明性的让 kubelet 知道容器监听了哪个端口它并不自动创建 Service 或转发规则。真正决定流量去向的是 Service 里的 selector 跟 Pod 的 label 是否匹配。应用配置并验证kubectl apply -f nginx-deploy.yaml kubectl -n svc-lab get pods -o wide等到 3 个 Pod 都变成 Running 状态记录下它们的 IP。后面实验会反复用到这些信息。到这里环境就绪可以进入正题了。3. ClusterIP每种 Service 的根本形态ClusterIP 是默认的 Service 类型它给一组 Pod 提供一个集群内虚拟 IP。很多人以为 ClusterIP 只能被集群内部访问这没错但它同时也是 NodePort 和 LoadBalancer 的基础。后两种类型的实现底层都会创建一个 ClusterIP 作为流量转发的入口。3.1 创建 ClusterIP Service创建一个指向刚才那些 Nginx Pod 的 ClusterIP ServiceapiVersion: v1 kind: Service metadata: name: nginx-svc namespace: svc-lab spec: type: ClusterIP selector: app: nginx-demo ports: - port: 80 targetPort: 80语义要弄清楚port是 Service 对外暴露的端口targetPort是 Pod 容器内实际监听流量的端口。两者可以不一致比如让 Service 监听 8080转发到容器的 80ports: - port: 8080 targetPort: 80这种灵活性在迁移场景下很有用——后端应用改监听端口时Service 不一定要跟着改。创建并查看kubectl apply -f svc-clusterip.yaml kubectl -n svc-lab get svc nginx-svc输出里能看到 ClusterIP 分配结果比如 10.109.15.158以及 PORT(S) 一列显示80/TCP。这个 ClusterIP 在整个集群网络中是唯一的它在集群生命周期内保持不变除非 Service 被删除重建。3.2 验证 ClusterIP 的三种访问方式从集群内访问 ClusterIP 服务常见方式有三种。方式一直接访问 ClusterIP在集群里的任意节点上用 curl 访问 ClusterIP 加端口curl http://10.109.15.158:80但这种方式有个明显缺陷ClusterIP 毕竟是一个虚拟 IP如果 Service 被删除再重建IP 可能会变。写死在脚本里风险很大。方式二通过 DNS 域名访问K8s 集群默认部署了 CoreDNS它会给每个 Service 生成一条 DNS 记录。同一个命名空间内直接使用 Service 名访问kubectl -n svc-lab run curl-test --imagecurlimages/curl --rm -it --restartNever -- curl http://nginx-svc:80跨命名空间就需要使用完整域名格式是service-name.namespace.svc.cluster.local比如从其他命名空间访问curl http://nginx-svc.svc-lab.svc.cluster.local:80这种方式是最推荐的因为 DNS 记录跟随 Service 生命周期Service 重建后 IP 变了DNS 也会自动更新。方式三通过环境变量访问K8s 在创建 Pod 时会注入当前命名空间中所有 Service 的环境变量命名规则是 Service 名的大写下划线形式。比如nginx-svc会变成NGINX_SVC_SERVICE_HOST和NGINX_SVC_SERVICE_PORT。这个机制有个陷阱环境变量只在 Pod 创建时注入一次。如果你的应用先启动之后才创建 Service那这个 Pod 里不会有对应的环境变量。所以依赖环境变量的应用Service 必须先于应用创建否则只能重建 Pod。3.3 ClusterIP 后面的流量路径ClusterIP 的工作原理值得花时间搞清楚因为很多网络排障都跟它有关。从一个 Pod 访问 ClusterIP 时请求会先发到节点上的 kube-proxy再由 kube-proxy 转发给后端的某个 Pod。kube-proxy 支持三种工作模式K8s 1.28 默认使用 IPVS 模式模式原理特点userspace用户态代理转发性能差基本淘汰iptables内核态 NAT 规则稳定规则多了性能下降ipvs内核态负载均衡性能好支持多种调度算法kube-proxy 需要监听 API Server 来获取 Service 和 Endpoints 的变化。每当你创建一个 Servicekube-proxy 会在所有节点上配置相应的转发规则保证集群内任何位置都能访问到这个 ClusterIP。有个小命令可以帮助验证负载均衡效果。进入一个测试容器用curl多次请求并观察 Nginx 返回的响应头里是否有节点标识。通常 Nginx 的默认配置不带Server信息区分实例所以我提前在每个 Pod 里写入了不同的首页内容来帮助分辨kubectl -n svc-lab exec -it curl-test -- sh -c for i in \$(seq 1 10); do curl -s http://nginx-svc | grep -o h1[^]*/h1; done如果看到三种不同的内容交替出现说明转发到了不同 Pod负载均衡生效。IPVS 模式下默认调度算法是轮询多条规则之间很少有偏差。3.4 ClusterIP 会话保持sessionAffinity 配置默认情况下每次请求都可能落在不同 Pod 上。如果你的应用需要保持会话比如带有状态的 Web 应用可以通过配置让同一客户端的请求始终打到同一个 PodsessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800这里的timeoutSeconds默认是 10800 秒3小时。设置之后kube-proxy 会按来源 IP 做哈希来自同一 IP 的请求会固定转发到一个后端。这个配置在调试线上问题时特别有用——你可以把请求固定到某个 Pod 上方便抓日志定位问题而不用反复在多个实例之间切换。4. NodePort打通集群外部访问的第一步ClusterIP 只能在集群内部访问外部流量如何进入最直接的方式就是用 NodePort。它会在每个节点上开放一个指定端口从节点的 IP 加这个端口就能访问 Service流量再通过 ClusterIP 转发到后端 Pod。4.1 创建 NodePort Service把刚才的 Service 改成 NodePort 类型apiVersion: v1 kind: Service metadata: name: nginx-nodeport namespace: svc-lab spec: type: NodePort selector: app: nginx-demo ports: - port: 80 targetPort: 80 nodePort: 30080如果不显式指定nodePortK8s 会在配置的范围内自动分配一个默认是 30000-32767。显式指定方便记忆和配置但要注意端口不能跟节点上已有服务冲突。查看创建结果kubectl -n svc-lab get svc nginx-nodeport输出内容大致如下NAME TYPE CLUSTER-IP PORT(S) AGE nginx-nodeport NodePort 10.104.12.77 80:30080/TCP 10s这里的关键信息是最后那个80:30080/TCP。它表示 Service 对外暴露的port是 80但需要通过节点的nodePort30080来访问。从集群内部看ClusterIP 的 80 端口依然可用从集群外部看节点的 30080 端口就是入口。4.2 从外部访问 NodePort拿到节点的 IP 后可以这样访问curl http://192.168.1.100:30080这里的 192.168.1.100 是你集群中任意一个节点的 IP。举个例子你的集群有三个节点node1、node2、node3每个节点上都会监听 30080 端口所以你访问任何一个节点都行。这种设计有不少优点其中之一是如果某个节点挂了你仍然可以通过另外的节点访问服务。生产环境通常在 NodePort 前面再加一层负载均衡器比如云上的 SLB把多个节点都挂到负载均衡后端实现高可用。考虑到浏览器访问的是一个端口这种通过多节点做高可用的方案确实非常实用。4.3 NodePort 端口范围的修改实例如果确实需要自定义范围可以在 API Server 的启动参数里修改--service-node-port-range20000-40000如果用 kubeadm 部署需要修改/etc/kubernetes/manifests/kube-apiserver.yaml这个静态 Pod 配置文件在spec.containers[0].command里加上这个参数然后 kubelet 会自动重建 API Server。修改之后NodePort 的可用范围就变了。这个操作一般只在有特殊需求时才需要大多数场景用默认的 30000-32767 就足够了。4.4 一个容易困惑的点NodePort 和 ClusterIP 并存NodePort Service 创建后会自动关联一个 ClusterIP。也就是说它同时被分配了 ClusterIP 和 NodePort 两个访问入口。NodePort 的流量路径是客户端 - 节点IP:NodePort - ClusterIP:Port - Pod:TargetPort这意味着在集群内部NodePort Service 也可以直接通过 ClusterIP 访问。理解这一点对排查问题有帮助——如果外部访问不通先确认是 NodePort 这一环的问题还是更底层的 ClusterIP 转发出了问题比如 Pod 标签不匹配。4.5 排障遇到 NodePort 访问不通怎么办这个问题被问过太多次这里把排查链路完整梳理一遍第一步登录节点用本地地址访问 NodePort确认 kube-proxy 的转发规则正常。curl http://127.0.0.1:30080如果通了说明问题出在外部防火墙或安全组规则上。如果不通继续下一步。第二步检查 Service 是否选择了正确的 Pod。kubectl -n svc-lab get endpoints nginx-nodeport如果 ENDPOINTS 列为空说明 selector 没匹配到 Pod。用下面的命令检查 Pod 的 label 是否和 Service 的 selector 对上kubectl -n svc-lab get pods --show-labels最常见的坑是Deployment 的 Pod template 里 label 写的是app: nginx-demo但 Service 的 selector 写成了app: nginx导致后端一直为空。第三步确认节点防火墙放行了 30080 端口。本机访问通但外部不通多半是防火墙的问题。Ubuntu 上可以用 ufw 查看sudo ufw status有没有 iptables 规则抢占了端口比如你曾经手动加过防火墙规则。这些边缘情况确实容易遗漏检查但往往问题就出在这里。第四步检查节点上的 kube-proxy 日志。kubectl -n kube-system logs ds/kube-proxy --tail100如果 kube-proxy 报错比如无法访问 API Server转发规则就不会被正确下发。这种情况通常伴随集群整体异常不只是某个 Service 的问题。权限和安全策略也是一个容易忽略的维度。新版集群里如果启用了 NetworkPolicy需要单独确认策略是否放行了 Service 的流量路径。5. LoadBalancer云环境下的最佳实践NodePort 把服务暴露到了节点端口但生产环境里你不会直接把节点 IP 暴露给用户因为单节点故障会导致入口不可用。LoadBalancer 类型就是为了解决这个问题而生的——它会在集群外部创建一个负载均衡器自动把流量分发到各节点的 NodePort 上并分配一个固定的外部 IP或域名。5.1 LoadBalancer 的适用场景和局限在云环境AWS、阿里云、腾讯云等中Create 一个 LoadBalancer Service 后云平台的控制器会调用云 API自动创建一个云负载均衡实例。此时的 YAML 很简单apiVersion: v1 kind: Service metadata: name: nginx-lb namespace: svc-lab spec: type: LoadBalancer selector: app: nginx-demo ports: - port: 80 targetPort: 80之后使用 cloud-provider 机制将节点转换为负载均衡后端实例。如果是本地自建的裸金属集群没有云控制器的配合这个 Service 会一直处于pending状态没有外部 IP 分配。这种场景需要一个内部实现比如 MetalLB后面我会专门讲。5.2 LoadBalancer 背后的流量模型很多人以为 LoadBalancer 是直接转发到 Pod 的实际上在多数云环境中负载均衡器的后端是一组节点的 NodePort而不是 Pod。完整的流量路径是外部客户端 - 云负载均衡器:80 - 节点IP:NodePort - ClusterIP:80 - Pod:80这就意味着即使你创建的是 LoadBalancer 类型K8s 也会同步创建 NodePort 和 ClusterIP。用kubectl get svc查看输出的 PORT(S) 列通常会是这样80:32451/TCP你虽然没有显式指定 nodePort但 K8s 自动分配了一个。这个相关的端口其实仍然在每个节点上监听即使外部流量主要走负载均衡器。这里有个性能细节值得注意流量从外部负载均衡器分发到节点再从节点转发到 Pod中间多了一跳。如果集群规模很大这一跳的损耗和 NAT 带来的源地址转换问题就需要处理。常见的缓解方案有两种方案一开启 externalTrafficPolicy: Localspec: externalTrafficPolicy: Local这告诉 kube-proxy只把流量转发到本节点上的 Pod不要跨节点转发。好处是保留客户端真实源 IP坏处是如果某个节点上没有对应 Pod这个节点上的 NodePort 就会丢包健康检查不通过负载均衡器会把它踢出后端池。简单说这是用吞吐效率和源地址保留换取部署上的均匀性要求。方案二使用云厂商的直连模式比如阿里云的负载均衡支持将 Pod IP 作为后端地址。这样做可以避免 NodePort 那层跳转但配置复杂度更高不同云厂商的实现差异也大需要根据实际情况选择。5.3 自建集群的正确方案MetalLB如果你的环境是机房裸金属机器没有云平台的负载均衡器接口那 LoadBalancer 类型基本处于无解状态。业界通常用 MetalLB 来解决它是纯软件实现的负载均衡器基于二层 ARP 或三层 BGP 协议工作。MetalLB 分两个组件组件角色说明Controller分配器监听 Service分配外部 IPSpeaker广播器在网络上广播 IP 地址使外部流量能到达本节点以二层模式为例部署过程大致分三步第一步安装 MetalLB。kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.13.12/config/manifests/metallb-native.yaml第二步配置 IP 地址池。创建一个 ConfigMap指定 LoadBalancer IP 的可分配范围需要保证这个网段跟你的集群节点网络互通且未被其他设备占用apiVersion: v1 kind: ConfigMap metadata: namespace: metallb-system name: config data: config: | address-pools: - name: first-pool protocol: layer2 addresses: - 192.168.1.200-192.168.1.250第三步创建 LoadBalancer Service 并验证。kubectl apply -f svc-lb.yaml kubectl -n svc-lab get svc nginx-lb等 EXTERNAL-IP 列出现你要的 IP比如 192.168.1.201用浏览器或 curl 测试curl http://192.168.1.201:80MetalLB 的 Controller 会申请 NodePort 和 ClusterIPSpeaker 负责把 IP 广播进二层网络。流量先到达持有该 IP 的节点然后走 NodePort 转发到 ClusterIP最终到达 Pod。这里有一点需要注意MetalLB 的 IP 池不要和集群内部 Pod 网段或 Service ClusterIP 网段重叠否则会出现路由冲突等难以排查的问题。在规划网段时就要做好隔离。5.4 LoadBalancer 的高级控制实践中发现 LoadBalancer 类型还有一些不怎么被提到但很有用的配置。第一个是loadBalancerIP字段。在云平台上你可以指定想要的 IP前提是云平台允许且该 IP 未被占用spec: loadBalancerIP: 192.168.1.210在 MetalLB 环境中这个字段的优先级高于地址池自动分配。第二个是loadBalancerSourceRanges。用于限制哪些来源 IP 可以访问这个负载均衡器spec: loadBalancerSourceRanges: - 192.168.1.0/24注意这个字段在云平台上的支持程度不同。部分云厂商直接通过云上安全组实现部分厂商配置可能直接忽略它所以在多团队共享集群时还是应该在 NetworkPolicy 层面再做一层防护而不是只依赖这个字段。6. 三种类型之间的选型逻辑到这一步三种 Service 类型都亲自验证过了。但对于实际应用选哪一种还得看场景。这里供你一个参考场景推荐类型理由集群内部服务间调用比如前端调后端 APIClusterIP稳定有 DNS 支持无需额外暴露需要从集群外访问但暂时不想引入外部负载均衡器NodePort简单直接暴露节点端口云环境下的生产服务对外发布LoadBalancer有云平台负载均衡器做高可用能分配固定 IP生产环境使用但已有 Ingress ControllerClusterIP配合 Ingress流量通过 Ingress NodePort 或 LB 进入再转给 ClusterIP 后端有一个普遍存在的认知偏差觉得 NodePort 在功能上比 ClusterIP 好、LoadBalancer 比 NodePort 好。从功能上看确实多了外部访问能力但从架构上讲它们并不是替代关系而是分层关系。ClusterIP 是底座NodePort 是在 ClusterIP 外面套了一层节点端口LoadBalancer 又在 NodePort 外面套了一层外部负载均衡。所以在设计集群网络时我的意见是内部调用全部用 ClusterIP需要外部入口时在入口层使用 NodePort 或 LoadBalancer再往上的高级路由需求交给 Ingress 处理。Ingress 跟 LoadBalancer 是不同的关注点。LoadBalancer 工作在四层传输层只做 IP 和端口转发Ingress 工作在七层应用层支持按域名、路径路由到不同的 Service。生产中很常见的拓扑是外部流量 - 云负载均衡器LoadBalancer Service- Ingress Controller - 多个 ClusterIP Service - 后端 Pod这样一来对外只需要一个入口 IP内部的 Service 全部保持 ClusterIP不暴露额外端口。相比给每个服务都建一个 LoadBalancer 或 NodePort这种结构的维护成本低很多。7. 实验中的排错案例看似正常却不工作的 Service这部分是我最想分享的。如果你已经配置好 Service但访问不通排查思路比直接复制配置更能帮助你成长。分享三个我在实验中真实遇到的案例。7.1 案例一Endpoints 一直为空现象Service 创建成功但kubectl get endpoints一直不显示后端地址。排查过程确认 Pod 状态kubectl get pods -n svc-labPod 是 Running。检查 Pod 标签kubectl get pods -n svc-lab --show-labels发现 Pod 的标签是app: web。检查 Service 选择器kubectl get svc nginx-svc -n svc-lab -oyaml发现 selector 写的是app: nginx-demo。两边根本没对上Service 自然找不到后端。解决办法很简单统一标签。要么改 Service 的 selector要么改 Deployment 的标签。改完之后等几秒Endpoints 就会自动填充。教训Service 和 Pod 之间没有任何显式的绑定关系全靠标签选择器连接。凡是 Endpoints 为空第一优先检查标签是否匹配。7.2 案例二NodePort 能从节点本机访问但外部访问不通现象在节点上执行curl http://127.0.0.1:30080正常返回但在办公网络访问http://节点IP:30080超时。排查过程确认 Service 的 nodePort 分配正常。从本机ss -lntp | grep 30080看到了 kube-proxy 的监听。后来发现云平台的安全组没有放行 30080 端口。这个案例在自建机房和云环境里都很常见。节点上监听不代表外部能到达该端口安全组、防火墙、iptables 都可能挡住。解决办法在云安全组里添加入方向规则放行 TCP 30080 端口。教训先确认从节点本机访问通不通能帮你快速区分是转发问题还是网络安全策略问题。7.3 案例三Pod 正常、Service 正常但访问时断时续现象curlService 的 ClusterIP 有时成功有时超时概率大概 50%。排查过程检查所有 Pod发现 3 个副本中有一个处于 CrashLoopBackOff。Service 的 Endpoints 里仍包含这个不健康 Pod 的 IP转发时有一定概率打到它上面。这种案例在需要大量创建 Pod 时并不少见。当 Pod 的 Readiness 探针没有被配置时即使容器已经进入不能正常服务状态kube-proxy 依然会把它视为可用端点。你看到的时断时续其实是一部分流量被路由到了坏掉的 Pod。解决办法给 Pod 配置 ReadinessProbe让 K8s 定期检查容器是否真正准备好对外服务。检查 Deployment 的strategy.rollingUpdate参数设置合适的maxUnavailable和maxSurge降低滚动更新期间出现异常 Pod 的几率。配置参考readinessProbe: httpGet: path: /healthz port: 80 initialDelaySeconds: 5 periodSeconds: 5教训Endpoints 不一定等于健康的 Pod它更接近存在且满足 Selector 的 Pod。要不要把请求路由过去取决于 Readiness 探针的结果。这三个案例对应了三类最常见的问题选择器错误、网络策略、健康检查缺失。每次碰到 Service 故障时我都会按照这个顺序自查绝大多数问题都能在十分钟内定位。8. 进阶认知DNS 解析细节与常见误区很多人在 K8s 里碰到网络问题时把锅甩给 Service 本身但实际是 DNS 解析出了问题。Service 的 DNS 机制有些细节很容易被忽略在这里统一整理出来。8.1 DNS 记录格式前面提过完整域名格式service-name.namespace.svc.cluster.local这个格式有如下要点同一命名空间内可以直接用service-name跨命名空间必须用service-name.namespace或者完整域名cluster.local是集群默认的域名后缀也可以在 kubeadm 初始化时通过--service-dns-domain指定其他值8.2 无头服务的 DNS 特性如果把 Service 的clusterIP设置为None这个 Service 就不分配 ClusterIP被称为无头服务Headless Service。它不给 Pod 提供负载均衡而是让 DNS 查询直接返回后端的 Pod IP 列表。看一个实际例子apiVersion: v1 kind: Service metadata: name: nginx-headless namespace: svc-lab spec: clusterIP: None selector: app: nginx-demo ports: - port: 80 targetPort: 80创建后用nslookup查看 DNS 响应kubectl -n svc-lab run dns-test --imagebusybox --rm -it --restartNever -- nslookup nginx-headless.svc-lab.svc.cluster.local返回的结果不是一条 ClusterIP而是三个 Pod 的 IP。无头服务的价值在于需要直连后端实例的场景比如每个 Pod 有独立状态、需要自行实现负载均衡或者做服务发现时要把所有实例都暴露出来。StatefulSet 配合无头服务是集群里很常见的一对组合。每个 Pod 有稳定的网络标识通过域名可以直接定位到具体 Pod。对于有状态的组件数据库、主从架构等这种能力基本是刚需。8.3 ExternalName把外部服务伪装成集群内服务还有一类 Service 不转发任何流量而是做 DNS 层面的 CNAME 跳转它就是 ExternalName。apiVersion: v1 kind: Service metadata: name: external-db namespace: svc-lab spec: type: ExternalName externalName: mydb.example.com创建后集群内的 Pod 可以通过external-db.svc-lab.svc.cluster.local访问到mydb.example.com。这对迁移场景非常友好应用目前还在集群外使用数据库后续要迁入集群内不需要修改应用代码只需要复用同一个 Service 名改一下 Service 类型指向新的内部数据库实例。简单补充一下ExternalName 没有 selector、没有 ClusterIP、没有 Endpoints它只是一条 DNS 记录。所以理解这类 Service 的重点是把它当作一个 DNS 别名机制而不是流量转发机制。9. 最终建议把你的 Service 配置规范固定下来这篇内容从 ClusterIP 讲到 LoadBalancer把原理、实验、排错都过了一遍。最后按我自己的经验给你几条可以落到团队里的规则。第一所有 Service 必须显式声明 type。默认值 ClusterIP 虽然安全但隐式依赖容易让人困惑。显式声明能让代码评审的人一眼看出意图。第二遵循命名规范。我习惯用应用名-svc的格式命名 Service。虽然 Service 名可以跟 Deployment 不同但保持一致性会让排查问题时减少不少认知负担。第三内部服务全部用 ClusterIP入口统一走 Ingress。除非确实需要临时调试否则不建议在生产环境创建一堆 NodePort 暴露额外端口尤其是在云环境NodePort 默认范围 30000-32767 容易被扫描器盯上安全检查一般也不愿意看到非预期端口开放。第四给后端 Pod 配置 ReadinessProbe。这不是可有可无的优化项而是在流量切换时保证 Service 可靠性的一道必选项。没有健康检查K8s 对我来说就不具备真正意义的流量管理能力。第五修改配置时记得更新 Endpoints 检查。Service 的 selector 改动不会自动删除旧的 Endpoints有时需要手动删掉重建或等待同步。如果你的 Service 出现改了 selector 却没反应试试用kubectl delete endpoints service-name强制刷新。这套规则我已经在两套集群上跑了一年多线上因为 Service 配置导致的故障基本归零。K8s 的网络模型看起来抽象但从 Service 出发把每个类型都亲自跑一遍、看一遍实际效果那些为什么这么设计的问题自然会解开。如果你正在被某个网络访问问题困扰建议先把这篇文章末尾提到的三个定位步骤过一遍。