文章目录
- [Kubernetes Service](#Kubernetes Service)
-
- 环境准备
- 先看两个例子
- [Service 介绍](#Service 介绍)
- [Service 基本管理](#Service 基本管理)
- [Service 发现](#Service 发现)
-
- [通过 IP 访问 Service](#通过 IP 访问 Service)
- [通过环境变量访问 Service](#通过环境变量访问 Service)
- [通过 DNS 名称访问 Service](#通过 DNS 名称访问 Service)
- [Service 类型](#Service 类型)
-
- 环境准备
- ClusterIP
- NodePort
- LoadBalancer
- ExternalName
- [Headless Services](#Headless Services)
- [Service 会话保持](#Service 会话保持)
- 金丝雀发布
- 环境清理
Kubernetes Service
学习参考:Service
环境准备
bash
# 创建名称空间services
root@master30:~# kubectl create ns services
# 将当前kubectl上下文的默认命名空间切换为services,后续操作默认作用于该命名空间
root@master30:~# kubectl config set-context --current --namespace services
先看两个例子
示例1:
bash
# 客户端模拟运行创建Pod,输出yaml格式并重定向保存到pod-web.yml,不实际提交到集群
root@master30:~# kubectl run web --image=hub.cyw.cloud/library/httpd --image-pull-policy=IfNotPresent -o yaml --dry-run=client > pod-web.yml
# 应用yaml清单,正式创建web Pod
root@master30:~# kubectl apply -f pod-web.yml
# 查看Pod详情,获取Pod IP与所在节点
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web 1/1 Running 0 10s 10.224.51.149 worker31.cyw.cloud <none> <none> # Pod IP属于集群内部虚拟网段
# 集群外节点直接访问Pod IP,默认无法连通
# 原因:Pod网络是集群内部Overlay网络,集群外主机没有到该网段的路由
root@client:~# curl http://10.224.193.65
# 访问失败,无法到达Pod
# 强制删除web Pod
root@master30:~# kubectl delete pod web --force
示例2:
bash
# 客户端模拟创建名为web的Deployment,镜像为httpd、副本数4;不实际提交到集群,输出yaml格式并重定向保存到deploy-web.yml
root@master30:~# kubectl create deployment web --image=hub.cyw.cloud/library/httpd --replicas=4 --dry-run=client -o yaml > deploy-web.yml
# 使用vim编辑生成的部署清单文件,可调整副本数、端口、资源限制等配置后再应用
root@master30:~# vim deploy-web.yml
yaml
apiVersion: apps/v1 # Deployment归属apps/v1稳定API组
kind: Deployment # 资源类型:Deployment,无状态应用部署控制器
metadata:
creationTimestamp: null # dry-run生成时创建时间留空,正式apply到集群后由apiserver自动填充
labels:
app: web # Deployment自身标签标识
name: web # Deployment资源名称,命名空间内唯一
spec:
replicas: 4 # 期望维持的Pod副本数量为4
selector:
matchLabels:
app: web # 标签选择器,只管理带有app=web标签的Pod
strategy: {} # 更新策略配置,空值使用默认滚动更新策略
template: # Pod模板,Deployment控制器基于该模板创建/更新Pod
metadata:
creationTimestamp: null
labels:
app: web # 生成Pod的标签,必须与selector.matchLabels完全匹配
spec:
containers:
- image: hub.cyw.cloud/library/httpd # httpd Web服务镜像地址
name: httpd # 容器名称,Pod内唯一
#添加imagePullPolicy
imagePullPolicy: IfNotPresent # 镜像拉取策略:节点本地已有该镜像则不再拉取
#添加ports
ports:
- containerPort: 80 # 容器内部服务监听的端口号
hostPort: 8080 # 将容器端口映射到Pod所在宿主机的8080端口,节点外部可直接通过节点IP+该端口访问
resources: {} # 容器CPU/内存资源限制,留空表示不做限制
status: {} # 资源运行状态,由集群自动维护,手动编写时无需填写
bash
# 应用deploy-web.yml清单,创建4副本的web Deployment
root@master30:~# kubectl apply -f deploy-web.yml
# 查看Pod运行状态,2个正常运行、2个处于Pending调度失败状态
root@master30:~# kubectl get pod
NAME READY STATUS RESTARTS AGE
web-79fc679949-2tr7x 0/1 Pending 0 7s # 调度失败,挂起中
web-79fc679949-msbc5 1/1 Running 0 7s # worker节点上调度成功
web-79fc679949-t6twf 0/1 Pending 0 7s # 调度失败,挂起中
web-79fc679949-vqfr8 1/1 Running 0 7s # worker节点上调度成功
# 根因:Pod配置了hostPort:8080,每个Pod独占宿主机8080端口;集群仅2个可用worker节点,最多承载2个同端口Pod,剩余2个无节点可调度
# 查看Pending Pod的事件详情,确认调度失败原因
root@master30:~# kubectl describe pod web-79fc679949-2tr7x
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 49s default-scheduler 0/3 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate, 2 node(s) didn't have free ports for the requested pod ports.
Warning FailedScheduling 49s default-scheduler 0/3 nodes are available: 1 node(s) had taint {node-role.kubernetes.io/master: }, that the pod didn't tolerate, 2 node(s) didn't have free ports for the requested pod ports.
# 两点原因:
# 1. master节点带控制平面污点,普通Pod无容忍度无法调度
# 2. 2个worker节点的8080端口均已被占用,无空闲端口分配给新Pod
# 清理实验环境,删除该Deployment及其关联资源
root@master30:~# kubectl delete deployments.apps web
问题总结:
- 集群外部客户端需要知道pod运行在哪个主机才能访问pod。
- pod通过控制器控制时候,无法在同一个主机上创建多个类似pod。
Service 介绍
如果你使用 Deployment 来运行你的应用, Deployment 可以动态地创建和销毁 Pod。 在任何时刻,你都不知道有多少个这样的 Pod 正在工作以及它们健康与否; 你甚至不知道如何辨别 Pod是否健康。 Kubernetes Pod 的创建和销毁是为了匹配集群的预期状态。 Pod 是临时资源(你不应该期待单个 Pod 既可靠又耐用)。
每个 Pod 会获得属于自己的 IP 地址(Kubernetes 使用网络插件来保证这一点)。 对于集群中给定的某个 Deployment,这一刻运行的 Pod 集合可能不同于下一刻运行该应用的 Pod 集合。
**这就带来了一个问题:**如果某组 Pod(称为"后端")为集群内的其他 Pod(称为"前端") 集合提供功能,前端要如何发现并跟踪要连接的 IP 地址,以便其使用负载的后端组件呢?
答案是Service。
-
Kubernetes 中 Service ,可以将运行在一个或一组 Pod 上的网络应用程序公开为网络服务。Service有自己的IP和端口,而且这个IP是不变的。Service为Pod提供了负载均衡。客户端只需要访问Service的IP, Kubernetes则负责建立和维护Service与Pod的映射关系。 无论后端Pod如何变化, 对客户端不会有任何影响, 因为Service没有变。
-
Kubernetes 中 Service 的一个关键目标:让你无需修改现有应用以使用某种不熟悉的服务发现机制。 你可以在 Pod 集合中运行代码,无论该代码是为云原生环境设计的, 还是被容器化的老应用。 你可以使用 Service 让一组 Pod 可在网络上访问,这样客户端就能与之交互。
Service 基本管理
环境准备:创建 deployment
bash
# 创建名为web的Deployment,使用httpd:2.4.58镜像,设置副本数为3
root@master30:~# kubectl create deployment web --image=hub.cyw.cloud/library/httpd:2.4.58 --replicas=3
# 查看Pod列表,--show-labels参数额外显示每个Pod的完整标签信息
root@master30:~# kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
web-5646dd6f6c-5hs6f 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c # app=web是业务标识标签;pod-template-hash是对应ReplicaSet的模板哈希值,Deployment用它区分不同版本的Pod
web-5646dd6f6c-6fjqs 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c # 相同hash说明属于同一个ReplicaSet,来自同一版Pod模板
web-5646dd6f6c-tvw78 1/1 Running 0 8m43s app=web,pod-template-hash=5646dd6f6c # 3个副本全部正常运行
创建 Service
yaml
# 创建ClusterIP类型的Service,名称为web;--tcp=服务端口:目标端口,映射服务8080端口到后端Pod的80端口
root@master30:~# kubectl create service clusterip web --tcp=8080:80
# 查看Service资源列表
root@master30:~# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web ClusterIP 10.103.19.150 <none> 8080/TCP 6m58s
## 选项--tcp=8080:80含义:访问ClusterIP的8080/TCP端口,流量会转发给后端Pod的80端口
## Service默认自动生成标签 app=<服务名>,同时默认selector也为 app=<服务名>,自动匹配同标签的后端Pod
# 查看Service详细配置信息
root@master30:~# kubectl describe service web
Name: web
Namespace: cyw
Labels: app=web
Annotations: <none>
Selector: app=web # 标签选择器,匹配所有app=web的Pod作为后端服务节点
Type: ClusterIP # 服务类型:ClusterIP,仅集群内部网络可访问
IP Family Policy: SingleStack
IP Families: IPv4
IP: 10.103.19.150 # Service的集群虚拟IP,固定不变
IPs: 10.103.19.150
Port: 8080-80 8080/TCP # Service对外提供服务的端口
TargetPort: 80/TCP # 转发到后端Pod的目标端口
Endpoints: 10.224.193.67:80,10.224.193.68:80,10.224.41.131:80 # 后端3个Pod的实际IP+端口,由Service自动发现并维护
Session Affinity: None # 会话亲和性,None表示默认轮询分发流量
Events: <none>
# 过滤查看Service的集群IP和后端端点列表
root@master30:~# kubectl describe service web |grep -e Endpoints -e IP:
IP: 10.103.19.150
Endpoints: 10.224.193.67:80,10.224.193.68:80,10.224.41.131:80
# 集群内访问测试:访问Service集群IP+8080端口,流量负载均衡转发到后端Pod
root@master30:~# curl 10.103.19.150:8080
<html><body><h1>It works!</h1></body></html> # 返回httpd默认页面,服务转发正常
验证 Service
- 更改每个pod主页,验证负载均衡功能。
bash
# 获取所有Pod的纯名称:-o name输出格式为pod/名称,awk以/为分隔符取第2字段,得到纯净Pod名称
root@master30:~# kubectl get pods -o name | awk -F/ '{print $2}'
web-5646dd6f6c-5hs6f
web-5646dd6f6c-6fjqs
web-5646dd6f6c-tvw78
# 批量修改每个Pod的首页内容:循环遍历所有Pod,进入容器将Pod名称写入httpd首页文件htdocs/index.html
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次访问Service地址,统计每个Pod响应的次数
root@master30:~# for i in {1..60};do curl -s 10.103.19.150:8080;done|sort |uniq -c
19 web-5646dd6f6c-5hs6f
19 web-5646dd6f6c-6fjqs
22 web-5646dd6f6c-tvw78
# 结论:3个Pod响应次数大致均等,ClusterIP Service默认采用轮询策略分发流量,实现负载均衡
- 此时如果创建一个具有相同标签的pod,service也会将请求转发到该pod
bash
# 创建一个独立的裸Pod,设置标签app=web;该标签与web Service的selector匹配,会自动被Service发现并加入后端池
root@master30:~# kubectl run web --image=hub.cyw.cloud/library/httpd --labels=app=web
# 连续60次访问Service地址,统计每种响应内容的出现次数
root@master30:~# for i in {1..60};do curl -s 10.103.19.150:8080;done|sort |uniq -c
14 <html><body><h1>It works!</h1></body></html> # 新增裸Pod的默认首页,未修改过内容
16 web-5646dd6f6c-5hs6f
17 web-5646dd6f6c-6fjqs
13 web-5646dd6f6c-tvw78
# 现象说明:Service通过标签选择器动态发现后端,**只要Pod标签匹配selector,无论Pod是Deployment创建还是独立创建,都会自动加入Endpoints负载均衡池**;此时共4个后端Pod共同分摊流量。
- 此时重启deploy,service仍然能动态发现后端pod
bash
# 触发web Deployment滚动重启,控制器会新建ReplicaSet,逐批用新Pod替换旧Pod,滚动过程中保持服务可用
root@master30:~# kubectl rollout restart deployment web
# 连续60次访问Service地址,统计响应结果,验证滚动重启期间的服务可用性与流量分发
root@master30:~# for i in {1..60};do curl -s 10.103.19.150:8080;done|sort |uniq -c
!note
现象说明:
- 滚动重启过程中,Service 会动态更新 Endpoints 后端池:旧 Pod 进入终止状态时自动从后端摘除,新 Pod 就绪后自动加入,因此业务访问不会中断。
- 重启完成后,所有 Pod 均为基于镜像重建的新容器,之前通过
kubectl exec写入容器的自定义首页会丢失,响应会恢复为 httpd 默认的It works!页面。- 新建 Pod 的名称后缀会更新,不再是重启前的旧 Pod 名称。
- 如果创建的pod具有标签app1=web1和app2=web2,而deploy控制器的selector匹配的标签为app1=web1,service匹配的标签为app2=web2也是可以的。
bash
# 清理当前实验环境,准备重建Deployment和Service
# 强制删除web Deployment,级联清除其管理的ReplicaSet和所有关联Pod
root@master30:~# kubectl delete deployments.apps web --force
# 删除web Service资源,清理服务访问入口与后端端点配置
root@master30:~# kubectl delete service web
# 编辑部署清单文件,调整配置后用于重新创建资源
root@master30:~# vim deploy-web.yml
yaml
apiVersion: apps/v1 # API版本,Deployment归属apps/v1稳定API组
kind: Deployment # 资源类型,无状态应用部署控制器,负责Pod副本管理与滚动更新
metadata: # 元数据区块,定义资源的名称、标签等标识属性
creationTimestamp: null # 创建时间戳,dry-run生成时留空,正式提交集群后由APIServer自动填充
labels: # 资源自身标签,用于分类筛选该Deployment资源
app: web # 业务标识标签
name: web # 资源名称,命名空间内唯一标识该Deployment
spec: # 资源规格定义,Deployment的核心配置区块
replicas: 2 # 期望Pod副本数量,控制器持续维持该数量
selector: # 标签选择器,匹配该Deployment管控的Pod,创建后不可修改
matchLabels: # 精确匹配规则,仅管理包含对应键值对标签的Pod
app1: web1 # 匹配标签键值,Pod必须包含该标签才会被纳入管控
strategy: {} # 更新策略配置,空值使用默认滚动更新策略
template: # Pod模板,控制器基于该模板创建、扩容、更新Pod
metadata: # Pod元数据,字段含义同顶级metadata
creationTimestamp: null
labels: # Pod标签,必须完整包含selector全部键值对,支持额外扩展
app1: web1 # 核心匹配标签,与selector对应
app2: web2 # 扩展标签,可用于多维度筛选,不影响归属关系
spec: # Pod运行规格,字段含义同Pod资源spec
containers: # 容器列表,定义Pod内运行的容器配置
- image: hub.cyw.cloud/library/httpd # 容器镜像地址,私有仓库中的httpd服务镜像
name: httpd # 容器名称,Pod范围内唯一
imagePullPolicy: IfNotPresent # 镜像拉取策略,节点本地已有该镜像则不重复拉取
resources: {} # 容器资源配额,空值表示不限制CPU、内存使用
status: {} # 资源运行状态,由集群自动维护,手动编写无需填写
!important
核心注意点:
- selector 采用子集匹配规则 :Pod 标签只要完整包含 selector 的所有键值对,就会被该 Deployment 管控;额外新增的
app2: web2不影响归属关系。- selector 在 Deployment 创建后不可修改,仅可修改 Pod 模板内的标签。
- 若后续创建 Service 对接该应用,Service 的 selector 必须使用
app1: web1才能匹配到后端 Pod,使用app: web会匹配失败。
bash
root@master30:~# kubectl apply -f deploy-web.yml
# 通过expose方式创建service
root@master30:~# kubectl expose deployment web --port=8080 --target-port=80 --selector=app2=web2
# 选项说明:
# --port=8080,定义service监听的端口
# --target-port=80,定义后端pod鉴定的端口
# --selector=app2=web2,定义service选择器标签
# 查看web Service的集群IP与后端端点列表,过滤只显示关键行
root@master30:~# kubectl describe svc web |grep -e IP: -e Endpoints
IP: 10.101.131.56 # Service集群虚拟IP,集群内部访问地址
Endpoints: 10.224.193.73:80,10.224.41.136:80 # 后端Pod的IP+端口列表,共2个后端实例
# 访问Service集群IP+端口,验证服务流量转发是否正常
root@master30:~# curl 10.101.131.56:8080
<html><body><h1>It works!</h1></body></html> # 返回httpd默认首页,服务转发正常
- 无法ping通service ip,但可以ping通pod。service只允许http方式访问80,其他没有做iptables映射。
yaml 文件创建
bash
# 删除已有的web Service资源,清理旧服务配置
root@master30:~# kubectl delete svc web
# 客户端模拟创建ClusterIP类型Service,输出yaml格式并重定向保存到文件,不实际提交到集群
root@master30:~# kubectl create service clusterip web --tcp=8080:80 -o yaml --dry-run=client > svc-web.yml
# 查看生成的Service清单文件内容
root@master30:~# cat svc-web.yml
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,用于服务发现与后端负载均衡
metadata: # 元数据区块,定义资源的名称、标签等标识属性
creationTimestamp: null # 创建时间戳,dry-run生成留空,正式提交集群后自动填充
labels: # 资源自身标签,用于分类筛选该Service
app: web # 业务标识标签,默认与服务名称同名
name: web # 服务名称,命名空间内唯一标识该Service
spec: # 服务规格核心配置区块
ports: # 端口列表,定义服务的端口映射规则
- name: 8080-80 # 端口条目名称,用于标识该端口映射
port: 8080 # 服务监听端口,集群内部访问Service的端口
protocol: TCP # 传输层协议类型
targetPort: 80 # 后端Pod的目标端口,流量转发到Pod的该端口
selector: # 标签选择器,匹配后端Pod的标签
app: web # 匹配规则,仅选择带app=web标签的Pod作为后端
type: ClusterIP # 服务类型,ClusterIP仅分配集群内部虚拟IP,仅集群内可访问
status: # 资源运行状态,由集群自动维护
loadBalancer: {} # 负载均衡器状态,ClusterIP类型无对应配置,留空
Service 发现
所谓发现 Service,是指集群内应用访问 Service。
我们介绍以下三种方式发现 Service:
- 通过 IP 访问 Service。
- 通过环境变量访问 Service。
- 通过 dns 解析的名称访问 Service。
通过 IP 访问 Service
实验准备:mysql+wordpress
准备mysql资源
bash
# 客户端模拟创建mysql Pod;配置数据库初始化环境变量,输出yaml清单到本地,不实际提交集群
root@master30:~# kubectl run mysql --image=hub.cyw.cloud/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
# 应用yaml清单,正式创建mysql Pod
root@master30:~# kubectl apply -f pod-mysql.yaml
# 暴露mysql Pod生成ClusterIP Service;映射服务端口3306到Pod目标端口3306
root@master30:~# kubectl expose pod mysql --port=3306 --target-port=3306
# 查看Service资源列表,确认mysql服务创建状态
root@master30:~# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysql ClusterIP 10.111.69.45 <none> 3306/TCP 25m # mysql服务集群IP为10.111.69.45,监听3306端口
# 安装mysql客户端工具,用于数据库连接测试
root@master30:~# apt install -y mysql-client
# 通过Service集群IP连接mysql;禁用SSL,执行查询验证数据库初始化结果
root@master30:~# mysql -u tom -predhat -h 10.111.69.45 --ssl-mode=DISABLED -e 'show databases;'
mysql: [Warning] Using a password on the command line interface can be insecure. # 命令行明文密码的安全提示
+--------------------+
| Database |
+--------------------+
| information_schema | # MySQL系统默认数据库
| blog | # 初始化时指定创建的业务数据库,环境变量配置生效
+--------------------+
准备WordPress资源
bash
# 创建wordpress Pod;配置数据库连接环境变量,数据库地址指向mysql服务的集群IP
root@master30:~# kubectl run wordpress \
--image=hub.cyw.cloud/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.111.69.45
# 暴露wordpress Pod生成Service;指定类型为NodePort,映射服务80端口到Pod的80端口
root@master30:~# kubectl expose pod wordpress --port=80 --target-port=80 --type NodePort
# 查看Service资源列表,确认服务类型与端口映射
root@master30:~# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
mysql ClusterIP 10.111.69.45 <none> 3306/TCP 41m # mysql为内部ClusterIP服务,仅集群内可访问
wordpress NodePort 10.106.106.246 <none> 80:31102/TCP 3s # wordpress为NodePort类型,随机分配节点端口31102,支持集群外访问
# 集群外客户端通过节点IP+NodePort端口访问wordpress页面
root@client:~# firefox 10.1.8.30:31102
通过环境变量访问 Service
获取环境变量信息
bash
# 创建临时交互式测试Pod;--rm退出后自动删除,-it分配终端,进入busybox容器执行shell
root@master30:~# kubectl run test --rm -it --image=hub.cyw.cloud/library/busybox --image-pull-policy=IfNotPresent sh
If you don't see a command prompt, try pressing enter. # 终端提示,无提示符时按回车即可
# 过滤查看MYSQL相关环境变量;K8s会自动为同命名空间下的Service注入服务发现环境变量
/ # env|grep MYSQL
MYSQL_PORT_3306_TCP_ADDR=10.111.69.45 # mysql Service的集群IP地址
MYSQL_PORT_3306_TCP_PORT=3306 # mysql Service的服务监听端口
MYSQL_SERVICE_HOST=10.111.69.45 # 标准服务主机变量,值为Service集群IP
MYSQL_PORT_3306_TCP_PROTO=tcp # 服务使用的传输层协议类型
MYSQL_PORT=tcp://10.111.69.45:3306 # 完整的TCP服务连接地址
MYSQL_SERVICE_PORT=3306 # 标准服务端口变量
MYSQL_PORT_3306_TCP=tcp://10.111.69.45:3306 # 带协议的完整服务端点地址
# 退出容器终端;配合--rm参数,退出后Pod会自动被集群清理
/ # exit
Session ended, resume using 'kubectl attach test -c test -i -t' command when the pod is running # 会话结束提示,Pod运行时可通过attach重连
pod "test" deleted # Pod已自动删除,--rm参数生效
说明:
- 可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。
- service创建后才能使用该环境变量。
- service属于namespace,pod只能访问同一个namespace中service。
准备WordPress资源
bash
# 强制删除wordpress Pod;清理旧实例,准备重新创建
root@master30:~# kubectl delete pod wordpress --force
# 重建wordpress Pod;使用$(VAR)语法引用K8s自动注入的Service环境变量,单引号避免宿主机shell提前解析
root@master30:~# kubectl run wordpress \
--image=hub.cyw.cloud/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)'
# 进入wordpress容器执行命令;过滤查看MYSQL相关环境变量,验证服务发现变量注入生效
root@master30:~# kubectl exec -it wordpress -- sh -c 'env|grep MYSQL'
MYSQL_PORT_3306_TCP_ADDR=10.111.69.45
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_PORT_3306_TCP_PROTO=tcp
MYSQL_SERVICE_HOST=10.111.69.45 # mysql服务集群IP,已被WORDPRESS_DB_HOST变量引用
MYSQL_PORT=tcp://10.111.69.45:3306
MYSQL_SERVICE_PORT=3306
MYSQL_PORT_3306_TCP=tcp://10.111.69.45:3306
# 集群外客户端访问wordpress;通过节点IP+NodePort端口访问Web页面
root@client:~# firefox 10.1.8.30:31102
通过 DNS 名称访问 Service
Kubernetes 还提供了更为方便的DNS访问。kubeadm部署时会默认安装coredns组件。
bash
# 查询指定命名空间的Deployment资源;--namespace指定目标命名空间为系统命名空间kube-system
root@master30:~# kubectl get deployments.apps --namespace=kube-system
NAME READY UP-TO-DATE AVAILABLE AGE
calico-kube-controllers 1/1 1 1 2d23h # Calico网络控制平面组件,1个副本全部就绪运行
coredns 2/2 2 2 3d # 集群核心DNS解析服务,2个副本全部就绪运行
coredns是一个DNS服务器。 每当有新的Service被创建, coredns会添加该Service的DNS记录。 Cluster中的Pod可以通过<SERVICE_NAME>.<NAMESPACE_NAME>访问Service。
bash
# 创建临时交互式busybox测试Pod;--rm退出后自动删除,-it分配终端,进入容器执行shell
root@master30:~# kubectl run busybox --rm -it --image=hub.cyw.cloud/library/busybox /bin/sh
If you don't see a command prompt, try pressing enter. # 终端提示,无提示符时按回车激活
# 查看容器内DNS解析配置文件;验证集群DNS服务地址与搜索域规则
/ # cat /etc/resolv.conf
nameserver 10.96.0.10 # 集群核心DNS服务地址,即CoreDNS的固定ClusterIP
search service.svc.cluster.local svc.cluster.local cluster.local cyw.cloud # DNS搜索域列表,访问短域名时自动按顺序补全后缀
options ndots:5 # 域名中点号数量小于5时,优先通过搜索域补全后解析
# 使用服务短域名访问wordpress服务;验证集群DNS解析能力与服务连通性
/ # wget wordpress.service:80
Connecting to wordpress.service:80 (10.106.106.246:80) # DNS解析成功,对应wordpress服务的ClusterIP
Connecting to wordpress.service:80 (10.106.106.246:80)
saving to 'index.html'
index.html 100% |***************************************| 11607 0:00:00 ETA
'index.html' saved # 页面文件下载成功,服务访问与DNS解析均正常
由于这个Pod与web service同属于cyw namespace, 因此可以省略cyw直接用web访问Service.
bash
# 删除之前下载的测试文件index.html,清理当前目录环境
/ # rm index.html
# 使用服务名称短域名访问wordpress的80端口;同命名空间下可直接用服务名完成DNS解析
/ # wget wordpress:80
Connecting to wordpress:80 (10.106.106.246:80) # 短域名通过搜索域自动补全后缀,解析到wordpress服务对应的ClusterIP
Connecting to wordpress:80 (10.106.106.246:80)
saving to 'index.html'
index.html 100% |***************************************| 11571 0:00:00 ETA
'index.html' saved # 页面文件下载成功,同命名空间内服务短域名访问正常
10.96.0.10是哪个DNS服务器呢?
bash
# 查看系统命名空间的Service资源列表;--namespace指定查询的目标命名空间
root@master30:~# 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 3d2h # 集群核心DNS服务,固定ClusterIP为10.96.0.10,提供域名解析与指标采集端口
接上面的WordPress示例
bash
# 强制删除旧wordpress Pod;--force强制立即删除,跳过优雅终止流程
root@master30:~# kubectl delete pod wordpress --force
# 重建wordpress Pod;数据库地址填写同命名空间服务名mysql,依靠集群DNS自动解析
root@master30:~# kubectl run wordpress \
--image=hub.cyw.cloud/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+NodePort端口访问wordpress前端页面
root@client:~# firefox 10.1.8.30:31102
# 清理实验环境;批量删除mysql、wordpress两个Service资源
root@master30:~# kubectl delete svc mysql wordpress
# 批量删除mysql、wordpress两个Pod资源,完成环境清空
root@master30:~# kubectl delete pod mysql wordpress
Service 类型
Kubernetes Service 支持以下四种类型:
-
ClusterIP:只能在集群内部访问。
-
NodePort:通过物理节点的端口来访问,每个物理节点都提供相同的端口。
-
LoadBalancer:负载均衡,来源于物理网段中一个独立的IP。
-
ExternalName:配置集群内部的CName。
-
Headless:只有服务名,不分配IP地址。
我们的应用可能希望将 Service 暴露在一个外部 IP 地址上。 Kubernetes 支持两种实现方式:NodePort 和 LoadBalancer。
环境准备
创建 deployment
bash
# 创建名为web的Deployment资源;指定httpd服务镜像,设置期望副本数为2
root@master30:~# kubectl create deployment web --image=hub.cyw.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-rangeCIDR 范围内。 如果你尝试创建一个带有非法clusterIP地址值的 Service,API 服务器会返回 HTTP 状态码 422, 表示值不合法。
其他信息参考上面的 发现 Service 章节。
NodePort
-
如果将Service 的
type字段设置为NodePort,则 Kubernetes 控制平面将在--service-node-port-range标志所指定的范围内分配端口(默认值:30000-32767 )。 Service 在其.spec.ports[*].nodePort字段中报告已分配的端口。 -
NodePort类型的 Service 通过每个节点上的 IP 和分配的端口(NodePort)公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 NodePort 类型的Service 配置 clusterIP 地址。每个节点将该端口(每个节点上的相同端口号)上的流量代理到 Service。
示例:
bash
# 基于web Deployment生成NodePort类型Service清单;客户端模拟不提交集群,输出yaml格式重定向保存到service-NodePort.yaml
root@master30:~# kubectl expose deployment web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-NodePort.yaml
# 编辑生成的Service配置文件;可调整端口、选择器、节点端口等参数后再应用
root@master30:~# vim service-NodePort.yaml
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,提供服务发现与后端负载均衡能力
metadata: # 元数据区块,定义资源的名称、标签等标识属性
creationTimestamp: null # 创建时间戳,dry-run生成时留空,正式提交集群后自动填充
labels: # 资源自身标签,用于分类筛选该Service
app: web # 业务标识标签,默认与后端Deployment标签对齐
name: web # 服务名称,命名空间内唯一标识该Service
spec: # 服务核心规格配置区块
ports: # 端口映射规则列表,定义服务端口与后端Pod端口的对应关系
- port: 8080 # 服务监听端口,集群内部访问Service时使用的端口
protocol: TCP # 传输层协议类型
targetPort: 80 # 后端Pod的目标端口,流量最终转发到Pod的该端口
selector: # 标签选择器,匹配符合标签规则的Pod作为后端节点
app: web # 匹配规则,仅选择带app=web标签的Pod
type: NodePort # 服务类型,在ClusterIP基础上额外在每个节点开放端口,支持集群外部通过节点IP访问
status: # 资源运行状态,由集群控制器自动维护
loadBalancer: {} # 负载均衡器状态,NodePort类型无对应配置,保持空值
bash
# 创建NodePort类型Service
root@master30:~# kubectl apply -f service-NodePort.yaml
# 查看node节点对应端口为31917
root@master30:~# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web NodePort 10.110.40.0 <none> 8080:31917/TCP 48s
# 说明:
# 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:31917
<html><body><h1>It works!</h1></body></html>
root@client:~# curl http://10.1.8.31:31917
<html><body><h1>It works!</h1></body></html>
root@client:~# curl http://10.1.8.32:31917
<html><body><h1>It works!</h1></body></html>
分析防火墙规则:
bash
# NodePort相比ClusterIP,会在每个节点的KUBE-NODEPORTS链新增节点端口规则;过滤查看31917端口对应的iptables条目
root@master30:~# iptables-save | grep 31917
-A KUBE-NODEPORTS -p tcp -m comment --comment "cyw/web:http" -m tcp --dport 31917 -j KUBE-MARK-MASQ # 匹配访问节点31917端口的TCP流量,打上伪装标记,为后续源地址转换做准备
-A KUBE-NODEPORTS -p tcp -m comment --comment "cyw/web:http" -m tcp --dport 31917 -j KUBE-SVC-WE5D4GWSX3MMPCHH # 将节点端口流量跳转到web服务对应的负载均衡链,转发到后端Pod
# 规则的含义是:访问当前节点31917端口的请求会应用规则KUBE-SVC-WE5D4GWSX3MMPCHH
# 规则逻辑:访问节点31917端口的请求会进入KUBE-SVC服务链,该链负责将流量负载分发到所有后端Pod
# 过滤查看该服务链的完整规则,验证负载均衡的实现方式
root@master30:~# iptables-save |grep KUBE-SVC-WE5D4GWSX3MMPCHH
-A KUBE-SERVICES -d 10.110.40.0/32 -p tcp -m comment --comment "cyw/web:http cluster IP" -m tcp --dport 8080 -j KUBE-SVC-WE5D4GWSX3MMPCHH # ClusterIP入口规则:访问服务集群IP的流量也进入同一条负载均衡链
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "cyw/web:http" -m statistic --mode random --probability 0.50000000000 -j KUBE-SEP-LUIIA2GDKT4VDBA2 # 第一条后端端点:50%概率随机转发到第一个Pod端点,两副本场景下概率为1/2
-A KUBE-SVC-WE5D4GWSX3MMPCHH -m comment --comment "cyw/web:http" -j KUBE-SEP-Y6B6PNSXFIBR4D2K # 第二条后端端点:剩余未匹配流量全部转发到第二个Pod端点,完成流量均分
NodePort默认的是随机选择, 我们可以使用nodePort指定为特定端口。
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,提供服务发现与后端负载均衡能力
metadata: # 元数据区块,定义资源的名称、标签等标识属性
creationTimestamp: null # 创建时间戳,dry-run生成时留空,正式提交集群后自动填充
labels: # 资源自身标签,用于分类筛选该Service
app: web # 业务标识标签,与后端Pod标签对齐
name: web # 服务名称,命名空间内唯一标识该Service
spec: # 服务核心规格配置区块
ports: # 端口映射规则列表,定义服务端口与后端Pod端口对应关系
- port: 8080 # 服务监听端口,集群内部访问Service时使用
protocol: TCP # 传输层协议类型
nodePort: 30080 # 指定固定节点端口,所有节点统一开放该端口,默认合法范围30000-32767
targetPort: 80 # 后端Pod的目标端口,流量最终转发到Pod的该端口
selector: # 标签选择器,匹配符合标签规则的Pod作为后端节点
app: web # 匹配规则,仅选择带app=web标签的Pod
type: NodePort # 服务类型,在ClusterIP基础上开放节点端口,支持集群外部访问
status: # 资源运行状态,由集群控制器自动维护
loadBalancer: {} # 负载均衡器状态,NodePort类型无对应配置,保持空值
端口说明:
- nodePort是节点上监听的端口。
- port是ClusterIP上监听的端口。
- targetPort是Pod监听的端口。
清理环境
bashroot@master30:~# 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
bash
# 下载MetalLB 0.14.8版本安装压缩包,获取官方部署配置文件
root@master30:~# wget https://cyw.cloud/class/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 # 控制器组件镜像,负责IP池管理、服务分配与状态同步
image: quay.io/metallb/speaker:v0.14.8 # 网络宣告组件镜像,节点级运行,发布ARP/BGP路由实现外部可达
# 批量替换镜像仓库前缀,将公网quay.io替换为私有仓库hub.cyw.cloud,规避公网拉取限制
root@master30:~# sed -i 's/quay.io/hub.cyw.cloud/g' metallb-0.14.8/config/manifests/metallb-native.yaml
# 应用部署清单,在集群中创建MetalLB相关命名空间、ServiceAccount与工作负载
root@master30:~# kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
# 查看metallb-system命名空间下所有资源状态;需等待所有Pod进入Running状态再进行后续IP池配置
root@master30:~# kubectl get all -n metallb-system
NAME READY STATUS RESTARTS AGE
pod/controller-786f9df989-98bjh 1/1 Running 0 85s # controller控制器Pod,单副本运行,统筹IP分配逻辑
pod/speaker-gthhx 1/1 Running 0 85s # speaker宣告Pod,DaemonSet调度,每个节点运行一个
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 # 准入校验Webhook服务,校验MetalLB自定义资源合法性
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 3 3 3 3 3 kubernetes.io/os=linux 85s # speaker以DaemonSet部署,自动覆盖所有Linux节点
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 85s # controller以Deployment部署,保障控制器服务可用性
NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-786f9df989 1 1 1 85s # controller对应的副本集,由Deployment自动管理
配置地址池
bash
# 创建MetalLB IP地址池配置文件;单引号包裹EOF避免shell变量解析,配置写入ippool.yaml
root@master30:~# cat << 'EOF' > ippool.yaml
apiVersion: metallb.io/v1beta1 # MetalLB自定义资源API版本,IP地址池所属API组
kind: IPAddressPool # 资源类型,定义MetalLB可分配的负载均衡IP地址池
metadata: # 元数据区块,定义资源标识属性
name: first-pool # 地址池名称,后续宣告配置需引用该名称
namespace: metallb-system # 资源必须部署在metallb-system命名空间下
spec: # 规格定义,地址池核心配置区块
addresses: # 可分配的IP地址列表,支持网段或起止范围格式
- 10.1.8.40-10.1.8.80 # IP地址范围,共41个可用地址,分配给LoadBalancer类型Service
EOF
# 应用IP地址池配置文件,使地址池规则在集群中正式生效
root@master30:~# kubectl apply -f ippool.yaml
配置 lay2
bash
# 创建MetalLB二层宣告配置文件;L2Advertisement用于二层网络场景对外宣告负载均衡IP,未指定地址池时默认关联所有IPAddressPool
root@master30:~# cat << 'EOF' > L2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
EOF
# 应用二层宣告配置,正式启用ARP模式IP宣告,LoadBalancer类型Service即可从地址池分配外部访问IP
root@master30:~# kubectl apply -f L2.yaml
测试
bash
# 基于web Deployment生成LoadBalancer类型Service清单;指定服务类型为LoadBalancer,客户端模拟不提交集群,输出yaml格式保存到文件
root@master30:~# kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml
# 查看生成的LoadBalancer类型Service配置文件的完整内容
root@master30:~# cat service-LoadBalancer.yaml
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,提供服务发现与后端负载均衡能力
metadata: # 元数据区块,定义资源的名称、标签等标识属性
creationTimestamp: null # 创建时间戳,dry-run生成时留空,正式提交后自动填充
labels: # 资源自身标签,用于分类筛选该Service
app: web # 业务标识标签,与后端Deployment标签对齐
name: web # 服务名称,命名空间内唯一标识该Service
spec: # 服务核心规格配置区块
ports: # 端口映射规则列表,定义服务端口与后端Pod端口对应关系
- port: 80 # 服务监听端口,集群内部访问Service时使用
protocol: TCP # 传输层协议类型
targetPort: 80 # 后端Pod的目标端口,流量最终转发到Pod的该端口
selector: # 标签选择器,匹配符合标签规则的Pod作为后端节点
app: web # 匹配规则,仅选择带app=web标签的Pod
type: LoadBalancer # 服务类型,对接MetalLB分配外部可达IP,支持集群外直接通过该IP访问服务
status: # 资源运行状态,由集群控制器自动维护
loadBalancer: {} # 负载均衡器状态,IP分配成功后会自动填充外部地址,dry-run阶段留空
bash
# 应用LoadBalancer类型Service配置;MetalLB自动从IP地址池分配外部可达IP
root@master30:~# kubectl apply -f service-LoadBalancer.yaml
# 查看Service资源列表,确认外部IP分配结果
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 # 已分配外部IP 10.1.8.40,同时保留NodePort端口作为节点访问入口
# 集群外客户端通过LoadBalancer外部IP+服务端口访问;无需使用节点端口,直接访问80端口即可
[root@client ~]# curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html> # 返回httpd默认首页,LoadBalancer外部访问链路正常
# 单行书写的批量修改脚本;do关键字缺少分隔符,触发shell语法错误
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
-bash: syntax error near unexpected token `done' # 语法报错,do需单独换行或用分号与前置语句分隔
# 修正格式后的for循环;分行书写语法规范,遍历所有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
# 循环持续访问LoadBalancer外部IP,每秒请求一次,验证流量负载分发效果
root@master30:~# while true;do curl -s 10.1.8.40;sleep 1;done
web-5885dfff8c-wmrvl
web-5885dfff8c-wmrvl
web-5885dfff8c-wmrvl
web-5885dfff8c-pdq8x
web-5885dfff8c-pdq8x
web-5885dfff8c-wmrvl
... # L2模式下IP固定宣告到单个节点,流量先集中到该节点再经kube-proxy分发,短时间内会出现同一Pod响应多次的现象
总结
- 客户端流量到达pod路径:客户端请求 → MetalLB 虚拟 IP(LB IP) → 节点 kube-proxy → Service 转发 → 后端 Pod。
- ✅ MetalLB 只做 ARP 宣告 + IP 占坑,转发全靠 kube-proxy + Service。
- MetalLB + Service 是标准 K8s 负载均衡流程。
详细流程图
#mermaid-svg-DcPRxujYlHorz88G{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-DcPRxujYlHorz88G .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-DcPRxujYlHorz88G .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-DcPRxujYlHorz88G .error-icon{fill:#552222;}#mermaid-svg-DcPRxujYlHorz88G .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-DcPRxujYlHorz88G .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-DcPRxujYlHorz88G .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-DcPRxujYlHorz88G .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-DcPRxujYlHorz88G .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-DcPRxujYlHorz88G .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-DcPRxujYlHorz88G .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-DcPRxujYlHorz88G .marker{fill:#333333;stroke:#333333;}#mermaid-svg-DcPRxujYlHorz88G .marker.cross{stroke:#333333;}#mermaid-svg-DcPRxujYlHorz88G svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-DcPRxujYlHorz88G p{margin:0;}#mermaid-svg-DcPRxujYlHorz88G .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-DcPRxujYlHorz88G .cluster-label text{fill:#333;}#mermaid-svg-DcPRxujYlHorz88G .cluster-label span{color:#333;}#mermaid-svg-DcPRxujYlHorz88G .cluster-label span p{background-color:transparent;}#mermaid-svg-DcPRxujYlHorz88G .label text,#mermaid-svg-DcPRxujYlHorz88G span{fill:#333;color:#333;}#mermaid-svg-DcPRxujYlHorz88G .node rect,#mermaid-svg-DcPRxujYlHorz88G .node circle,#mermaid-svg-DcPRxujYlHorz88G .node ellipse,#mermaid-svg-DcPRxujYlHorz88G .node polygon,#mermaid-svg-DcPRxujYlHorz88G .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-DcPRxujYlHorz88G .rough-node .label text,#mermaid-svg-DcPRxujYlHorz88G .node .label text,#mermaid-svg-DcPRxujYlHorz88G .image-shape .label,#mermaid-svg-DcPRxujYlHorz88G .icon-shape .label{text-anchor:middle;}#mermaid-svg-DcPRxujYlHorz88G .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-DcPRxujYlHorz88G .rough-node .label,#mermaid-svg-DcPRxujYlHorz88G .node .label,#mermaid-svg-DcPRxujYlHorz88G .image-shape .label,#mermaid-svg-DcPRxujYlHorz88G .icon-shape .label{text-align:center;}#mermaid-svg-DcPRxujYlHorz88G .node.clickable{cursor:pointer;}#mermaid-svg-DcPRxujYlHorz88G .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-DcPRxujYlHorz88G .arrowheadPath{fill:#333333;}#mermaid-svg-DcPRxujYlHorz88G .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-DcPRxujYlHorz88G .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-DcPRxujYlHorz88G .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DcPRxujYlHorz88G .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-DcPRxujYlHorz88G .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DcPRxujYlHorz88G .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-DcPRxujYlHorz88G .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-DcPRxujYlHorz88G .cluster text{fill:#333;}#mermaid-svg-DcPRxujYlHorz88G .cluster span{color:#333;}#mermaid-svg-DcPRxujYlHorz88G div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-DcPRxujYlHorz88G .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-DcPRxujYlHorz88G rect.text{fill:none;stroke-width:0;}#mermaid-svg-DcPRxujYlHorz88G .icon-shape,#mermaid-svg-DcPRxujYlHorz88G .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-DcPRxujYlHorz88G .icon-shape p,#mermaid-svg-DcPRxujYlHorz88G .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-DcPRxujYlHorz88G .icon-shape .label rect,#mermaid-svg-DcPRxujYlHorz88G .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-DcPRxujYlHorz88G .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-DcPRxujYlHorz88G .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-DcPRxujYlHorz88G :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 访问 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
ExternalName,将服务映射到 externalName 字段的内容(例如,api.foo.bar.example)。 该映射将集群的 DNS 服务器配置为返回具有该外部主机名值的 CNAME 记录。 集群不会为之创建任何类型代理。
例如,将当前命名空间中的 www 服务映射到 www.cyw.cloud。
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,提供服务发现与域名映射能力
metadata: # 元数据区块,定义资源的标识属性
name: www # 服务名称,集群内部访问时使用的服务域名
spec: # 服务核心规格配置区块
type: ExternalName # 服务类型,通过CNAME记录将集群内服务名映射到外部域名
externalName: www.cyw.cloud # 外部目标域名,访问服务时DNS会返回该域名的解析结果
当查找主机 www.当前命名空间名称.svc.cluster.local 时,集群 DNS 服务返回 CNAME 记录, 其值为 www.cyw.cloud。访问 www 的方式与访问其他 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: ExternalNameService,查找和配置其 CNAME 记录; -
对所有其他类型的 Service,针对 Service 的就绪端点的所有 IP 地址,查找和配置 DNS A/AAAA 记录:对于 IPv4 端点,DNS 系统创建 A 记录;对于 IPv6 端点,DNS 系统创建 AAAA 记录。
当你定义无选择算符的无头 Service 时,
port必须与targetPort匹配。
-
Service 会话保持
会话保持介绍
如果要确保来自特定客户端的连接每次都传递给同一个 Pod, 你可以通过设置 Service 的 .spec.sessionAffinity 为 ClientIP 来设置基于客户端 IP 地址的会话亲和性(默认为 None)。
你还可以通过设置 Service 的 .spec.sessionAffinityConfig.clientIP.timeoutSeconds 来设置最大会话粘性时间(默认值为 10800,即 3 小时)。
工作原理:
- 客户端首次访问服务,Service将请求转发给某个Pod,Service记录客户端和Pod的对应关系。
- 客户端的后续请求继续转发给同一个Pod。
适合有状态服务(如:Java Web、WebSocket、游戏服务):
- 登录状态不丢失
- 购物车不丢失
- 长连接稳定
准备测试环境
bash
# 创建web应用Deployment;指定httpd服务镜像,设置期望副本数为2
root@master30:~# kubectl create deployment web --image=hub.cyw.cloud/library/httpd --replicas=2
# 暴露web Deployment生成ClusterIP Service;--port指定服务端口为80,默认与Pod目标端口一致
root@master30:~# kubectl expose deployment web --port 80
# 遍历所有Pod批量修改首页内容;将Pod自身名称写入httpd默认首页文件,用于区分后端节点
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
# 查看web Service资源信息;确认服务类型、集群IP与端口配置
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 # ClusterIP类型服务,固定集群IP为10.111.90.183,监听80端口
# 连续20次访问服务集群IP;统计各Pod响应次数,验证负载均衡分发效果
root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
10 web-6db76cb4fc-5gbx2 # 两个后端Pod各响应10次,流量完全均分
10 web-6db76cb4fc-b8f7l
设置会话保持
bash
# 补丁更新web Service配置;设置会话亲和性为ClientIP,基于客户端IP实现会话保持,同一IP的请求始终转发到同一个后端Pod
root@master30:~# kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'
# 连续20次访问服务集群IP;统计各Pod响应次数,验证会话亲和性生效效果
root@master30:~# for i in {1..20};do curl -s 10.111.90.183;done|sort |uniq -c
20 web-6db76cb4fc-5gbx2 # 全部请求都落在同一个Pod,ClientIP会话保持策略生效
会话保持与 kube-proxy IPVS 的关系
1. service 里的 sessionAffinity 优先级最高
service 配置 ClientIP → kube-proxy 自动使用 IPVS 的 SH 算法
- SH = Source Hashing 源地址哈希
- 保证同一 IP → 同一 Pod
2. 如果 service 不配置 sessionAffinity
kube-proxy 就用ipvs scheduler 里设置的算法:
- rr(轮询)
- lc(最少连接)
- wrr(加权轮询)
金丝雀发布
学习参考:金丝雀部署
环境准备:
bash
# 创建web目录,用于存放生成ConfigMap的素材文件
root@master30:~# mkdir web
# 写入测试页面内容到web目录下的html文件,模拟不同版本的静态资源
root@master30:~# echo hello nginx 1.28 > web/index28.html
root@master30:~# echo hello nginx 1.29 > web/index29.html
# 基于本地目录创建ConfigMap资源;--from-file将目录下每个文件转为独立的键值对条目
root@master30:~# kubectl create configmap web --from-file=./web
# 查看web ConfigMap的yaml格式内容;过滤data段及后续4行,验证文件内容导入结果
root@master30:~# kubectl get configmaps web -o yaml |grep ^data -A4
data: # ConfigMap数据存储段,所有配置键值对均在此区块
index28.html: |
hello nginx 1.28 # 文件名作为键,文件内容作为对应的值
index29.html: |
hello nginx 1.29
使用金丝雀发布部署应用新版本 ,同时保留用旧版本。 这样,新版本在完全发布之前也可以接收实时的生产流量。
例如,你可以使用 track 标签来区分不同的版本。
- 主要稳定的发行版将有一个
track标签,其值为stable:
bash
root@master30:~# vim webapp-1.28.yaml
yaml
apiVersion: apps/v1 # API版本,Deployment归属apps/v1稳定API组
kind: Deployment # 资源类型,无状态应用部署控制器
metadata: # 元数据区块,定义资源标识属性
labels: # 资源自身标签,用于分类筛选
app: web # 业务标识标签
name: web-28 # 资源名称,标识该部署为nginx 1.28版本
spec: # 核心规格配置区块
replicas: 10 # 期望副本数,持续维持10个Pod实例
selector: # 标签选择器,匹配管控的Pod,创建后不可修改
matchLabels: # 精确匹配规则,Pod需包含全部指定标签才会被管控
app: web # 匹配标签1:业务标识
tier: frontend # 匹配标签2:层级标识,前端层
track: stable # 匹配标签3:发布轨道标识,稳定版
template: # Pod模板,控制器基于该模板创建、扩容Pod
metadata: # Pod元数据,字段含义同顶级metadata
labels: # Pod标签,必须完整包含selector全部键值对
app: web
tier: frontend
track: stable
spec: # Pod运行规格配置
containers: # 容器列表,定义Pod内运行的容器配置
- image: hub.cyw.cloud/library/nginx:1.28 # 容器镜像,1.28版本nginx服务
name: nginx # 容器名称,Pod范围内唯一
imagePullPolicy: IfNotPresent # 镜像拉取策略,本地已存在镜像则不重复拉取
ports: # 容器暴露端口列表
- containerPort: 80 # 容器监听端口,对应nginx默认服务端口
volumeMounts: # 卷挂载配置,将存储卷映射到容器内指定路径
- name: webcontent # 挂载的卷名称,需与下方volumes定义的名称对应
mountPath: "/usr/share/nginx/html" # 容器内挂载路径,对应nginx静态资源目录
volumes: # 存储卷列表,定义Pod可使用的存储资源
- name: webcontent # 卷名称,供容器挂载时引用
configMap: # 卷类型为ConfigMap,将配置资源以文件形式挂载为卷
name: web # 引用的ConfigMap资源名称
items: # 指定挂载的键值条目,支持自定义挂载后的文件名
- key: index28.html # ConfigMap中的源数据键名
path: index.html # 挂载到容器内的最终文件名称
bash
root@master30:~# kubectl apply -f webapp-1.28.yaml
- 创建 service
bash
root@master30:~# vim webapp-svc.yaml
yaml
apiVersion: v1 # API版本,Service归属core/v1核心API组
kind: Service # 资源类型,提供服务发现与后端负载均衡能力
metadata: # 元数据区块,定义资源的名称、标签等标识属性
labels: # 资源自身标签,用于分类筛选该Service
app: web # 业务标识标签
name: web # 服务名称,命名空间内唯一标识
spec: # 服务核心规格配置区块
ports: # 端口映射规则列表,定义服务与后端Pod的端口对应关系
- port: 80 # 服务监听端口,集群内部访问时使用
protocol: TCP # 传输层协议类型
targetPort: 80 # 后端Pod的目标端口,流量最终转发到该端口
selector: # 标签选择器,匹配符合规则的Pod作为后端节点
app: web # 匹配业务标签为web的Pod
tier: frontend # 匹配层级标签为frontend的Pod,缩小后端选择范围
bash
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
- 部署应用新版本。新的发行版将有一个
track标签,其值为canary:
bash
root@master30:~# vim webapp-1.29.yaml
yaml
apiVersion: apps/v1 # API版本,Deployment归属apps/v1稳定API组
kind: Deployment # 资源类型,无状态应用部署控制器
metadata: # 元数据区块,定义资源的名称、标签等标识属性
labels: # 资源自身标签,用于分类筛选该Deployment
app: web # 业务标识标签
name: web-29 # 部署名称,标识为nginx 1.29金丝雀版本
spec: # 核心规格配置区块
replicas: 1 # 期望副本数,金丝雀版本仅1实例,分流少量流量
selector: # 标签选择器,匹配管控的Pod,创建后不可修改
matchLabels: # 精确匹配规则,Pod需包含全部指定标签才会被管控
app: web
tier: frontend # 层级标识,前端服务层
track: canary # 发布轨道标识,标记为金丝雀测试版本
template: # Pod模板,控制器基于该模板创建、扩容Pod
metadata: # Pod元数据配置
labels: # Pod标签,必须完整包含selector全部键值对
app: web
tier: frontend
track: canary
spec: # Pod运行规格配置
containers: # 容器列表,定义Pod内运行的容器配置
- image: hub.cyw.cloud/library/nginx:1.29 # 容器镜像,1.29版本nginx,金丝雀测试版本
name: nginx # 容器名称,Pod范围内唯一
imagePullPolicy: IfNotPresent # 镜像拉取策略,本地已有镜像则不重复拉取
ports: # 容器暴露端口列表
- containerPort: 80 # 容器监听端口,对应nginx默认服务端口
volumeMounts: # 卷挂载配置,将存储卷映射到容器内指定路径
- name: webcontent # 挂载的卷名称,需与下方volumes定义的名称对应
mountPath: "/usr/share/nginx/html" # 容器内挂载路径,对应nginx静态资源目录
volumes: # 存储卷列表,定义Pod可使用的存储资源
- name: webcontent # 卷名称,供容器挂载时引用
configMap: # 卷类型为ConfigMap,将配置资源以文件形式挂载为卷
name: web # 引用的ConfigMap资源名称
items: # 指定挂载的键值条目,支持自定义挂载后的文件名
- key: index29.html # ConfigMap中的源数据键名,对应1.29版本测试页面
path: index.html # 挂载到容器内的最终文件名称
bash
root@master30:~# kubectl apply -f webapp-1.29.yaml
验证访问比例:
bash
# 调整web-28稳定版Deployment的副本数为8;缩减原10副本规模,为金丝雀版本释放流量配额
root@master30:~# kubectl scale deployment web-28 --replicas 8
# 调整web-29金丝雀版Deployment的副本数为2;提升金丝雀版本的流量占比,逐步扩大验证范围
root@master30:~# kubectl scale deployment web-29 --replicas 2
# 连续50次访问服务集群IP;统计两个版本的响应次数,验证流量分配比例是否符合副本数配比
root@master30:~# for i in {1..50}; do curl -s 10.98.235.36; done | sort -n|uniq -c
39 hello nginx 1.28 # 稳定版响应39次,占比约80%,与8副本的配比基本一致
11 hello nginx 1.29 # 金丝雀版响应11次,占比约20%,与2副本的配比基本一致
- 总pod数量不变的情况下,逐步减少旧版本和增加新版本副本数量,。
bash
# 调整web-28稳定版Deployment副本数为6;进一步缩减稳定版规模,提升金丝雀版本流量占比
root@master30:~# kubectl scale deployment web-28 --replicas 6
# 调整web-29金丝雀版Deployment副本数为4;扩大金丝雀验证范围,流量占比提升至40%
root@master30:~# kubectl scale deployment web-29 --replicas 4
# 连续50次访问服务集群IP;统计两个版本响应次数,验证流量分配比例
root@master30:~# for i in {1..50}; do curl -s 10.98.235.36; done | sort -n|uniq -c
31 hello nginx 1.28 # 稳定版响应31次,占比约62%,与6副本配比基本吻合
19 hello nginx 1.29 # 金丝雀版响应19次,占比约38%,与4副本配比基本吻合
# 实时监控Pod资源状态;每秒刷新一次,观察副本调整过程中Pod的启停状态
root@master30:~# watch -n1 kubectl get pods
环境清理
bash
root@master30:~# kubectl delete ns services