Kubernetes Service

学习参考:Service

环境准备

 [root@master30 ~ 11:41:46]# kubectl  create  ns services
 namespace/services created
 [root@master30 ~ 11:51:50]# kubectl  config  set-context  --current  --namespace  services
 Context "cluster1-context" modified.
 ​

Service 介绍

Service 产生的背景与核心定位

在 Kubernetes 中,Pod 是应用运行的最小计算单元,但 Pod 本身具有临时性与动态性

  • Deployment 执行滚动更新、扩缩容时,旧 Pod 会被销毁、新 Pod 会被创建,Pod IP 会发生变化;

  • 节点故障、资源驱逐时,Pod 会漂移到其他节点重建,IP 同样会改变。

如果前端业务直接通过 Pod IP 访问后端服务,后端 Pod 的频繁变动会导致前端配置持续修改,业务无法稳定运行。Service 就是为了解决这一问题而设计的网络抽象层

Service 的核心作用: 为一组提供相同服务的 Pod 提供固定且唯一的访问入口,并自动维护后端可用 Pod 的端点列表,实现请求的负载分发。客户端只需访问 Service 的固定地址,无需感知后端 Pod 的数量、IP 变化,实现了服务调用方与服务提供方的解耦。

Service 的核心工作机制

Service 通过标签选择器(Label Selector) 与后端 Pod 建立关联,整体工作逻辑如下:

  1. Service 在配置中通过 spec.selector 定义一组标签规则;

  2. 系统自动筛选集群中所有标签与规则匹配的 Pod,收集其 IP 与端口,生成 Endpoints(端点列表);

  3. 当有请求访问 Service 地址时,系统会从 Endpoints 中选择一个健康的后端 Pod,将流量转发过去。

这种机制的核心特点:

  • 松耦合:Service 与 Pod 没有直接绑定关系,仅通过标签匹配;

  • 自动更新:当后端 Pod 新增、删除、状态变化时,Endpoints 列表会自动同步更新;

  • 透明无感知:后端 Pod 的变动对调用方完全透明,调用方始终访问同一个 Service 地址。

Service 核心字段解析

以最基础的 ClusterIP 类型 Service 为例,核心配置字段如下:

字段 含义 说明
spec.type Service 类型 默认为 ClusterIP,分配集群内部可用的固定 IP,仅集群内部可访问;此外还有 NodePort、LoadBalancer 等扩展类型。
spec.ports[].port Service 监听端口 Service 自身对外提供服务的端口,客户端通过该端口访问服务。
spec.ports[].targetPort 后端 Pod 端口 后端容器实际监听的端口,Service 会将请求转发到 Pod 的该端口。
spec.selector 标签选择器 用于匹配后端 Pod 的标签规则,只有标签完全匹配的 Pod 才会被纳入后端端点列表。
ClusterIP 集群虚拟 IP Service 的固定访问地址,在 Service 生命周期内保持不变。
Endpoints 后端端点列表 系统自动维护的后端 Pod IP + 端口集合,与标签选择器匹配的 Pod 会自动加入该列表。

Service 基本管理

1. 准备后端工作负载

首先通过 Deployment 创建一组后端 Pod,作为 Service 的服务提供方。

# 创建包含 3 个 httpd 副本的 Deployment
[root@master30 ~ 13:55:06]# kubectl  create deployment web --image=docker.io/library/httpd --replicas=3
deployment.apps/web created

# 查看 Pod 信息,可看到所有 Pod 均带有 `app=web` 标签
[root@master30 ~ 13:55:52]# kubectl  get pods --show-labels 
NAME                   READY   STATUS    RESTARTS   AGE   LABELS
web-6db76cb4fc-ndn77   1/1     Running   0          21s   app=web,pod-template-hash=6db76cb4fc
web-6db76cb4fc-q49cj   1/1     Running   0          21s   app=web,pod-template-hash=6db76cb4fc
web-6db76cb4fc-vfhmp   1/1     Running   0          21s   app=web,pod-template-hash=6db76cb4fc


Deployment 会保证始终有 3 个 Pod 运行,且所有 Pod 统一打上 app=web 标签,为后续 Service 匹配提供依据。

2. 创建 Service 并验证访问

通过命令行创建 ClusterIP 类型的 Service,关联上述 Pod。

# 创建 Service,监听 8080 端口,转发至后端 Pod 的 80 端口
[root@master30 ~ 14:01:35]# kubectl  create  service clusterip web --tcp=8080:80
service/web created

# 查看 Service 基本信息
[root@master30 ~ 14:01:43]# kubectl  get svc
NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
web    ClusterIP   10.110.92.161   <none>        8080/TCP   19s

## 选项--tcp=8080:80代表访问集群ip的8080/TCP,将转发给pod的80端口
## svc默认标签是:app=<svc-name>

# 查看Service详细信息
[root@master30 ~ 14:02:03]# kubectl  describe  service web
Name:              web
Namespace:         services
Labels:            app=web
Annotations:       <none>
Selector:          app=web     #- Selector: app=web:标签选择规则,与 Pod 标签对应;
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.110.92.161
IPs:               10.110.92.161
Port:              8080-80  8080/TCP
TargetPort:        80/TCP                
Endpoints:         10.224.170.26:80,10.224.170.31:80,10.224.215.147:80     #- Endpoints:自动生成的后端 Pod IP 列表,包含 3 个 Pod 的 IP:80。
Session Affinity:  None
Events:            <none>


[root@master30 ~ 14:02:32]# kubectl  describe service web | grep -e Endpoints -e IP:
IP:                10.110.92.161
Endpoints:         10.224.170.26:80,10.224.170.31:80,10.224.215.147:80

# 访问测试,访问service-ip对应的8080端口
[root@master30 ~ 14:04:54]# curl 10.110.92.161:8080
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
3. 验证负载均衡能力

Service 默认采用轮询方式将请求分发到后端多个 Pod,实现流量负载均衡。

通过修改每个 Pod 的首页内容,连续多次访问 Service 地址,统计各 Pod 的命中次数:

# 获取pod名称
[root@master30 ~ 14:05:18]# kubectl  get pods -o name | awk -F/ '{print $2}'
web-6db76cb4fc-ndn77
web-6db76cb4fc-q49cj
web-6db76cb4fc-vfhmp


# 更改每个pod主页,# 批量修改每个 Pod 的首页为自身 Pod 名称
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

# 验证效果,# 连续访问 60 次,统计各 Pod 被访问的次数
[root@master30 ~ 14:08:00]# for i in {1..60};do curl -s 10.110.92.161:8080;done|sort |uniq -c
     20 web-6db76cb4fc-ndn77
     20 web-6db76cb4fc-q49cj
     20 web-6db76cb4fc-vfhmp
    
