Kubernetes Service 类型与访问方式全解析:从 ClusterIP 到金丝雀发布
Kubernetes Service 类型与访问方式全解析:从 ClusterIP 到金丝雀发布
适合读者:已经理解 Service 是什么、会做基本管理,想搞清楚“集群内外到底怎么访问”的同学。
上一篇我们说过,Service 是 Pod 的“稳定门牌号”,解决了 Pod IP 漂移、后端动态变化的问题。但 Service 的“门牌号”也有不同的暴露方式:有些只允许集群内部访问,有些可以让任意节点的端口对外服务,有些能分配独立的负载均衡 IP,甚至有一种“门牌号”可以只是 DNS 层面的别名。
这篇文章把 Kubernetes Service 的类型、会话保持和金丝雀发布一次讲透,全部配实操命令。
一、Service 类型总览
先创建一组后端 Pod,后面所有实验都基于它:
kubectl create deployment web --image=hub.shaka.cn/library/httpd --replicas=2
Kubernetes Service 主要支持以下几种形式:
| 类型 | 访问范围 | 核心特点 | 典型场景 |
|---|---|---|---|
| ClusterIP | 仅集群内部 | 分配集群内部虚拟 IP,默认类型 | 微服务间调用、内部中间件 |
| NodePort | 集群外部 | 每个节点都监听同一个端口 | 测试环境、无 LB 的对外访问 |
| LoadBalancer | 集群外部 | 分配独立负载均衡 IP | 生产环境对外服务 |
| ExternalName | DNS 层 | 返回 CNAME,无代理 | 把外部服务“伪装”成内部服务 |
| Headless | 无 ClusterIP | 直连后端 Pod,不做负载均衡 | 数据库主从、StatefulSet |
其中 NodePort 和 LoadBalancer 是 Kubernetes 官方给出的两种把 Service 暴露到外部 IP 的实现方式。
二、ClusterIP:集群内部访问
ClusterIP 是默认类型,通过集群内部 IP 公开 Service,只能在集群内部访问。
要点:
- ClusterIP 从集群预留的地址池(
service-cluster-ip-range)中分配一个 IP; - 其他所有 Service 类型(NodePort、LoadBalancer)都是基于 ClusterIP 构建的;
- 可以手动指定
.spec.clusterIP,但必须落在合法的 CIDR 范围内,非法值会被 API Server 拒绝(HTTP 422); - 如果把
clusterIP设为"None",就变成无头服务(Headless),详见后文。
kubectl create service clusterip web --tcp=8080:80
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.103.19.150 <none> 8080/TCP 6m58s
集群内的 Pod 可以通过 web、web.命名空间 或 ClusterIP 访问它。跨 Namespace 访问时使用 <svc名>.<命名空间名>。
三、NodePort:通过节点端口访问
如果把 type 改成 NodePort,Kubernetes 控制平面会在 --service-node-port-range 指定的范围内分配端口,默认范围是 30000-32767。每个节点都会监听这个端口,把流量代理到 Service,再从 Service 转发到后端 Pod。
kubectl expose deployment web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-NodePort.yaml
kubectl apply -f service-NodePort.yaml
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.110.40.0 <none> 8080:31917/TCP 48s
这里 PORT(S) 显示为 8080:31917:
8080是 ClusterIP 上监听的端口;31917是节点上监听的端口(NodePort);EXTERNAL-IP显示为nodes,意味着集群中任意节点的 IP + 31917 都可以访问。
curl http://10.1.8.30:31917
curl http://10.1.8.31:31917
curl http://10.1.8.32:31917
看穿 NodePort 的底层规则
与 ClusterIP 相比,每个节点的 iptables 里会额外多出两条规则:
iptables-save | grep 31917
-A KUBE-NODEPORTS -p tcp -m comment --comment "shaka/web:http" -m tcp --dport 31917 -j KUBE-SVC-WE5D4GWSX3MMPCHH
含义:访问当前节点 31917 端口的请求,会跳转到 Service 的负载均衡链 KUBE-SVC-...。再往下追,就是负载均衡到各个 Pod 的规则:
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LUIIA2GDKT4VDBA2
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m statistic --mode random --probability 1.00000000000 -j KUBE-SEP-Y6B6PNSXFIBR4D2K
所以 NodePort 的本质是:节点端口 → iptables 规则 → Service 负载均衡 → Pod。
固定 NodePort 端口
NodePort 默认是随机分配,也可以在 YAML 里指定:
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web
spec:
ports:
- port: 8080
protocol: TCP
nodePort: 30080
targetPort: 80
selector:
app: web
type: NodePort
三个端口的关系一定要记牢:
| 字段 | 谁监听 | 作用 |
|---|---|---|
nodePort | 每个节点 | 外部访问入口 |
port | ClusterIP | Service 自己的端口 |
targetPort | Pod | 后端容器端口 |
四、LoadBalancer:借助 MetalLB 实现负载均衡
Kubernetes 本身没有实现 LoadBalancer,它需要借助第三方工具。公有云环境由云厂商提供,自建集群常用 MetalLB。创建 LoadBalancer 类型的 Service 很简单,把 type 改成 LoadBalancer 即可,剩下的交给 MetalLB 分配一个独立 IP。
1. 部署 MetalLB
wget http://192.168.42.200/course-materials/softwares/stage03/metallb-0.14.8.tar.gz
tar -xf metallb-0.14.8.tar.gz
# 查看镜像并替换为自己的镜像仓库(这里示例改为 hub.shaka.cn)
grep image metallb-0.14.8/config/manifests/metallb-native.yaml
sed -i 's/quay.io/hub.shaka.cn/g' metallb-0.14.8/config/manifests/metallb-native.yaml
kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
kubectl get all -n metallb-system
等 controller 和每台节点的 speaker Pod 都 Running 后,继续配置。
2. 配置 IP 地址池
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.40-10.1.8.80
kubectl apply -f ippool.yaml
3. 配置 L2 广播
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
kubectl apply -f L2.yaml
4. 创建 LoadBalancer 类型 Service 并测试
kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml
kubectl apply -f service-LoadBalancer.yaml
kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.109.101.67 10.1.8.40 80:32101/TCP 5s
MetalLB 从地址池中分配了 10.1.8.40。注意访问时用的是 Service 的 80 端口,不是 32101:
curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html>
5. MetalLB 的转发原理
客户端流量到达 Pod 的完整路径:
客户端 → MetalLB 虚拟 IP(LB IP)→ 节点 kube-proxy → Service 转发 → 后端 Pod
关键结论:
- MetalLB 只做 ARP 宣告 + IP 占坑,让流量进入集群某个节点;
- 真正的转发完全靠 kube-proxy + Service 完成;
- MetalLB + Service 就是自建 K8s 的标准负载均衡流程。
具体步骤:
- 客户端访问 MetalLB 分配的 LB VIP;
- MetalLB 使用 Layer2 模式通过 ARP 广播声明 VIP 归属,流量进入集群任意节点;
- 节点识别目标地址为 Service LB IP,交给内核网络框架;
- kube-proxy 匹配 iptables/IPVS 规则,确定流量所属 Service;
- Service 从后端 Endpoints 中按策略选择一个健康 Pod;
- 若 Pod 不在当前节点,流量通过 Calico/Flannel 等 CNI 网络转发到目标节点;
- 目标节点通过 veth-pair 把流量送入 Pod 网络命名空间;
- Pod 中的业务容器接收请求并响应。
五、ExternalName:DNS 层的“改名服务”
ExternalName 很特殊:它把 Service 映射到 externalName 字段指定的外部域名,集群 DNS 返回的是 CNAME 记录,而不是 A 记录。它不创建任何代理,请求最终直接打到外部域名。
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: database.example.com
当集群内查找 my-service.prod.svc.cluster.local 时,CoreDNS 返回 CNAME:database.example.com。访问方式和访问普通 Service 一样,只是“重定向”发生在 DNS 层,而不是通过代理或转发。
适用场景:把集群外部的托管数据库、SaaS API 等包装成集群内的服务名,业务代码不用关心服务到底部署在哪里。
六、Headless Service:不要 IP,直连 Pod
有些场景不需要负载均衡,也不需要单独的 Service IP——把 clusterIP 显式设为 None 即可创建无头服务(Headless Service)。
Headless Service 的特点:
- 不分配 ClusterIP;
- kube-proxy 不处理这类 Service,不提供负载均衡和路由;
- DNS 的配置方式取决于是否定义了 selector。
带 selector 的 Headless Service:控制平面会创建 EndpointSlice,DNS 直接返回后端 Pod 的 A/AAAA 记录。客户端拿到的是所有 Pod 的真实 IP,适合需要自己控制连接对象的场景(如 StatefulSet、数据库集群)。
不带 selector 的 Headless Service:控制平面不会创建 EndpointSlice,DNS 会:
- 对
type: ExternalName,配置 CNAME 记录; - 对其他类型,针对所有就绪端点配置 A/AAAA 记录。
注意:定义不带 selector 的 Headless Service 时,
port必须与targetPort匹配。
七、会话保持:让同一个客户端始终访问同一个 Pod
默认情况下,Service 对每次请求做负载均衡,同一个客户端的不同请求可能落在不同 Pod 上。对有状态应用(Java Web、WebSocket、游戏服务),这可能造成登录状态丢失、购物车丢失、长连接不稳定。
解决办法是设置会话亲和性(Session Affinity):
kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'
工作原理:
- 客户端首次访问,Service 把请求转发给某个 Pod,并记录客户端 IP 与 Pod 的对应关系;
- 后续来自同一客户端 IP 的请求,继续转发给同一个 Pod。
可以通过 .spec.sessionAffinityConfig.clientIP.timeoutSeconds 设置最大会话粘性时间,默认 10800 秒(3 小时)。
实测效果:20 次访问全部落在同一个 Pod:
# 未设置:两个 Pod 各约 10 次
10 web-6db76cb4fc-5gbx2
10 web-6db76cb4fc-b8f7l
# 设置 ClientIP 后:20 次全部落在同一个 Pod
20 web-6db76cb4fc-5gbx2
会话保持与 kube-proxy IPVS 的关系
如果 kube-proxy 使用 IPVS 模式,两个配置的优先级如下:
- Service 里的
sessionAffinity优先级最高:配置ClientIP后,kube-proxy 自动使用 IPVS 的 SH(Source Hashing,源地址哈希) 算法,保证同一 IP 哈希到同一 Pod; - Service 不配置 sessionAffinity 时,kube-proxy 使用 ipvs scheduler 中设置的算法,如
rr(轮询)、lc(最少连接)、wrr(加权轮询)。
八、金丝雀发布:用 Service 的标签选择器控制流量
金丝雀发布(Canary Deployment)的思路是:新版本先部署少量副本,接收一部分真实流量,验证没问题后再逐步扩大比例。
实现原理非常巧妙:Service 只按标签选后端,而两个 Deployment 的 Pod 拥有共同的标签(app=web, tier=frontend),只是用 track 标签区分版本。这样 Service 不用改,新旧版本各占多少流量,只取决于各自的副本数。
1. 准备版本内容
mkdir web
echo "hello nginx 1.28" > web/index28.html
echo "hello nginx 1.29" > web/index29.html
kubectl create configmap web --from-file=./web
2. 部署稳定版(track: stable)
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web-28
spec:
replicas: 10
selector:
matchLabels:
app: web
tier: frontend
track: stable
template:
metadata:
labels:
app: web
tier: frontend
track: stable
spec:
containers:
- image: hub.shaka.cn/library/nginx:1.28
name: nginx
ports:
- containerPort: 80
volumeMounts:
- name: webcontent
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
configMap:
name: web
items:
- key: index28.html
path: index.html
kubectl apply -f webapp-1.28.yaml
3. 创建 Service
Service 只选择 app=web, tier=frontend,不区分 track:
apiVersion: v1
kind: Service
metadata:
labels:
app: web
name: web
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: web
tier: frontend
kubectl apply -f webapp-svc.yaml
4. 部署金丝雀版(track: canary)
把镜像换成 1.29,track: canary,副本数先设为 1:
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web-29
spec:
replicas: 1
selector:
matchLabels:
app: web
tier: frontend
track: canary
template:
metadata:
labels:
app: web
tier: frontend
track: canary
spec:
containers:
- image: hub.shaka.cn/library/nginx:1.29
name: nginx
ports:
- containerPort: 80
volumeMounts:
- name: webcontent
mountPath: "/usr/share/nginx/html"
volumes:
- name: webcontent
configMap:
name: web
items:
- key: index29.html
path: index.html
kubectl apply -f webapp-1.29.yaml
5. 用副本数控制流量比例
稳定版 10 副本、金丝雀 1 副本时,请求几乎都到旧版本:
kubectl scale deployment web-28 --replicas 8
kubectl scale deployment web-29 --replicas 2
for i in {1..50}; do curl -s 10.98.235.36; done | sort -n | uniq -c
39 hello nginx 1.28
11 hello nginx 1.29
验证没问题后,逐步把流量切到新版本:
kubectl scale deployment web-28 --replicas 6
kubectl scale deployment web-29 --replicas 4
for i in {1..50}; do curl -s 10.98.235.36; done | sort -n | uniq -c
31 hello nginx 1.28
19 hello nginx 1.29
新版本的流量占比 = 新版本副本数 / 总副本数。整个过程 Service 和客户端都不用改,流量比例完全由副本数控制,这就是金丝雀发布最优雅的地方。
九、总结与选型建议
| 需求 | 推荐类型 |
|---|---|
| 集群内微服务互调 | ClusterIP |
| 临时对外测试 | NodePort |
| 自建集群生产环境对外服务 | LoadBalancer(MetalLB)+ 域名/Ingress |
| 对接集群外部的已有服务 | ExternalName |
| 数据库等需要直连每个 Pod | Headless |
| 有状态应用需要稳定会话 | ClusterIP/LoadBalancer + sessionAffinity |
| 新版本灰度上线 | Service + 多 Deployment 金丝雀发布 |
一句话总结:ClusterIP 是基础,NodePort 和 LoadBalancer 负责把服务送出去,ExternalName 和 Headless 是两个“非常规但很实用”的特型,而会话保持和金丝雀发布,则是在 Service 之上最常见的两个生产玩法。
更多推荐



所有评论(0)