K8s 核心控制器与 Service 全解
Kubernetes Controllers
ReplicationController
ReplicationController,简称RC,功能与ReplicaSets类似。 ReplicaSet 是 RC 的升级版, 支持新的基于集合的标签选择算符。
这里我们不详细介绍ReplicationController。
Deployment
学习参考:Deployment
Deployment 介绍
Deployment,简称 deploy,为 Pod 和 ReplicaSet 提供声明式的更新能力。
ReplicaSet 能确保运行指定数量的pod。Deployment能管理ReplicaSets,并提供对pod的更新等功能。因此,我们建议你使用Deployment来管理ReplicaSets,除非你需要自定义更新编排。这意味着你可能永远不需要操作ReplicaSet对象,而是使用Deployment替代管理 。
Deployment 用例
以下是 Deployments 的典型用例:
-
创建 Deployment 以将 ReplicaSet 上线。检查 ReplicaSet 的上线状态,查看其是否成功。
-
通过更新 Deployment 的 PodTemplateSpec,声明 Pod 的新状态 。 新的 ReplicaSet 会被创建,Deployment 以受控速率将 Pod 从旧 ReplicaSet 迁移到新 ReplicaSet。 每个新的 ReplicaSet 都会更新 Deployment 的修订版本。
-
如果 Deployment 的当前状态不稳定,回滚到较早的 Deployment 版本。 每次回滚都会更新 Deployment 的修订版本。
-
暂停 Deployment 的上线 以应用对 PodTemplateSpec 所作的多项修改, 然后恢复其执行以启动新的上线版本。
-
使用 Deployment 状态来判定上线过程是否出现停滞。
Deployment 管理
创建
命令行方式
root@master30:~# kubectl create deployment web --image=hub.laoma.cloud/library/nginx:1.27 --replicas=2
# 查看deployment创建的资源
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/web-b78cbd74b-6fjxs 1/1 Running 0 3s
pod/web-b78cbd74b-mh6fh 1/1 Running 0 3s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 2/2 2 2 3s
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-b78cbd74b 2 2 2 3s
# 查看deployment详细信息
root@master30:~# kubectl describe deployments.apps web
Name: web
Namespace: laoma
CreationTimestamp: Sat, 21 Oct 2023 13:17:06 +0000
Labels: app=web
Annotations: deployment.kubernetes.io/revision: 1
Selector: app=web
Replicas: 2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType: RollingUpdate
MinReadySeconds: 0
RollingUpdateStrategy: 25% max unavailable, 25% max surge
Pod Template:
Labels: app=web
Containers:
nginx:
Image: nginx:1.27
Port: <none>
Host Port: <none>
Environment: <none>
Mounts: <none>
Volumes: <none>
Conditions:
Type Status Reason
---- ------ ------
Available True MinimumReplicasAvailable
Progressing True NewReplicaSetAvailable
OldReplicaSets: <none>
NewReplicaSet: web-b78cbd74b (2/2 replicas created)
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal ScalingReplicaSet 21s deployment-controller Scaled up replica set web-b78cbd74b to 2
# ReplicaSet与Deployment关系
root@master30:~# kubectl describe replicaset.apps web-b78cbd74b | grep Controlled
Controlled By: Deployment/web
# 指明ReplicaSet是由Deployment/web创建的。
# pod与ReplicaSet关系
root@master30:~# kubectl describe pod web-b78cbd74b-6fjxs | grep Controlled
Controlled By: ReplicaSet/web-b78cbd74b
# 指明pod是由ReplicaSet/web-b78cbd74b创建的。
yaml 文件方式
# 获取deployment的yaml文件
root@master30:~# kubectl create deployment web --image=nginx:1.27 --replicas=2 --dry-run=client -o yaml > deployment-web.yaml
root@master30:~# cat deployment-web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: web
name: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- image: nginx:1.27
name: nginx
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
格式说明:
① apiVersion,是当前配置格式的版本。
② kind,是要创建的资源类型, 这里是Deployment。
③ metadata,是该资源的元数据, name是必需的元数据项。
④ spec,是该Deployment的规格说明。
⑤ spec.replicas,指明副本数量, 默认为1。
⑥ spec.template,定义Pod的模板, 这是配置文件的重要部分。
⑦ spec.template.metadata,定义Pod的元数据, 至少要定义一个label。 label的key和value可以任意指定。
⑧ spec.template.spec,描述Pod的规格, 此部分定义Pod中每一个容器的属性,name和image是必需的。
root@master30:~# kubectl apply -f deployment-web.yaml
创建方式进行比较
- 基于命令行的方式:
- 简单、 直观、 快捷, 上手快。
- 适合临时测试或实验。
- 基于配置文件的方式:
- 配置文件描述了What, 即应用最终要达到的状态。
- 配置文件提供了创建资源的模板, 能够重复部署。
- 可以像管理代码一样管理部署。
- 适合正式的、 跨环境的、 规模化部署。
- 这种方式要求熟悉配置文件的语法, 有一定难度。
**kubectl apply **命令不但能够创建Kubernetes资源, 也能对资源进行更新, 非常方便。
编辑
# 方法1:命令行直接修改
root@master30:~# kubectl edit deployments.apps web
# 方法2:编辑资源文件,然后apply应用
# 方法3:命令行修改,例如修改deployment副本数
root@master30:~# kubectl scale deployment web --replicas=4
删除
删除deployments时,默认会删除deployments管理的子资源。
root@master30:~# kubectl delete deployments.apps web
deployment.apps "web" deleted
使用**–cascade=orphan选项**删除deployments,不会删除deployments管理的子资源。
root@master30:~# kubectl apply -f deployment-web.yaml
root@master30:~# kubectl delete deployments.apps web --cascade=orphan
# 先确山删除对象
root@master30:~# kubectl get all -l app=web
# 然后再删除
root@master30:~# kubectl delete all -l app=web
root@master30:~# kubectl get all -l app=web
水平伸缩
# 创建 deployment
root@master30:~# kubectl create deployment web --image=nginx:1.27 --replicas=2
root@master30:~# kubectl scale deployment web --replicas=3
# 或者
root@master30:~# kubectl edit deployments.apps web
# 或者修改yaml文件并apply
root@master30:~# kubectl get deployments.apps web -o yaml > web.yaml
root@master30:~# vim web.yaml
root@master30:~# kubectl get pod
NAME READY STATUS RESTARTS AGE
web-5899d78c9-cckln 1/1 Running 0 9m28s
web-5899d78c9-p8pk4 1/1 Running 0 43s
web-5899d78c9-phntq 1/1 Running 0 9m28s
健壮性测试
关闭 worker节点,测试pod重建。
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
web-5899d78c9-cckln 1/1 Running 0 14m 10.224.51.134 worker31.gsb.cloud <none> <none>
web-5899d78c9-p8pk4 1/1 Running 0 6m12s 10.224.51.135 worker31.gsb.cloud <none> <none>
web-5899d78c9-phntq 1/1 Running 0 14m 10.224.225.69 worker32.gsb.cloud <none> <none>
# 关闭 worker32
root@worker32:~# init 0
# 等待一段时间(5分钟), Kubernetes 判定 worker32不可用, 将worker32上的Pod标记为Unknown状态, 并在worker31上重建Pod,维持总副本数为3
# 当worker32恢复后, Unknown的Pod会被删除, 已经运行的Pod不会重新调度回worker32。
K8s 判断节点宕机,要经过 3 个阶段:
-
Node 节点上kubelet 默认 每 10s 发一次心跳上报自身状态(kubelet → kube-apiserver)
-
Master 上
controller-manager每 5 秒检查一次心跳,连续 40s 没收到 Node节点心跳,判定 Node 节点不健康,b标记为NotReady。参数:node-monitor-period=5s 和 node-monitor-grace-period=40s。
该参数属于 kube-controller-manager 组件,该组件以静态 Pod 形式运行在 master 节点,路径:
/etc/kubernetes/manifests/kube-controller-manager.yaml编辑静态 Pod 配置文件,找到
command段,添加或修改--pod-eviction-timeout参数,保存文件,触发静态 Pod 重启。 -
节点 NotReady 持续满 5 分钟,开始把 Pod 驱逐到别的节点。
参数:pod-eviction-timeout=300s。
该参数也属于 kube-controller-manager 组件,该组件以静态 Pod 形式运行在 master 节点。修改方法同上。
更新镜像
# 设置 deployment 的 image 为 nginx:1.28
# 获取容器名称
root@master30:~# kubectl get deployments.apps -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
web 3/3 3 3 139m nginx nginx:1.27 app=web
# 或者
root@master30:~# kubectl describe deployments.apps web |grep Container -i -A2
Containers:
nginx:
Image: nginx:1.27
# 更新镜像为nginx:1.28
root@master30:~# kubectl set image deployment/web nginx=nginx:1.28 --record
# 或者直接编辑deploy设置镜像
root@master30:~# kubectl edit deployments.apps web
# 新增了一个replicaset,用于创建新的pod
root@master30:~# kubectl get rs
NAME DESIRED CURRENT READY AGE
replicaset.apps/web-5899d78c9 0 0 0 26m
replicaset.apps/web-6c57bdf5f4 3 3 3 9s
# 查看镜像版本
root@master30:~# kubectl get deployments.apps web -o wide
NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR
web 3/3 3 3 26m nginx nginx:1.28 app=web
版本控制
使用 kubectl rollout 命令控制deployment版本。
root@master30:~# kubectl rollout -h
Manage the rollout of one or many resources.
Valid resource types include:
* deployments
* daemonsets
* statefulsets
Examples:
# Rollback to the previous deployment
kubectl rollout undo deployment/abc
# Check the rollout status of a daemonset
kubectl rollout status daemonset/foo
# Restart a deployment
kubectl rollout restart deployment/abc
# Restart deployments with the 'app=nginx' label
kubectl rollout restart deployment --selector=app=nginx
Available Commands:
history View rollout history
pause Mark the provided resource as paused
restart Restart a resource
resume Resume a paused resource
status Show the status of the rollout
undo Undo a previous rollout
Usage:
kubectl rollout SUBCOMMAND [options]
Use "kubectl rollout <command> --help" for more information about a given command.
Use "kubectl options" for a list of global command-line options (applies to all commands).
示例:
# 再次更新镜像为 nginx:1.29
root@master30:~# kubectl set image deployment/web nginx=nginx:1.29 --record
# 查看更新记录
root@master30:~# kubectl rollout history deployment web
deployment.apps/web
REVISION CHANGE-CAUSE
1 <none>
2 kubectl set image deployment/web nginx=nginx:1.28 --record=true
3 kubectl set image deployment/web nginx=nginx:1.29 --record=true
# 回滚到版本1
root@master30:~# kubectl rollout undo deployment web --to-revision=1
deployment.apps/web rolled back
root@master30:~# kubectl rollout history deployment web
deployment.apps/web
REVISION CHANGE-CAUSE
2 kubectl set image deployment/web *=nginx:1.28 --record=true
3 kubectl set image deployment/web *=nginx:1.29 --record=true
4 <none>
滚动更新
Kubernetes提供了两个参数maxSurge和maxUnavailable来精细控制Pod的替换数量 。
-
**maxSurge,此参数控制滚动更新过程中副本总数超过 DESIRED 的上限的数量。**maxSurge可以是具体的整数(比如3) , 也可以是百分百, 向上取整。 maxSurge默认值为25%。
例如, DESIRED为10, 那么副本总数的最大值为 roundUp(10 + 10 * 25%) =13, 所以我们看到 CURRENT 就是13。
-
maxUnavailable,此参数控制滚动更新过程中不可用的副本相占DESIRED的最大比例。 maxUnavailable可以是具体的整数(比如3), 也可以是百分百, 向下取整。 maxUnavailable默认值为25%。
例如, DESIRED为10, 那么可用的副本数至少要为10 - roundDown(10 * 25%)= 8, 所以我们看到AVAILABLE是8。
总结:
- maxSurge 值越大, 初始创建的新副本数量就越多。
- **maxUnavailable **值越大, 初始销毁的旧副本数量就越多,更新初期造成不可用副本数量越多。
理想情况下, 我们这个案例滚动更新的过程应该是这样的:
- 创建3个新副本,此时Running副本数为10,maxSurge副本总数达到13。
- 销毁2个旧副本,同时再创建2个新副本。此时Running副本数为8。如果之前创建的3个副本状态没有变更为Running,则ContainerCreating副本数为5,maxSurge副本总数仍为13。
- 当新副本状态变更为Running, 例如之前创建的5个新副本在同一时刻状态变为running。当然这是一种理想情况。
- 此时running状态副本为13个,那么此时可以一次性销毁5个旧副本,同时又可以一次性创建5个新副本,使running副本数回到8。
- 这个过程会持续进行, 直到所有的旧副本被新副本替换,滚动更新完成。
**实践:**更新deployment镜像,并使用以下脚本监控:
root@master30:~# vim monitor_pod_numbers
#!/bin/bash
while true
do
echo '===================' >> output.log
kubectl get pods --no-headers |awk '{print $3}' |sort | uniq -c |sed -r 's/^ +//' &>> output.log
sleep 0.5
done
日志内容类似:
===================
10 Running
===================
5 ContainerCreating
8 Running
2 Terminating
===================
5 ContainerCreating
8 Running
2 Terminating
===================
5 ContainerCreating
8 Running
===================
5 ContainerCreating
8 Running
===================
3 ContainerCreating
1 Pending
9 Running
4 Terminating
===================
5 ContainerCreating
8 Running
5 Terminating
===================
5 ContainerCreating
8 Running
5 Terminating
===================
5 ContainerCreating
8 Running
3 Terminating
===================
10 Running
4 Terminating
===================
10 Running
2 Terminating
===================
10 Running
===================
10 Running
===================
DaemonSet
学习参考:DaemonSet
DaemonSet 介绍
DaemonSet,简写DS,确保全部(或者某些)节点上运行一个 Pod 的副本。 当有新节点加入集群时, 也会在新节点上新增一个 Pod 。 当有节点从集群移除时,移除节点上的 Pod 也会被回收。
DaemonSet 用例
DaemonSet 的一些典型用例:
- 在每个节点上运行集群守护进程,例如存储守护进程 glusterd 和 ceph。
- 在每个节点上运行日志收集守护进程, 例如 flunentd或logstash。
- 在每个节点上运行监控守护进程,例如 Prometheus Node Exporter或 collectd。
DaemonSet 用法
简单的用法:为每种类型的守护进程在所有的节点上都启动一个 DaemonSet。
复杂的用法:为同一种守护进程部署多个 DaemonSet;每个具有不同的标志, 并且对不同硬件类型具有不同的内存、CPU 要求。
DaemonSet 使用
DaemonSet 创建
root@master30:~# vim daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: busybox
spec:
selector:
matchLabels:
app: busybox
template:
metadata:
labels:
app: busybox
spec:
containers:
- name: busybox
image: busybox
imagePullPolicy: IfNotPresent
command:
- sleep
- "36000"
root@master30:~# kubectl apply -f daemonset.yaml
DaemonSet 查看
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox-5hfv6 1/1 Running 0 102s 10.224.41.132 worker31.gsb.cloud <none> <none>
busybox-jdnsv 1/1 Running 0 102s 10.224.193.66 worker32.gsb.cloud <none> <none>
root@master30:~# kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
busybox 2 2 2 2 2 <none> 118s
DaemonSet 调度
# master节点是默认是不可调度节点
root@master30:~# kubectl describe node master.laoma.cloud |grep Taints
Taints: node-role.kubernetes.io/control-plane:NoSchedule
# 设置master节点可调度
root@master30:~# kubectl taint node master.gsb.cloud node-role.kubernetes.io/control-plane-
root@master30:~# kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
busybox 3 3 3 3 3 <none> 7m43s
# 设置master节点不可调度
root@master30:~# kubectl taint node master.gsb.cloud node-role.kubernetes.io/control-plane:NoSchedule
root@master30:~# kubectl get ds
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
busybox 2 2 2 2 2 <none> 8m37s
# pod数量为2个,但是master节点上的busybox pod不会自动删除,也不会计数到这里。
DaemonSet 健壮性
# 删除其中一个pod
root@master30:~# kubectl delete pod busybox-5hfv6
# 自动创建新pod
root@master30:~# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
busybox-jdnsv 1/1 Running 0 7m6s 10.224.193.66 worker32.cai.cloud <none> <none>
busybox-wlqqv 1/1 Running 0 8s 10.224.41.133 worker31.cai.cloud <none> <none>
DaemonSet 删除
删除 DaemonSet 时,默认会删除它创建的所有 Pod,使用**–cascade=orphan**选项,将保留DaemonSet 创建 Pod。
[root@master master]# kubectl delete daemonsets.apps busybox
daemonset.apps "busybox" deleted
K8s 集群中 DS
- Kubernetes 使用 DaemonSet 控制器运行系统组件,例如kube-proxy、calico-node。
root@master30:~# kubectl get daemonsets.apps --namespace kube-system
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
calico-node 3 3 3 3 3 kubernetes.io/os=linux 4h12m
kube-proxy 3 3 3 3 3 kubernetes.io/os=linux 4h56m
- 分析calico-node 的yaml文件:
kind: DaemonSet
apiVersion: apps/v1
metadata:
name: calico-node
namespace: kube-system
labels:
k8s-app: calico-node
spec:
selector:
matchLabels:
k8s-app: calico-node
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
template:
metadata:
labels:
k8s-app: calico-node
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ''
spec:
nodeSelector:
kubernetes.io/os: linux
hostNetwork: true
containers:
- name: calico-node
image: hub.laoma.cloud/calico/node:v3.28.0
注意: 完整配置文件内容要更复杂一些, 为了方便学习DaemonSet, 这里只保留了最重要的内容。
生产级示例
采集节点日志-Fluent Bit
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit-ds
namespace: kube-system
spec:
selector:
matchLabels:
app: fluent-bit
template:
metadata:
labels:
app: fluent-bit
spec:
containers:
- name: fluent-bit
image: cr.fluentbit.io/fluent/fluent-bit:latest
volumeMounts:
- name: varlog
mountPath: /var/log
- name: varlibdockercontainers
mountPath: /var/lib/docker/containers
readOnly: true
volumes:
- name: varlog
hostPath:
path: /var/log
- name: varlibdockercontainers
hostPath:
path: /var/lib/docker/containers
采集节点监控指标-Node Exporter
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: node-exporter-ds
namespace: monitoring
spec:
selector:
matchLabels:
app: node-exporter
template:
metadata:
labels:
app: node-exporter
spec:
containers:
- name: node-exporter
image: prom/node-exporter:latest
ports:
- containerPort: 9100
volumeMounts:
- name: proc
mountPath: /host/proc
readOnly: true
- name: sys
mountPath: /host/sys
readOnly: true
volumes:
- name: proc
hostPath:
path: /proc
- name: sys
hostPath:
path: /sys
Job
学习参考:Job
Job 介绍
- Job 用于运行一次性任务。
- 如果pod运行任务失败,则创建新的pod继续运行任务,直到任务运行完成,也就是pod中任务退出代码为0, Job结束。
- 在job运行过程中,如果托管pod的节点发生故障,Job pod将被自动重新安排到另一个节点。
- 删除 Job 的操作会清除所创建的全部 Pod。
- 挂起 Job 的操作会删除 Job 的所有活跃 Pod,直到 Job 被再次恢复执行。
Job 用例
简单的使用场景:
- 执行数据库清理
- 备份 Kubernetes 集群
Job 使用
Job 基本管理
root@master30:~# kubectl create job -h
Create a job with the specified name.
Examples:
# Create a job
kubectl create job my-job --image=busybox
# Create a job with command
kubectl create job my-job --image=busybox -- date
......
Usage:
kubectl create job NAME --image=image [--from=cronjob/name] -- [COMMAND]
[args...] [options]
示例1:
root@master30:~# kubectl create job myjob --image=busybox -- echo hello k8s job!
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-pdrwv 0/1 Completed 0 11m
NAME COMPLETIONS DURATION AGE
job.batch/myjob 1/1 20s 11m
root@master30:~# kubectl logs myjob-pdrwv
hello k8s job!
# 删除 Job 的操作会清除所创建的全部 Pod
# 使用--cascade=orphan选项,可以保留job创建的pod
root@master30:~# kubectl delete jobs.batch myjob
通过 yaml 文件创建job。
root@master30:~# vim job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echo", "hello k8s job! "]
restartPolicy: Never
root@master30:~# kubectl apply -f job.yaml
**示例2:**计算pi,保留小数点200位。
root@master30:~# vim job-pi.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: pi
spec:
template:
spec:
containers:
- name: pi
image: perl
imagePullPolicy: IfNotPresent
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(200)"]
restartPolicy: Never
backoffLimit: 4
root@master30:~# kubectl apply -f job-pi.yaml
# 等几分钟
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/pi-2hn9n 0/1 Completed 0 3m13s
NAME COMPLETIONS DURATION AGE
job.batch/pi 1/1 76s 3m13s
root@master30:~# kubectl logs pi-qzxv4
3.1415926535897932384626433832795028841971693993751058209749445923078164062862089986280348253421170679821480865132823066470938446095505822317253594081284811174502841027019385211055596446229489549303820
restartPolicy
restart 策略只能是:
- Nerver:只要任务没有完成,就会创建新的 pod,直到 job 完成,所以有可能会产生多个pod。
- OnFailure:只要任务没有完成,就会重启 pod,直到job完成。
我们做个试验, 修改job.yaml, 故意引入一个错误 :echo 修改为 echoxxx。
root@master30:~# vim job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echoxxx", "hello k8s job! "]
restartPolicy: Never
再次应用,验证。
root@master30:~# kubectl apply -f job.yaml
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-6hp5t 0/1 StartError 0 33s
pod/myjob-gczbz 0/1 StartError 0 2s
pod/myjob-n4bpv 0/1 StartError 0 22s
NAME COMPLETIONS DURATION AGE
job.batch/myjob 0/1 33s 33s
# 可以看到有多个Pod, 状态均不正常。
# 查看某个 pod 详细信息
root@master30:~# kubectl describe pod myjob-6hp5t
......
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 50s default-scheduler Successfully assigned laoma/myjob-6hp5t to worker32.cai.cloud
Normal Pulled 50s kubelet Container image "busybox" already present on machine
Normal Created 50s kubelet Created container hello
Warning Failed 50s kubelet Error: failed to create containerd task: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "echoxxx": executable file not found in $PATH: unknown
日志显示没有可执行程序, 符合我们的预期。
问题现象: 为什么会看到这么多失败的Pod?
解释: 当第一个Pod启动时, 容器失败退出, 根据 restartPolicy: Never, 此失败容器不会被重启, 但 Job 默认预期完成的Pod数量是1, 目前COMPLETIONS为0, 不满足, 所以 Job 会创建新的Pod, 直到 COMPLETIONS 为1。 对于我们这个例子, SUCCESSFUL 永远也到不了1, 所以Job 会一直创建新的Pod。 为了终止这个行为, 我们删除Job 。
清理 job
root@master30:~# kubectl delete jobs.batch myjob
如果将 restartPolicy 设置为 OnFailure 会怎么样?
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echoxxxx", "hello k8s job! "]
restartPolicy: OnFailure
再次应用,验证。
root@master30:~# kubectl apply -f job.yaml
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-xbwxh 0/1 CrashLoopBackOff 1 68s
NAME COMPLETIONS DURATION AGE
job.batch/myjob 0/1 68s 68s
# pod执行失败后,会重启。
# pod 数量总是1,RESTARTS数值不断增加。
backoffLimit
如果Job执行失败,我们可以指定job执行失败最大次数。
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
backoffLimit: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echoxx", "hello k8s job! "]
restartPolicy: Never
测试结果:最多重建2次。
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-bdgxq 0/1 StartError 0 30s
pod/myjob-ffth2 0/1 StartError 0 83s
pod/myjob-abcde 0/1 StartError 0 83s
NAME COMPLETIONS DURATION AGE
job.batch/myjob 0/1 83s 83s
completions
我们还可以通过 completions 指定 Pod 执行完成多少次,才算Job执行完成。
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
completions: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echo", "hello k8s job! "]
restartPolicy: Never
上面配置的含义是: 每次运行两个Pod, 直到总共有6个Pod成功完成。
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-72zgz 0/1 Completed 0 4s
pod/myjob-8brdq 0/1 Completed 0 8s
NAME COMPLETIONS DURATION AGE
job.batch/myjob 2/2 12s 12s
如果不指定completions, 默认值均为1。
parallelism
有时我们希望Job同时运行多个Pod, 提高Job的执行效率,通过parallelism设置并行Pod数量 。
apiVersion: batch/v1
kind: Job
metadata:
name: myjob
spec:
completions: 6
parallelism: 2
template:
metadata:
name: myjob
spec:
containers:
- name: hello
image: busybox
imagePullPolicy: IfNotPresent
command: ["echo", "hello k8s job! "]
restartPolicy: Never
root@master30:~# kubectl get all
NAME READY STATUS RESTARTS AGE
pod/myjob-72zgz 0/1 Completed 0 4s
pod/myjob-8brdq 0/1 Completed 0 8s
pod/myjob-8l5cx 0/1 Completed 0 8s
pod/myjob-9gkt8 0/1 Completed 0 12s
pod/myjob-wcwwh 0/1 Completed 0 12s
pod/myjob-xs6pd 0/1 Completed 0 4s
NAME COMPLETIONS DURATION AGE
job.batch/myjob 6/6 12s 12s
效果: 每次运行2个Pod, 直到总共有6个Pod成功完成。
如果不指定parallelism, 默认值均为1。
上面的例子只是为了演示Job的并行特性, 实际用途不大。 不过现实中确实存在很多需要并行处理的场景。 比如批处理程序, 每个副本(Pod) 都会从任务池中读取任务并执行, 副本越多, 执行时间就越短, 效率就越高。这种类似的场景都可以用Job来实现。
activeDeadlineSeconds
一旦 Job 运行时间达到该值,其所有运行中的 Pod 都会被终止,并且 Job 的状态更新为 type: Failed 及 reason: DeadlineExceeded。该值适用于 Job 的整个生命期,无论 Job 创建了多少个 Pod。
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-timeout
spec:
backoffLimit: 5
activeDeadlineSeconds: 10
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
Job 的 .spec.activeDeadlineSeconds 优先级高于其 .spec.backoffLimit 设置。 因此,如果一个 Job 正在重试一个或多个失效的 Pod,该 Job 一旦到达 activeDeadlineSeconds 所设的时限即不再部署额外的 Pod,即使其重试次数还未达到 backoffLimit 所设的限制。
ttlSecondsAfterFinished
通过设置 Job 的 .spec.ttlSecondsAfterFinished 字段,job完成后多长时间删除job自身。
TTL 控制器清理 Job 时,会级联式地删除 Job 对象。 换言之,它会删除所有依赖的对象,包括 Pod 及 Job 本身。 注意,当 Job 被删除时,系统会考虑其生命周期保障,例如其 Finalizers。
例如:
apiVersion: batch/v1
kind: Job
metadata:
name: pi-with-ttl
spec:
ttlSecondsAfterFinished: 10
template:
spec:
containers:
- name: pi
image: perl
command: ["perl", "-Mbignum=bpi", "-wle", "print bpi(2000)"]
restartPolicy: Never
Job pi-with-ttl 在结束 10 秒之后,可以成为被自动删除的对象。
如果该字段设置为 0,Job 在结束之后立即成为可被自动删除的对象。 如果该字段没有设置,Job 不会在结束之后被 TTL 控制器自动清除。
注意这种 TTL 机制仍然是一种 Alpha 状态的功能特性,需要配合 TTLAfterFinished 特性门控使用。有关详细信息,可参考 TTL 控制器的文档。
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 使用
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]
示例:
root@master30:~# kubectl create cronjob mycronjob --image=busybox --schedule='*/2 * * * *' -- echo hello k8s job!
等效的配置文件:
apiVersion: batch/v1beta1
kind: CronJob
metadata:
name: mycronjob
spec:
schedule: "*/2 * * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: hello
image: busybox
command: ["echo", "hello k8s job! "]
restartPolicy: Never
等待一段时间,最多创建3个job。
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 语法。
# ┌───────────── 分钟 (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 的语法完全一样, 只不过它是嵌套的,没有 apiVersion 和 kind。 你可以为模板化的 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:定期清理主机目录内容
创建一个pod定期每分钟执行一次清空worker31节点**/var/data**目录内容。
思路:
- 清理目录:rm -fr /var/data/*
- pod每次都在worker31上运行:使用 nodeName: worker31
- 将物理主机/var/data挂载给pod:使用 hostPath 类型 volume
- 周期性执行使用 cronjob
apiVersion: batch/v1
kind: CronJob
metadata:
name: clean
spec:
jobTemplate:
metadata:
name: clean
spec:
template:
metadata:
spec:
nodeName: worker31.cai.cloud
containers:
- command:
- sh
- -c
- sleep 3 && rm -fr /var/data/*
image: busybox
name: clean
volumeMounts:
- mountPath: /var/data
name: data
volumes:
- name: data
hostPath:
path: /var/data
restartPolicy: OnFailure
schedule: '* * * * *'
案例2:通过 cronjob 来操作 k8s 集群
思路:
- 找到一个具有kubectl命令的镜像,我们这里使用 bitnami/kubectl。
- 将当前集群的凭据映射到pod中给kubectl使用。
过程如下:
# 创建cm保存kubectl凭据
[root@master controller]# kubectl create cm kubeconfig --from-file=config=/root/.kube/config
# 通过 volume 挂载给pod
apiVersion: batch/v1
kind: CronJob
metadata:
name: clean
spec:
schedule: '* * * * *'
jobTemplate:
metadata:
name: clean
spec:
template:
spec:
restartPolicy: Never
containers:
- image: hub.laoma.cloud/kubernetes/kubectl:1.30.2
imagePullPolicy: IfNotPresent
name: kubectl
args:
- delete
- pods
- -l
- app=hello
- -n
- controllers
volumeMounts:
- name: kubeconfig
mountPath: "/.kube"
volumes:
- name: kubeconfig
configMap:
name: kubeconfig
hub.laoma.cloud/kubernetes/kubectl:v1.36.0 来源 bitnami/kubectl.
bitnami/kubectl镜像使用参考。
环境清理
root@master30:~# kubectl delete ns controllers
Kubernetes Service
学习参考:Service
环境准备
root@master30:~# kubectl create ns services
root@master30:~# kubectl config set-context --current --namespace services
先看两个例子
示例1:
root@master30:~# kubectl run web --image=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.gsb.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
apiVersion: v1
kind: Pod
metadata:
creationTimestamp: null
labels:
run: web
name: web
spec:
containers:
- image: httpd
imagePullPolicy: IfNotPresent
name: web
# 添加ports参数
ports:
- containerPort: 80
hostPort: 8080
resources: {}
dnsPolicy: ClusterFirst
restartPolicy: Always
status: {}
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.gsb.cloud <none> <none>
# 外部节点可以通过worker31的8080端口访问pod
[root@client ~]# curl http://worker31.gsb.cloud:8080
<html><body><h1>It works!</h1></body></html>
# 结论,pod只能通过所在主机的ip访问,集群外部需要知道pod运行在哪个主机。
# 如果pod通过控制器管理,在一个主机上创建多个相同的pod,则出现端口冲突
# 清理 pod
root@master30:~# kubectl delete pod web --force
示例2:
root@master30:~# kubectl create deployment web --image=httpd --replicas=4 --dry-run=client -o yaml > deploy-web.yml
root@master30:~# vim deploy-web.yml
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: httpd
name: httpd
#添加imagePullPolicy
imagePullPolicy: IfNotPresent
#添加ports
ports:
- containerPort: 80
hostPort: 8080
resources: {}
status: {}
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
问题总结:
- 集群外部客户端需要知道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
# 创建 Deployment
root@master30:~# kubectl create deployment web --image=httpd:2.4.58 --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
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
- 更改每个pod主页,验证负载均衡功能。
# 获取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
- 此时如果创建一个具有相同标签的pod,service也会将请求转发到该pod
root@master30:~# kubectl run web --image=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
- 此时重启deploy,service仍然能动态发现后端pod
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
- 如果创建的pod具有标签app1=web1和app2=web2,而deploy控制器的selector匹配的标签为app1=web1,service匹配的标签为app2=web2也是可以的。
# 清理环境,重建 Deployment和Service
root@master30:~# kubectl delete deployments.apps web --force
root@master30:~# kubectl delete service web
root@master30:~# vim deploy-web.yml
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
replicas: 2
selector:
matchLabels:
app1: web1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app1: web1
app2: web2
spec:
containers:
- image: httpd
name: httpd
imagePullPolicy: IfNotPresent
resources: {}
status: {}
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>
- 无法ping通service ip,但可以ping通pod。service只允许http方式访问80,其他没有做iptables映射。
yaml 文件创建
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
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:
- 通过 IP 访问 Service。
- 通过环境变量访问 Service。
- 通过 dns 解析的名称访问 Service。
通过 IP 访问 Service
实验准备:mysql+wordpress
准备mysql资源
root@master30:~# kubectl run mysql --image=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资源
root@master30:~# kubectl run wordpress \
--image=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
获取环境变量信息
root@master30:~# kubectl run test --rm -it --image=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
说明:
- 可以通过环境变量MYSQL_SERVICE_HOST访问服务mysql。
- service创建后才能使用该环境变量。
- service属于namespace,pod只能访问同一个namespace中service。
准备WordPress资源
# 删除 pod-WordPress 资源,重新创建
root@master30:~# kubectl delete pod wordpress --force
root@master30:~# kubectl run wordpress \
--image=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组件。
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。
root@master30:~# kubectl run busybox --rm -it --image=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同属于gsb namespace, 因此可以省略gsb直接用web访问Service.
/ # 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服务器呢?
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示例
# 删除 pod-WordPress 资源,重新创建
root@master30:~# kubectl delete pod wordpress --force
root@master30:~# kubectl run wordpress \
--image=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
root@master30:~# kubectl create deployment web --image=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。
示例:
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
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 8080
protocol: TCP
targetPort: 80
selector:
app: web
type: NodePort
status:
loadBalancer: {}
# 创建NodePort类型Service
root@master30:~# 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
分析防火墙规则:
# 与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指定为特定端口。
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 8080
protocol: TCP
# 指定节点固定端口
nodePort: 30080
targetPort: 80
selector:
app: web
type: NodePort
status:
loadBalancer: {}
端口说明:
- nodePort是节点上监听的端口。
- port是ClusterIP上监听的端口。
- targetPort是Pod监听的端口。
清理环境
root@master30:~# kubectl delete svc web
LoadBalancer
kubernetes并没有真正实现 LoadBalancer,需要借助第三方工具实现,例如metallb。
每个LoadBalancer类型的service需要关联一个公网IP。创建LoadBalancer类型的service只需要将Service 的 Type 改成 LoadBalancer。
这里我们使用 metallb。
官方地址: https://metallb.universe.tf/
github地址:https://github.com/metallb/metallb
部署
部署 metallb
root@master30:~# wget http://192.168.42.200/course-materials/softwares/stage03/metallb-0.14.8.tar.gz
root@master30:~# tar -xf metallb-0.14.8.tar.gz
# 查看镜像
root@master30:~# grep image metallb-0.14.8/config/manifests/metallb-native.yaml
image: quay.io/metallb/controller:v0.14.8
image: quay.io/metallb/speaker:v0.14.8
# 按需修改镜像
root@master30:~# sed -i 's/quay.io/hub.laoma.cloud/g' metallb-0.14.8/config/manifests/metallb-native.yaml
root@master30:~# kubectl apply -f metallb-0.14.8/config/manifests/metallb-native.yaml
# 等待着所有pod正常运行再进行下一步
root@master30:~# kubectl get all -n metallb-system
NAME READY STATUS RESTARTS AGE
pod/controller-786f9df989-98bjh 1/1 Running 0 85s
pod/speaker-gthhx 1/1 Running 0 85s
pod/speaker-jwj25 1/1 Running 0 85s
pod/speaker-s5zvq 1/1 Running 0 85s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/webhook-service ClusterIP 10.106.37.32 <none> 443/TCP 85s
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
daemonset.apps/speaker 3 3 3 3 3 kubernetes.io/os=linux 85s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/controller 1/1 1 1 85s
NAME DESIRED CURRENT READY AGE
replicaset.apps/controller-786f9df989 1 1 1 85s
配置地址池
root@master30:~# cat << 'EOF' > ippool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: first-pool
namespace: metallb-system
spec:
addresses:
- 10.1.8.40-10.1.8.80
EOF
root@master30:~# kubectl apply -f ippool.yaml
配置 lay2
root@master30:~# cat << 'EOF' > L2.yaml
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: example
namespace: metallb-system
EOF
root@master30:~# kubectl apply -f L2.yaml
测试
root@master30:~# kubectl expose deployment web --type LoadBalancer --port=80 --target-port=80 -o yaml --dry-run=client > service-LoadBalancer.yaml
root@master30:~# cat service-LoadBalancer.yaml
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: web
name: web
spec:
ports:
- port: 80
protocol: TCP
targetPort: 80
selector:
app: web
type: LoadBalancer
status:
loadBalancer: {}
root@master30:~# kubectl apply -f service-LoadBalancer.yaml
root@master30:~# kubectl get service
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
web LoadBalancer 10.109.101.67 10.1.8.40 80:32101/TCP 5s
# 访问测试,端口号使用service的80,非32101端口
[root@client ~]# curl -s http://10.1.8.40:80
<html><body><h1>It works!</h1></body></html>
总结
- 客户端流量到达pod路径:客户端请求 → MetalLB 虚拟 IP(LB IP) → 节点 kube-proxy → Service 转发 → 后端 Pod。
- ✅ MetalLB 只做 ARP 宣告 + IP 占坑,转发全靠 kube-proxy + Service。
- MetalLB + Service 是标准 K8s 负载均衡流程。
详细流程图
- 客户端发起请求:用户通过浏览器 / 应用访问 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 记录。 集群不会为之创建任何类型代理。
例如,将 prod 命名空间中的 my-service 服务映射到 database.example.com。
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: 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、游戏服务):
- 登录状态不丢失
- 购物车不丢失
- 长连接稳定
准备测试环境
root@master30:~# kubectl create deployment web --image=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
设置会话保持
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 配置 ClientIP → kube-proxy 自动使用 IPVS 的 SH 算法
- SH = Source Hashing 源地址哈希
- 保证同一 IP → 同一 Pod
2. 如果 service 不配置 sessionAffinity
kube-proxy 就用ipvs scheduler 里设置的算法:
- rr(轮询)
- lc(最少连接)
- wrr(加权轮询)
金丝雀发布
学习参考:金丝雀部署
环境准备:
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 标签来区分不同的版本。
-
主要稳定的发行版将有一个
track标签,其值为stable:root@master30:~# vim webapp-1.28.yamlapiVersion: 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: 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.htmlroot@master30:~# kubectl apply -f webapp-1.28.yaml -
创建 service
root@master30:~# vim webapp-svc.yamlapiVersion: v1 kind: Service metadata: labels: app: web name: web spec: ports: - port: 80 protocol: TCP targetPort: 80 selector: app: web tier: frontendroot@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:root@master30:~# vim webapp-1.29.yamlapiVersion: 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: 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.htmlroot@master30:~# kubectl apply -f webapp-1.29.yaml验证访问比例:
root@master30:~# kubectl scale deployment web-28 --replicas 8 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 39 hello nginx 1.28 11 hello nginx 1.29 -
总pod数量不变的情况下,逐步减少旧版本和增加新版本副本数量,。
root@master30:~# kubectl scale deployment web-28 --replicas 6 root@master30:~# kubectl scale deployment web-29 --replicas 4 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
环境清理
root@master30:~# kubectl delete ns services
更多推荐

所有评论(0)