统计结果会显示 3 个 Pod 的访问次数大致均等(我这个是实验了好几次选的最佳结果),验证了 Service 的负载分发能力。
4. 验证后端自动发现

Service 的端点列表会随符合标签规则的 Pod 增减自动更新。手动创建一个带有 app=web 标签的 Pod,观察其是否自动被 Service 纳管:

此时如果创建一个具有相同标签的pod,service也会将请求转发到该pod

## 创建一个独立 Pod,设置标签 app=web
[root@master30 ~ 14:08:03]# kubectl  run web --image=docker.io/library/httpd  --labels=app=web
pod/web created

[root@master30 ~ 14:09:52]# kubectl  exec -it web -- bash -c "echo zhangyi > htdocs/index.html"

## 再次统计访问分布
[root@master30 ~ 14:11:58]# for i in {1..80};do curl -s 10.110.92.161:8080;done|sort |uniq -c
     19 web-6db76cb4fc-ndn77
     22 web-6db76cb4fc-q49cj
     18 web-6db76cb4fc-vfhmp
     21 zhangyi

     
此时访问结果会包含新增的 Pod,说明 Service 自动将符合标签规则的新 Pod 加入后端列表,无需手动配置。

5. 验证 Pod 动态变化的无感知性

当后端 Pod 因更新、重启全部重建时,Service 地址保持不变,业务访问不受影响。

# 滚动重启 Deployment,所有旧 Pod 销毁并重建
[root@master30 ~ 14:12:01]# kubectl  rollout restart  deployment  web
deployment.apps/web restarted

# 持续访问 Service,业务不会中断
#为什么下面访问变成那样了,因为前面是你手动设置了页面的内容,现在pod更新,旧的被删除,新的重建,访问新的pod的原始页面就是这样的
[root@master30 ~ 14:16:19]# for i in {1..80};do curl -s 10.110.92.161:8080;done|sort |uniq -c
     56 </body>
     56 <body>
     56 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
     56 </head>
     56 <head>
     56 </html>
     56 <html>
     56 <p>It works!</p>
     56 <title>It works! Apache httpd</title>
[root@master30 ~ 14:16:23]# for i in {1..80};do curl -s 10.110.92.161:8080;done|sort |uniq -c
     80 </body>
     80 <body>
     80 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
     80 </head>
     80 <head>
     80 </html>
     80 <html>
     80 <p>It works!</p>
     80 <title>It works! Apache httpd</title>
[root@master30 ~ 14:16:25]# for i in {1..80};do curl -s 10.110.92.161:8080;done|sort |uniq -c
     80 </body>
     80 <body>
     80 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
     80 </head>
     80 <head>
     80 </html>
     80 <html>
     80 <p>It works!</p>
     80 <title>It works! Apache httpd</title>


时间轴:
14:16:09  → 执行 rollout restart,开始创建新 Pod
14:16:19  → 新 Pod 尚未完全就绪,旧 Pod 部分终止,造成 24 个请求失败
14:16:23  → 新 Pod 陆续就绪,旧 Pod 大部分终止,全部请求成功
14:16:25  → 所有新 Pod 就绪,完全稳定



该验证体现了 Service 最核心的价值:后端 Pod 生命周期的变化完全不影响前端调用,实现了服务访问的稳定性。结论:滚动重启正在顺利进行,新版本逐步替换旧版本,最终所有流量都指向新 Pod。

6. 验证选择器的独立性

Deployment 的标签选择器与 Service 的标签选择器相互独立,二者可以使用不同的标签规则,只要 Pod 同时携带两组标签即可。

例如:

  • Deployment 通过 app1=web1 管理 Pod 副本数量;

  • Service 通过 app2=web2 筛选后端转发目标;

  • Pod 同时配置 app1=web1app2=web2 两个标签,同时被两者匹配。

这种设计实现了副本管理与流量转发的职责分离,适用于复杂的业务场景。

如果创建的pod具有标签app1=web1和app2=web2,而deploy控制器的selector匹配的标签为app1=web1,service匹配的标签为app2=web2也是可以的。

# 清理环境,重建 Deployment和Service
[root@master30 ~ 14:21:55]# kubectl  delete  deployments.apps  web --force 
Warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
deployment.apps "web" force deleted
[root@master30 ~ 14:33:05]# kubectl  delete  service web
service "web" deleted
[root@master30 ~ 14:33:05]# vim deploy-web.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app1: web1
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app1: web1
        app2: web2
    spec:
      containers:
      - image: docker.io/library/httpd
        name: httpd
        imagePullPolicy: IfNotPresent
        resources: {}
status: {}
[root@master30 ~ 14:34:33]# kubectl  apply  -f deployment-web.yaml 
deployment.apps/web created

# 通过expose方式创建service
[root@master30 ~ 14:35:14]# kubectl  expose deployment web  --port=8080 --target-port=80 --selector=app2=web2
service/web exposed

# 选项说明:
# --port=8080,定义service监听的端口
# --target-port=80,定义后端pod鉴定的端口
# --selector=app2=web2,定义service选择器标签

[root@master30 ~ 14:36:35]# kubectl  describe  svc web  | grep -e IP: -e Endpoints
IP:                10.106.186.131
Endpoints:         10.224.170.18:80,10.224.215.150:80

[root@master30 ~ 14:37:21]# curl 10.106.186.131:8080
<html><body><h1>It works!</h1></body></html>
7.常见注意点:ClusterIP 无法 ping 通
一、这句话到底是什么意思

我们分两层把逻辑讲透,你就能完全理解这个现象:

  1. ClusterIP 不是一个 “真实存在” 的 IP

Service 的 ClusterIP 是虚拟 IP,它没有绑定在任何一台服务器的真实网卡上,也没有对应的操作系统网络栈去响应请求。 它的本质是:kube-proxy 组件在集群每个节点上,通过 iptablesipvs 生成了一系列转发规则 ——只有命中指定端口的 TCP/UDP 数据包,才会被规则转发到后端 Pod

  1. ping 和 curl 走的是完全不同的协议

  • ping 命令使用的是 ICMP 协议(网络层协议),它发的是 “回声请求包”,需要目标 IP 有真实网络栈来回应。 而 iptables 只给 ClusterIP 配置了 TCP/UDP 端口的转发规则,没有处理 ICMP 协议的规则,所以 ping 包发出去后没人响应,自然就 “不通”。

  • curl 命令使用的是 TCP 协议(传输层协议),它会和目标 IP 的指定端口建立连接。 这个 TCP 包正好命中了 iptables 里配置的转发规则,系统会把它转发到后端健康的 Pod 上,所以能正常拿到返回结果。

