
1. 从一次“服务找不到后端”的排查说起很多人学 Kubernetes 的时候前面几篇讲 Namespace、Pod、Label、Deployment 都还能跟上一到 Service 就开始犯迷糊明明 Pod 跑得好好的kubectl get pods一看全是 Running可业务方就是连不上日志里全是连接超时。我当年第一次搭测试环境就栽在这上面折腾了大半夜最后发现是 Service 的 selector 和 Pod 的 label 对不上——一个字母的差别流量就是进不来。这篇就专门聊Service顺带把前面几个对象串起来讲清楚它们是怎么咬合在一起工作的。Service 在 Kubernetes 里扮演的角色说白了就是“给一组随时会生老病死的 Pod 提供一个固定入口”。Pod 的 IP 是会变的重建一次换一个你不可能让上游服务去记这些飘忽不定的地址Service 就是那层稳定的抽象对外给一个虚拟 IP 和 DNS 名对内靠 Label 选择器把流量分发到健康的 Pod 上。适合谁看如果你已经能跑起一个 Deployment知道 Pod 和 Label 是干嘛的但一碰到 Service 的几种类型、端口映射、Endpoints 就发怵那这篇正好对上你的需求。我会把 ClusterIP、NodePort、LoadBalancer 这几种类型的取舍讲透把端口那三个容易搞混的字段拆开讲再补上无头 Service、会话保持这些实战里绕不开的点。全程按我实际搭环境的顺序来参数怎么算、为什么这么配都给你交代清楚。2. Service 到底解决了什么问题先想清楚再动手2.1 Pod IP 的“短命”本质决定了必须有一层抽象要理解 Service得先接受一个事实Pod 是不可靠的、临时的。Deployment 管理下的 Pod滚动更新会重建、节点故障会漂移、扩缩容会增减每一次变动 IP 都可能变。这不是设计缺陷而是云原生“ cattle not pets ”牲口而非宠物理念的体现——单个 Pod 不值得被精心呵护坏了就换一个新的。问题随之而来如果前端服务要调用后端它该连哪个 IP如果直接连 Pod IP那后端一重建前端配置就得跟着改这在生产环境里根本不可维护。Service 的出现就是为了切断这种耦合前端只认 Service 的固定地址后端 Pod 怎么换对前端透明。我用一个生活化的类比帮你建立直觉。Pod 就像一家公司里不断入职离职的员工工位和分机号一直在变Service 则是公司的总机号码外部客户永远只拨总机至于总机把电话转给哪个在职员工那是内部调度的事。Label 选择器就是“哪些员工属于这个总机负责的部门”的判定规则。2.2 Service 与 Label、Endpoints 的三角关系很多人配 Service 出问题根子在于没搞清这三者的联动机制。我把它拆成一条链来看Label打在 Pod 上的键值对标签比如app: web-backend是“身份标识”。Service 的 selector一段匹配规则比如selector: app: web-backend负责“挑人”。EndpointsKubernetes 自动维护的一个对象记录当前被 selector 选中且处于 Ready 状态的 Pod IP 列表是“实际名单”。这条链的关键在于Endpoints 是自动生成的不是你手写的。Service 创建后控制器会持续监听 Pod 的变化把符合 selector 且通过就绪探针的 Pod IP 塞进 Endpoints。流量转发时kube-proxy 依据的就是这份 Endpoints 名单。所以当你的服务连不上时第一件事不是去看 Service 配置而是kubectl get endpoints service名看名单是不是空的——空名单意味着 selector 没匹配上任何 Pod或者 Pod 没通过就绪检查。提示Endpoints 为空是 Service 不通的头号原因九成以上是 label 拼写不一致或命名空间对不上。养成先查 Endpoints 的习惯能省掉大量瞎猜的时间。2.3 为什么不用直接暴露 Pod四种 Service 类型的选型逻辑Service 有四种类型选错了要么暴露不出去要么暴露得太狠。我按“暴露范围从小到大”给你排一下顺便说清各自适用场景类型暴露范围典型场景关键特点ClusterIP集群内部内部微服务互调默认类型只有集群内可访问NodePort每个节点的固定端口测试环境、临时对外在 ClusterIP 基础上每个节点开一个 30000-32767 的端口LoadBalancer外部负载均衡器生产环境对外服务在 NodePort 基础上向云厂商申请外部 LBExternalName无把外部域名映射成集群内服务名不做代理只做 DNS CNAME选型逻辑其实很朴素内部调用一律 ClusterIP别多此一举开 NodePort需要集群外访问又没有云负载均衡器时用 NodePort 过渡生产环境对外且云平台支持才上 LoadBalancer。ExternalName 是个特殊存在常用于把集群外的数据库、第三方 API 包装成一个集群内服务名让代码里写死的地址统一迁移时只改一处。我见过不少团队在测试环境图省事全用 NodePort结果端口号满天飞配置管理一团糟。ClusterIP 才是绝大多数服务的正确默认值NodePort 应该是有明确对外需求时的例外而不是顺手的选择。3. 端口三兄弟port、targetPort、nodePort 别再搞混3.1 三个端口各管一段画清楚就不乱Service 的端口配置是新手最容易翻车的地方因为一个 Service 里能出现三个“端口”名字还都带 port。我用一张表把它们钉死字段含义谁在用举例portService 自身在集群内的虚拟端口集群内调用方80targetPort流量最终转发到 Pod 容器的哪个端口Pod 容器8080nodePort每个节点上暴露的物理端口集群外调用方30080理解的关键是分清“两段路”第一段是调用方到 Service用的是port第二段是 Service 到 Pod用的是targetPort。nodePort只在 NodePort/LoadBalancer 类型下才有意义是节点上额外开的一个口子。举个具体例子。你的应用容器里监听的是 8080但你想让集群内其他服务用标准的 80 端口访问它那就配port: 80、targetPort: 8080。这样别人访问http://服务名:80流量被转到 Pod 的 8080。targetPort甚至可以写端口名比如容器里定义了name: http这样容器换端口时 Service 不用改是个很实用的小技巧。3.2 targetPort 与容器端口的对齐一个高频错误最常见的错误是targetPort和容器实际监听端口不一致。比如容器 Dockerfile 里EXPOSE 8080、程序也监听 8080但 Service 里targetPort手滑写成 80结果就是连接被拒。这里有个认知要纠正EXPOSE只是声明不决定实际监听端口真正决定的是程序启动时 bind 的端口。所以配targetPort时以程序实际监听为准别盯着 Dockerfile 的 EXPOSE 抄。排查这类问题有个笨但有效的办法进到 Pod 里curl localhost:端口试一下能通说明容器端口没问题问题就在 Service 配置不通说明是应用本身没起来或监听错了地址比如只监听了 127.0.0.1 而非 0.0.0.0这种情况外部永远连不上是个隐蔽的坑。3.3 nodePort 的取值范围与冲突处理nodePort默认范围是 30000-32767这是 kube-apiserver 启动参数里定的。你可以手动指定也可以不写让系统自动分配。手动指定时要注意两点一是别撞上已占用的端口二是范围外的值会被拒绝。自动分配省心但端口不固定脚本里不好引用手动指定可控但要自己维护一张端口分配表团队大了容易冲突。我的做法是测试环境自动分配生产环境如果确实需要 NodePort就按业务模块划段管理比如 A 模块用 30001-30020B 模块用 30021-30040写进文档避免踩踏。注意NodePort 是在每个节点上都开这个端口不是只开在 Pod 所在节点。所以哪怕 Pod 只在某一台机器上你访问任意节点的该端口都能通这背后是 kube-proxy 做的转发。理解这点才不会误以为“Pod 不在这个节点所以连不上”。4. 动手搭一套从 Deployment 到 Service 的完整链路4.1 先有 Deployment 和带 Label 的 PodService 不能凭空存在它得有 Pod 可选。所以我按真实顺序来先写 Deployment确保 Pod 带正确的 label再写 Service 去选它。下面这个 Deployment 是我常用的最小模板跑一个返回固定响应的 Web 服务apiVersion: apps/v1 kind: Deployment metadata: name: web-backend namespace: demo spec: replicas: 3 selector: matchLabels: app: web-backend template: metadata: labels: app: web-backend tier: backend spec: containers: - name: app image: my-registry/web-demo:1.0 ports: - containerPort: 8080 name: http readinessProbe: httpGet: path: /healthz port: 8080 initialDelaySeconds: 5 periodSeconds: 10这里有几个细节值得说。replicas: 3是为了后面验证负载分发单副本看不出效果。template.metadata.labels里的app: web-backend就是 Service 要匹配的目标tier: backend是额外标签方便以后做更细的选择。containerPort给了个名字http这样 Service 的targetPort可以直接写http容器换端口时不用改 Service。就绪探针readinessProbe是重点。它决定了 Pod 什么时候被算作“可接流量”。探针没通过Pod 就算 Running 也不会进 Endpoints。很多人只配存活探针不配就绪探针结果应用还在初始化就被塞流量报一堆错。就绪探针是 Service 能否稳定工作的隐形前提别省。4.2 写一个 ClusterIP 类型的 ServicePod 就绪后写 ServiceapiVersion: v1 kind: Service metadata: name: web-backend-svc namespace: demo spec: type: ClusterIP selector: app: web-backend ports: - name: http protocol: TCP port: 80 targetPort: http注意namespace必须和 Deployment 一致跨命名空间的 Service 选不到 Pod这是另一个高频坑。selector只写了app: web-backend没写tier意味着只要带这个 app 标签的 Pod 都会被选中——选择器是“包含匹配”不是“完全相等匹配”多写标签会收窄范围少写会放宽按需取舍。应用之后按这个顺序验证一步都别跳kubectl get pods -n demo -o wide # 确认 3 个 Pod 都 Running 且 Ready kubectl get endpoints web-backend-svc -n demo # 确认名单里有 3 个 IP kubectl get svc web-backend-svc -n demo # 确认 ClusterIP 和端口Endpoints 里出现 3 个IP:8080说明链路通了。如果名单是空的回到 2.2 节那条链去查 label 和命名空间。4.3 集群内验证与负载分发的观察验证连通性最省事的是起一个临时 Pod 在集群内 curlkubectl run curl-test -n demo --imagecurlimages/curl --rm -it --restartNever -- \ curl -s http://web-backend-svc.demo.svc.cluster.local/healthz这里用了完整的集群内 DNS 名service.namespace.svc.cluster.local。同命名空间内其实可以直接用服务名web-backend-svc但写全名在任何命名空间都能用调试时更稳。多 curl 几次如果后端返回里带了 Pod 标识你能看到请求在 3 个 Pod 间轮转——这就是 kube-proxy 默认的负载均衡行为。想更直观地看分发可以让后端在响应里回显自己的 hostname连续请求十次统计各 Pod 出现次数大致均匀就说明分发正常。这里不追求绝对平均iptables 模式的随机性会导致短时间内的偏差样本大了才趋于均匀。5. 进阶场景无头 Service、会话保持与外部访问5.1 无头 Service不要虚拟 IP要真实 Pod 列表有些场景你不需要 Service 做负载均衡反而想要“拿到所有后端 Pod 的真实 IP”。典型如 StatefulSet 管理的数据库集群节点之间要互相知道对方地址做组网。这时候用无头 ServiceHeadless Service把clusterIP设为NoneapiVersion: v1 kind: Service metadata: name: db-headless namespace: demo spec: clusterIP: None selector: app: db ports: - port: 5432 targetPort: 5432无头 Service 不分配虚拟 IPDNS 查询它会直接返回所有后端 Pod 的 IP 列表A 记录。配合 StatefulSet 的稳定网络标识每个 Pod 还能拿到pod名.service名形式的独立 DNS 名这对有状态集群太重要了。我第一次用无头 Service 搭主从复制时就是靠这个让从节点稳定地找到主节点不用硬编码 IP。5.2 会话保持什么时候需要怎么配默认情况下 Service 是无状态轮询同一个客户端的连续请求可能落到不同 Pod。对无状态服务这没问题但对依赖本地 session 的老应用就是灾难——用户在 A Pod 登录下一个请求到 B Pod 就掉线了。解决办法是配sessionAffinity: ClientIPspec: sessionAffinity: ClientIP sessionAffinityConfig: clientIP: timeoutSeconds: 10800这样同一源 IP 的请求在超时时间内固定落到同一个 Pod。但我得提醒一句会话保持是权宜之计不是长久方案。它破坏了负载均衡的均匀性NAT 后面大量客户端共享一个出口 IP 时会全压到一个 Pod 上。更根本的做法是把 session 外置到 Redis 之类的共享存储让 Pod 真正无状态。会话保持适合改造周期长的遗留系统过渡用。5.3 NodePort 与 LoadBalancer 的落地差异需要集群外访问时NodePort 是最不依赖云平台的选择spec: type: NodePort selector: app: web-backend ports: - port: 80 targetPort: http nodePort: 30080外部通过任意节点IP:30080访问。缺点是端口号难看、要自己管防火墙、节点 IP 变动要跟着改。LoadBalancer 则把这些脏活交给云平台type: LoadBalancer一配云厂商自动分配一个外部 IP 并做好负载均衡你只管用。代价是绑定云环境自建集群得额外装负载均衡组件才能生效否则外部 IP 会一直 pending。选型上我的经验是临时演示和内部测试用 NodePort 够了正式对外、且跑在支持 LB 的云上直接上 LoadBalancer省下的运维精力远超那点成本。6. 踩坑实录Service 不通的排查清单6.1 一张速查表覆盖九成故障Service 连不上别慌按这张表从上往下查基本能定位现象最可能原因排查命令解决Endpoints 为空selector 与 Pod label 不匹配kubectl get ep svc -n ns对齐 label 和 namespaceEndpoints 有 IP 但连不上targetPort 与容器端口不符kubectl exec进 Pod curl localhost改 targetPort集群内 DNS 解析失败服务名或命名空间写错nslookup svc.ns用完整 DNS 名外部访问 NodePort 不通节点防火墙拦截检查安全组/iptables放行端口段间歇性连接失败就绪探针缺失流量打到未就绪 Podkubectl describe pod看 Ready补 readinessProbe全部请求压一个 Pod开了会话保持或客户端同源 IP看 sessionAffinity 配置评估是否真需要6.2 两个我亲身踩过的隐蔽坑第一个坑是应用只监听 127.0.0.1。容器里程序 bind 到 localhost容器内部 curl 通但 Service 转发过来的是从 Pod IP 进来的流量程序不认直接拒。这个坑隐蔽在于“容器内测试一切正常”很容易误判成 Service 问题。解决办法是让程序监听0.0.0.0绝大多数框架的默认 host 配置项都能改。第二个坑是命名空间错位。Deployment 建在defaultService 建在demoselector 写得再对也选不到因为 Service 的 selector 只在自己所在命名空间内匹配 Pod。这个错误在多人协作时特别常见各自kubectl apply时忘了带-n。我的习惯是所有命令都显式带命名空间宁可多敲几个字也不给这种低级错误留机会。提示排查时善用kubectl describe svc 名字它会把 Endpoints 列表、事件都列出来再配合kubectl get ep交叉验证比盲目改配置高效得多。6.3 关于 kube-proxy 模式的一点认知底层转发靠 kube-proxy它有 iptables 和 IPVS 两种模式。iptables 模式在服务数量上千后规则链会变长性能下降IPVS 模式用哈希表大规模下更稳。这个一般不用你手动管但知道它存在有好处当你发现集群规模上来后 Service 转发变慢可以往这个方向查。小集群用默认的 iptables 完全够别过早优化。7. 把五个对象串成一条线回过头看Namespace、Pod、Label、Deployment、Service 这五个对象其实是一条完整的叙事线Namespace 划地盘Deployment 管副本Pod 是运行单元Label 是身份标签Service 靠 Label 找到 Pod 并提供稳定入口。单独看每个都简单难的是理解它们之间的咬合关系——尤其是 Label 作为“胶水”把 Deployment 的 Pod 和 Service 的 selector 连起来这一环。我在实际项目里养成的一个习惯是每建一个 Service立刻kubectl get endpoints确认名单非空把它当成一个强制的自检动作。这个动作花不了几秒却能挡掉后面一大半“服务不通”的工单。另外端口配置我坚持用命名端口targetPort: http而非数字容器端口调整时 Service 零改动长期维护下来省心不少。这套东西搭顺了之后再往上走就是 Ingress七层路由、NetworkPolicy网络策略这些但地基就是这篇讲的 Service 与 Label 的配合。地基打牢后面加什么组件都不会晃。