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,负责“挑人”。 - Endpoints:Kubernetes 自动维护的一个对象,记录当前被 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 基础上,向云厂商申请外部 LB |
| ExternalName | 无 | 把外部域名映射成集群内服务名 | 不做代理,只做 DNS CNAME |
选型逻辑其实很朴素:内部调用一律 ClusterIP,别多此一举开 NodePort;需要集群外访问又没有云负载均衡器时用 NodePort 过渡;生产环境对外且云平台支持,才上 LoadBalancer。ExternalName 是个特殊存在,常用于把集群外的数据库、第三方 API 包装成一个集群内服务名,让代码里写死的地址统一,迁移时只改一处。
我见过不少团队在测试环境图省事全用 NodePort,结果端口号满天飞,配置管理一团糟。ClusterIP 才是绝大多数服务的正确默认值,NodePort 应该是有明确对外需求时的例外,而不是顺手的选择。
3. 端口三兄弟:port、targetPort、nodePort 别再搞混
3.1 三个端口各管一段,画清楚就不乱
Service 的端口配置是新手最容易翻车的地方,因为一个 Service 里能出现三个“端口”,名字还都带 port。我用一张表把它们钉死:
| 字段 | 含义 | 谁在用 | 举例 |
|---|---|---|---|
port | Service 自身在集群内的虚拟端口 | 集群内调用方 | 80 |
targetPort | 流量最终转发到 Pod 容器的哪个端口 | Pod 容器 | 8080 |
nodePort | 每个节点上暴露的物理端口 | 集群外调用方 | 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-30020,B 模块用 30021-30040,写进文档,避免踩踏。
注意:NodePort 是在每个节点上都开这个端口,不是只开在 Pod 所在节点。所以哪怕 Pod 只在某一台机器上,你访问任意节点的该端口都能通,这背后是 kube-proxy 做的转发。理解这点,才不会误以为“Pod 不在这个节点所以连不上”。
4. 动手搭一套:从 Deployment 到 Service 的完整链路
4.1 先有 Deployment 和带 Label 的 Pod
Service 不能凭空存在,它得有 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 类型的 Service
Pod 就绪后,写 Service:
apiVersion: 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 在集群内 curl:
kubectl run curl-test -n demo --image=curlimages/curl --rm -it --restart=Never -- \ 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 管理的数据库集群,节点之间要互相知道对方地址做组网。这时候用无头 Service(Headless Service),把clusterIP设为None:
apiVersion: v1 kind: Service metadata: name: db-headless namespace: demo spec: clusterIP: None selector: app: db ports: - port: 5432 targetPort: 5432无头 Service 不分配虚拟 IP,DNS 查询它会直接返回所有后端 Pod 的 IP 列表(A 记录)。配合 StatefulSet 的稳定网络标识,每个 Pod 还能拿到<pod名>.<service名>形式的独立 DNS 名,这对有状态集群太重要了。我第一次用无头 Service 搭主从复制时,就是靠这个让从节点稳定地找到主节点,不用硬编码 IP。
5.2 会话保持:什么时候需要,怎么配
默认情况下 Service 是无状态轮询,同一个客户端的连续请求可能落到不同 Pod。对无状态服务这没问题,但对依赖本地 session 的老应用就是灾难——用户在 A Pod 登录,下一个请求到 B Pod 就掉线了。
解决办法是配sessionAffinity: ClientIP:
spec: 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 和 namespace |
| Endpoints 有 IP 但连不上 | targetPort 与容器端口不符 | kubectl exec进 Pod curl localhost | 改 targetPort |
| 集群内 DNS 解析失败 | 服务名或命名空间写错 | nslookup <svc>.<ns> | 用完整 DNS 名 |
| 外部访问 NodePort 不通 | 节点防火墙拦截 | 检查安全组/iptables | 放行端口段 |
| 间歇性连接失败 | 就绪探针缺失,流量打到未就绪 Pod | kubectl 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 建在default,Service 建在demo,selector 写得再对也选不到,因为 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 的配合。地基打牢,后面加什么组件都不会晃。