Kubernetes 基础体系 · 第 26/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes Service:ClusterIP、NodePort、LoadBalancer、EndpointSlice 和流量
Kubernetes Pod 是可替换的:Pod 重建后 IP 可能改变,副本扩缩容后后端集合也会变化。Service 的核心作用,是为一组后端 Pod 提供稳定的访问抽象:
其中“稳定地址”通常是 DNS 名称和虚拟 IP,“后端集合”由 EndpointSlice 描述,“如何转发”则由 kube-proxy 或其他服务代理实现。
Service 不是应用层网关。它通常只根据目标 IP、端口和传输协议转发 TCP、UDP 或 SCTP 流量,不理解 HTTP URL、Host、TLS 证书或业务路由。需要七层路由时,应使用 Ingress 或 Gateway API。
一、先建立完整的对象模型
一个典型 Service 至少涉及四类信息:
- Service 对象:定义稳定名称、虚拟 IP、端口映射、流量策略等。
- Pod 标签:Service 通过 selector 选择后端 Pod。
- EndpointSlice 对象:记录当前后端 Pod 的地址、端口和就绪状态。
- 节点上的服务转发规则:通常由 kube-proxy 根据 Service 和 EndpointSlice 同步生成。
例如:
apiVersion: v1
kind: Service
metadata:
name: web
namespace: default
spec:
selector:
app: web
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: nginx:1.27
ports:
- name: http
containerPort: 8080
这里有三个不同的端口概念:
port: 80:Service 对客户端暴露的端口。targetPort: 8080:Service 转发到后端 Pod 的端口。containerPort: 8080:容器声明的端口,主要用于描述和工具发现,不会单独创建转发规则。
如果 targetPort 使用字符串,例如:
ports:
- name: http
port: 80
targetPort: http
Kubernetes 会根据 Pod 容器端口中名为 http 的端口解析目标端口。使用命名端口可以避免应用端口变更时同时修改多个对象,但名称必须真实存在且协议匹配。
Service selector 中的标签匹配是逻辑上的集合运算:
Service 本身不直接保存这个集合;控制器会根据 Pod 状态生成或更新 EndpointSlice。
二、Service 的稳定地址:DNS、ClusterIP 与端口
创建 Service 后,集群 DNS 通常提供:
web.default.svc.cluster.local
在同一命名空间中,客户端通常可以直接使用:
web
完整 DNS 名称的组成是:
<service>.<namespace>.svc.<cluster-domain>
集群内客户端解析 Service 名称时,通常得到一个或多个 Service IP,而不是某个 Pod IP。对于普通 Service,这个 IP 是 clusterIP,也称为虚拟 IP(virtual IP)。
Service 的虚拟 IP 通常不会作为某个网卡接口上的真实地址存在。节点上的转发规则会匹配发往这个 IP 和端口的连接,然后选择后端 Endpoint。可将其抽象为:
其中:
SIP是 Service IP;SP是 Service port;EIP_i是第 个后端地址;EP_i是后端端口。
例如:
10.96.20.10:80 -> 10.244.1.12:8080
或
10.244.2.7:8080
或
10.244.3.4:8080
客户端通常只感知 10.96.20.10:80,后端 Pod 的变化由控制器和节点转发规则处理。
clusterIP 的地址范围
集群控制面会从配置的 Service CIDR 中分配 ClusterIP。这个地址应被视为集群内部的虚拟地址,而不是可从互联网直接路由到的地址。
可以查看实际分配结果:
kubectl get svc web -o wide
kubectl get svc web -o jsonpath='{.spec.clusterIP}{"\n"}'
典型输出可能是:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
web ClusterIP 10.96.20.10 <none> 80/TCP 20s app=web
具体 IP、Service CIDR 和输出格式取决于集群配置。
三、ClusterIP:集群内部的虚拟服务
ClusterIP 是默认 Service 类型。它的语义是:
- 分配一个集群内部可访问的虚拟 IP;
- 通过 DNS 提供稳定名称;
- 由服务转发组件将流量送往 EndpointSlice 中的后端;
- 默认不承诺从集群外部直接访问。
配置可以显式写出:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ClusterIP
selector:
app: api
ports:
- name: http
port: 80
targetPort: 8080
客户端测试:
kubectl run curl --rm -it \
--image=curlimages/curl:8.10.1 \
--restart=Never -- \
curl -v http://api.default.svc.cluster.local/
前置条件是 api Service 存在,并且后端容器确实在 8080 端口提供 HTTP 服务。命令中的临时 Pod 必须能够访问集群网络;如果网络策略、DNS 或镜像拉取受到限制,失败原因可能不在 Service 本身。
ClusterIP 并不等于“任意节点都监听这个地址”
常见误解是:每个节点都有一个真正监听 clusterIP:port 的进程。通常并非如此。kube-proxy 会在节点内核的数据路径中安装规则,数据包进入节点后被规则匹配和改写。
不同 kube-proxy 模式的细节不同:
- iptables 模式通常使用 DNAT 规则和概率或统计匹配在多个 Endpoint 之间选择。
- IPVS 模式使用内核 IPVS 虚拟服务和后端集合;具体可用性取决于集群版本和发行版配置。
- nftables 模式使用 nftables 数据路径;它是较新的 kube-proxy 实现,启用条件和默认值依 Kubernetes 版本、发行版而变化。
- 也存在替代 kube-proxy 的实现,例如基于 eBPF 的网络组件;它们可能不遵循 kube-proxy 的具体实现方式,但仍需实现 Kubernetes Service 语义。
因此,Service API 是规范保证;iptables、IPVS、nftables 和 eBPF 的规则形态是实现细节,不能把某一种节点命令输出当作所有集群的通用事实。
四、Service 的端口映射与协议
Service 的每个端口至少包含:
ports:
- name: https
protocol: TCP
port: 443
targetPort: 8443
name:在多端口 Service 中应使用唯一名称;命名端口还可供其他对象引用。protocol:常见为TCP、UDP或SCTP,默认是 TCP。port:Service 对外提供的端口。targetPort:后端端口,可以是数字或命名端口。
Service 不会自动把 HTTP 的 443 转成 TLS,也不会检查证书。若后端服务提供的是纯 HTTP,客户端访问 Service 的 443 仍然只是向某个 TCP 端口建立连接,是否能完成 TLS 取决于后端应用。
当一个 Service 同时暴露多个端口时,端口名称和协议尤其重要:
ports:
- name: http
protocol: TCP
port: 80
targetPort: 8080
- name: metrics
protocol: TCP
port: 9090
targetPort: 9090
对 TCP 和 UDP 使用相同端口号并不表示它们冲突;传输层协议不同,Service 端口条目的匹配空间也不同。
五、EndpointSlice:Service 后端状态的实际载体
5.1 为什么需要 EndpointSlice
早期 Kubernetes 使用 Endpoints 对象描述 Service 后端。当一个 Service 拥有大量 Pod 时,单个对象会不断变大,每次 Pod 变化都可能导致整个对象更新和传输。
EndpointSlice 将后端拆成多个切片。一个 EndpointSlice 通常包含一组具有相同地址族、协议、端口描述和拓扑属性的 Endpoint。这样可以降低大规模集群中单个对象更新的成本。
查看 EndpointSlice:
kubectl get endpointslice \
-l kubernetes.io/service-name=web
kubectl get endpointslice \
-l kubernetes.io/service-name=web \
-o yaml
选择器 kubernetes.io/service-name=web 用于找到属于 web Service 的切片。不要假设只存在一个 EndpointSlice。
5.2 EndpointSlice 的关键字段
EndpointSlice 的常见结构可以简化为:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: web-abcde
labels:
kubernetes.io/service-name: web
addressType: IPv4
ports:
- name: http
protocol: TCP
port: 8080
endpoints:
- addresses:
- 10.244.1.12
conditions:
ready: true
serving: true
terminating: false
nodeName: worker-a
targetRef:
kind: Pod
name: web-7d9f...
- addresses:
- 10.244.2.7
conditions:
ready: false
serving: true
terminating: false
nodeName: worker-b
关键字段的语义如下:
addressType:例如IPv4、IPv6或FQDN。ports:该切片中 Endpoint 对应的端口定义。addresses:后端地址。对普通 Pod Service,通常是 Pod IP。conditions.ready:后端是否就绪,通常与 Pod readiness 相关。conditions.serving:后端是否仍在提供服务。conditions.terminating:后端是否正在终止。nodeName:后端所在节点,可用于拓扑和本地流量选择。targetRef:通常指向对应 Pod,便于追踪来源。
实际的服务转发通常优先选择可用、就绪的 Endpoint。终止中的 Endpoint 具有更细的状态组合,供控制器和实现处理优雅终止;不能简单地把所有出现在 EndpointSlice 中的地址都理解成“新连接一定会被发送到的后端”。
5.3 EndpointSlice 的生成过程
对带 selector 的 Service,控制器大致执行以下过程:
- 观察 Service 的 selector。
- 找到标签匹配的 Pod。
- 检查 Pod IP、容器端口和就绪状态。
- 按地址族、端口和其他兼容属性分组。
- 创建、更新或删除 EndpointSlice。
- kube-proxy 或其他服务实现观察 Service 与 EndpointSlice。
- 节点数据路径更新后,新的连接才使用新的后端集合。
这意味着 Pod 已经变为 Ready 与节点转发规则完成更新之间存在传播延迟。规范提供最终一致性,而不是每个状态变化都同步完成的瞬间一致性。
5.4 没有 selector 的 Service
Service 可以不写 selector:
apiVersion: v1
kind: Service
metadata:
name: external-api
spec:
ports:
- name: https
port: 443
targetPort: 443
这时 Kubernetes 不会根据 Pod 自动生成后端。管理员或控制器可以手动创建 EndpointSlice,例如:
apiVersion: discovery.k8s.io/v1
kind: EndpointSlice
metadata:
name: external-api-1
labels:
kubernetes.io/service-name: external-api
addressType: IPv4
ports:
- name: https
protocol: TCP
port: 443
endpoints:
- addresses:
- 192.0.2.20
conditions:
ready: true
这种方式可把 Service 抽象到集群外部的固定地址,但需要自行维护地址的正确性、健康状态和安全边界。EndpointSlice 地址不能随意指向集群控制面或 Kubernetes API Server 等受限制目标;Admission 和服务实现可能拒绝或限制某些地址。
5.5 Headless Service
设置:
spec:
clusterIP: None
得到 Headless Service。它没有虚拟 ClusterIP,DNS 通常直接返回后端 Pod 地址或相关记录:
apiVersion: v1
kind: Service
metadata:
name: db
spec:
clusterIP: None
selector:
app: db
ports:
- name: client
port: 5432
targetPort: 5432
Headless Service 的含义不是“没有服务发现”,而是“不提供一个由 Service 虚拟 IP 汇聚的单一入口”。客户端或上层系统会得到多个后端地址,并自行决定连接哪个地址。
StatefulSet 常用 Headless Service 配合稳定的 Pod DNS 名称进行成员发现。它不会自动提供数据库主从选举、故障转移或连接池管理。
六、NodePort:在每个节点暴露一个端口
NodePort 在 ClusterIP 的基础上,为 Service 分配一个节点端口:
apiVersion: v1
kind: Service
metadata:
name: web-nodeport
spec:
type: NodePort
selector:
app: web
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30080
此时通常存在两条访问路径:
节点 IP:30080 -> Service 转发逻辑 -> Pod:8080
以及:
节点 IP:30080 -> 对应的 ClusterIP:80 -> Pod:8080
NodePort 的默认分配范围由集群配置决定,常见默认范围是 30000-32767,但不能把这个范围当成所有集群的规范保证。显式指定 nodePort 时必须位于允许范围内,并且不能与同一协议下已有端口冲突。
查看分配结果:
kubectl get svc web-nodeport
kubectl describe svc web-nodeport
典型端口列可能显示:
80:30080/TCP
这表示 Service port 是 80,节点端口是 30080,而不是后端 Pod 必须监听 30080。
NodePort 的真实边界
NodePort 需要满足两个条件:
- 访问者能够到达某个节点的节点 IP 和 NodePort。
- 节点防火墙、云安全组、网络 ACL 和 kube-proxy 数据路径允许该流量通过。
Kubernetes 创建 NodePort 不会自动替云环境开放安全组,也不会保证所有节点 IP 都能从互联网路由到。裸机环境中还要考虑节点上的其他进程是否占用端口、外部负载均衡器探活是否能到达节点,以及节点下线时如何摘除流量。
NodePort 常被外部负载均衡器作为后端目标:
外部 LB:443
-> node-a:30080
-> node-b:30080
-> node-c:30080
但如果外部 LB 本身能够直接发现 Pod,并且 Kubernetes Service 实现支持绕过 NodePort,则不一定需要这层节点端口。
七、LoadBalancer:请求外部负载均衡能力,而不是 Kubernetes 自己实现公网入口
LoadBalancer Service 表达的是:
请基础设施或云控制器为这个 Service 提供一个外部可达的负载均衡入口。
示例:
apiVersion: v1
kind: Service
metadata:
name: web-public
spec:
type: LoadBalancer
selector:
app: web
ports:
- name: http
port: 80
targetPort: 8080
创建过程通常是:
- API Server 接受 Service。
- 云控制器或其他 LoadBalancer 实现观察到
type: LoadBalancer。 - 它创建或配置外部负载均衡器。
- 外部地址写入
status.loadBalancer.ingress。 - 外部负载均衡器把流量送到节点端口或直接送到 Pod,取决于实现。
- 节点服务转发和 EndpointSlice 变化继续影响后端选择。
查看状态:
kubectl get svc web-public -w
EXTERNAL-IP 长时间处于 <pending>,通常意味着没有可用的 LoadBalancer Controller、云凭据错误、子网或配额问题,或者控制器无法创建外部资源。这不是应用容器监听错误的直接证据。
LoadBalancer 与 NodePort 的关系
历史上,大量 Kubernetes 实现通过 NodePort 实现 LoadBalancer:
外部地址:80
-> 节点:NodePort
-> Service ClusterIP/后端规则
-> Pod
这也是为什么一个 LoadBalancer Service 常常同时显示:
80:31234/TCP
但当前 Service API 允许实现使用:
spec:
allocateLoadBalancerNodePorts: false
前提是底层 LoadBalancer 实现支持不依赖 NodePort 的流量路径,例如直接把外部负载均衡器配置到 Pod 后端。设置该字段不会凭空让云平台获得“直达 Pod”的能力;不支持该语义的实现可能导致流量不可达。对于已有 Service,关闭 NodePort 分配也不会自动删除历史上已经分配的 nodePort,通常需要显式清理。
loadBalancerClass
集群中可能存在多个 LoadBalancer 实现。可以通过 loadBalancerClass 指定由哪一类实现处理:
apiVersion: v1
kind: Service
metadata:
name: web-private
spec:
type: LoadBalancer
loadBalancerClass: example.com/internal-lb
selector:
app: web
ports:
- port: 80
targetPort: 8080
具体 class 名称必须由实际安装的控制器定义。不能任意写一个字符串后期待 Kubernetes 自动创建负载均衡器。
云厂商差异
以下行为没有统一的跨云保证:
- 公网还是内网负载均衡器;
- 是否自动分配公网地址;
- 是否支持跨区域后端;
- 健康检查探测哪个端口;
- 是否保留客户端源 IP;
- 是否支持
externalTrafficPolicy: Local; - 是否支持直接 Pod 后端;
- 注解和参数名称;
- 删除 Service 后外部负载均衡器资源是否同步删除。
因此,LoadBalancer 是 Kubernetes 对控制器的请求,不是一个内置、跨平台完全同构的云产品 API。
八、从客户端到 Pod:一条 TCP 流量的完整路径
以同一集群内的客户端访问 ClusterIP 为例:
sequenceDiagram
participant C as 客户端 Pod
participant D as 集群 DNS
participant K as 节点服务转发规则
participant E as Endpoint Pod
C->>D: 查询 web.default.svc.cluster.local
D-->>C: 返回 ClusterIP
C->>K: 连接 ClusterIP:80
K->>K: 根据 Service/EndpointSlice 选择后端
K->>E: DNAT 或等效转发到 PodIP:8080
E-->>K: 返回响应
K-->>C: 返回响应
具体执行并不是“每个请求都重新查询 DNS 并随机选 Pod”:
- DNS 解析结果通常会被客户端、语言运行时或本地 DNS 缓存。
- 对 TCP,客户端先建立连接;一旦连接选择了后端,后续数据通常沿同一连接到同一后端。
- kube-proxy 或替代实现使用 Service 和 EndpointSlice 状态更新节点数据路径。
- 连接是否能在 Endpoint 删除后继续存在,取决于连接状态、协议、终止处理和实现;删除 Endpoint 不等于立即强制关闭所有已建立连接。
- 新连接通常不会继续选择已经不满足就绪条件的后端,但状态传播存在短暂延迟。
对 UDP,连接状态不具备 TCP 的同样语义。实现可能使用流表、连接跟踪或其他机制保持一段时间的后端关联,因此“每个 UDP 数据报都严格重新随机选择”也不是可以泛化的保证。
后端选择不是应用层负载均衡
Service 通常在传输层做后端选择。它不会知道:
- 某个 HTTP 请求是否是登录请求;
- 某个用户是否已经登录到某个 Pod;
- 某个后端的业务队列是否已满;
- URL
/orders/1应该路由到哪个版本。
因此,Service 的“负载均衡”更准确地说是连接或流量转发层面的后端分配。需要基于请求内容的路由,应使用 Ingress、Gateway API 或应用层代理。
九、SNAT、源 IP 与返回路径
转发过程中可能发生两种地址转换:
- DNAT:把目标从 Service IP 改为某个 Endpoint IP。
- SNAT:把客户端源 IP 改为节点 IP 或其他地址。
理想情况下:
客户端源 IP -> Pod
Pod 返回客户端源 IP
但跨节点转发时,若 Pod 的返回路径无法正确回到原客户端,服务代理可能需要 SNAT:
客户端 IP -> 节点 A
节点 A DNAT -> 节点 B 上的 Pod
节点 B 看到的源地址可能是节点 A
SNAT 的直接代价是应用日志看到的源地址可能不再是原客户端 IP。是否发生、发生在哪个方向,取决于服务转发实现、网络插件、路由能力和流量策略。
externalTrafficPolicy
externalTrafficPolicy 主要影响从集群外部进入 Service 的流量:
spec:
type: LoadBalancer
externalTrafficPolicy: Local
两种主要语义:
Cluster
默认常见值。外部流量可以从接收流量的节点转发到集群中其他节点的后端。
优点是节点上的后端选择更灵活;代价可能是:
- 发生跨节点转发;
- 客户端源 IP 可能被 SNAT;
- 外部负载均衡器不一定需要把流量精确送到有 Pod 的节点。
Local
只使用接收流量节点上的本地 Endpoint:
客户端 -> node-a:NodePort
|
+-- node-a 有本地 Pod:转发
|
+-- node-a 没有本地 Pod:通常丢弃或被健康检查摘除
它通常用于保留客户端源 IP,或减少跨节点转发。但它带来明确风险:如果外部负载均衡器把流量发送到没有本地后端的节点,而该节点没有被正确摘除,流量可能失败。
因此使用 Local 时,必须验证:
- 外部 LB 是否支持并正确执行节点健康检查;
- 健康检查端口和探测路径是否符合该实现;
- 各节点后端分布是否均衡;
- 节点排空和 Pod 滚动升级时是否会产生短暂无本地后端。
internalTrafficPolicy: Local 是另一个维度,用于集群内部访问:
spec:
internalTrafficPolicy: Local
它要求集群内部客户端优先或仅使用本节点 Endpoint,具体无本地 Endpoint 时的行为应按当前 Kubernetes 版本和实现验证。不能用 externalTrafficPolicy 推断集群内部流量的行为。
sessionAffinity
默认情况下,Service 不保证同一个客户端总是访问同一个后端。可以配置基于客户端 IP 的会话亲和性:
spec:
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
它的直觉是:
但这个“客户端源地址”可能已经被 NAT,多个真实客户端可能共享一个出口 IP;反过来,同一客户端地址变化后也可能被分配到不同后端。会话亲和性通常依赖节点服务转发和连接跟踪状态,不是跨所有故障、节点重启和实现切换的持久会话数据库。
如果应用需要真正可靠的用户会话,应把会话放在共享存储,或使用应用层可验证的粘性机制,而不能把 Service 的 ClientIP 当成完整的会话存储方案。
十、Endpoint 就绪、探针与优雅终止
Service 是否把 Pod 当作可用后端,和容器进程是否存在不是一回事。
例如:
readinessProbe:
httpGet:
path: /ready
port: 8080
periodSeconds: 5
当 readiness probe 失败时,Pod 仍可能处于 Running,但通常不再被作为普通新流量的就绪 Endpoint。这样可以把“进程活着”和“能够接收业务流量”区分开。
一个典型滚动更新过程是:
- 新 Pod 启动,但 readiness 失败。
- 新 Pod 出现在 Pod 列表中,但尚未作为就绪后端接收普通流量。
- 新 Pod readiness 成功,EndpointSlice 标记其可服务。
- Deployment 增加新 Pod 流量并减少旧 Pod。
- 旧 Pod 进入 terminating 状态。
- EndpointSlice 反映旧 Endpoint 的终止状态。
- kube-proxy 和外部负载均衡器逐步停止向旧 Pod 建立新连接。
- 旧 Pod 在
terminationGracePeriodSeconds内处理已有连接并退出。
这不是绝对无损的协议保证。应用必须配合:
- readiness 探针在真正能服务前保持失败;
- 收到终止信号后停止接收新业务;
- 正确处理已有连接;
- 设置合理的优雅终止时间;
- 外部 LB 的摘除延迟不能被忽略。
publishNotReadyAddresses: true 会改变服务发现行为,常用于需要发现尚未 Ready 成员的系统,例如某些有自己选举或启动协调过程的有状态系统:
spec:
publishNotReadyAddresses: true
这意味着客户端可能得到尚未通过 readiness 的地址。它不是“让探针失效”,而是明确要求服务发现发布未就绪地址;如果普通 HTTP 服务误用,可能把启动中的 Pod 暴露给客户端。
十一、双栈与 IP 家族
当前 Kubernetes Service API 支持 IPv4、IPv6 和双栈场景,但是否能实际使用取决于集群是否启用对应网络能力。
可以查看:
kubectl get svc web -o yaml
关注:
spec:
ipFamilies:
- IPv4
ipFamilyPolicy: SingleStack
常见策略包括:
SingleStack:使用单一地址族;PreferDualStack:在支持时尽量使用双栈;RequireDualStack:要求双栈,否则创建或配置不能满足。
EndpointSlice 也按 addressType 区分地址族。一个 IPv4 EndpointSlice 不应被当作 IPv6 后端使用。双栈环境中,客户端 DNS 结果、应用连接策略、网络插件和外部负载均衡器都必须共同支持双栈,不能只看 Service YAML 就假设 IPv6 流量可达。
十二、外部地址、ExternalName 与 Service 的边界
externalIPs
Service 可以声明:
spec:
externalIPs:
- 203.0.113.20
这不会自动购买、配置或宣告这个 IP。它只是允许服务实现把发往该地址的流量视作该 Service 的流量。集群网络必须真的能让该地址的流量到达正确节点,地址所有权和防火墙也必须由管理员负责。
误用 externalIPs 可能造成地址劫持或绕过网络边界,不应把它当作云 LoadBalancer 的替代品。
ExternalName
apiVersion: v1
kind: Service
metadata:
name: payments
spec:
type: ExternalName
externalName: payments.example.net
ExternalName 主要创建 DNS CNAME 语义,不创建 ClusterIP,也不创建 EndpointSlice,不提供 kube-proxy 的四层转发。客户端解析到外部域名后,能否访问由外部 DNS、路由、TLS 和目标服务决定。
因此:
ClusterIP:集群内虚拟 IP 加服务转发;- 无 selector 的 Service 加 EndpointSlice:手工维护后端;
ExternalName:DNS 别名;LoadBalancer:请求外部负载均衡实现。
这四者不能互换。
十三、从头创建一个可验证的 Service
下面的示例让容器明确监听 8080,避免把镜像默认端口和 Service 映射混淆:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-web
spec:
replicas: 2
selector:
matchLabels:
app: demo-web
template:
metadata:
labels:
app: demo-web
spec:
containers:
- name: app
image: hashicorp/http-echo:1.0.0
args:
- "-listen=:8080"
- "-text=hello from kubernetes"
ports:
- name: http
containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: demo-web
spec:
selector:
app: demo-web
ports:
- name: http
port: 80
targetPort: http
type: ClusterIP
应用并验证:
kubectl apply -f demo-web.yaml
kubectl rollout status deployment/demo-web
kubectl get svc demo-web
kubectl get endpointslice \
-l kubernetes.io/service-name=demo-web \
-o wide
然后从集群内部访问:
kubectl run test-client --rm -it \
--image=curlimages/curl:8.10.1 \
--restart=Never -- \
curl -sS http://demo-web.default.svc.cluster.local/
预期会得到:
hello from kubernetes
这个结果依赖四个映射同时成立:
demo-web.default.svc.cluster.local
-> demo-web 的 ClusterIP:80
-> EndpointSlice 中的 PodIP:8080
-> 容器中的 http-echo 监听 :8080
如果将 targetPort 错写为 80,Service 仍可能创建成功,DNS 也仍能解析,但连接会在 Pod 的 80 端口失败。这说明 Service API 校验“字段格式和引用关系”,通常不会探测应用是否真的在目标端口监听。
排查顺序可以是:
kubectl get svc demo-web -o yaml
kubectl get pods -l app=demo-web -o wide
kubectl describe pod -l app=demo-web
kubectl get endpointslice \
-l kubernetes.io/service-name=demo-web -o yaml
重点检查:
- selector 是否与 Pod 标签完全匹配;
- Pod 是否 Ready;
- EndpointSlice 是否包含预期地址;
targetPort数字或命名端口是否正确;- 容器是否监听正确地址和端口;
- NetworkPolicy 是否阻止客户端到 Pod 的流量;
- DNS 解析得到的地址是否属于预期 Service。
十四、如何判断问题发生在哪一层
一次访问失败可能发生在不同层,不能只执行 kubectl get svc 就下结论。
1. DNS 层
kubectl run dns-test --rm -it \
--image=busybox:1.36 \
--restart=Never -- \
nslookup demo-web.default.svc.cluster.local
若名称解析失败,优先检查 CoreDNS、客户端 Pod 的 DNS 配置和网络连通性,而不是先检查 EndpointSlice。
2. Service 对象层
kubectl get svc demo-web -o yaml
检查:
spec.type;spec.clusterIP;spec.ports;spec.selector;spec.ipFamilies;spec.internalTrafficPolicy、spec.externalTrafficPolicy;status.loadBalancer。
3. 后端发现层
kubectl get endpointslice \
-l kubernetes.io/service-name=demo-web \
-o yaml
如果没有切片或没有就绪地址,常见原因包括 selector 不匹配、Pod 没有可用 IP、readiness 失败、端口定义错误或 Service 是无 selector 且无人手动维护后端。
4. Pod 应用层
直接绕过 Service 测试 Pod:
kubectl get pods -l app=demo-web -o wide
kubectl port-forward pod/<pod-name> 18080:8080
curl http://127.0.0.1:18080/
port-forward 主要用于诊断,不等价于生产流量路径。它能验证应用是否监听和响应,但不能证明 Service、节点路由或外部 LoadBalancer 正常。
5. 节点服务转发层
不同 kube-proxy 模式需要不同工具。例如:
kubectl -n kube-system get pods -l k8s-app=kube-proxy
kubectl -n kube-system logs <kube-proxy-pod>
在节点上还可能使用:
iptables-save
ipvsadm -Ln
nft list ruleset
这些命令必须在对应节点上执行,并且需要权限。没有看到 iptables 规则不代表 Service 一定没有生效,因为集群可能使用 IPVS、nftables、eBPF 或其他实现。
6. 外部负载均衡器层
对于 LoadBalancer:
kubectl describe svc web-public
kubectl get events --sort-by=.lastTimestamp
检查事件、外部地址、健康检查和云控制器日志。外部地址已分配只说明控制器写入了入口状态,不证明:
- 入口端口已在安全组放行;
- LB 后端健康;
- 节点端口可达;
- Pod 应用正在监听;
- TLS 配置正确。
十五、常见反例与误解
反例一:Service selector 写成 Deployment 名称
spec:
selector:
app: demo-web
selector 匹配的是 Pod 的标签,不是 Deployment 名称、容器名称或 Service 名称。如果 Pod 模板没有 app: demo-web,Service 可以正常创建,但 EndpointSlice 为空。
反例二:把 containerPort 当成自动监听
ports:
- containerPort: 8080
这不会让进程自动监听 8080。真正决定应用监听端口的是进程参数和应用配置。
反例三:把 NodePort 当成外部防火墙配置
NodePort 只在 Kubernetes 服务转发层声明了一个端口。云安全组、裸机防火墙、路由器和上游负载均衡器仍可能阻止它。
反例四:把 LoadBalancer 当成 HTTP 路由器
多个 HTTP 服务不能仅靠一个 Service 按 URL 路径分流。Service 只按其地址、端口和协议转发;HTTP 路由应由 Ingress 或 Gateway API Controller 完成。
反例五:认为删除 Endpoint 会立即断开所有连接
EndpointSlice 更新主要影响后续后端选择。已经建立的 TCP 连接是否继续、何时关闭,取决于连接本身、应用优雅终止、conntrack 和服务实现。需要强制切断连接时,应处理应用和节点连接状态,而不是只修改标签。
反例六:对外部客户端启用 externalTrafficPolicy: Local 却没有本地后端
这会形成一个可预测的失败路径:
外部 LB -> node-a
node-a 没有本地 Endpoint
externalTrafficPolicy=Local
结果:连接被丢弃,或节点被健康检查摘除后不再接收流量
它不是“更保留源 IP 但仍自动跨节点转发”的配置。
十六、生产取舍:选择哪种 Service 类型
可以用访问范围和转发责任来区分:
| 类型 | 稳定入口 | 主要可达范围 | 是否依赖外部实现 |
|---|---|---|---|
ClusterIP |
ClusterIP 与 DNS | 集群内部 | 通常不依赖云 LB |
NodePort |
每节点 IP + 端口 | 能到达节点的网络 | 需要网络侧放通 |
LoadBalancer |
外部入口地址 | 由基础设施决定 | 需要 LoadBalancer Controller |
| Headless | DNS 返回后端地址 | 由客户端访问后端 | 不提供虚拟 IP 汇聚 |
ExternalName |
DNS 别名 | 由外部 DNS 和网络决定 | 不提供四层转发 |
对于内部微服务,ClusterIP 通常足够;需要将节点端口交给已有外部负载均衡器时使用 NodePort;需要由云或其他控制器创建外部入口时使用 LoadBalancer。如果需求是基于域名、路径、TLS 或策略进行 HTTP 路由,应增加 Ingress 或 Gateway API,而不是把多个业务端口堆到一个 Service 上。
十七、规范保证、实现差异与版本边界
需要明确区分以下三类事实:
Kubernetes API 语义
Service 类型、端口字段、selector、EndpointSlice API、externalTrafficPolicy、internalTrafficPolicy、sessionAffinity 等由 Kubernetes API 定义。对象状态和控制器行为遵循 Kubernetes 的兼容语义。
常见实现
kube-proxy 的 iptables、IPVS、nftables 模式,以及云厂商 LoadBalancer Controller 的具体规则,是实现选择。它们在后端选择、连接跟踪、健康检查、源地址保留和故障收敛方面可能存在差异。
环境经验
NodePort 默认端口范围、Service CIDR、云厂商注解、负载均衡器类型、跨区域能力和安全组行为,可能由集群安装方式或厂商决定。部署到新环境时应读取该环境的控制面、网络插件和云控制器文档,而不能只复制另一套集群的 YAML。
EndpointSlice 是当前服务发现与后端表示的主 API;旧的 Endpoints API 在大规模场景下存在扩展性限制,并已被 Kubernetes 文档标记为弃用方向。编写新控制器或诊断工具时,应优先使用 discovery.k8s.io/v1 的 EndpointSlice,而不是假设所有后端都集中在一个 Endpoints 对象中。
理解 Service 的关键,不是记住四个 type 名称,而是沿着这条状态链验证:
Service 配置
-> DNS / ClusterIP / NodePort
-> selector 或手工 EndpointSlice
-> 就绪与终止状态
-> 节点服务转发实现
-> SNAT、流量策略和返回路径
-> 外部 LoadBalancer(如果存在)
-> 应用端口与应用协议
链条中任意一层不成立,最终都可能表现为“访问 Service 失败”;只有把每一层的对象、状态和数据流分别验证,才能区分是服务发现问题、转发问题、网络策略问题、外部负载均衡问题,还是应用自身没有正确监听端口。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Pod 拓扑设计:Zone、Node、故障域、反亲和和数据局部性
- 下一篇:kube-proxy 与服务转发:iptables、IPVS、nftables、Session 和诊断
- 延伸:Ingress 与 Gateway API:路由、TLS、Controller、策略和迁移
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论