08.CronJob和Service

2026-04-27

复习和预习

昨天课堂内容

  1. 综合实验-blog
  2. DaemonSet
  3. Job

课前复习

今天课堂内容

  1. CronJob
  2. service

Controllers

CronJob

学习参考:CronJob

CronJob 介绍

Linux中有cron程序定时执行任务, Kubernetes的CronJob提供了类似的功能, 用于周期性地执行Job,例如备份、生成报告等。

CronJob 创建新的 Job 和(间接)Pod 时,CronJob 的 .metadata.name 是命名这些 Pod 的部分基础。 CronJob 的名称必须是一个合法的 DNS 子域值, 但这会对 Pod 的主机名产生意外的结果。为获得最佳兼容性,名称应遵循更严格的 DNS 标签规则。 即使名称是一个 DNS 子域,它也不能超过 52 个字符。这是因为 CronJob 控制器将自动在你所提供的 Job 名称后附加 11 个字符,并且存在 Job 名称的最大长度不能超过 63 个字符的限制。

CronJob 实践

bash 复制代码
[root@master30 ~]# kubectl create cronjob -h
Create a cronjob with the specified name.

Aliases:
cronjob, cj

Examples:
  # Create a cronjob
  kubectl create cronjob my-job --image=busybox --schedule="*/1 * * * *"
  
  # Create a cronjob with command
  kubectl create cronjob my-job --image=busybox --schedule="*/1 * * * *" -- date

......

Usage:
  kubectl create cronjob NAME --image=image --schedule='0/5 * * * ?' --
[COMMAND] [args...] [flags] [options]

示例:

bash 复制代码
[root@master30 ~]# kubectl create cronjob mycronjob --image=docker.io/library/busybox --schedule='*/2 * * * *' -- echo hello k8s job!

等效的配置文件:

yaml 复制代码
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: mycronjob
spec:
  schedule: "*/2 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: docker.io/library/busybox
            command: ["echo", "hello k8s job! "]
          restartPolicy: Never

等待一段时间,最多创建3个job。

bash 复制代码
[root@master30 ~]# kubectl get all
NAME                           READY   STATUS      RESTARTS   AGE
pod/mycronjob-28301160-gvm2v   0/1     Completed   0          5m18s
pod/mycronjob-28301162-25jll   0/1     Completed   0          3m18s
pod/mycronjob-28301164-h7mf7   0/1     Completed   0          78s

NAME                      SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
cronjob.batch/mycronjob   */2 * * * *   False     0        78s             36m

NAME                           COMPLETIONS   DURATION   AGE
job.batch/mycronjob-28301160   1/1           18s        5m18s
job.batch/mycronjob-28301162   1/1           19s        3m18s
job.batch/mycronjob-28301164   1/1           19s        78s

CronJob 参数

Cron 时间表

.spec.schedule 字段是必需的,格式遵循 Cron 语法。

bash 复制代码
# ┌───────────── 分钟 (0 - 59)
# │ ┌───────────── 小时 (0 - 23)
# │ │ ┌───────────── 月的某天 (1 - 31)
# │ │ │ ┌───────────── 月份 (1 - 12)
# │ │ │ │ ┌───────────── 周的某天 (0 - 6)(周日到周一;在某些系统上,7 也是星期日)
# │ │ │ │ │                          或者是 sun,mon,tue,web,thu,fri,sat
# │ │ │ │ │
# │ │ │ │ │
# * * * * *

例如 0 0 13 * 5 表示此任务必须在每个星期五的午夜以及每个月的 13 日的午夜开始。

任务模板

.spec.jobTemplate ,为 CronJob 创建的 Job 定义模板,它是必需的。它和 Job 的语法完全一样, 只不过它是嵌套的,没有 apiVersionkind。 你可以为模板化的 Job 指定通用的元数据, 例如标签注解。 有关如何编写一个任务的 .spec, 请参考编写 Job 规约

任务延迟开始的最后期限