一句话总结:ClusterIP 是 “端口级别的虚拟入口”,不是 “主机级别的真实 IP”。它只处理你配置过的 TCP/UDP 端口,不响应 ICMP 的 ping 请求,这是正常设计,不是故障。

二、怎么验证这个结论

你可以按下面步骤一步步验证,对比现象就能直观理解。

  1. 验证现象:ping ClusterIP 确实不通

在 master 或任意节点上执行 ping 命令,目标是你的 Service ClusterIP:

ping 10.110.92.161

预期结果:一直超时、丢包,完全不通。 这就验证了「ICMP 协议无法到达 ClusterIP」。

  1. 验证现象:TCP 端口访问完全正常

就是你写的这条 curl 命令,它本身就是最好的验证:

for i in {1..60};do curl -s 10.110.92.161:8080;done

预期结果:60 次请求全部正常返回页面内容,没有报错。 这就验证了「TCP 端口的转发规则是生效的,Service 工作完全正常」。

这两个命令一对比,你就能清晰看到:不是 Service 不能用,只是 ping 这种方式不对

三:关键结论
  1. 永远不要用 ping 来判断 Service 是否正常,ping 不通是正常现象;

  2. 判断 Service 可用性,正确方式是访问对应端口(curl、nc、telnet 均可);

  3. 这个特性只针对 ClusterIP 类型;如果是 NodePort、LoadBalancer 类型,访问节点 IP / 公网 IP 是可以 ping 通的,因为那是真实的主机 IP。

yaml 文件创建

[root@master30 ~ 14:47:21]# kubectl  delete svc  web
service "web" deleted

# 获取Service资源yaml文件模版
[root@master30 ~ 14:54:35]# kubectl  create  service clusterip web --tcp=8080:80 -o yaml --dry-run=client > svc-web.yml
[root@master30 ~ 14:55:21]# cat svc-web.yml 
apiVersion: v1
kind: Service
metadata:
  labels:
    app: web
  name: web
spec:
  ports:
  - name: web-8080-8
    port: 8080          # Service 对外端口
    protocol: TCP       # 网络协议,默认为 TCP
    targetPort: 80      # 后端容器端口
  selector:
    app: web            # 标签选择器
  type: ClusterIP       # Service 类型

Service 发现

所谓发现 Service,是指集群内应用访问 Service。大白话讲:集群里跑着多个服务(比如网站 WordPress、数据库 MySQL),前端服务要找到后端服务的地址、才能连上它,这个 “找地址” 的过程就叫服务发现。

因为 Pod 是动态的 —— 删了重建 IP 就会变,所以我们不会直接记 Pod 的 IP,而是给一组 Pod 套一层 Service。Service 有固定不变的访问入口,服务之间找对方,都是找对方的 Service。 K8s 提供了 3 种找到 Service 地址的方式,从简单到好用依次是:硬编码 IP、环境变量、DNS 域名。

我们介绍以下三种方式发现 Service:

  1. 通过 IP 访问 Service。

  2. 通过环境变量访问 Service。

  3. 通过 dns 解析的名称访问 Service。

通过 IP 访问 Service

核心逻辑:你手动查出来 Service 的 ClusterIP(比如 MySQL 的 IP 是 10.111.69.45),直接把这个 IP 写到程序配置里,程序通过这个固定 IP 访问服务。

优缺点: 优点:逻辑最简单,一看就懂,临时测试很方便。 缺点:非常不灵活。如果删除重建 MySQL Service,IP 会变,WordPress 就会连不上数据库,必须改配置重启。服务数量多了根本维护不过来。 总结:能用,但生产环境没人这么干。

实验准备:mysql+wordpress

准备mysql资源

[root@master30 ~ 15:00:35]# kubectl run mysql --image=docker.io/library/mysql \
--image-pull-policy=IfNotPresent \
--env=MYSQL_ROOT_PASSWORD=redhat \
--env=MYSQL_USER=tom \
--env=MYSQL_PASSWORD=redhat \
--env=MYSQL_DATABASE=blog \
--dry-run=client -o yaml > pod-mysql.yaml

[root@master30 ~ 15:01:02]# kubectl  apply  -f pod-mysql.yaml 
pod/mysql created
[root@master30 ~ 15:03:09]# kubectl  expose  pod  mysql --port=3306 --target-port=3306
service/mysql exposed
[root@master30 ~ 15:03:47]# kubectl  get service
NAME    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
mysql   ClusterIP   10.102.42.65   <none>        3306/TCP   8s


[root@master30 ~ 15:03:55]# apt install -y mysql-client
[root@master30 ~ 15:04:20]# mysql  -u tom -predhat -h 10.102.42.65 
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 9
Server version: 9.6.0 MySQL Community Server - GPL

Copyright (c) 2000, 2026, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> exit
Bye

准备WordPress资源

[root@master30 ~ 15:05:24]# kubectl run wordpress \
--image=docker.io/library/wordpress \
--image-pull-policy=IfNotPresent \
--env=WORDPRESS_DB_USER=tom \
--env=WORDPRESS_DB_PASSWORD=redhat \
--env=WORDPRESS_DB_NAME=blog \
--env=WORDPRESS_DB_HOST=10.102.42.65
pod/wordpress created


# 为了测试方便,我们这里创建NodePort类型Service
[root@master30 ~ 15:17:18]# kubectl  expose  pod  wordpress  --port=80 --target-port=80 --type NodePort
service/wordpress exposed

[root@master30 ~ 15:17:32]# kubectl  get service
NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
mysql       ClusterIP   10.102.42.65   <none>        3306/TCP       13m
wordpress   NodePort    10.100.118.1   <none>        80:30691/TCP   14s

浏览器访问10.1.8.30:30691

通过环境变量访问 Service

1.核心逻辑:只要同一个命名空间里先创建好了 Service,后面新启动的 Pod,K8s 会自动把这个 Service 的 IP、端口,以环境变量的形式塞进 Pod 里。程序直接读取变量名,就能拿到 Service 地址,不用手动写死 IP 了。

2.比如名为 mysql 的服务,会自动生成这些常用变量: MYSQL_SERVICE_HOST:存 Service 的 IP MYSQL_SERVICE_PORT:存 Service 的端口

