Kubernetes Service

摘要:本文全面介绍了 Kubernetes Service 的核心概念与实践应用。首先概述了 Service 的四种类型:ClusterIP(集群内部访问)、NodePort(节点端口暴露)、LoadBalancer(负载均衡器)以及 ExternalName 和 Headless Services。随后详细讲解了 Service 会话保持(Session Affinity)的配置原理及其与 kube-proxy IPVS 的关系。最后通过金丝雀发布(Canary Deployment)的完整示例,演示了如何实现渐进式应用更新。文章包含丰富的命令行操作、YAML 配置及 Mermaid 流程图,适合作为 Kubernetes 网络与服务发现的实践指南。

学习参考:Service

环境准备

root@worker30 ~ 10:03:42# kubectl create ns services

root@master30 ~ 09:49:39# kubectl config set-context --current --namespace services
Context "kubernetes-admin@kubernetes" modified.

Service 类型

Kubernetes Service 支持以下四种类型:

  • ClusterIP:只能在集群内部访问。

  • NodePort:通过物理节点的端口来访问,每个物理节点都提供相同的端口。

  • LoadBalancer:负载均衡,来源于物理网段中一个独立的IP。

  • ExternalName:配置集群内部的CName。

  • Headless:只有服务名,不分配IP地址。

我们的应用可能希望将 Service 暴露在一个外部 IP 地址上。 Kubernetes 支持两种实现方式:NodePort 和 LoadBalancer。

环境准备

创建 deployment

root@master30 ~ 10:13:26# kubectl create deployment web --image=hub.laoma.cloud/library/httpd --replicas=2

ClusterIP

ClusterIP,是通过集群的内部 IP 公开 Service,选择该值时 Service 只能够在集群内部访问。 这也是服务类型的默认值。

  • ClusterIP 从集群中预留的 IP 地址池中分配一个 IP 地址。其他几种 Service 类型在 ClusterIP 类型的基础上进行构建。

  • 在创建 Service 的请求中,可以通过设置 .spec.clusterIP 字段来指定自己的集群 IP 地址。

    如果将 Service 的 .spec.clusterIP 设置为 "None",则 Kubernetes 不会为其分配 IP 地址。

  • 所选择的 IP 地址必须是合法的 IPv4 或者 IPv6 地址,并且这个 IP 地址在 API 服务器上所配置的 service-cluster-ip-range CIDR 范围内。 如果你尝试创建一个带有非法 clusterIP 地址值的 Service,API 服务器会返回 HTTP 状态码 422, 表示值不合法。

其他信息参考上面的 发现 Service 章节。

NodePort

  1. 如果将Service 的 type 字段设置为 NodePort,则 Kubernetes 控制平面将在 --service-node-port-range 标志所指定的范围内分配端口(默认值:30000-32767)。 Service 在其 .spec.ports[*].nodePort 字段中报告已分配的端口。

  2. NodePort 类型的 Service 通过每个节点上的 IP 和分配的端口(NodePort)公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 NodePort 类型的Service 配置 clusterIP 地址。每个节点将该端口(每个节点上的相同端口号)上的流量代理到 Service。

示例:

root@master30 ~ 10:13:28# kubectl expose deployment web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-NodePort.yaml

root@master30:~# vim service-NodePort.yaml
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  ports:
  - port: 8080
    protocol: TCP
    targetPort: 80
  selector:
    app: web
  type: NodePort
status:
  loadBalancer: {}
# 创建NodePort类型Service
root@master30:~# kubectl apply -f service-NodePort.yaml

# 查看node节点对应端口为31917
root@master30 ~ 10:14:41# kubectl get service
NAME   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
web    NodePort   10.104.229.35   <none>        8080:31667/TCP   4s


# 说明:
# 1-EXTERNAL-IP为nodes, 表示可通过Cluster每个节点自身的IP访问Service。
# 2-PORT(S)为8080:31917。 8080是ClusterIP监听的端口,31917则是节点上监听的端口。
#   Kubernetes会从30000~32767中分配一个可用的端口,每个节点都会监听此端口并将请求转发给Service

# 访问集群中任一节点测试
root@client:~# curl http://10.1.8.30:31667
root@client:~# curl http://10.1.8.31:31667
root@client:~# curl http://10.1.8.32:31667

分析防火墙规则:

# 与ClusterIP对比,每个节点的iptables中额外增加了下面两条规则:
root@master30 ~ 10:20:04# iptables-save | grep 31667
-A KUBE-NODEPORTS -p tcp -m comment --comment "services/web" -m tcp --dport 31917 -j KUBE-SVC-WE5D4GWSX3MMPCHH
# 规则的含义是:访问当前节点31917端口的请求会应用规则KUBE-SVC-WE5D4GWSX3MMPCHH

# 进一步分析,其作用就是负载均衡到每一个Pod。
root@master30:~# iptables-save |grep KUBE-SVC-WE5D4GWSX3MMPCHH
-A KUBE-SERVICES -d 10.110.40.0/32 -p tcp -m comment --comment "laoma/web:http cluster IP" -m tcp --dport 8080 -j KUBE-SVC-WE5D4GWSX3MMPCHH
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "services/web" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LUIIA2GDKT4VDBA2
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "services/web" -j KUBE-SEP-Y6B6PNSXFIBR4D2K

NodePort默认的是随机选择, 我们可以使用nodePort指定为特定端口。

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  ports:
  - port: 8080
    protocol: TCP
    # 指定节点固定端口
    nodePort: 30080
    targetPort: 80
  selector:
    app: web
  type: NodePort
status:
  loadBalancer: {}

端口说明:

  • nodePort是节点上监听的端口。
  • port是ClusterIP上监听的端口。
  • targetPort是Pod监听的端口。

清理环境

root@master30 ~ 10:20:32# kubectl delete svc web 

LoadBalancer

kubernetes并没有真正实现 LoadBalancer,需要借助第三方工具实现,例如metallb。

每个LoadBalancer类型的service需要关联一个公网IP。创建LoadBalancer类型的service只需要将Service 的 Type 改成 LoadBalancer。

这里我们使用 metallb。

官方地址: https://metallb.universe.tf/

github地址:https://github.com/metallb/metallb

部署

部署 metallb

root@master30 ~ 11:14:48# wget http://192.168.46.200/class/course-materials/softwares/stage03/metallb-0.14.8.tar.gz

root@master30 ~ 11:16:12# tar -xf metallb-0.14.8.tar.gz

# 查看镜像
root@master30:~# grep image metallb-0.14.8/config/manifests/metallb-native.yaml
        image: quay.io/metallb/controller:v0.14.8
        image: quay.io/metallb/speaker:v0.14.8
# 按需修改镜像
root@master30:~# sed -i 's/quay.io/hub.laoma.cloud/g' metallb-0.14.8/config/manifests/metallb-native.yaml

root@master30 ~ 11:16:59# kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml

# 等待着所有pod正常运行再进行下一步
root@master30 ~ 11:17:45# kubectl get all -n metallb-system 
NAME                             READY   STATUS    RESTARTS   AGE
pod/controller-d6499775f-kdmvt   1/1     Running   0          40s
pod/speaker-gvlm4                1/1     Running   0          40s
pod/speaker-vkws9                1/1     Running   0          40s
pod/speaker-wv6rg                1/1     Running   0          40s

NAME                              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/metallb-webhook-service   ClusterIP   10.103.135.65   <none>        443/TCP   40s

NAME                     DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/speaker   3         3         3       3            3           kubernetes.io/os=linux   40s

NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/controller   1/1     1            1           40s

NAME                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/controller-d6499775f   1         1         1       40s

配置地址池

root@master30 ~ 11:17:46# cat << 'EOF' > ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
  name: first-pool
  namespace: metallb-system
spec:
  addresses:
  - 10.1.8.40-10.1.8.80
EOF

root@master30 ~ 11:17:55# kubectl apply -f ippool.yaml

配置 lay2

root@master30 ~ 11:17:59# cat << 'EOF' > L2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
EOF

root@master30 ~ 11:18:06# kubectl apply -f L2.yaml

测试
root@master30 ~ 11:18:10# kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml 
root@master30 ~ 11:18:16# cat service-LoadBalancer.yaml 
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: web
  type: LoadBalancer
status:
  loadBalancer: {}
root@master30 ~ 11:18:24# kubectl apply -f service-LoadBalancer.yaml
service/web created

root@master30 ~ 11:18:32# kubectl get service
NAME   TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
web    LoadBalancer   10.107.58.174   10.1.8.40     80:32620/TCP   5s