.spec.startingDeadlineSeconds 字段是可选的。 它表示任务如果由于某种原因错过了调度时间,开始该任务的截止时间的秒数。

  • 过了截止时间,CronJob 就不会开始该任务的实例(未来的任务仍在调度之中)。 例如,你每天运行两次备份任务,允许它最多延迟 8 小时开始,但不能更晚, 因为更晚进行的备份将变得没有意义:你宁愿等待下一次计划的运行。

  • 对于错过已配置的最后期限的 Job,Kubernetes 将其视为失败的任务。 如果你没有为 CronJob 指定 startingDeadlineSeconds,那 Job 就没有最后期限。

  • 如果 .spec.startingDeadlineSeconds 字段被设置(非空), CronJob 控制器将会计算从预期创建 Job 到当前时间的时间差。 如果时间差大于该限制,则跳过此次执行。例如,如果将其设置为 200,则 Job 控制器允许在实际调度之后最多 200 秒内创建 Job。

并发性规则

.spec.concurrencyPolicy 也是可选的。它声明了 CronJob 创建的任务执行时发生重叠如何处理。 仅能声明下列规则中的一种:

  • Allow(默认):CronJob 允许并发任务执行。
  • Forbid: CronJob 不允许并发任务执行;如果新任务的执行时间到了而老任务没有执行完,CronJob 会忽略新任务的执行。
  • Replace:如果新任务的执行时间到了而老任务没有执行完,CronJob 会用新任务替换当前正在运行的任务。

请注意,并发性规则仅适用于相同 CronJob 创建的任务。如果有多个 CronJob,它们相应的任务总是允许并发执行的。

任务历史限制

.spec.successfulJobsHistoryLimit.spec.failedJobsHistoryLimit 字段是可选的。 这两个字段指定应保留多少已完成和失败的任务。 默认设置分别为 3 和 1。将限制设置为 0 代表相应类型的任务完成后不会保留。

有关自动清理任务的其他方式, 参见自动清理完成的 Job

综合案例

案例1:定期清理主机目录内容

每分钟执行一次清空worker31节点**/var/data**目录内容。