3.三个必须记住的限制: 3.1有严格的先后顺序:必须先建 Service,再启动 Pod。如果先启 Pod、后建 Service,Pod 里不会有这些变量,必须重启 Pod 才会刷新。 3.2跨命名空间无效:只能拿到同一个命名空间的 Service 变量,别的命名空间的服务获取不到。 3.3服务数量多了,环境变量会非常冗余杂乱。 总结:比硬编码灵活一点,但限制依旧很多,一般只做辅助方案,不做主方案。

获取环境变量信息

[root@master30 ~ 15:21:25]# kubectl  run test --rm -it --image=docker.io/library/busybox  --image-pull-policy=IfNotPresent sh
If you don't see a command prompt, try pressing enter.
/ # env | grep MYSQL
MYSQL_PORT_3306_TCP_ADDR=10.102.42.65
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_SERVICE_HOST=10.102.42.65
MYSQL_PORT_3306_TCP_PROTO=tcp
MYSQL_SERVICE_PORT=3306
MYSQL_PORT=tcp://10.102.42.65:3306
MYSQL_PORT_3306_TCP=tcp://10.102.42.65:3306
/ # exit
Session ended, resume using 'kubectl attach test -c test -i -t' command when the pod is running
pod "test" deleted

说明:

  1. 可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。

  2. service创建后才能使用该环境变量。

  3. service属于namespace,pod只能访问同一个namespace中service。

准备WordPress资源

# 删除 pod-WordPress 资源,重新创建
root@master30:~# kubectl delete pod wordpress --force
root@master30:~# kubectl run wordpress \
--image=docker.io/library/wordpress \
--image-pull-policy=IfNotPresent \
--env=WORDPRESS_DB_USER=tom \
--env=WORDPRESS_DB_PASSWORD=redhat \
--env=WORDPRESS_DB_NAME=blog \
--env=WORDPRESS_DB_HOST='$(MYSQL_SERVICE_HOST)'

# 查看Service变量
[root@master30 ~ 15:40:28]# kubectl  exec  -it wordpress  -- sh -c 'env | grep MYSQL'
MYSQL_PORT_3306_TCP_ADDR=10.102.42.65
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_SERVICE_HOST=10.102.42.65
MYSQL_PORT_3306_TCP_PROTO=tcp
MYSQL_SERVICE_PORT=3306
MYSQL_PORT=tcp://10.102.42.65:3306
MYSQL_PORT_3306_TCP=tcp://10.102.42.65:3306

[root@master30 ~ 15:41:12]# kubectl  get svc
NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
mysql       ClusterIP   10.102.42.65   <none>        3306/TCP       38m
wordpress   NodePort    10.100.118.1   <none>        80:30691/TCP   25

访问测试

image-20260629154314940

通过 DNS 名称访问 Service(生产首选,最重要)

核心逻辑: 集群里默认装了一个叫 CoreDNS 的内部域名解析服务,相当于公司的内部通讯录。 每创建一个新的 Service,CoreDNS 会自动新增一条记录:服务名 → Service的IP。 集群里所有 Pod,默认都用这个 DNS 服务器解析地址。你直接写服务的名字,它就能自动解析出对应的 IP,完成访问。

域名规则: 完整写法:服务名.命名空间.svc.cluster.local 同一个命名空间内,可以直接简写为服务名。比如 WordPress 和 MySQL 在同一个命名空间,WordPress 连数据库直接写 mysql 就行。

Kubernetes 还提供了更为方便的DNS访问。kubeadm部署时会默认安装coredns组件。

[root@master30 ~ 15:42:42]# kubectl  get deployments.apps   --namespace=kube-system 
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
calico-kube-controllers   1/1     1            1           6d1h
coredns                   2/2     2            2           6d1h

coredns是一个DNS服务器。 每当有新的Service被创建, coredns会添加该Service的DNS记录。 Cluster中的Pod可以通过<SERVICE_NAME>.<NAMESPACE_NAME>访问Service。

[root@master30 ~ 15:45:52]# kubectl  run busybox --rm -it --image=docker.io/library/busybox /bin/sh
If you don't see a command prompt, try pressing enter.
#查看 Linux 系统默认的 DNS 配置文件,这是所有域名解析的依据。
/ # cat /etc/resolv.conf 
search services.svc.cluster.local svc.cluster.local cluster.local
nameserver 10.96.0.10
options ndots:5

#nameserver 10.96.0.10
这是 Pod 的 DNS 服务器地址,所有域名查询请求都会发给这个地址。
它不是某个节点的 IP,而是 Kubernetes 集群内置 DNS 服务的 ClusterIP,所有 Pod 创建时都会自动配置这个地址,不用手动设置。
#search services.svc.cluster.local svc.cluster.local cluster.local
这是域名自动补全列表,也是 “只写服务名就能访问” 的核心原因。
当你输入一个短名字(比如 wordpress),系统不会直接去解析,而是按顺序拼接后面的后缀,挨个去 DNS 查询,直到查到结果为止。


#操作说明:用 wget 工具访问 wordpress 服务的 80 端口,下载首页文件。
/ # wget wordpress:80
Connecting to wordpress:80 (10.100.118.1:80)
Connecting to wordpress:80 (10.100.118.1:80)
saving to 'index.html'
index.html           100% |*************************|  7617  0:00:00 ETA
'index.html' saved

#wget wordpress:80:这里的 wordpress 是服务名(Service Name)。
• 为什么没写 IP? 因为 BusyBox 容器内的 /etc/resolv.conf 配置了 Kubernetes 的 DNS 服务器(CoreDNS)。当你输入 wordpress 时,DNS 会自动补全成 wordpress.services.svc.cluster.local(因为你的 search 域里有 services),所以成功解析到了 ClusterIP 10.100.118.1

由于这个Pod与web service同属于一个namespace, 因此可以直接用web访问Service.

/ # rm index.html 
/ # wget wordpress:80
Connecting to wordpress:80 (10.100.118.1:80)
Connecting to wordpress:80 (10.100.118.1:80)
saving to 'index.html'
index.html           100% |*************************| 13445  0:00:00 ETA
'index.html' saved

10.96.0.10是哪个DNS服务器呢?

[root@master30 ~ 15:54:31]# kubectl  get service --namespace kube-system NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   6d1h

