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生产环境对外服务
ExternalNameDNS 层返回 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 可以通过 webweb.命名空间 或 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每个节点外部访问入口
portClusterIPService 自己的端口
targetPortPod后端容器端口

四、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 的标准负载均衡流程。

具体步骤:

  1. 客户端访问 MetalLB 分配的 LB VIP;
  2. MetalLB 使用 Layer2 模式通过 ARP 广播声明 VIP 归属,流量进入集群任意节点;
  3. 节点识别目标地址为 Service LB IP,交给内核网络框架;
  4. kube-proxy 匹配 iptables/IPVS 规则,确定流量所属 Service;
  5. Service 从后端 Endpoints 中按策略选择一个健康 Pod;
  6. 若 Pod 不在当前节点,流量通过 Calico/Flannel 等 CNI 网络转发到目标节点;
  7. 目标节点通过 veth-pair 把流量送入 Pod 网络命名空间;
  8. 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"}}'

工作原理:

  1. 客户端首次访问,Service 把请求转发给某个 Pod,并记录客户端 IP 与 Pod 的对应关系;
  2. 后续来自同一客户端 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 模式,两个配置的优先级如下:

  1. Service 里的 sessionAffinity 优先级最高:配置 ClientIP 后,kube-proxy 自动使用 IPVS 的 SH(Source Hashing,源地址哈希) 算法,保证同一 IP 哈希到同一 Pod;
  2. 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
数据库等需要直连每个 PodHeadless
有状态应用需要稳定会话ClusterIP/LoadBalancer + sessionAffinity
新版本灰度上线Service + 多 Deployment 金丝雀发布

一句话总结:ClusterIP 是基础,NodePort 和 LoadBalancer 负责把服务送出去,ExternalName 和 Headless 是两个“非常规但很实用”的特型,而会话保持和金丝雀发布,则是在 Service 之上最常见的两个生产玩法。

更多推荐