思路:

  1. 清理目录:rm -fr /var/data/*
  2. pod每次都在 worker31上运行:使用 nodeName: worker31
  3. 将物理主机 /var/data 挂载给pod:使用 hostPath 类型 volume
  4. 周期性执行使用 cronjob
yaml 复制代码
apiVersion: batch/v1
kind: CronJob
metadata:
  name: clean
spec:
  jobTemplate:
    metadata:
      name: clean
    spec:
      template:
        metadata:
        spec:
          nodeName: worker31.laoma.cloud
          containers:
          - command:
            - sh
            - -c
            - sleep 3 && rm -fr /var/data/*
            image: docker.io/library/busybox
            name: clean
            volumeMounts:
            - mountPath: /var/data
              name: data
          volumes:
          - name: data
            hostPath:
              path: /var/data
          restartPolicy: OnFailure
  schedule: '* * * * *'
案例2:通过 cronjob 来操作 k8s 集群

思路:

  1. 找到一个具有kubectl命令的镜像,我们这里使用 bitnami/kubectl。
  2. 将当前集群的凭据映射到pod中给kubectl使用。

过程如下:

bash 复制代码
# 创建cm保存kubectl凭据
[root@master ~]# kubectl create cm kubeconfig --from-file=config=/root/.kube/config

# 通过 volume 挂载给pod
[root@master ~]# vim cronjob-delete-pod.yaml
yaml 复制代码
apiVersion: batch/v1
kind: CronJob
metadata:
  name: clean
spec:
  jobTemplate:
    metadata:
      name: clean
    spec:
      template:
        spec:
          restartPolicy: Never
          containers:
          - image: docker.io/bitnami/kubectl
            imagePullPolicy: IfNotPresent
            name: kubectl
            args:
            - delete
            - pods
            - -l
            - app=hello
            - -n
            - controller
            volumeMounts:
            - name: kubeconfig
              # 该镜像默认从/.kube/config读取凭据
              mountPath: "/.kube"
          volumes:
          - name: kubeconfig
            configMap:
              name: kubeconfig
  schedule: '* * * * *'

bitnami/kubectl镜像使用参考

环境清理

bash 复制代码
[root@master30 ~]# kubectl delete ns controllers

Service

学习参考:Service

环境准备

bash 复制代码
[root@master30 ~]# kubectl create ns service
[root@master30 ~]# kubectl config set-context --current --namespace service

先看两个例子

示例1:

bash 复制代码
[root@master30 ~]# kubectl run web --image=docker.io/library/httpd --image-pull-policy=IfNotPresent -o yaml --dry-run=client > pod-web.yml
[root@master30 ~]# kubectl apply -f pod-web.yml
[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.laoma.cloud   <none>           <none>

# 此时集群外节点无法通过pod-ip访问pod
root@client:~# curl http://10.224.193.65

# 删除pod
[root@master30 ~]# kubectl delete pod web --force

# 重新创建一个可以访问的pod,修改如下
[root@master30 ~]# vim pod-web.yml 
yaml 复制代码
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: web
  name: web
spec:
  containers:
  - image: docker.io/library/httpd
    imagePullPolicy: IfNotPresent
    name: web
    # 添加ports参数
    ports:
    - containerPort: 80
      hostPort: 8080
      
    resources: {}
  dnsPolicy: ClusterFirst
  restartPolicy: Always
status: {}
bash 复制代码
[root@master30 ~]# kubectl apply -f pod-web.yml 
[root@master30 ~]# kubectl get pod -o wide
NAME   READY   STATUS    RESTARTS   AGE   IP              NODE               NOMINATED NODE   READINESS GATES
web    1/1     Running   0          5s    10.224.51.150   worker31.laoma.cloud   <none>           <none>

# 外部节点可以通过worker31的8080端口访问pod
[root@client ~]# curl http://worker31.laoma.cloud:8080
<html><body><h1>It works!</h1></body></html>

# 结论,pod只能通过所在主机的ip访问,集群外部需要知道pod运行在哪个主机。
# 如果pod通过控制器管理,在一个主机上创建多个相同的pod,则出现端口冲突

# 清理 pod
[root@master30 ~]# kubectl delete pod web --force

示例2:

bash 复制代码
[root@master30 ~]# kubectl create deployment web --image=docker.io/library/httpd --replicas=4 --dry-run=client -o yaml > deploy-web.yml
[root@master30 ~]# vim deploy-web.yml
yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  replicas: 4
  selector:
    matchLabels:
      app: web
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: web
    spec:
      containers:
      - image: docker.io/library/httpd
        name: httpd
        
        #添加imagePullPolicy
        imagePullPolicy: IfNotPresent
        
        #添加ports
        ports:
        - containerPort: 80
          hostPort: 8080
          
        resources: {}
status: {}
bash 复制代码
[root@master30 ~]# kubectl apply -f deploy-web.yml
[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
web-79fc679949-t6twf   0/1     Pending   0          7s
web-79fc679949-vqfr8   1/1     Running   0          7s

# 有2个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.

# 清理环境
[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 复制代码
# 创建 Deployment
[root@master30 ~]# kubectl create deployment web --image=docker.io/library/httpd --replicas=3

# 查看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
web-5646dd6f6c-6fjqs   1/1     Running   0          8m43s   app=web,pod-template-hash=5646dd6f6c
web-5646dd6f6c-tvw78   1/1     Running   0          8m43s   app=web,pod-template-hash=5646dd6f6c

创建 Service

yaml 复制代码
[root@master30 ~]# kubectl create service clusterip web --tcp=8080:80
[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代表访问集群ip的8080/TCP,将转发给pod的80端口
## svc默认标签是:app=<svc-name>

# 查看Service详细信息
[root@master30 ~]# kubectl describe service web 
Name:              web
Namespace:         laoma
Labels:            app=web
Annotations:       <none>
Selector:          app=web
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.103.19.150
IPs:               10.103.19.150
Port:              8080-80  8080/TCP
TargetPort:        80/TCP
Endpoints:         10.224.193.67:80,10.224.193.68:80,10.224.41.131:80
Session Affinity:  None
Events:            <none>


[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端口
[root@master30 ~]# curl 10.103.19.150:8080
<html><body><h1>It works!</h1></body></html>

验证 Service

  1. 更改每个pod主页,验证负载均衡功能。
bash 复制代码
# 获取pod名称
[root@master30 ~]# kubectl get pods -o name | awk -F/  '{print $2}'
web-5646dd6f6c-5hs6f
web-5646dd6f6c-6fjqs
web-5646dd6f6c-tvw78

# 更改每个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

# 验证效果
[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
  1. 此时如果创建一个具有相同标签的pod,service也会将请求转发到该pod
bash 复制代码
[root@master30 ~]# kubectl run web --image=docker.io/library/httpd --labels=app=web
[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>
     16 web-5646dd6f6c-5hs6f
     17 web-5646dd6f6c-6fjqs
     13 web-5646dd6f6c-tvw78
  1. 此时重启deploy,service仍然能动态发现后端pod
bash 复制代码
[root@master30 ~]# kubectl rollout restart deployment web
[root@master30 ~]# for i in {1..60};do curl -s 10.103.19.150:8080;done|sort |uniq -c
  1. 如果创建的pod具有标签app1=web1和app2=web2,而deploy控制器的selector匹配的标签为app1=web1,service匹配的标签为app2=web2也是可以的。
bash 复制代码
# 清理环境,重建 Deployment和Service
[root@master30 ~]# kubectl delete deployments.apps web --force
[root@master30 ~]# kubectl delete service web
[root@master30 ~]# vim deploy-web.yml
yaml 复制代码
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: {}
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选择器标签

[root@master30 ~]# kubectl describe svc web |grep -e IP: -e Endpoints
IP:                10.101.131.56
Endpoints:         10.224.193.73:80,10.224.41.136:80

[root@master30 ~]# curl 10.101.131.56:8080
<html><body><h1>It works!</h1></body></html>
  1. 无法ping通service ip,但可以ping通pod。service只允许http方式访问80,其他没有做iptables映射。

yaml 文件创建

bash 复制代码
[root@master30 ~]# kubectl delete svc web

# 获取Service资源yaml文件模版
[root@master30 ~]# kubectl create service clusterip web --tcp=8080:80 -o yaml --dry-run=client > svc-web.yml
[root@master30 ~]# cat svc-web.yml 
yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  ports:
  - name: 8080-80
    port: 8080
    protocol: TCP
    targetPort: 80
  selector:
    app: web
  type: ClusterIP
status:
  loadBalancer: {}

Service 发现

所谓发现 Service,是指集群内应用访问 Service。

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

  1. 通过 IP 访问 Service。
  2. 通过环境变量访问 Service。
  3. 通过 dns 解析的名称访问 Service。

通过 IP 访问 Service

实验准备:mysql+wordpress

准备mysql资源

bash 复制代码
[root@master30 ~]# 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 ~]# kubectl apply -f pod-mysql.yaml
[root@master30 ~]# kubectl expose pod mysql --port=3306 --target-port=3306
[root@master30 ~]# kubectl get service
NAME    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
mysql   ClusterIP   10.111.69.45   <none>        3306/TCP   25m

[root@master30 ~]# apt install -y mysql-client
[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 |
| blog               |
+--------------------+

准备WordPress资源

bash 复制代码
[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=10.111.69.45

# 为了测试方便,我们这里创建NodePort类型Service
[root@master30 ~]# kubectl expose pod wordpress --port=80 --target-port=80 --type NodePort
[root@master30 ~]# kubectl get service
NAME        TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)    AGE
mysql       ClusterIP   10.111.69.45     <none>        3306/TCP   41m
wordpress   NodePort    10.106.106.246   <none>        80:31102/TCP   3s

# 访问测试
root@client:~# firefox 10.1.8.30:31102

通过环境变量访问 Service

获取环境变量信息

bash 复制代码
[root@master30 ~]# 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.111.69.45
MYSQL_PORT_3306_TCP_PORT=3306
MYSQL_SERVICE_HOST=10.111.69.45
MYSQL_PORT_3306_TCP_PROTO=tcp
MYSQL_PORT=tcp://10.111.69.45:3306
MYSQL_SERVICE_PORT=3306
MYSQL_PORT_3306_TCP=tcp://10.111.69.45:3306
/ # exit
Session ended, resume using 'kubectl attach test -c test -i -t' command when the pod is running
pod "test" deleted

说明:

  1. 可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。
  2. service创建后才能使用该环境变量。
  3. service属于namespace,pod只能访问同一个namespace中service。

准备WordPress资源

bash 复制代码
# 删除 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 ~]# 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_PORT=tcp://10.111.69.45:3306
MYSQL_SERVICE_PORT=3306
MYSQL_PORT_3306_TCP=tcp://10.111.69.45:3306

# 访问测试
root@client:~# firefox 10.1.8.30:31102

通过 DNS 名称访问 Service

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

bash 复制代码
[root@master30 ~]# kubectl get deployments.apps --namespace=kube-system 
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
calico-kube-controllers   1/1     1            1           2d23h
coredns                   2/2     2            2           3d

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

bash 复制代码
[root@master30 ~]# kubectl run busybox --rm -it --image=docker.io/library/busybox /bin/sh
If you don't see a command prompt, try pressing enter.
/ # cat /etc/resolv.conf 
nameserver 10.96.0.10
search service.svc.cluster.local svc.cluster.local cluster.local laoma.cloud
options ndots:5
/ # wget wordpress.service:80
Connecting to wordpress.service:80 (10.106.106.246:80)
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

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

bash 复制代码
/ # rm index.html
/ # wget wordpress:80
Connecting to wordpress:80 (10.106.106.246:80)
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 复制代码
[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

接上面的WordPress示例

bash 复制代码
# 删除 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

# 访问测试
root@client:~# firefox 10.1.8.30:31102

# 清理环境
[root@master30 ~]# kubectl delete svc mysql wordpress
[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 复制代码
[root@master30 ~]# kubectl create deployment web --image=docker.io/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 和分配的端口公开 Service。 为了让 Service 可通过节点端口访问,Kubernetes 会为 NodePort 类型的Service 配置 clusterIP 地址。每个节点将该端口(每个节点上的相同端口号)上的流量代理到 Service。

示例:

bash 复制代码
[root@master30 ~]# kubectl expose deployment web --type NodePort --port=8080 --target-port=80 -o yaml --dry-run=client > service-NodePort.yaml
[root@master30 ~]# vim service-NodePort.yaml
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: {}
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
root@client:~# curl http://10.1.8.31:31917
root@client:~# curl http://10.1.8.32:31917

分析防火墙规则:

bash 复制代码
# 与ClusterIP对比,每个节点的iptables中额外增加了下面两条规则:
[root@master30 ~]# iptables-save | grep 31917
-A KUBE-NODEPORTS -p tcp -m comment --comment "laoma/web:http" -m tcp --dport 31917 -j KUBE-MARK-MASQ
-A KUBE-NODEPORTS -p tcp -m comment --comment "laoma/web:http" -m tcp --dport 31917 -j KUBE-SVC-WE5D4GWSX3MMPCHH
# 规则的含义是:访问当前节点31917端口的请求会应用规则KUBE-SVC-WE5D4GWSX3MMPCHH

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

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

yaml 复制代码
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监听的端口。

清理环境

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

部署
bash 复制代码
[root@master30 ~]# tar -xf metallb-0.14.8.tar.gz

# 记得提前导入镜像
[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

配置地址池

bash 复制代码
[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

bash 复制代码
[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
测试
bash 复制代码
[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 
yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: web
  name: web
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: web
  type: LoadBalancer
status:
  loadBalancer: {}
bash 复制代码
[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>
总结
  • 客户端流量到达pod路径:客户端请求 → MetalLB 虚拟 IP(LB IP) → 节点 kube-proxy → Service 转发 → 后端 Pod。
  • MetalLB 只做 ARP 宣告 + IP 占坑,转发全靠 kube-proxy + Service
  • MetalLB + Service 是标准 K8s 负载均衡流程。
详细流程图

访问 LB VIP:10.1.8.40
客户端用户
ARP 寻址 → 流量进入集群任意节点
节点内核拦截 Service IP 流量
kube-proxy 处理 iptables/IPVS 规则
Service 负载均衡:选择一个健康 Pod
通过 CNI 网络转发到 Pod 所在节点
流量进入 Pod 网络命名空间
目标 Pod 接收并响应请求

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

ExternalName

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

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

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

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

Headless Services

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

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

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

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

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

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

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

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

Service 会话保持

会话保持介绍

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

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

工作原理:

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

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

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

准备测试环境

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

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

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

设置会话保持

bash 复制代码
[root@master30 ~]# kubectl patch svc web -p '{"spec":{"sessionAffinity":"ClientIP"}}'

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

与 kube-proxy IPVS 的关系

1. service 里的 sessionAffinity 优先级最高

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

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

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

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

金丝雀发布

学习参考:金丝雀部署

环境准备:

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

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

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

  1. 主要稳定的发行版将有一个 track 标签,其值为 stable
bash 复制代码
[root@master30 ~]# vim webapp-1.28.yaml
yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web
  name: web-28
spec:
  replicas: 10
  selector:
    matchLabels:
      app: web
      tier: frontend
      track: stable
  template:
    metadata:
      labels:
        app: web
        tier: frontend
        track: stable
    spec:
      containers:
      - image: 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:
          name: web
          items:
            - key: index28.html
              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
kind: Service
metadata:
  labels:
    app: web
  name: web
spec:
  ports:
  - port: 80
    protocol: TCP
    targetPort: 80
  selector:
    app: web
    tier: frontend
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
kind: Deployment
metadata:
  labels:
    app: web
  name: web-29
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
      tier: frontend
      track: canary
  template:
    metadata:
      labels:
        app: web
        tier: frontend
        track: canary
    spec:
      containers:
      - image: 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:
          name: web
          items:
            - key: index29.html
              path: index.html
bash 复制代码
[root@master30 ~]# kubectl apply -f webapp-1.29.yaml

验证访问比例:

bash 复制代码
[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
  1. 总pod数量不变的情况下,逐步减少旧版本和增加新版本副本数量,。
bash 复制代码
[root@master30 ~]# kubectl scale deployment web-28 --replicas 3
[root@master30 ~]# kubectl scale deployment web-29 --replicas 2
[root@master30 ~]# for i in {1..50}; do curl -s 10.98.235.36; done | sort -n|uniq -c
     31 hello nginx 1.28
     19 hello nginx 1.29

环境清理

bash 复制代码
[root@master30 ~]# kubectl delete ns service
相关推荐
AOwhisky3 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes
SamDeepThinking3 小时前
中小团队需要一个资源微服务
后端·微服务·架构
两万五千个小时3 小时前
为什么你的 Agent 读了文件,却好像什么都没读到?
人工智能·程序员·架构
星恒讯工业路由器4 小时前
星恒讯工业生产自动化解决方案
运维·物联网·自动化·智能路由器·信息与通信
非优秀程序员4 小时前
智能体的构成--深入探讨Anthropic、OpenAI、Perplexity和LangChain究竟在构建什么。
人工智能·架构·开源
码点滴4 小时前
从“失忆症“到“数智分身“:Hermes Agent 如何重塑你的 AI 交互体验?
人工智能·架构·prompt·ai编程·hermes
郑寿昌4 小时前
GPU显存HPA:K8s智能扩缩实战
云原生·容器·kubernetes
狗哥哥4 小时前
面包屑自动推导的算法设计:从“最短路径匹配”到工程可落地
算法·架构
CinzWS5 小时前
A53性能验证:从微架构到系统级——芯片性能的“全息检测“
架构·芯片验证·原型验证·a53