创建新的 WordPress Pod,数据库地址配置为 mysql(直接写 MySQL 服务的服务名)。意图:之前的 WordPress 要么是硬编码 MySQL IP,要么是用环境变量方式连接数据库,现在要替换成 DNS 服务名的方式,所以需要删掉旧的、重新创建。

 # 删除 pod-WordPress 资源,重新创建WordPress
 root@master30:~# kubectl delete pod wordpress --force
 root@master30:~# kubectl run wordpress \
 --image=docker.io/library/wordpress \
 --image-pull-policy=IfNotPresent \
 --env=WORDPRESS_DB_USER=tom \
 --env=WORDPRESS_DB_PASSWORD=redhat \
 --env=WORDPRESS_DB_NAME=blog \
 --env=WORDPRESS_DB_HOST=mysql
 ​
 意图:
 这是整个实验最核心的一步:不再写死 IP,不再引用环境变量,直接用服务名 mysql 作为数据库地址。
 验证真实业务程序中,仅通过服务名就能完成跨服务访问,完全不用关心后端 Service 的 IP 是什么、有没有变化。
 ​
 ​
 # 访问测试
 10.1.8.30:30691
 ​
 # 清理环境
 [root@master30 ~ 15:55:12]# kubectl delete svc mysql wordpress
 service "mysql" deleted
 service "wordpress" deleted
 [root@master30 ~ 15:56:45]# kubectl delete pod mysql wordpress
 pod "mysql" deleted
 pod "wordpress" deleted
 ​
 为什么它是生产环境的标准方案?四个核心优势:
 ​
 1.IP 变更零感知:Service 删了重建、IP 换了完全不影响业务 —— 域名永远是 mysql,DNS 记录会自动更新,程序配置不用改。
 ​
 2.支持跨命名空间访问:写完整域名就能访问任意命名空间的服务,突破了环境变量的命名空间限制。
 ​
 3.没有创建顺序要求:先启动 Pod、后创建 Service 也能用,随时查随时有,不用重启 Pod。
 ​
 4.配置易维护:配置里都是 mysql、redis 这种有业务含义的名字,比一串数字 IP 好懂很多,排查问题也方便。

Service 类型

Kubernetes Service 支持以下五种类型:

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

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

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

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

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

Service 的核心作用是为后端 Pod 提供固定访问入口,五种类型的本质区别是访问范围、暴露方式和适用场景不同。其中 ClusterIP 是基础,NodePort、LoadBalancer 都是在它的基础上扩展对外访问能力;ExternalName 和 Headless 是两种特殊用途的类型,分别解决外部服务映射、直连 Pod 的场景。

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

环境准备

创建 deployment

 [root@master30 ~ 16:39:50]# kubectl  create  deployment  web --image=docker.io/library/httpd --replicas=2
 deployment.apps/web created
 ​

一、ClusterIP:默认类型,集群内部专用

1. 核心定位

ClusterIP 是 Service 的默认类型,会分配一个集群内部专属的虚拟 IP,这个 IP 仅在集群内部(节点、Pod 之间)可访问,外部网络无法直接连通。

其他所有 Service 类型(NodePort、LoadBalancer)都是在 ClusterIP 的基础上构建的 —— 它们都会先生成 ClusterIP,再额外扩展对外访问的能力。

2. 工作原理
  • 创建 Service 时,系统从预留的集群 IP 地址池(service-cluster-ip-range)中分配一个固定虚拟 IP;

  • 通过标签选择器匹配后端 Pod,自动维护端点列表(Endpoints);

  • 节点上的 kube-proxy 组件生成 iptables/ipvs 规则,将发往 ClusterIP 的流量负载均衡到后端 Pod。

3. 关键特性
  • IP 固定不变,只要 Service 不删除重建,地址就不会变;

  • 仅支持 TCP/UDP 端口级别的转发,ICMP(ping)无法连通,因为它是虚拟 IP,没有真实网络栈响应;

  • 可以手动指定 spec.clusterIP 字段来自定义 IP,但必须在集群地址池范围内。

  • 如果将 clusterIP 设置为 None,就变成了无头服务(Headless Service),后文单独讲解。

4. 适用场景

集群内部服务之间的调用,例如微服务之间互相访问、应用程序连接内部数据库、缓存等,是生产环境内部服务的标准形态。

5. 对应实验说明

之前服务发现章节中,mysql、wordpress 的内部 Service 都是 ClusterIP 类型,Pod 之间通过 ClusterIP、环境变量、DNS 互相访问,就是最典型的 ClusterIP 使用场景。

二、NodePort:通过节点端口对外暴露服务

1. 核心定位

在 ClusterIP 的基础上,在集群的每一个节点上都开放一个相同的端口(默认端口范围 30000-32767)。外部客户端只要能访问任意一台节点的 IP,加上这个端口,就能访问到后端服务。

2. 工作原理
  1. 系统先为 Service 分配 ClusterIP,再从预设的端口范围内随机分配一个节点端口(nodePort);

  2. 所有节点都会在本机监听这个端口,并生成对应的 iptables 规则;

  3. 外部流量访问 任意节点IP:nodePort 时,节点内核会将流量转发到 Service 的 ClusterIP,最终负载均衡到后端 Pod。

3、三个端口的明确区分(重点)

很多初学者容易混淆三个端口,这里逐一对应:

端口名称 位置 作用 示例
targetPort 后端 Pod 上 容器内程序实际监听的端口 80(httpd 默认端口)
port Service 的 ClusterIP 上 Service 自身对外提供服务的端口,集群内部访问用这个端口 8080
nodePort 每个节点的主机上 外部访问时使用的端口,所有节点端口一致 31917

访问链路:

  • 集群内部访问:ClusterIP:port → 转发到 PodIP:targetPort

  • 外部访问:节点IP:nodePort → 转发到 ClusterIP:port → 最终到 PodIP:targetPort

示例:

[root@master30 ~ 16:39:54]# kubectl  expose  deployment  web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-Nodeport.yaml
[root@master30 ~ 16:58:07]# 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 ~ 16:58:31]# kubectl  apply -f service-Nodeport.yaml 
service/web created


# 查看node节点对应端口为30706
[root@master30 ~ 16:59:43]# kubectl  get service
NAME   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
web    NodePort   10.107.225.47   <none>        8080:30706/TCP   26s

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

# 访问集群中任一节点测试
[root@master30 ~ 17:00:09]# curl http://10.1.8.30:30706
hello nginx 1.28
[root@master30 ~ 17:00:09]# curl http://10.1.8.31:30706
[root@master30 ~ 17:00:09]# curl http://10.1.8.32:30706

分析防火墙规则:

# 与ClusterIP对比,每个节点的iptables中额外增加了下面两条规则:
[root@master30 ~ 17:04:11]# iptables-save | grep 30706
-A KUBE-NODEPORTS -p tcp -m comment --comment "services/web" -m tcp --dport 30706 -j KUBE-EXT-7D76YWGERGEPC4GC

# 规则的含义是:访问当前节点31917端口的请求会应用规则KUBE-EXT-7D76YWGERGEPC4GC