# 访问测试,端口号使用service的80,非32101端口
root@master30 ~ 11:18:37# curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html>

总结
  • 客户端流量到达pod路径:客户端请求 → MetalLB 虚拟 IP(LB IP) → 节点 kube-proxy → Service 转发 → 后端 Pod。
  • MetalLB 只做 ARP 宣告 + IP 占坑,转发全靠 kube-proxy + Service
  • MetalLB + Service 是标准 K8s 负载均衡流程。
详细流程图

访问 LB VIP:10.1.8.40

客户端用户

ARP 寻址 → 流量进入集群任意节点

节点内核拦截 Service IP 流量

kube-proxy 处理 iptables/IPVS 规则

Service 负载均衡:选择一个健康 Pod

通过 CNI 网络转发到 Pod 所在节点

流量进入 Pod 网络命名空间

目标 Pod 接收并响应请求

  1. 客户端发起请求:用户通过浏览器 / 应用访问 MetalLB 分配的 LB VIP(如 10.1.8.200)。
  2. ARP 寻址,流量进入集群节点:MetalLB 使用 Layer2 模式,通过 ARP 广播声明 VIP 归属,流量进入集群任意一个节点
  3. **节点内核拦截流量:**节点识别目标地址为 Service LB IP,将流量交给内核网络框架处理。
  4. **kube-proxy 执行转发规则:**kube-proxy 匹配 iptables/IPVS 规则,确定流量所属 Service。
  5. **Service 负载均衡选择 Pod:**从 Service 后端 endpoints 中,按策略选择一个健康 Pod。
  6. **CNI 网络跨节点转发:**若 Pod 不在当前节点,流量通过 Calico/Flannel 等 CNI 网络转发到目标节点。
  7. **流量进入 Pod:**目标节点通过 veth-pair 设备,将流量送入 Pod 网络命名空间。
  8. **Pod 响应请求:**业务容器接收请求并返回数据,原路响应给客户端。

ExternalName

ExternalName,将服务映射到 externalName 字段的内容(例如,api.foo.bar.example)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。

例如,将 prod 命名空间中的 my-service 服务映射到 database.example.com

apiVersion: v1
kind: Service
metadata:
  name: my-service
  namespace: prod
spec:
  type: ExternalName
  externalName: database.example.com

当查找主机 my-service.prod.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 database.example.com。访问 my-service 的方式与访问其他 Service 的方式相同, 主要区别在于重定向发生在 DNS 级别,而不是通过代理或转发来完成。

Headless Services

有时并不需要负载均衡,也不需要单独的 Service IP。遇到这种情况,可以通过显式设置ClusterIP的值为 None 来创建无头服务(Headless Service)

无头 Service 不会获得集群 IP,kube-proxy 不会处理这类 Service, 而且平台也不会为它们提供负载均衡或路由支持。

取决于 Service 是否定义了选择算符,DNS 会以不同的方式被自动配置。

  • 带选择算符的服务,对定义了选择算符的无头 Service,Kubernetes 控制平面在 Kubernetes API 中创建 EndpointSlice 对象,并且修改 DNS 配置返回 A 或 AAAA 记录(IPv4 或 IPv6 地址), 这些记录直接指向 Service 的后端 Pod 集合。

  • 无选择算符的服务,对没有定义选择算符的无头 Service,控制平面不会创建 EndpointSlice 对象。 然而 DNS 系统会执行以下操作之一:

    • 对于 type: ExternalName Service,查找和配置其 CNAME 记录;

    • 对所有其他类型的 Service,针对 Service 的就绪端点的所有 IP 地址,查找和配置 DNS A/AAAA 记录:对于 IPv4 端点,DNS 系统创建 A 记录;对于 IPv6 端点,DNS 系统创建 AAAA 记录。

      当你定义无选择算符的无头 Service 时,port 必须与 targetPort 匹配。

Service 会话保持

会话保持介绍

如果要确保来自特定客户端的连接每次都传递给同一个 Pod, 你可以通过设置 Service 的 .spec.sessionAffinityClientIP 来设置基于客户端 IP 地址的会话亲和性(默认为 None)。

你还可以通过设置 Service 的 .spec.sessionAffinityConfig.clientIP.timeoutSeconds 来设置最大会话粘性时间(默认值为 10800,即 3 小时)。

