Kubernetes Service详解:service介绍、service基本管理、service发现、service类型、service会话保持,金丝雀发布
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 建立关联,整体工作逻辑如下:
-
Service 在配置中通过
spec.selector定义一组标签规则; -
系统自动筛选集群中所有标签与规则匹配的 Pod,收集其 IP 与端口,生成
Endpoints(端点列表); -
当有请求访问 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=web1和app2=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 通
一、这句话到底是什么意思
我们分两层把逻辑讲透,你就能完全理解这个现象:
-
ClusterIP 不是一个 “真实存在” 的 IP
Service 的 ClusterIP 是虚拟 IP,它没有绑定在任何一台服务器的真实网卡上,也没有对应的操作系统网络栈去响应请求。 它的本质是:kube-proxy 组件在集群每个节点上,通过 iptables 或 ipvs 生成了一系列转发规则 ——只有命中指定端口的 TCP/UDP 数据包,才会被规则转发到后端 Pod。
-
ping 和 curl 走的是完全不同的协议
-
ping命令使用的是 ICMP 协议(网络层协议),它发的是 “回声请求包”,需要目标 IP 有真实网络栈来回应。 而 iptables 只给 ClusterIP 配置了 TCP/UDP 端口的转发规则,没有处理 ICMP 协议的规则,所以 ping 包发出去后没人响应,自然就 “不通”。 -
curl命令使用的是 TCP 协议(传输层协议),它会和目标 IP 的指定端口建立连接。 这个 TCP 包正好命中了 iptables 里配置的转发规则,系统会把它转发到后端健康的 Pod 上,所以能正常拿到返回结果。
一句话总结:ClusterIP 是 “端口级别的虚拟入口”,不是 “主机级别的真实 IP”。它只处理你配置过的 TCP/UDP 端口,不响应 ICMP 的 ping 请求,这是正常设计,不是故障。
二、怎么验证这个结论
你可以按下面步骤一步步验证,对比现象就能直观理解。
-
验证现象:ping ClusterIP 确实不通
在 master 或任意节点上执行 ping 命令,目标是你的 Service ClusterIP:
ping 10.110.92.161
预期结果:一直超时、丢包,完全不通。 这就验证了「ICMP 协议无法到达 ClusterIP」。
-
验证现象:TCP 端口访问完全正常
就是你写的这条 curl 命令,它本身就是最好的验证:
for i in {1..60};do curl -s 10.110.92.161:8080;done
预期结果:60 次请求全部正常返回页面内容,没有报错。 这就验证了「TCP 端口的转发规则是生效的,Service 工作完全正常」。
这两个命令一对比,你就能清晰看到:不是 Service 不能用,只是 ping 这种方式不对。
三:关键结论
-
永远不要用 ping 来判断 Service 是否正常,ping 不通是正常现象;
-
判断 Service 可用性,正确方式是访问对应端口(curl、nc、telnet 均可);
-
这个特性只针对 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:
-
通过 IP 访问 Service。
-
通过环境变量访问 Service。
-
通过 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
说明:
-
可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。
-
service创建后才能使用该环境变量。
-
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
访问测试

通过 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. 工作原理
-
系统先为 Service 分配 ClusterIP,再从预设的端口范围内随机分配一个节点端口(nodePort);
-
所有节点都会在本机监听这个端口,并生成对应的 iptables 规则;
-
外部流量访问
任意节点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 接收并响应请求
-
客户端发起请求:用户通过浏览器 / 应用访问 MetalLB 分配的 LB VIP(如 10.1.8.200)。
-
ARP 寻址,流量进入集群节点:MetalLB 使用 Layer2 模式,通过 ARP 广播声明 VIP 归属,流量进入集群任意一个节点。
-
节点内核拦截流量:节点识别目标地址为 Service LB IP,将流量交给内核网络框架处理。
-
kube-proxy 执行转发规则:kube-proxy 匹配 iptables/IPVS 规则,确定流量所属 Service。
-
Service 负载均衡选择 Pod:从 Service 后端 endpoints 中,按策略选择一个健康 Pod。
-
CNI 网络跨节点转发:若 Pod 不在当前节点,流量通过 Calico/Flannel 等 CNI 网络转发到目标节点。
-
流量进入 Pod:目标节点通过 veth-pair 设备,将流量送入 Pod 网络命名空间。
-
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 的转发规则实现,完整流程如下:
-
客户端首次访问 Service 时,Service 按照默认负载均衡规则选中一个健康的后端 Pod 处理请求,同时在节点的转发规则中生成一条「客户端 IP → 对应后端 Pod」的映射记录;
-
该客户端后续发起的所有请求,会先匹配这条映射记录,直接转发到绑定的后端 Pod,不再重新执行负载均衡选路;
-
当超过设定的超时时间后,映射记录会自动过期清除,绑定关系失效。
准备测试环境
[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 的标签选择器机制,不需要额外安装组件,核心逻辑如下:
-
部署两个独立的 Deployment,分别运行旧稳定版和新版本的应用。两套 Pod 拥有共同的业务标签(如
app: web),同时用一个额外的标签(如track: stable/track: canary)区分版本; -
创建一个公共的 Service,它的标签选择器只匹配公共业务标签,不包含区分版本的
track标签; -
这样两个 Deployment 的 Pod 都会被 Service 纳入后端端点列表,请求会按照负载均衡策略分发到两个版本的 Pod 上;
-
调整两个 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 标签来区分不同的版本。
-
主要稳定的发行版将有一个
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
-
创建 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
-
部署应用新版本。新的发行版将有一个
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 -
逐步切流,完成全量验证:总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
更多推荐

所有评论(0)