# 进一步分析,其作用就是负载均衡到每一个Pod。
[root@master30 ~ 17:16:24]# iptables-save |grep KUBE-EXT-7D76YWGERGEPC4GC:KUBE-EXT-7D76YWGERGEPC4GC - [0:0]
-A KUBE-EXT-7D76YWGERGEPC4GC -m comment --comment "masquerade traffic for services/web external destinations" -j KUBE-MARK-MASQ
-A KUBE-EXT-7D76YWGERGEPC4GC -j KUBE-SVC-7D76YWGERGEPC4GC
-A KUBE-NODEPORTS -p tcp -m comment --comment "services/web" -m tcp --dport 30706 -j KUBE-EXT-7D76YWGERGEPC4GC

NodePort默认的是随机选择, 我们可以使用nodePort指定为特定端口。默认端口是系统从 30000-32767 中随机分配,也可以在 YAML 中通过 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:~# kubectl delete svc web 

三、LoadBalancer:负载均衡 IP 对外暴露

1. 核心定位与原生限制

LoadBalancer 类型会为 Service 分配一个独立的外部 IP,用户直接访问这个 IP + 服务端口即可访问服务,无需关心节点 IP 和端口号。

但 Kubernetes 原生只提供了接口,没有具体实现:

  • 在公有云(阿里云、AWS、Azure)上,会自动调用云厂商的负载均衡器,分配公网 IP;

  • 在自建的裸金属集群中,原生不提供负载均衡实现,创建 LoadBalancer 类型 Service 会一直处于 pending 状态。

MetalLB 就是为裸金属集群提供 LoadBalancer 实现的开源组件,让自建集群也能拥有和云环境一致的 LoadBalancer 体验。

2. MetalLB 是什么

MetalLB 是专门针对裸金属 Kubernetes 集群的负载均衡实现,它的核心作用是:

  • 从你配置的地址池中,为 LoadBalancer 类型的 Service 分配外部 IP;

  • 通过标准网络协议(二层 ARP / 三层 BGP)将这个 IP 宣告到局域网中,让外部网络能找到这个 IP。

注意:MetalLB 只负责「IP 分配 + 网络宣告」,不负责流量负载均衡。真正的流量转发和负载均衡,依然由节点上的 kube-proxy + Service 机制完成。

3.部署

部署 metallb

root@master30:~# wget http://192.168.42.200/course-materials/softwares/stage03/metallb-0.14.8.tar.gz
root@master30:~# 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/docker.io/g' metallb-0.14.8/config/manifests/metallb-native.yaml

root@master30:~# kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
# 等待着所有pod正常运行再进行下一步
root@master30:~# kubectl get all -n metallb-system 
NAME                              READY   STATUS    RESTARTS   AGE
pod/controller-786f9df989-98bjh   1/1     Running   0          85s
pod/speaker-gthhx                 1/1     Running   0          85s
pod/speaker-jwj25                 1/1     Running   0          85s
pod/speaker-s5zvq                 1/1     Running   0          85s

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

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

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

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/controller-786f9df989   1         1         1       85s

配置地址池

root@master30:~# 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:~# kubectl apply -f ippool.yaml

配置 lay2

root@master30:~# cat << 'EOF' > L2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
  name: example
  namespace: metallb-system
EOF
root@master30:~# kubectl apply -f L2.yaml
4.测试
root@master30:~# kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml 
root@master30:~# cat service-LoadBalancer.yaml 
 apiVersion: v1
 kind: Service
 metadata:
   labels:
     app: web
   name: web
 spec:
   ports:
   - port: 80
     protocol: TCP
     targetPort: 80
   selector:
     app: web
   type: LoadBalancer
root@master30:~# kubectl apply -f service-LoadBalancer.yaml
root@master30:~# 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

# 访问测试,端口号使用service的80,非32101端口
[root@client ~]# curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html>
5.总结
  • 客户端流量到达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:DNS 别名,映射外部服务

1. 核心定位

这是一种非常特殊的 Service 类型,它没有后端 Pod,也不分配 ClusterIP,只做 DNS 层面的域名别名

它的作用是:将一个外部域名,映射成集群内部的服务名。集群内的程序只需要访问内部服务名,就会自动解析到外部真实域名的地址。

2. 工作原理

创建 ExternalName 类型 Service 后,CoreDNS 会添加一条 CNAME 解析记录: 内部服务名.命名空间.svc.cluster.local外部真实域名

当集群内 Pod 访问内部服务名时,DNS 会返回 CNAME 记录,最终解析到外部服务的 IP,实现通过内部名字访问外部服务。

3. 配置示例
 apiVersion: v1
 kind: Service
 metadata:
   name: my-db
   namespace: prod
 spec:
   type: ExternalName
   externalName: database.example.com

集群内程序访问 my-db 时,实际会解析到 database.example.com

五、Headless Service:无头服务,无 IP 无负载均衡

1. 核心定位

也叫 “无头服务”,通过将 spec.clusterIP 设置为 None 来创建。它不分配 ClusterIP,不做负载均衡,kube-proxy 也不会为它生成转发规则

它的核心价值是:让客户端直接获取到所有后端 Pod 的真实 IP,而不是通过 Service 代理访问。

2. 两种场景与 DNS 行为
场景 1:带标签选择器(最常用)

Service 配置了 selector,匹配后端 Pod。

  • 系统会自动创建 EndpointSlice,记录所有匹配的 Pod IP;

  • DNS 解析服务名时,直接返回所有后端 Pod 的 IP 地址列表,而不是 Service 的 IP;

  • 客户端拿到所有 Pod 的 IP 后,可以自己选择连接哪一个。

场景 2:不带标签选择器

Service 不配置 selector,通常用于映射集群外部的固定 IP。

  • 不会自动生成 EndpointSlice,需要手动创建 Endpoints 资源,绑定外部 IP;

  • DNS 解析时返回手动配置的外部 IP 列表。

3. 适用场景
  • 有状态服务(StatefulSet):比如数据库集群、分布式缓存集群,客户端需要直接和具体的 Pod 通信(例如主从同步、选主、数据分片),不需要 Service 做负载均衡;

  • 需要自主实现服务发现、自定义负载策略的场景。

总结:五种类型对比

类型 访问范围 核心用途 生产推荐度
ClusterIP 仅集群内部 内部服务间调用 ✅ 内部服务默认方案
NodePort 外部通过节点 IP + 端口访问 临时测试、简单暴露 ⚠️ 测试可用,生产不推荐
LoadBalancer 外部通过独立 IP 访问 生产环境对外暴露服务 ✅ 自建集群配合 MetalLB 使用
ExternalName 集群内部访问外部服务 外部服务域名映射 ✅ 外部服务统一入口
Headless 集群内部直连 Pod 有状态服务、自定义服务发现 ✅ StatefulSet 配套使用