工作原理:

  1. 客户端首次访问服务,Service将请求转发给某个Pod,Service记录客户端和Pod的对应关系。
  2. 客户端的后续请求继续转发给同一个Pod。

适合有状态服务(如:Java Web、WebSocket、游戏服务):

  • 登录状态不丢失
  • 购物车不丢失
  • 长连接稳定

准备测试环境

root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/httpd --replicas=2
root@master30:~# kubectl expose deployment web --port 80
root@master30:~# for pod in $(kubectl get pods -o name | awk -F/  '{print $2}'); do    kubectl exec -it $pod -- bash -c "echo $pod > htdocs/index.html"; done

root@master30:~# kubectl get svc web
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
web    ClusterIP   10.111.90.183   <none>        80/TCP    9m25s

root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
     10 web-6db76cb4fc-5gbx2
     10 web-6db76cb4fc-b8f7l

设置会话保持

root@master30:~# kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'

# 再次访问:结果保持一致
root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
     20 web-6db76cb4fc-5gbx2

会话保持与 kube-proxy IPVS 的关系

1. service 里的 sessionAffinity 优先级最高

service 配置 ClientIPkube-proxy 自动使用 IPVS 的 SH 算法

  • SH = Source Hashing 源地址哈希
  • 保证同一 IP → 同一 Pod
2. 如果 service 不配置 sessionAffinity

kube-proxy 就用ipvs scheduler 里设置的算法:

  • rr(轮询)
  • lc(最少连接)
  • wrr(加权轮询)

金丝雀发布

学习参考:金丝雀部署

环境准备:

root@master30:~# mkdir web
root@master30:~# echo hello nginx 1.28 > web/index28.html
root@master30:~# echo hello nginx 1.29 > web/index29.html
root@master30:~# kubectl create configmap web --from-file=./web
root@master30:~# kubectl get configmaps web -o yaml |grep ^data -A4
data:
index28.html: |
hello nginx 1.28
index29.html: |
hello nginx 1.29

使用金丝雀发布部署应用新版本 ,同时保留用旧版本。 这样,新版本在完全发布之前也可以接收实时的生产流量。

例如,你可以使用 track 标签来区分不同的版本。

  1. 主要稳定的发行版将有一个 track 标签,其值为 stable

    root@master30:~# vim webapp-1.28.yaml
    
    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.laoma.cloud/library/nginx:1.28
            name: nginx
            imagePullPolicy: IfNotPresent
            ports:
            - containerPort: 80
            volumeMounts:
            - name: webcontent
              mountPath: "/usr/share/nginx/html"
          volumes:
          - name: webcontent
            configMap:
              name: web
              items:
                - key: index28.html
                  path: index.html
    
    root@master30:~# kubectl apply -f webapp-1.28.yaml
    
  2. 创建 service

    root@master30:~# vim webapp-svc.yaml
    
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: web
      name: web
    spec:
      ports:
      - port: 80
        protocol: TCP
        targetPort: 80
      selector:
        app: web
        tier: frontend
    
    root@master30:~# kubectl apply -f webapp-svc.yaml
    root@master30:~# kubectl get svc
    NAME   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
    web    ClusterIP   10.98.235.36   <none>        80/TCP    5m31s
    
  3. 部署应用新版本。新的发行版将有一个 track 标签,其值为 canary

    root@master30:~# vim webapp-1.29.yaml
    
    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.laoma.cloud/library/nginx:1.29
            name: nginx
            imagePullPolicy: IfNotPresent
            ports:
            - containerPort: 80
            volumeMounts:
            - name: webcontent
              mountPath: "/usr/share/nginx/html"
          volumes:
          - name: webcontent
            configMap:
              name: web
              items:
                - key: index29.html
                  path: index.html
    
    root@master30:~# kubectl apply -f webapp-1.29.yaml
    

    验证访问比例:

    root@master30:~# kubectl scale deployment web-28 --replicas 8
    root@master30:~# kubectl scale deployment web-29 --replicas 2
    root@master30:~# 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
    
  4. 总pod数量不变的情况下,逐步减少旧版本和增加新版本副本数量,。

    root@master30:~# kubectl scale deployment web-28 --replicas 6
    root@master30:~# kubectl scale deployment web-29 --replicas 4
    root@master30:~# 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
    

环境清理

root@master30:~# kubectl delete ns services

更多推荐