K8s 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

问题总结:

  1. 集群外部客户端需要知道pod运行在哪个主机才能访问pod。
  2. 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

  1. 更改每个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默认采用轮询策略分发流量,实现负载均衡
  1. 此时如果创建一个具有相同标签的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共同分摊流量。
  1. 此时重启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

现象说明:

  1. 滚动重启过程中,Service 会动态更新 Endpoints 后端池:旧 Pod 进入终止状态时自动从后端摘除,新 Pod 就绪后自动加入,因此业务访问不会中断。
  2. 重启完成后,所有 Pod 均为基于镜像重建的新容器,之前通过 kubectl exec 写入容器的自定义首页会丢失,响应会恢复为 httpd 默认的 It works! 页面。
  3. 新建 Pod 的名称后缀会更新,不再是重启前的旧 Pod 名称。
  1. 如果创建的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

核心注意点:

  1. selector 采用子集匹配规则 :Pod 标签只要完整包含 selector 的所有键值对,就会被该 Deployment 管控;额外新增的app2: web2不影响归属关系。
  2. selector 在 Deployment 创建后不可修改,仅可修改 Pod 模板内的标签。
  3. 若后续创建 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默认首页,服务转发正常
  1. 无法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:

  1. 通过 IP 访问 Service。
  2. 通过环境变量访问 Service。
  3. 通过 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参数生效

说明:

  1. 可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。
  2. service创建后才能使用该环境变量。
  3. 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-range CIDR 范围内。 如果你尝试创建一个带有非法 clusterIP 地址值的 Service,API 服务器会返回 HTTP 状态码 422, 表示值不合法。

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

NodePort

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

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

示例:

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监听的端口。

清理环境

bash 复制代码
root@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 接收并响应请求

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

ExternalName

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

例如,将当前命名空间中的 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: ExternalName Service,查找和配置其 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 小时)。

工作原理:

  1. 客户端首次访问服务,Service将请求转发给某个Pod,Service记录客户端和Pod的对应关系。
  2. 客户端的后续请求继续转发给同一个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 标签来区分不同的版本。

  1. 主要稳定的发行版将有一个 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
  1. 创建 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
  1. 部署应用新版本。新的发行版将有一个 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副本的配比基本一致
  1. 总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
相关推荐
choumin2 小时前
C++ 标准容器为什么不能存引用
c++·容器·引用类型
fb_123457 小时前
K8s 网络通关手册:Ingress-Nginx 生产级路由 + Calico 三种隧道模式 + NetworkPolicy 白名单实战
网络·nginx·kubernetes
KIDULT°7 小时前
K8s 网络进阶实战:Ingress-Nginx、Calico 与 NetworkPolicy 实验 + 排坑记录
网络·nginx·云原生·kubernetes
fengkai45458 小时前
7、Kubernetes的Ingress&Network&Scheduler
运维·kubernetes·k8s·ingress·network·scheduler
姜鱼问生1 天前
网页一片白?Docker 跑 Nginx 读不到文件,403 Forbidden 排查实录
nginx·docker·容器
\光辉岁月/1 天前
2.spring5-IOC
java·容器·ioc·注解
java_logo1 天前
Docker 部署 Excalidraw:轻松搭建手绘风格白板
运维·docker·容器·excalidraw·白板·轩辕镜像
程序员清风1 天前
LibreChat 实战:用 Docker 搭建可扩展的多模型对话平台
运维·docker·容器
Web3&Basketball1 天前
从0到1落地AI Agent沙箱隔离Docker实战:踩坑全记录
运维·docker·容器·agent·容器安全·沙箱隔离