Service 会话保持

1. 什么是会话保持

会话保持也叫会话亲和性(Session Affinity),是 Service 的一项可选高级特性。 默认情况下,Service 会按照负载均衡策略将请求轮流分发给后端各个 Pod,保证每个 Pod 压力均匀;开启会话保持后,来自同一个客户端 IP 的所有请求,会始终转发到同一个后端 Pod,不会在多个 Pod 之间随机切换。

可以用线下门店服务做类比:

  • 普通负载均衡:门店迎宾按顺序把客人分配给不同导购,保证每个人工作量平均;

  • 会话保持:给客人绑定专属导购,同一个客人每次到店都由同一位导购接待,服务过程连贯不中断。

2. 为什么需要会话保持

默认的轮询负载均衡非常适合无状态服务(比如静态网页、公共接口),但对于有状态的业务场景,请求在 Pod 之间随意切换会直接导致业务异常:

  • 登录状态丢失:用户的登录信息保存在单个 Pod 的内存中,请求切换到其他 Pod 后会被判定为未登录,需要重新认证;

  • 会话数据丢失:购物车、临时表单、操作缓存这类存在本地的数据,切换 Pod 后会全部清空;

  • 长连接中断:WebSocket、实时消息、游戏服务这类长连接场景,连接被切换到其他 Pod 会直接断开,影响使用体验。

开启会话保持后,同一客户端的请求始终落在同一个 Pod 上,可以保障业务状态的连续性,适配有状态服务的需求。

3. 核心配置参数

Kubernetes 通过 Service 的 spec 下的字段控制会话保持行为,有两个核心配置项:

(1)会话保持开关:sessionAffinity
  • 字段路径:spec.sessionAffinity

  • 默认值:None,不开启会话保持,Service 正常执行负载均衡分发;

  • 可选值:ClientIP,开启基于客户端 IP 的会话保持,同一客户端 IP 的请求会固定转发到同一个后端 Pod。

(2)会话超时时间:timeoutSeconds
  • 字段路径:spec.sessionAffinityConfig.clientIP.timeoutSeconds

  • 作用:设置客户端 IP 与后端 Pod 的绑定关系的有效期,默认值为 10800 秒(即 3 小时)。

  • 规则:如果同一个客户端 IP 超过该时长没有发起任何请求,绑定关系会自动失效;该客户端再次访问时,会重新分配后端 Pod 并建立新的绑定。

4. 工作原理

基于 ClientIP 的会话保持,底层由 kube-proxy 的转发规则实现,完整流程如下:

  1. 客户端首次访问 Service 时,Service 按照默认负载均衡规则选中一个健康的后端 Pod 处理请求,同时在节点的转发规则中生成一条「客户端 IP → 对应后端 Pod」的映射记录;

  2. 该客户端后续发起的所有请求,会先匹配这条映射记录,直接转发到绑定的后端 Pod,不再重新执行负载均衡选路;

  3. 当超过设定的超时时间后,映射记录会自动过期清除,绑定关系失效。

准备测试环境

 [root@master30 ~ 17:26:30]# kubectl create deployment web --image=docker.io/library/httpd --replicas=2
 deployment.apps/web created
 ​
 # 创建ClusterIP类型的Service,暴露80端口
 [root@master30 ~ 17:34:30]#  kubectl expose deployment web --port 80
 service/web exposed
 ​
 # 批量修改每个Pod的首页为Pod自身名称
 [root@master30 ~ 17:34:38]# 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 ~ 17:34:44]# kubectl get svc web
 NAME   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
 web    ClusterIP   10.106.47.56   <none>        80/TCP    11s
 ​
 #验证默认负载均衡效果:未开启会话保持时,连续访问 20 次 Service 地址,请求会均匀分布在两个后端 Pod 上:
 [root@master30 ~ 17:35:55]# for i in {1..20};do curl -s 10.106.47.56;done|sort |uniq -c
       9 web-6db76cb4fc-9pdqk
      11 web-6db76cb4fc-v46gv
 ​
      
 预期结果:两个 Pod 各处理约 10 次请求,符合默认轮询负载均衡的特性。

设置会话保持

 #通过 patch 命令在线修改 Service 配置,开启 ClientIP 会话保持:
 [root@master30 ~ 17:35:58]# kubectl  patch  svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'
 service/web patched
 ​
 # 再次访问:结果保持一致
 [root@master30 ~ 17:38:04]# for i in {1..20};do curl -s 10.106.47.56;done|sort |uniq -c
      20 web-6db76cb4fc-v46gv
      
 预期结果:20 次请求全部落在同一个 Pod 上,说明当前客户端 IP 已经与该 Pod 建立了绑定关系,会话保持生效。

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

Service 的负载均衡、会话保持能力,底层都是由节点上的 kube-proxy 组件实现的。kube-proxy 主流有 iptables 和 IPVS 两种工作模式,其中 IPVS 模式下,调度算法与会话保持有明确的优先级关系:

(1)Service 会话保持配置优先级最高

只要 Service 中配置了 sessionAffinity: ClientIP,无论 IPVS 全局设置了哪种调度算法,都会自动切换为 SH(Source Hashing,源地址哈希)算法。 SH 算法的逻辑是对客户端 IP 做哈希运算,计算结果唯一对应一个后端 Pod,因此同一个客户端 IP 永远会命中同一个后端,从底层实现会话保持。

(2)未开启会话保持时,使用 IPVS 默认调度算法

如果 Service 没有开启会话保持,kube-proxy 会采用 IPVS 全局配置的调度算法分发请求,三种最常用的算法:

  • rr(轮询):请求按顺序依次分配给后端 Pod,是最通用、最常用的默认算法;

  • lc(最少连接):优先将请求分配给当前活跃连接数最少的 Pod,适合请求处理时长差异大的场景,避免单个 Pod 压力过载;

  • wrr(加权轮询):可以为不同性能的后端 Pod 设置权重,性能越好的 Pod 分配到的请求占比越高,适配异构服务器集群。

金丝雀发布

1. 什么是金丝雀发布

金丝雀发布也叫灰度发布,是一种低风险的应用版本发布方式。它的名字来源于早期矿工下井的习惯:矿工下矿井时会带一只金丝雀,金丝雀对有毒气体更敏感,如果瓦斯泄漏,金丝雀会先出现异常,给矿工提前预警。对应到发布场景,新版本就相当于探路的金丝雀,先承接小部分流量验证风险,没问题再逐步全量。

核心思路是:先让一小部分流量访问新版本应用,验证功能、性能和稳定性正常后,再逐步扩大新版本的流量占比,最终完成全量更新

可以用餐厅上新做类比:餐厅推出新菜品,不会直接替换所有旧菜,而是先每天限量卖几份,收集客人反馈。如果反响好,就逐步增加供应量,最终全量上架;如果反馈不好,直接停售即可,几乎不会影响大部分客人的用餐体验。

它和 Deployment 自带的滚动更新有明显区别:

  • 滚动更新是系统自动按固定节奏替换旧 Pod,过程不可手动控制节奏,也不能长时间保留双版本;

  • 金丝雀发布可以人为精准控制新旧版本的流量比例,也可以长时间保留双版本并行运行,充分验证后再全量,更适合核心业务的重要版本更新。

2. 实现核心原理:标签匹配 + 流量分发

Kubernetes 原生实现金丝雀发布,完全依托 Service 的标签选择器机制,不需要额外安装组件,核心逻辑如下:

  1. 部署两个独立的 Deployment,分别运行旧稳定版新版本的应用。两套 Pod 拥有共同的业务标签(如 app: web),同时用一个额外的标签(如 track: stable / track: canary)区分版本;

  2. 创建一个公共的 Service,它的标签选择器只匹配公共业务标签,不包含区分版本的 track 标签

  3. 这样两个 Deployment 的 Pod 都会被 Service 纳入后端端点列表,请求会按照负载均衡策略分发到两个版本的 Pod 上;

  4. 调整两个 Deployment 的副本数量,就能间接控制新旧版本的流量占比:新版副本越少,分到的流量越少,风险越低。

学习参考:金丝雀部署

环境准备:制作区分版本的页面文件

#操作意图:
两个版本都使用 nginx 基础镜像,默认首页内容完全一致,访问时无法区分请求落到了哪个版本。因此提前创建 ConfigMap,分别存放两个版本的首页文本,后续挂载到不同版本的 Pod 中,通过返回内容的差异就能直观统计流量的分布比例。

[root@master30 ~ 17:57:14]# echo hello nginx 1.28 > web/index28.html
[root@master30 ~ 17:57:24]# echo hello nginx 1.29 > web/index29.html
[root@master30 ~ 18:00:04]# kubectl create configmap web --from-file=./web
configmap/web created
[root@master30 ~ 18:00:07]# 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 ~ 18:00:44]# vim webapp-1.28.yaml
     apiVersion: apps/v1
     kind: Deployment
     metadata:
       labels:
         app: web
       name: web-28
     spec:
       replicas: 10    #初始设置 10 个副本,作为业务的主力服务;
       selector:
         matchLabels:
           app: web
           tier: frontend
           track: stable
       template:
         metadata:
           labels:
             app: web
             tier: frontend
             track: stable    #track: stable,用于标记这是稳定版本,和后续的金丝雀版本做区分;
         spec:
           containers:
           - image: docker.io/library/nginx:1.28
             name: nginx
             imagePullPolicy: IfNotPresent
             ports:
             - containerPort: 80
             volumeMounts:
             - name: webcontent
               mountPath: "/usr/share/nginx/html"
           volumes:
           - name: webcontent
             configMap:    #- 通过 ConfigMap 挂载,将 index28.html 作为 nginx 的默认首页,访问该 Pod 会返回 hello nginx 1.28。
               name: web
               items:
                 - key: index28.html
                   path: index.html
    [root@master30 ~ 18:01:43]# kubectl apply -f webapp-1.28.yaml
    deployment.apps/web-28 created
    
  2. 创建 service

    [root@master30 ~ 18:01:52]# 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
     ​
     配置核心要点:
     Service 的标签选择器只配置了 app: web 和 tier: frontend 两个公共标签,没有包含 track 标签。
     这是实现金丝雀分流的关键:只要带有这两个标签的 Pod,无论 track 是 stable 还是 canary,都会被 Service 选中并纳入后端端点列表,共同承接流量。
    [root@master30 ~ 18:02:27]# kubectl apply -f webapp-svc.yaml
    Warning: resource services/web is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
    service/web configured
    [root@master30 ~ 18:02:35]# kubectl get svc
    NAME   TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
    web    ClusterIP   10.106.47.56   <none>        80/TCP    28m
    
  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    #- 这是运行 nginx 1.29 的金丝雀版本 Deployment,初始仅设置 1 个副本,只承接很小比例的流量,最大限度降低发布风险;
       selector:
         matchLabels:
           app: web
           tier: frontend
           track: canary   #- Pod 标签中包含 track: canary,和稳定版做明确区分;
       template:
         metadata:
           labels:
             app: web
             tier: frontend
             track: canary
         spec:
           containers:
           - image: docker.io/library/nginx:1.29
             name: nginx
             imagePullPolicy: IfNotPresent
             ports:
             - containerPort: 80
             volumeMounts:
             - name: webcontent
               mountPath: "/usr/share/nginx/html"
           volumes:
           - name: webcontent
             configMap:          #- 同样通过 ConfigMap 挂载,将 index29.html 作为默认首页,返回内容和旧版形成区分。
               name: web
               items:
                 - key: index29.html
                   path: index.html
                   
     部署完成后,Service 后端共有 10 个稳定版 Pod + 1 个金丝雀版 Pod,流量大致按照 10:1 的比例分配到两个版本。 
     [root@master30 ~ 18:03:36]# kubectl apply -f webapp-1.29.yaml
     deployment.apps/web-29 created
     ​
    验证流量比例

    调整副本数为 8 个旧版 + 2 个新版,总副本数保持 10 个,保证服务整体处理能力不变:

     [root@master30 ~ 18:03:42]#  kubectl scale deployment web-28 --replicas 8
     deployment.apps/web-28 scaled
     [root@master30 ~ 18:03:53]# kubectl scale deployment web-29 --replicas 2
     deployment.apps/web-29 scaled
     ​
     #连续访问 50 次 Service 地址,统计两个版本的命中次数:(4:1)
     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 ~ 18:21:09]# kubectl scale deployment web-28 --replicas 6
     deployment.apps/web-28 scaled
     [root@master30 ~ 18:22:34]# kubectl scale deployment web-29 --replicas 4
     deployment.apps/web-29 scaled
     ​
     #3:2
     [root@master30 ~ 18:22:46]# for i in {1..50}; do curl -s 10.106.47.56; done | sort -n|uniq -c
          29 hello nginx 1.28
          21 hello nginx 1.29
     ​
     3:2
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