kubernetes 实践操作

官方文档:https://kubernetes.io/zh-cn/docs/home/

API概述

REST API是 Kubernetes 的基本结构。 所有操作和组件之间的通信及外部用户命令都是调用 API 服务器处理的 REST API。 因此,Kubernetes 平台视一切皆为 API 对象, 且它们在 API 中有相应的定义。

Kubernetes API 参考 列出了 Kubernetes v1.35 版本的 API。

如需了解一般背景信息,请查阅 Kubernetes APIKubernetes API 控制访问描述了客户端如何向 Kubernetes API 服务器进行身份认证以及他们的请求如何被鉴权。

更多相关信息,请参考官方文档-API概述

API 版本控制

JSON 和 Protobuf 序列化模式遵循相同的模式更改原则。 以下描述涵盖了这两种格式。

API 版本控制和软件版本控制是间接相关的。 API 和发布版本控制提案描述了 API 版本控制和软件版本控制间的关系。

不同的 API 版本代表着不同的稳定性和支持级别。 你可以在 API 变更文档 中查看到更多的不同级别的判定标准。

下面是每个级别的摘要:

  • Alpha:

    • 版本名称包含 alpha(例如:v1alpha1)。
    • 内置的 Alpha API 版本默认被禁用且必须在 kube-apiserver 配置中显式启用才能使用。
    • 软件可能会有 Bug。启用某个特性可能会暴露出 Bug
    • 对某个 Alpha API 特性的支持可能会随时被删除。
    • API 可能在以后的软件版本中以不兼容的方式更改。
    • 由于缺陷风险增加和缺乏长期支持,建议该软件仅用于短期测试集群
  • Beta:

    • 版本名称包含 beta(例如:v2beta3)。

    • 内置的 Beta API 版本默认被禁用且必须在 kube-apiserver 配置中显式启用才能使用 (例外情况是 Kubernetes 1.22 之前引入的 Beta 版本的 API,这些 API 默认被启用)。

    • 内置 Beta API 版本从引入到弃用的最长生命周期为 9 个月或 3 个次要版本(以较长者为准), 从弃用到移除的最长生命周期为 9 个月或 3 个次要版本(以较长者为准)。

    • 软件被很好的测试过。启用某个特性被认为是安全的。

    • 尽管一些特性会发生细节上的变化,但它们将会被长期支持。

    • 在随后的 Beta 版或 Stable 版中,对象的模式和(或)语义可能以不兼容的方式改变。 当这种情况发生时,将提供迁移说明。 适配后续的 Beta 或 Stable API 版本可能需要编辑或重新创建 API 对象,这可能并不简单。 对于依赖此功能的应用程序,可能需要停机迁移。

    • 该版本的软件不建议生产使用。 后续发布版本可能会有不兼容的变动。 一旦 Beta API 版本被弃用且不再提供服务, 则使用 Beta API 版本的用户需要转为使用后续的 Beta 或 Stable API 版本。

  • Stable:

    • 版本名称如 vX,其中 X 为整数。
    • 特性的 Stable 版本会出现在后续很多版本的发布软件中。 Stable API 版本仍然适用于 Kubernetes 主要版本范围内的所有后续发布, 并且 Kubernetes 的主要版本当前没有移除 Stable API 的修订计划。

Alpha为先行版本(不稳定),Beta基于Alpha优化了稳定性,Stable为长期稳定版本

资源清单

在k8s中,一般使用**yaml**格式的文件来创建符合我们预期的pod,这样的yaml文件我们一般称为资源清单。

YAML语法

yaml是一个可读性高,用来表达数据序列的格式;yaml的意思其实是:仍是一种标记语言,但为了强调这种语言以数据为中心,而不是以标记语言为重点。

基本语法:

  • 缩进时不允许使用Tab键,只允许使用空格。
  • 缩进的空格数量不重要,只要相同层级左侧对齐即可
  • # 标识注释,从这个字符一直到行尾,都会被解释器忽略

支持的数据结构:

  • 对象:键值对的集合,又称映射(mapping)/哈希(hashes)/字典(dictionary)
  • 数组:一组按次序排列的值,又称为序列(sequence)/列表(list)
  • 纯量(scalars):单个的、不可再分的值

常用字段解释说明

必须存在的属性

参数名字段类型说明
versionString这里指的是K8s API的版本,目前基本上是v1,可以用kubectl api-versions命令查询
kindString这里指的是yaml文件定义的资源类型和角色,比如:pod
metadataObject元数据对象,固定值就写 metadata
metadata.nameString元数据对象的名字,这里由我们编写,比如命令Pod的名字
metadata.namespaceString元数据对象的命名空间,由我们自身定义
specObject详细定义对象,固定值就写 Spec
spec.containers[]list这里是Spec对象的容器列表定义,是个列表
spec.containers[].nameString这里定义容器的名字
spec.containers[].imageString这里定义要用到的镜像名称

主要对象

参数名字段类型说明
spec.containers[].nameString定义容器的名字
spec.containers[].imageString定义要用到的镜像名称
spec.containers[].imagePullRolicyString定义镜像拉取策略,有Always、Never、IfNotPresent三个选项; Always:每次都尝试重新拉取镜像,默认Always Never:仅使用本地镜像 IfNotPresent:如果本地有镜像就使用本地就像,如果没有就拉取在线镜像;
spec.containers[].command[]List指定容器启动命令,因为是数组可以指定多个,不指定则使用镜像打包时使用的启动命令。
spec.containers[].args[]List指定容器启动命令参数,因为是数组可以指定多个。
spec.containers[].workingDirString指定容器的工作目录。
spec.containers[].volumeMounts[]List指定容器内部的存储卷配置
spec.containers[].volumeMounts[].nameString指定可以被容器挂载的存储卷的名称
spec.containers[].volumeMounts[].mountPathString指定可以被容器挂载的存储卷的路径
spec.containers[].volumeMounts[].readOnlyString设置存储卷路径的读写模式,true或者false,默认为读写模式
spec.containers[].ports[]List指定容器需要用到的端口列表
spec.containers[].ports[].nameString指定端口名称
spec.containers[].containerPortString指定容器需要监听的端口号
spec.containers[].ports[].hostPortString指定容器所在主机需要监听的端口号,默认跟上面containerPort相同;注意设置了hostPort同一台主机无法启动该容器的相同副本(因为主机的端口号不能相同,这样会冲突)
spec.containers[].ports[].protocolString指定端口协议,支持TCP和UDP,默认为TCP
spec.containers[].env[]List指定容器运行前需设置的环境变量列表

额外的参数项

参数名字段类型说明
spec.restartPolicyString定义Pod的重启策略,可选值为Always、OnFailure,默认值为Always; 1.Always:pod一旦终止运行,则无论容器是如何终止的,kubelet服务都将重启它。 2.OnFailure:只有pod以非零退出码终止时,kubelet才会重启该容器;如果容器正常退出(退出码为0),则kubelet将不会重启它; 3.Never:pod终止后,kubelet将退出码报告给master,不会重启pod。
spec.nodeSelectorObject定义Node的Label过滤标签,以key: value格式指定
spec.imagePullSecretsObject定义pull镜像时使用sercet名称,以name: secretkey格式指定
spec.hostNetworkBoolean定义是否使用主机网络模式,默认值false;设置true表示使用宿主机网络,不使用docker网桥,同时设置了true将无法再同一台宿主机上启动第二个副本。

Pod详解

Pod 是一个抽象的逻辑概念,它是一组(一个或者多个)容器的集合,这些容器之间共享同一份存储、网络等资源。

使用 kubectl get po -o wide 可以查看 pod 的列表,其中 READY 列代表该 Pod 总共有 1 个容器,并且该容器已经成功启动,可以对外提供服务了

pod启动阶段(相位 phase)

Pod 创建完之后,一直到持久运行起来,中间有很多步骤,也就有很多出错的可能,因此会有很多不同的状态。

一般来说,pod 这个过程包含以下几个步骤:

(1)调度到某台 node 上。kubernetes 根据一定的优先级算法选择一台 node 节点将其作为 Pod 运行的 node

(2)拉取镜像

(3)挂载存储配置等

(4)运行起来。如果有健康检查,会根据检查的结果来设置其状态。

Pod启动阶段

第一步:controller manager管理的控制器创建pod副本

第二步:scheduler调度器根据调度算法选择最合适的node节点调度pod

第三步:kubelet拉取镜像

第四步:kubelet挂载存储卷

第五步:kubelet创建并运行容器

第六步:kubelet根据容器探针的探测结果设置Pod状态

phase 状态

phase 可能的状态包括:

  • Pending:表示APIServer创建了Pod资源对象并已经存入了etcd中,但是它并未被调度完成(比如还没有调度到某台node上),或者仍然处于从仓库下载镜像的过程中。
  • Running:Pod已经被调度到某节点之上,并且Pod中所有容器都已经被kubelet创建。至少有一个容器正在运行,或者正处于启动或者重启状态(也就是说Running状态下的Pod不一定能被正常访问)。
  • Succeeded:有些pod不是长久运行的,比如job、cronjob,一段时间后Pod中的所有容器都被成功终止,并且不会再重启。需要反馈任务执行的结果。
  • Failed:Pod中的所有容器都已终止了,并且至少有一个容器是因为失败终止。也就是说,容器以非0状态退出或者被系统终止,比如 command 写的有问题。
  • Unknown:表示无法读取 Pod 状态,通常是 kube-controller-manager 无法与 Pod 通信。

创建Pod

K8S遵循一切皆资源的概念,可以使用资源清单创建资源,资源清单具体表现为一个yaml文件,现在,我们根据资源清单的yaml参数,创建一个pod

  • 创建一个命名空间,以便将本练习中创建的资源与集群的其余部分隔离。
# 创建一个名为pod-example的命名空间 [root@kubernetes-master ~]# kubectl create namespace pod-example namespace/pod-example created # 查看命名空间 [root@kubernetes-master ~]# kubectl get ns NAME STATUS AGE default Active 13h kube-flannel Active 13h kube-node-lease Active 13h kube-public Active 13h kube-system Active 13h pod-example Active 5s
  • 创建pod资源清单
apiVersion: v1 # API 版本(K8s 核心组的稳定版本 v1) kind: Pod # 资源对象类型,也可以配置Deployment等资源对象 metadata: # Pod相关的元数据,用于描述Pod name: nginx-demo # Pod名称 labels: # 定义 Pod的标签,可以自定义信息 type: app # 自定义标签,名为type,值为app test-label-name: test-label-value namespace: pod-example # Pod 的命名空间 spec: # 期望Pod按照如下描述进行创建 containers: # 对Pod中容器的描述 - name: nginx # 容器名称 image: docker.io/library/nginx:latest # 容器镜像,使用docker images或者crictl images可以查看已安装的镜像,注意需要在nodes节点查看 imagePullPolicy: IfNotPresent # 拉取镜像策略,若本地有就不拉取 command: # 指定容器启动时需要执行的命令 - nginx - -g - 'daemon off;' # nginx -g 'daemon off;'让nginx前台运行(容器必须前台进程才不会退出) workingDir: /usr/share/nginx/html # 指定容器启动后的工作目录 ports: # 端口定义 - name: http # 端口名称 containerPort: 80 # 描述容器内要暴露的端口 protocol: TCP # 指定通信协议 env: # 环境变量,可自定义 - name: JVM_OPTS # 变量名称 value: 'test-value' # 变量值 resources: # 资源请求 requests: # 容器运行的最小资源保障,CPU 100毫核(0.1核),内存 128Mi(mebibyte,1Mi=1024×1024字节) cpu: 100m memory: 128Mi limits: # 资源限制,容器能使用的最大资源上限,CPU 1000毫核(1核),内存 1024Mi(1Gi) cpu: 1000m memory: 1024Mi restartPolicy: OnFailure # Pod重启策略,仅当容器运行失败(退出码非0)时重启容器

imagePullPolicy:如果镜像标签为 latest(或缺省),则默认策略是Always,如果镜像标签为具体版本号,默认策略为IfNotPresent

  • 创建Pod
# 根据文件创建资源 [root@kubernetes-master ~]# kubectl apply -f nginx-demo.yaml pod/nginx-demo created # 查看命名空间pod-example内的资源 [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 13s # 查看Pod nginx-demo的状态信息 [root@kubernetes-master ~]# kubectl describe pod nginx-demo -n pod-example ......... Events: # 事件 Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 32s default-scheduler Successfully assigned pod-example/nginx-demo to kubernetes-node1 # 分配节点 Normal Pulled 32s kubelet Container image "docker.io/library/nginx:latest" already present on machine # 拉取镜像 Normal Created 32s kubelet Created container nginx # 创建容器 Normal Started 32s kubelet Started container nginx # 启动容器
  • 验证创建状态
# 查看Pod详细信息 [root@kubernetes-master ~]# kubectl get pod -n pod-example -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-demo 1/1 Running 0 5m51s 10.244.1.16 kubernetes-node1 <none> <none> # 查看Pod标签 [root@kubernetes-master ~]# kubectl get pod -n pod-example --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-demo 1/1 Running 0 5m16s test-label-name=test-label-value,type=app # 查看工作目录和写入的环境变量 [root@kubernetes-master ~]# kubectl exec -it nginx-demo -n pod-example -- pwd /usr/share/nginx/html [root@kubernetes-master ~]# kubectl exec -it nginx-demo -n pod-example -- bash -c 'echo $JVM_OPTS' test-value # 验证nginx服务 [root@kubernetes-master ~]# curl 10.244.1.16 <!DOCTYPE html> ......... # 查看路由状态 [root@kubernetes-master ~]# route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 ens33 10.244.1.0 10.244.1.0 255.255.255.0 UG 0 0 0 flannel.1 10.244.2.0 10.244.2.0 255.255.255.0 UG 0 0 0 flannel.1 192.168.0.0 0.0.0.0 255.255.255.0 U 100 0 0 ens33

根据上述信息,kubernetes-master能跟pod通信,是因为pod对外暴露了80端口(暴露到宿主机node1节点的80端口上),因为安装了flannel,master节点能够和node1通过隧道flannel.1通信。所以在kubernetes-mastercurlpod中的nginx,流量请求为:

kubernetes-master —(flannel.1)—> node1 —(containerPort: 80)—> pod: nginx-demo

探针技术

容器内的监测机制,根据不同的探针判断容器当前的状态,K8S提供了三种探针:存活探针、就绪探针、启动探针

  • 启动探针(StartupProbe)

启动探针检查容器内的应用是否已启动。 启动探针可以用于对慢启动容器进行存活性检测,避免它们在启动运行之前就被 kubelet 杀掉。

如果配置了这类探针,它会禁用存活探针和就绪探针,直到启动探针成功为止。

这类探针仅在启动时执行,不像存活探针和就绪探针那样周期性地运行。

  • 就绪探针(ReadinessProbe)

就绪探针决定容器何时准备好接受流量。 这种探针在等待应用执行耗时的初始任务时非常有用; 例如:建立网络连接、加载文件和预热缓存。在容器的生命周期后期, 就绪探针也很有用,例如,从临时故障或过载中恢复时。

如果就绪探针返回的状态为失败,Kubernetes 会将该 Pod 从所有对应服务的端点中移除。

就绪探针在容器的整个生命期内持续运行。

  • 存活探针(LivenessProbe)

存活探针决定何时重启容器。 例如,当应用在运行但无法取得进展时,存活探针可以捕获这类死锁。

如果一个容器的存活探针失败多次,kubelet 将重启该容器。

存活探针不会等待就绪探针成功。 如果你想在执行存活探针前等待,你可以定义 initialDelaySeconds,或者使用启动探针。

探针的探测方式分为三种:

  • ExecAction:执行某个命令,根据执行成功与否确认存活状态
  • TCPSocketAction:TCP端口探测,根据端口是否开放决定存活状态
  • HTTPGetAction:HTTP请求探测,根据返回状态码决定存活状态

启动探针

HTTPGetAction

基于上一章节创建POD的yaml文件,新增一个启动探针,使用HTTP的探测方式

# 在spec.containers下配置startupProbe [root@kubernetes-master ~]# vim nginx-demo.yaml metadata: name: nginx-demo-startup-probe # 修改pod名称,便于测试 spec: containers: - name: nginx startupProbe: # 启动探针配置 httpGet: # 探测方式,基于http请求探测 path: /api/version # http请求路径 port: 80 # 请求端口 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 periodSeconds: 3 # 探测间隔时间 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间

因为配置了探测nginx的/api/version,而实际上nginx-demo没有这个接口,所以探测会失败,现在运行容器查看状态、

startupProbe 探测路径 /api/version 是 Nginx 不存在的路径,返回 404,导致启动探针持续失败,容器反复重启后因 restartPolicy: OnFailure(仅失败时重启),最终容器正常退出(Exit Code 0)变为 Completed 状态;

[root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 54m [root@kubernetes-master ~]# kubectl apply -f nginx-demo.yaml pod/nginx-demo-startup-probe created # 重新创建资源,新资源的READY为0/1,RESTARTS为2,即重启了2次 [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 59m nginx-demo-startup-probe 0/1 Completed 2 5m7s # 查看详情信息 [root@kubernetes-master ~]# kubectl describe pod -n pod-example nginx-demo-startup-probe ......... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 83s default-scheduler Successfully assigned pod-example/nginx-demo-startup-probe to kubernetes-node1 Normal Pulled 66s (x3 over 83s) kubelet Container image "docker.io/library/nginx:latest" already present on machine Normal Created 66s (x3 over 83s) kubelet Created container nginx Normal Started 66s (x3 over 83s) kubelet Started container nginx Warning Unhealthy 57s (x9 over 81s) kubelet Startup probe failed: HTTP probe failed with statuscode: 404 # http探测失败 Normal Killing 57s (x3 over 75s) kubelet Container nginx failed startup probe, will be restarted # startup probe探测失败,重启pod Warning BackOff 57s kubelet Back-off restarting failed container

TCPSocketAction

修改yaml文件的启动探针资源配置,使用TCP的探测方式

# 删除上一轮测试中启动失败的容器 [root@kubernetes-master ~]# kubectl delete pod -n pod-example nginx-demo-startup-probe # 在spec.containers下配置startupProbe [root@kubernetes-master ~]# vim nginx-demo.yaml metadata: name: nginx-demo-startup-probe # 修改pod名称,便于测试 spec: containers: - name: nginx startupProbe: # 启动探针配置 tcpSocket: # 探测方式,基于tcp探测 port: 80 # 请求端口 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 periodSeconds: 3 # 探测间隔时间 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间

因为配置了探测nginx的80端口,nginx启动后探测就会成功,现在运行容器查看状态

[root@kubernetes-master ~]# kubectl apply -f nginx-demo.yaml pod/nginx-demo-startup-probe created [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 69m nginx-demo-startup-probe 0/1 Running 0 2s [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 69m nginx-demo-startup-probe 1/1 Running 0 30s

ExecAction

修改yaml文件的启动探针资源配置,使用exec的探测方式,exec会执行一个命令,判断探针命令的退出码(0 = 成功,非 0 = 失败)

# 删除上一轮测试中启动失败的容器 [root@kubernetes-master ~]# kubectl delete pod -n pod-example nginx-demo-startup-probe # 在spec.containers下配置startupProbe [root@kubernetes-master ~]# vim nginx-demo.yaml metadata: name: nginx-demo-startup-probe # 修改pod名称,便于测试 spec: containers: - name: nginx startupProbe: # 启动探针配置 exec: command: - sh - -c - 'echo "success Message!" > success.txt;' # 执行命令 sh -c 'echo "success message" > success.txt;'命令正常退出则判断存活 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 periodSeconds: 3 # 探测间隔时间 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间

现在查看容器状态,在容器工作目录下应该有新建的文件和内容

[root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 93m nginx-demo-startup-probe 1/1 Running 0 5s [root@kubernetes-master ~]# kubectl exec -it -n pod-example nginx-demo-startup-probe -- bash -c 'cat success.txt;' success Message!

存活探针

许多长时间运行的应用最终会进入损坏状态,除非重新启动,否则无法被恢复。 Kubernetes 提供了存活探针来发现并处理这种情况。

构建一个存活探针,每隔五秒探测一次

[root@kubernetes-master ~]# cat nginx-demo.yaml apiVersion: v1 kind: Pod metadata: name: nginx-demo-liveness-probe # Pod名称 spec: containers: - name: nginx # 容器名称 startupProbe: # 启动探针配置 exec: command: - sh - -c - 'echo "<h1>hello</h1>" > wellcome.html;' failureThreshold: 3 # 请求失败3次即判定为存活探测失败 periodSeconds: 3 # 探测间隔时间 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间 livenessProbe: # 存活探针配置 httpGet: path: /wellcome.html port: 80 initialDelaySeconds: 5 # 指定第一次探测前等待5秒 periodSeconds: 5 # 每隔5秒进行一个探测 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间

加载资源,查看pod状态,等待pod运行起来后,删除掉pod里的wellcome.html再查看pod状态

[root@kubernetes-master ~]# kubectl apply -f nginx-demo.yaml pod/nginx-demo-liveness-probe created [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 109m nginx-demo-liveness-probe 1/1 Running 0 5s # 删除掉pod的index.html,重启一次后,pod依旧正常运行 [root@kubernetes-master ~]# kubectl exec -it -n pod-example nginx-demo-liveness-probe -- bash -c "rm -f wellcome.html;" [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 111m nginx-demo-liveness-probe 1/1 Running 1 (22s ago) 97s # 存活探针(livenessProbe)检测失败触发容器重启,清空可写层,恢复镜像原始状态 [root@kubernetes-master ~]# kubectl exec -it -n pod-example nginx-demo-liveness-probe -- bash -c "ls" 50x.html index.html wellcome.html

就绪探针

就绪探针的配置和存活探针的配置相似。 唯一区别就是要使用 readinessProbe 字段,而不是 livenessProbe 字段。

HTTP 和 TCP 的就绪探针配置也和存活探针的配置完全相同。

就绪和存活探测可以在同一个容器上并行使用。 两者共同使用,可以确保流量不会发给还未就绪的容器,当这些探测失败时容器会被重新启动。

构建一个就绪探针,仅当nginx目录下存在wellcome.html时,认为服务就绪

[root@kubernetes-master ~]# cat nginx-demo.yaml apiVersion: v1 kind: Pod metadata: name: nginx-demo-readiness-probe spec: containers: - name: nginx # 容器名称 startupProbe: # 启动探针配置 httpGet: path: /index.html port: 80 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 periodSeconds: 3 # 探测间隔时间 successThreshold: 1 # 请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间 livenessProbe: # 存活探针配置 httpGet: path: /index.html port: 80 initialDelaySeconds: 5 # 指定第一次探测前等待5秒 periodSeconds: 5 # 每隔5秒进行一个探测 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 successThreshold: 1 # http请求成功1次即判定为存活探测成功 timeoutSeconds: 5 # 请求超时时间 readinessProbe: # 就绪探针配置 httpGet: path: /wellcome.html port: 80 periodSeconds: 5 # 每隔5秒进行一个探测 failureThreshold: 3 # 请求失败3次即判定为存活探测失败 successThreshold: 1 # http请求成功1次即判定为探测成功 timeoutSeconds: 5 # 请求超时时间

构建资源,查看资源状态,因为启动时目录下没有wellcome.html,pod不会被认为是READY,即不接受流量

[root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 131m [root@kubernetes-master ~]# kubectl apply -f nginx-demo.yaml pod/nginx-demo-readiness-probe created # pod READY为0/1,未准备好 [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 131m nginx-demo-readiness-probe 0/1 Running 0 3s # 查看pod信息,就绪探针探测失败 [root@kubernetes-master ~]# kubectl describe pod nginx-demo-readiness-probe -n pod-example ......... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Scheduled 28s default-scheduler Successfully assigned pod-example/nginx-demo-readiness-probe to kubernetes-node1 Normal Pulled 28s kubelet Container image "docker.io/library/nginx:latest" already present on machine Normal Created 28s kubelet Created container nginx Normal Started 28s kubelet Started container nginx Warning Unhealthy 1s (x10 over 25s) kubelet Readiness probe failed: HTTP probe failed with statuscode: 404

现在,将文件写入pod中,再查看pod状态

[root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 134m nginx-demo-readiness-probe 0/1 Running 0 3m5s [root@kubernetes-master ~]# kubectl exec -it -n pod-example nginx-demo-readiness-probe -- bash -c 'echo "<h1>wellcome</h1>" > wellcome.html;' # 写入后,就绪探针成功探活,pod变为1/1 [root@kubernetes-master ~]# kubectl get pod -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 136m nginx-demo-readiness-probe 1/1 Running 0 4m24s # 查看日志信息 [root@kubernetes-master ~]# kubectl logs -f nginx-demo-readiness-probe -n pod-example ......... # 写入wellcome.html前 2026/01/02 10:09:08 [error] 9#9: *177 open() "/usr/share/nginx/html/wellcome.html" failed (2: No such file or directory), client: 10.244.1.1, server: localhost, request: "GET /wellcome.html HTTP/1.1", host: "10.244.1.27:80" # 写入wellcome.html后 10.244.1.1 - - [02/Jan/2026:10:09:08 +0000] "GET /index.html HTTP/1.1" 200 615 "-" "kube-probe/1.23" "-" 10.244.1.1 - - [02/Jan/2026:10:09:08 +0000] "GET /wellcome.html HTTP/1.1" 404 153 "-" "kube-probe/1.23" "-" 10.244.1.1 - - [02/Jan/2026:10:09:11 +0000] "GET /wellcome.html HTTP/1.1" 200 18 "-" "kube-probe/1.23" "-"

Kubernetes 中的 生命周期钩子(Lifecycle Hooks) 是在容器生命周期的特定阶段执行操作的机制。通过钩子,可以在容器启动后(PostStart)或停止前(PreStop)执行一些初始化或清理工作。

PostStart(启动后)

  • 在容器启动后立即触发执行。
  • 用于完成启动后的初始化操作,例如加载配置、启动辅助进程。

PreStop(停止前)

  • 在容器收到终止信号(如 SIGTERM)时触发执行。
  • 用于执行停止前的清理工作,例如保存状态、关闭连接、释放资源。

注意:postStart,不一定在容器的 command 之前运行,我们可以使用init容器去实现一些我们需要在容器运行前的初始化操作

钩子支持以下两种执行方式:

  • exec: 直接在容器内部运行指定命令。
  • httpGet:通过 HTTP GET 请求调用一个端点

钩子定义的基本结构

lifecycle: postStart: exec: command: ["sh", "-c", "echo 'Container started'"] preStop: exec: command: ["sh", "-c", "echo 'Container stopping'; sleep 5"]

参照如下示例

apiVersion: v1 kind: Pod metadata: name: lifecycle-demo spec: containers: - name: nginx image: nginx:1.21.1 lifecycle: postStart: exec: command: - "/bin/sh" - "-c" - | echo "PostStart hook triggered! Initializing..."; sleep 3 preStop: exec: command: - "/bin/sh" - "-c" - | echo "PreStop hook triggered! Cleaning up..."; sleep 5 ports: - containerPort: 80 resources: requests: memory: "64Mi" cpu: "250m"

生命周期钩子

工作原理与解释

PostStart:

  • 容器启动后,PostStart 钩子立即执行。
  • 在示例中,PostStart 钩子通过 echo 输出一条日志并等待 3 秒。

PreStop:

  • 当容器收到终止信号(如 kubectl delete podkubectl scale)时,PreStop 钩子立即执行。
  • 在示例中,PreStop 钩子输出一条日志并等待 5 秒,模拟资源清理。

注意事项

执行时间限制:

  • 钩子执行时间受 Pod 的 terminationGracePeriodSeconds 控制,默认宽限时间为 30 秒。如果 PreStop 未在宽限时间内完成,容器将被强制终止。

运行环境:

  • 钩子在容器的文件系统和环境变量中运行,因此可以直接访问容器内部的资源。

错误处理:

  • 如果钩子失败,容器不会因此失败,但会记录错误日志。

钩子与主进程的关系:

  • 钩子运行与容器主进程无直接依赖关系。PostStart 并不阻塞主进程启动,而 PreStop 是在终止信号发送后触发。

Pause 容器

使用 kubectl get po -o wide 查看刚才创建的pod所在的节点,登录到对应节点,有以下几种方法可以查到 pause 容器

# 方法 1: 查找 Pod 的 sandbox ID crictl pods --name <pod-name> # 方法 2: 查看 sandbox 详情(包含 PID 和镜像) crictl inspectp <sandbox-id> | grep -E "pid|image" # 方法 3: 直接查看进程 ps aux | grep pause # 方法 4: 查看 Pod 的所有容器(包括 Pause) crictl ps -a --pod <sandbox-id>

pause 容器是一个特殊容器,它又叫 infra 容器,是每个 Pod 都会自动创建的容器,它不属于用户自定义的容器。

使用 docker insepct [CONTAAINER_ID] 查看一下 pause 容器的详情信息,可以发现 pause 容器使用的镜像为:registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6

该镜像非常小,只有 484KB,由于它总是处于 Pause (暂时)状态,所以取名叫 pause

想了解该 pause 容器的构成(代码是用 C 语言写的)的可以去官方仓库上一看究竟:pause

pause 容器作用

上面我们说,一个 Pod 是由一组容器组成的,这些容器之间共享存储和网络资源,那么网络资源是如何共享的呢?

假设现在有一个 Pod,它包含两个容器(A 和 B),K8S 是通过让他们加入(join)另一个第三方容器的network namespace实现的共享,而这个第三方容器就是 pause 容器。

这么做的目的,其实很简单,想象一下,如果没有这样的第三方容器,会发生怎样的结果?

没有 pause 容器,那么 A 和 B 要共享网络,要不就是 A 加入 B 的 network namespace,或者 B 加入 A 的 network namespace, 而无论是谁加入谁,只要 network 的 owner 退出了,该 Pod 里的所有其他容器网络都会立马异常,这显然是不合理的。

反过来,由于 pause 里只有是挂起一个容器,里面没有任何复杂的逻辑,只要不主动杀掉 Pod,pause 都会一直存活,这样一来就能保证在 Pod 运行期间同一 Pod 里的容器网络的稳定。

我们在同一 Pod 里所有容器里看到的网络视图,都是完全一样的,包括网络设备、IP 地址、Mac 地址等等,因为他们其实全是同一份,而这一份都来自于 Pod 第一次创建的这个 Infra container。

由于所有的业务容器都要依赖于 pause 容器,因此在 Pod 启动时,它总是创建的第一个容器,可以说 Pod 的生命周期就是 pause 容器的生命周期。

模拟 Pod

从上面我们已经知道,一个 Pod 从表面上来看至少由一个容器组成,而实际上一个 Pod 至少要有包含两个容器,一个是业务容器,一个是 pause 容器。

理解了这个模型,我们就可以用以前熟悉的 docker 容器,手动创建一个真正意义上的 Pod。

不使用K8S,仅通过crictl(或者docker)实现一个pod:pause(提供网络),nginx(中间件),ghost(博客演示镜像)

这里我们找一台服务器部署docker并演示,因为使用containerd时会遇到部分问题:

crictl 使用 containerd 作为运行时,而 containerd 配置了 Calico CNI 插件 。当 crictl runp 创建 Pod Sandbox 时,它会调用 CNI 插件来设置网络,而 Calico 会检查 Pod 是否在 K8S API 中注册。需要使用 hostNetwork: true 来绕过 CNI 网络:

创建 pause 容器

使用 docker run 加如下参数:

  • --name:指定 pause 容器的名字,fake_k8s_pod_pause

  • -p 8888:80:将宿主机的 8888 端口映射到容器的 80 端口

sudo docker run -d -p 8888:80 \ --ipc=shareable \ --name fake_k8s_pod_pause \ registry.aliyuncs.com/google_containers/pause:3.6

创建 nginx 容器

创建容器之前先准备一下 nginx.conf 配置文件

cat << EOF >> nginx.conf error_log stderr; events { worker_connections 1024; } http { access_log /dev/stdout combined; server { listen 80 default_server; server_name example.com www.example.com; location / { proxy_pass http://127.0.0.1:2368; } } } EOF

然后运行如下命令创建名字 fake_k8s_pod_nginx 的 nginx 容器

sudo docker run -d --name fake_k8s_pod_nginx \ -v `pwd`/nginx.conf:/etc/nginx/nginx.conf \ --net=container:fake_k8s_pod_pause \ --ipc=container:fake_k8s_pod_pause \ --pid=container:fake_k8s_pod_pause \ nginx

其中 -v 参数是将宿主机上的 nginx.conf 文件挂载给 nginx 容器

除此之外,还有另外三个核心参数:

  • --net:指定 nginx 要 join 谁的 network namespace,即前面创建的fake_k8s_pod_pause

  • --ipc:指定 ipc mode, 指定前面创建的fake_k8s_pod_pause

  • --pid:指定 nginx 要 join 谁的 pid namespace,即之前创建的fake_k8s_pod_pause

创建 ghost 容器

有了 nginx 还不够,还需要有人提供网页的数据,这里使用 ghost 这个博客应用,参数和上面差不多,这里不再赘述。

sudo docker run -d --name ghost \ --net=container:fake_k8s_pod_pause \ --ipc=container:fake_k8s_pod_pause \ --pid=container:fake_k8s_pod_pause \ -e database__client=sqlite3 \ -e database__connection__filename=/var/lib/ghost/content/data/ghost.db \ ghost

到这里,就纯手工模拟出了一个符合 K8S Pod 模型的 “Pod” ,只是它并不由 K8S 进行管理。

访问服务器的8888端口,即可看到ghost页面

这个 “Pod” 由一个 fake_k8s_pod_pause 容器(负责提供可稳定共享的命名空间)和两个共享 fake_k8s_pod_pause 容器命名空间的两业务容器。

如上操作,在 K8S 生态之外,单纯使用 Docker 创建了三个容器(Pause、Nginx、Ghost),这三个容器的的组合,在 K8S 中称之为 Pod。

如果没有 K8S 的 Pod ,你启动一个 ghost 博客服务,你需要手动创建三个容器,当你想销毁这个服务时,同样需要删除三个容器。

而有了 K8S 的 Pod,这三个容器在逻辑上就是一个整体,创建 Pod 就会自动创建三个容器,删除 Pod 就会删除三个容器,从管理上来讲,方便了不少。

现在,我们使用pod来实现:

创建一个configmap,用于存储nginx.conf,配置沿用上述nginx.conf文件

ConfigMap 也是 K8S 中的一个对象,目前还没有学到,你只要知道它是一个用来存储信息的对象即可

kubectl create configmap nginx-config --from-file=nginx.conf

现在,创建一个ghost的pod

apiVersion: v1 kind: Pod metadata: name: ghost namespace: default spec: containers: - image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent name: nginx ports: - containerPort: 80 protocol: TCP hostPort: 8888 volumeMounts: - mountPath: /etc/nginx/ name: nginx-config readOnly: true - image: docker.io/library/ghost:5.82.0 imagePullPolicy: IfNotPresent name: ghost env: - name: database__client value: "sqlite3" - name: database__connection__filename value: "/var/lib/ghost/content/data/ghost.db" volumeMounts: - name: ghost-data mountPath: /var/lib/ghost/content volumes: - name: nginx-config configMap: name: nginx-config - name: ghost-data emptyDir: {}

init 容器

init 容器和 pause 容器有相同点,也有不同点

  • 相同点在于:它们都有固定用途,是专用的特殊容器
  • 不同点在于: init容器是用户级的容器,它是由用户来定义的,而 pause 容器是系统级容器,它不是由用户定义的。

init 容器会在应用(业务)容器启动之前运行,用来包含一些应用镜像中不存在的实用工具或安装脚本。

init 容器的运行机制

init 容器,从名字上来看,也能看出是的用途就是运行一些初始化任务,来保证应用容器运行环境。

这就决定了:

  • init 容器必须先于 应用容器启动
  • 仅当 init 容器完成后,才能运行应用容器
  • 一个 Pod 允许有多个 init 容器,做不同的初始化任务

当一个 Pod 有多个 init 容器时,这些 init 容器是顺序运行的,一个 init 容器完成之后,才会运行下一个 init 容器。

如果你在 kubectl get po 时加一个 -w 参数,就能看到 Pod 状态的变化过程

[root@kubernetes-master ~]# kubectl get pod -w NAME READY STATUS RESTARTS AGE test-init-pod 0/1 Init:0/2 0 3s # 创建第一个init容器 test-init-pod 0/1 Init:0/2 0 4s test-init-pod 0/1 Init:1/2 0 11s # 创建第二个init容器 test-init-pod 0/1 Init:1/2 0 12s test-init-pod 0/1 PodInitializing 0 22s # init运行完成,开始创建应用容器 test-init-pod 1/1 Running # 创建应用容器完成

在正常情况下(默认 Pod 的 restartPolicy 为 Never),只要有一个 init 容器运行失败,整个 Pod 便会不停地重启,直到 init 容器全部运行完成为止。

而只要一个 Pod 重启,不管init 容器之前有没有执行过,所有的 init 容器都要重新执行一遍。

init 容器与应用容器,除个别配置差异,其余基本一致,以下说明差异项:

  • 定义位置不同。

应用容器定义在 Pod.Spec.Containers,是必填字段,而 init 是定义在 Pod.Spec.initContainers 中,是可选字段。

  • 部分配置不同。

init 容器没有 Lifecycle actions, Readiness probes, Liveness probes 和 Startup probes,而这些应用容器都有。

另外,虽然 init 容器与应用容器是两个类别的容器,但由于属于同一个 Pod ,因此容器的名字是不能重复的。

init 容器的资源请求

当一个 Pod 只有应用容器时,那么在 kube-scheduler 调度该 Pod 时,会将所有的应用容器的 requests/limits 资源进行相加,得到一个requests/limits 的总量。

然后拿这个总量,去跟 node 上的可用资源进行比较,若 node 上的资源充足,则允许调度过去,反之则不允许。

而现在有了 init 容器后,情况会稍微复杂一,先说结论:

  • 当只有应用容器时:由于应用容器是同时运行的,因此为了保证应用容器的正常运行,请求的资源总量应当是所有应用容器]的各自请求的资源之和。
  • 当有 init 容器时:由于 init 容器会先于应用容器运行,只有当 init 运行成功并且退出后,应用容器才会运行,因此为了保证所有的容器(不仅包括应用容器,还包括 init 容器)的运行,pod 的资源总量的计算公式为max(应用容器请求资源之和,max(所有的 init 容器请求资源))

以 requests.cpu 为例,来实践一下 kube-scheduler到底是如何请求的资源问题的。

而 limits 以及其他资源类型(如 memory)都是一样的道理。

下面是一个包含一个应用容器和两个 init 容器的 Pod 的资源文件

# init-pod.yml apiVersion: v1 kind: Pod metadata: name: test-init-pod labels: app: init spec: containers: - name: busybox image: docker.io/library/busybox:1.31.1 resources: requests: cpu: 100m command: ['sh', '-c', 'echo The app is running! && sleep 3600'] initContainers: - name: init-01 image: docker.io/library/busybox:1.31.1 command: ['sh', '-c', 'sleep 10'] resources: requests: cpu: 20m - name: init-02 image: docker.io/library/busybox:1.31.1 command: ['sh', '-c', 'sleep 10'] resources: requests: cpu: 30m

使用 kubectl apply -f init-pod.yml 创建该 pod 之后,再使用 kubectl get po -o wide 查看一下创建到哪一台 node 上。

假设创建到 node1 上,使用 kubectl describe node kubernetes-node1 就可以看到该 node 上的所有 pod 的详情,包括资源占用情况。

可以看到 requests.cpu 总量计算为 100m,这刚好是应用容器的 requests.cpu

[root@kubernetes-master ~]# kubectl describe node kubernetes-node1 Non-terminated Pods: (5 in total) Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age --------- ---- ------------ ---------- --------------- ------------- --- default test-init-pod 100m (2%) 0 (0%) 0 (0%) 0 (0%) 3m7s kube-system calico-kube-controllers-64cc74d646-d99w2 0 (0%) 0 (0%) 0 (0%) 0 (0%) 27h kube-system calico-node-jnsqh 250m (6%) 0 (0%) 0 (0%) 0 (0%) 27h kube-system coredns-6d8c4cb4d-jnk9p 100m (2%) 0 (0%) 70Mi (1%) 170Mi (4%) 28h kube-system kube-proxy-zdzpp 0 (0%) 0 (0%) 0 (0%) 0 (0%) 27h Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 450m (11%) 0 (0%) memory 70Mi (1%) 170Mi (4%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 0 (0%) 0 (0%)

删掉pod,把应用容器的cpu下调到10m(小于其他任意init容器)时,再创建pod并观察:该 pod 的 requests.cpu 只剩下 30m,刚好是 init-02 请求的资源值

[root@kubernetes-master ~]# kubectl describe node kubernetes-node1 Non-terminated Pods: (5 in total) Namespace Name CPU Requests CPU Limits Memory Requests Memory Limits Age --------- ---- ------------ ---------- --------------- ------------- --- default test-init-pod 30m (0%) 0 (0%) 0 (0%) 0 (0%) 46s kube-system calico-kube-controllers-64cc74d646-d99w2 0 (0%) 0 (0%) 0 (0%) 0 (0%) 27h kube-system calico-node-jnsqh 250m (6%) 0 (0%) 0 (0%) 0 (0%) 27h kube-system coredns-6d8c4cb4d-jnk9p 100m (2%) 0 (0%) 70Mi (1%) 170Mi (4%) 28h kube-system kube-proxy-zdzpp 0 (0%) 0 (0%) 0 (0%) 0 (0%) 28h Allocated resources: (Total limits may be over 100 percent, i.e., overcommitted.) Resource Requests Limits -------- -------- ------ cpu 380m (9%) 0 (0%) memory 70Mi (1%) 170Mi (4%) ephemeral-storage 0 (0%) 0 (0%) hugepages-1Gi 0 (0%) 0 (0%) hugepages-2Mi 0 (0%) 0 (0%) Events: <none>

init 容器的应用场景

上面我们以简单的例子来理解 init 容器的运行机器和资源计算,每个容器都运行简单的 sleep 命令,并没有代入实际的业务场景,也许会让你以为 init 容器和普通的应用容器没什么区别。实际上并不是那样子的,那 init 容器到底有什么用呢?它的应用场景又有哪些呢?

举一个最简单的例子:假设我们有一个 Web 服务,该服务又依赖于另外一个数据库服务。但是在在启动这个 Web 服务的时候,我们并不能保证依赖的这个数据库服务就已经启动起来了,所以可能会出现一段时间内 Web 服务连接数据库异常。

要解决这个问题的话我们就可以在 Web 服务的 Pod 中使用一个InitContainer,在这个初始化容器中去检查数据库是否已经准备好了,准备好了过后初始化容器就结束退出,然后我们的主容器 Web 服务被启动起来,这个时候去连接数据库就不会有问题了。

资源调度

标签和选择器

在 Kubernetes 集群中,会运行大量不同类型的资源(Pod、Service、Deployment、Node 等)。随着集群规模扩大,如何高效分类、筛选、关联这些资源成为核心问题。标签(Label)和选择器(Selector)正是为解决这一问题而生的核心机制,具体价值体现在以下 3 点:

  1. 资源分类与筛选:通过标签为资源添加多维度的自定义标识(如环境、应用、版本、角色),再通过选择器快速筛选出目标资源。例如:区分开发环境(env=dev)和生产环境(env=prod)的 Pod,筛选某个应用(app=nginx)的所有实例。
  2. 资源松耦合关联:实现不同资源之间的动态绑定,无需硬编码。例如:Service 通过选择器关联后端 Pod,无论 Pod 如何重启、扩容,只要标签匹配,Service 就能自动转发流量;Deployment 通过选择器管理对应的 Pod 副本。
  3. 灵活的资源管理:支持多标签组合筛选,满足复杂的资源管理需求。例如:筛选 app=nginxversion=1.29env=prod 的 Pod。

定义标签

标签是附在资源 metadata.labels 上的键值对,有两种定义方式:

  • 在资源yaml文件中通过 metadata.labels 定义,以 Pod 为例
apiVersion: v1 kind: Pod metadata: name: nginx-demo namespace: pod-example labels: # 定义标签 type: app # 资源类型:应用 app: nginx # 应用名称:nginx version: 1.29.4 # 应用版本:1.29.4 env: test # 运行环境:测试

临时创建 Label

通过kubectl label创建临时Label,格式为kubectl label pod <podname> <键=值>

# 为pod新增一个临时label [root@kubernetes-master ~]# kubectl label pod nginx-demo-readiness-probe -n pod-example version=0.1 pod/nginx-demo-readiness-probe labeled [root@kubernetes-master ~]# kubectl get pod -n pod-example --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-demo-readiness-probe 1/1 Running 0 114s version=0.1

修改 Label

在创建label的基础上,添加--overwrite可以修改临时label参数

# 修改pod的label [root@kubernetes-master ~]# kubectl label pod nginx-demo-readiness-probe -n pod-example version=0.2 --overwrite pod/nginx-demo-readiness-probe labeled [root@kubernetes-master ~]# kubectl get pod -n pod-example --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-demo-readiness-probe 1/1 Running 0 5m34s test-label-name=test-label-value,type=app,version=0.2

查询标签

kubectl get pod为例,使用-l参数即可查询指定标签的pod

# 查看pod的标签 [root@kubernetes-master ~]# kubectl get pod -n pod-example --show-labels NAME READY STATUS RESTARTS AGE LABELS nginx-demo 1/1 Running 0 5m29s type=app,version=1.0.0 nginx-demo-2 1/1 Running 0 3m41s version=1.0.2 # 指定查看version 在(1.0.0,1.0.2)中的pod [root@kubernetes-master ~]# kubectl get pod -l 'version in (1.0.0,1.0.2)' -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 5m39s nginx-demo-2 1/1 Running 0 3m51s # 查看version!=1.0.2 的pod [root@kubernetes-master ~]# kubectl get pod -l version!=1.0.2 -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 5m44s # 查看version!=1.0.2且type=app的pod [root@kubernetes-master ~]# kubectl get pod -l 'version!=1.0.2,type=app' -n pod-example NAME READY STATUS RESTARTS AGE nginx-demo 1/1 Running 0 5m48s

删除标签

在添加标签的语法上,键名后加 - 表示删除该标签:kubectl label pod <节点名> <标签键>-

# 删除标签type=test [root@kubernetes-master ~]# kubectl label pod nginx type-

Deployment

在前面的章节中,我们通过 YAML 直接创建 Pod,但这种方式创建的 Pod 是 “一次性资源”:Pod 异常退出后无法自动重建、无法批量扩缩容、无法平滑升级,管理成本极高。

Kubernetes 中,ReplicaSet(RS)是副本控制器,核心作用是保证指定数量的 Pod 副本始终运行(Pod 挂了自动重建、副本数不足自动补充);而 Deployment 是对 ReplicaSet 的更高层次编排控制器,在 RS 的基础上增加了滚动升级、版本回滚、扩缩容、暂停 / 恢复部署等核心能力,是管理无状态 Pod 的首选方式(有状态应用推荐使用 StatefulSet)。

  • 层级关系本质:Deployment 不直接操作 Pod,而是通过控制 ReplicaSet 的 “版本” 实现对 Pod 的管理(比如升级 Deployment 时,会创建新的 RS,逐步缩容旧 RS、扩容新 RS,实现滚动升级);

  • ReplicaSet 的核心价值:仅负责 “维持 Pod 副本数”,无版本管理、滚动升级能力;

  • Deployment 的核心优势:相比直接管理 RS/Pod,支持一键扩缩容(kubectl scale deploy nginx-deploy --replicas=3)、滚动升级(kubectl set image deploy nginx-deploy nginx=nginx:1.28)、版本回滚(kubectl rollout undo deploy nginx-deploy)等运维操作

Deployment、ReplicaSet、Pod 存在明确的层级关系:Deployment 管理 ReplicaSet(每个版本对应一个 RS),ReplicaSet 管控 Pod 的副本数,我们可通过实操验证这一层级

# 创建一个名为 nginx-deploy 的Deployment,指定本地nginx镜像 [root@kubernetes-master ~]# kubectl create deployment nginx-deploy --image=docker.io/library/nginx:latest deployment.apps/nginx-deploy created # 查看 Deployment(READY=就绪副本数/期望副本数,UP-TO-DATE=最新版本副本数,AVAILABLE=可用副本数) [root@kubernetes-master ~]# kubectl get deploy NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 1/1 1 1 11s # 查看 ReplicaSet(名称规则:Deployment名称-随机哈希值,哈希由Deployment的配置生成,每个版本对应一个唯一RS) [root@kubernetes-master ~]# kubectl get replicaset NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 1 1 1 37s # 查看 Pod(名称规则:ReplicaSet名称-随机字符串,RS会保证该Pod的副本数始终等于DESIRED值) [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE nginx-deploy-64b476c78b-dqjxp 1/1 Running 0 41s # 查看pod,rs,deployment的标签信息 [root@kubernetes-master ~]# kubectl get pod,rs,deploy --show-labels NAME READY STATUS RESTARTS AGE LABELS pod/nginx-deploy-64b476c78b-dqjxp 1/1 Running 0 25m app=nginx-deploy,pod-template-hash=64b476c78b NAME DESIRED CURRENT READY AGE LABELS replicaset.apps/nginx-deploy-64b476c78b 1 1 1 25m app=nginx-deploy,pod-template-hash=64b476c78b NAME READY UP-TO-DATE AVAILABLE AGE LABELS deployment.apps/nginx-deploy 1/1 1 1 25m app=nginx-deploy

创建Deployment

我们可以通过一个简单方式获取deployment的yaml资源文件

# 创建一个名为 nginx-deploy 的Deployment,指定本地nginx镜像 [root@kubernetes-master ~]# kubectl create deployment nginx-deploy --image=docker.io/library/nginx:latest deployment.apps/nginx-deploy created # 将名为 nginx-deploy的Deployment以yaml的形式输出 [root@kubernetes-master ~]# kubectl get deploy nginx-deploy -o yaml > nginx-deploy.yaml

之后,通过编辑nginx-deploy.yaml,删除掉status之后的内容,再删除掉一些不必要的信息,我们可以获得一个简单的deployment

apiVersion: apps/v1 # deployment api 版本 kind: Deployment # 资源类型 metadata: # 元信息 labels: # deployment 标签 app: nginx-deploy name: nginx-deploy namespace: default # 命名空间 spec: replicas: 1 # 期望副本数 revisionHistoryLimit: 10 # 进行滚动更新,保留的历史版本数 selector: # 选择器,用于找到匹配的rs matchLabels: # 按照标签匹配,需要包含pod标签,用于匹配pod app: nginx-deploy # 匹配的标签key/value strategy: # 更新策略 rollingUpdate: # 滚动更新配置 maxSurge: 25% # 滚动更新时,可同时启最多25%的实例 maxUnavailable: 25% # 滚动更新时,可同时停止最多25%的实例 type: RollingUpdate # 更新类型: 滚动更新 template: # pod模板 metadata: # pod元信息 labels: # pod标签 app: nginx-deploy spec: # pod期望信息 containers: # pod容器 - image: docker.io/library/nginx:1.20.2 # 镜像 imagePullPolicy: IfNotPresent # 拉取策略 name: nginx # 容器名称 restartPolicy: Always # 重启策略 terminationGracePeriodSeconds: 30 # 删除操作最多宽限时长(优雅退出时间)

如果想对某个已经创建的deployment修改资源配置文件,也可以使用edit

[root@kubernetes-master ~]# kubectl edit deploy nginx-deploy

现在,通过这个yaml可以创建一个简单的deployment

[root@kubernetes-master ~]# kubectl apply -f nginx-deploy.yaml

水平扩展/收缩

水平扩展 / 收缩非常容易实现,Deployment 只需要修改它所控制的ReplicaSetPod 副本个数就可以了。比如,把这个值从 1 改成 3,那么 Deployment 所对应的 ReplicaSet,就会根据修改后的值自动创建两个新的Pod,“水平收缩”则反之。这个操作的指令也非常简单,我们可以直接通过修改deployment资源文件或者kubectl scale命令实现

# 修改deployment实现水平扩展,修改 spec.replicasc参数 [root@kubernetes-master ~]# kubectl edit deploy nginx-deploy spec: replicas: 1 # 副本数 # 使用scale命令修改副本数 [root@kubernetes-master ~]# kubectl scale deployment nginx-deploy --replicas=4

如果你手快点还能通过kubectl rollout status deployment <deployment_name>看到扩展过程中Deployment对象的状态变化:

[root@kubernetes-master ~]# kubectl rollout status deploy nginx-deploy Waiting for deployment "nginx-deploy" rollout to finish: 2 of 4 updated replicas are available... Waiting for deployment "nginx-deploy" rollout to finish: 3 of 4 updated replicas are available... deployment "nginx-deploy" successfully rolled out # ReplicaSet的Name没有发生变化 [root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 4 4 4 69m

这证明了 Deployment水平扩展和收缩副本集是不会创建新的ReplicaSet的,但是涉及到Pod模板的更新后,比如更改容器的镜像,那么Deployment会用创建一个新版本的ReplicaSet用来替换旧版本。

滚动更新

当修改deployment资源文件中template的属性时,会触发更新操作

在上面的Deployment定义里,Pod模板里的容器镜像设置的是,如果项目代码更新了,打包了新的镜像 ,部署新镜像的过程就会触发Deployment的滚动更新。

有两种方式更新镜像,一种是更新deployment.yaml里的镜像名称,然后执行 kubectl apply -f deployment.yaml。一般公司里的Jenkins等持续继承工具用的就是这种方式。还有一种就是使用kubectl set image 命令。

# 修改image: docker.io/library/nginx:1.28 [root@kubernetes-master ~]# kubectl edit deploy nginx-deploy deployment.apps/nginx-deploy edited # 直接设置镜像:kubectl set image deployment/<Deployment名称> <容器名>=<新镜像地址> [--record] [root@kubernetes-master ~]# kubectl set image deployment/nginx-deploy nginx=1.9.1

执行滚动更新后通过命令行查看ReplicaSet的状态会发现Deployment用新版本的ReplicaSet对象替换旧版本对象的过程。

[root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 3 3 3 76m nginx-deploy-6cbf9498b8 2 2 0 4s [root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 0 0 0 79m nginx-deploy-6cbf9498b8 4 4 4 2m41s

通过这个Deployment的Events可以查看到这次滚动更新的详细过程:

[root@kubernetes-master ~]# kubectl describe deploy nginx-deploy ......... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal ScalingReplicaSet 13m (x2 over 25m) deployment-controller Scaled up replica set nginx-deploy-64b476c78b to 4 Normal ScalingReplicaSet 4m32s deployment-controller Scaled up replica set nginx-deploy-6cbf9498b8 to 1 Normal ScalingReplicaSet 4m32s deployment-controller Scaled down replica set nginx-deploy-64b476c78b to 3 Normal ScalingReplicaSet 4m32s deployment-controller Scaled up replica set nginx-deploy-6cbf9498b8 to 2 Normal ScalingReplicaSet 3m47s (x2 over 13m) deployment-controller Scaled down replica set nginx-deploy-64b476c78b to 1 Normal ScalingReplicaSet 3m47s deployment-controller Scaled down replica set nginx-deploy-64b476c78b to 2 Normal ScalingReplicaSet 3m47s deployment-controller Scaled up replica set nginx-deploy-6cbf9498b8 to 3 Normal ScalingReplicaSet 3m47s deployment-controller Scaled up replica set nginx-deploy-6cbf9498b8 to 4 Normal ScalingReplicaSet 3m45s deployment-controller Scaled down replica set nginx-deploy-64b476c78b to 0

当你修改了Deployment里的Pod定义之后,Deployment 会使用这个修改后的 Pod 模板,创建一个新的 ReplicaSet,这个新的ReplicaSet 的初始Pod副本数是:0。然后Deployment 开始将这个新的ReplicaSet所控制的Pod 副本数从 0 个变成 1 个,即:“水平扩展”出一个副本。紧接着Deployment又将旧的 ReplicaSet所控制的旧 Pod 副本数减少一个,即:“水平收缩”成3个副本。如此交替进行就完成了这一组Pod 的版本升级过程。像这样,将一个集群中正在运行的多个 Pod 版本,交替地逐一升级的过程,就是 “滚动更新”。

为了保证服务的连续性,Deployment 还会确保,在任何时间窗口内,只有指定比例的Pod 处于离线状态。同时,它也会确保,在任何时间窗口内,只有指定比例的新 Pod 被创建出来。这两个比例的值都是可以配置的,默认都是期望状态里spec.relicas值的 25%。所以,在上面这个 Deployment 的例子中,它有 4 个 Pod 副本,那么控制器在“滚动更新”的过程中永远都会确保至少有 2 个Pod 处于可用状态,至多只有 2 个 Pod 同时存在于集群中。这个策略可以通过Deployment 对象的一个字段,RollingUpdateStrategy来设置。

设置注解

--record=truekubectl 命令的可选参数,这个参数能让Kubernetes在这个Deployment的变更记录里记录上产生变更当时执行的命令。--record 只对修改 Deployment 状态的命令生效(如 scaleset imageapply 改配置),纯查询命令(如 getdescribe)加 --record 无意义。

执行kubectl rollout history deployment <deploy_name> 就能看到这个Deployment的更新记录:

[root@kubernetes-master ~]# kubectl rollout history deploy nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 1 <none> 2 <none>

废弃说明--recordkubectl 1.14+ 版本后被标记为 “废弃(deprecated)”(但仍可正常使用),K8s 官方推荐手动添加注解替代(效果一致):

执行 kubectl annotatedeploy 添加的 change-cause 注解仅会作用于后续新建的 Revision

# 手动添加变更原因注解 kubectl annotate deployment nginx-deploy kubectl.kubernetes.io/change-cause="更新版本到1.28" # 如果需要修改,则添加`--overwrite`字段进行强制修改 kubectl annotate deployment nginx-deploy kubectl.kubernetes.io/change-cause="更新版本到1.28" --overwrite # 进行滚动更新 kubectl set deployment/nginx-deploy nginx=nginx:1.28

回滚

执行kubectl rollout history deployment <deployment_name>就能看到这个Deployment的更新记录,如果设置了注解,则在CHANGE-CAUSE能看到对应的注解

[root@kubernetes-master ~]# kubectl rollout history deploy nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 1 <none> 2 <none>

kubectl rollout history deploy <deployment_name> --revision=1就能看到对应revision的详细信息

[root@kubernetes-master ~]# kubectl rollout history deploy nginx-deploy --revision=1 deployment.apps/nginx-deploy with revision #1 Pod Template: Labels: app=nginx-deploy pod-template-hash=64b476c78b Containers: nginx: Image: docker.io/library/nginx:latest Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none> [root@kubernetes-master ~]# kubectl rollout history deploy nginx-deploy --revision=2 deployment.apps/nginx-deploy with revision #2 Pod Template: Labels: app=nginx-deploy pod-template-hash=6cbf9498b8 Containers: nginx: Image: docker.io/library/nginx:1.28 Port: <none> Host Port: <none> Environment: <none> Mounts: <none> Volumes: <none>

kubectl rollout undo deployment <deployment_name> --to-revision=1命令实现Deployment对象的版本回滚。

# 回滚到REVISION 1 [root@kubernetes-master ~]# kubectl rollout undo deployment nginx-deploy --to-revision=1 deployment.apps/nginx-deploy rolled back

执行完后我们会发现:以前那个版本的ReplicaSet的Pod的数又变回4,新ReplicaSet的Pod数变成了0。

[root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 4 4 4 121m nginx-deploy-6cbf9498b8 0 0 0 44m [root@kubernetes-master ~]#

证明Deployment在上次滚动更新后并不会把旧版本的ReplicaSet删掉,而是留着回滚的时候用,所以ReplicaSet相当于一个基础设施层面的应用的版本管理。

回滚后在看变更记录,发现已经没有修订号1的内容了,而是多了修订号为3的内容,这个版本的变更内容其实就是回滚前修订号1里的变更内容。

[root@kubernetes-master ~]# kubectl rollout history deployment nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 2 <none> 3 <none>

控制ReplicaSet版本数

你可能已经想到了一个问题:我们对Deployment 进行的每一次更新操作,都会生成一个新的ReplicaSet 对象,是不是有些多余,甚至浪费资源?所以,Kubernetes 项目还提供了一个指令,使得我们对 Deployment 的多次更新操作,最后只生成一个ReplicaSet对象。

在更新Deployment前,你要先执行一条 kubectl rollout pause deployment <deployment_name>指令,这个命令会让Deployment进入了一个”暂停”状态。由于此时Deployment正处于“暂停”状态,所以我们对Deployment的所有修改,都不会触发新的“滚动更新”,也不会创建新的ReplicaSet

而等到我们对 Deployment 修改操作都完成之后,只需要再执行一条 kubectl rollout resume deployment <deployment_name> 指令,就可以把这个 它恢复回来。

# 暂停deployment更新 [root@kubernetes-master ~]# kubectl rollout pause deploy nginx-deploy deployment.apps/nginx-deploy paused # 设置注解 [root@kubernetes-master ~]# kubectl annotate deployment nginx-deploy kubectl.kubernetes.io/change-cause="更新版本到1.9.1" --overwrite deployment.apps/nginx-deploy annotated # 修改deployment镜像版本,此时不会更新 [root@kubernetes-master ~]# kubectl set image deployment/nginx-deploy nginx=1.9.1 deployment.apps/nginx-deploy image updated # 查看rs状态,没有更新行为 [root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 4 4 4 150m nginx-deploy-6cbf9498b8 0 0 0 74m # 启动deployment更新 [root@kubernetes-master ~]# kubectl rollout resume deployment nginx-deploy deployment.apps/nginx-deploy resumed # 再次查看rs状态,正在进行更新 [root@kubernetes-master ~]# kubectl get rs NAME DESIRED CURRENT READY AGE nginx-deploy-64b476c78b 3 3 3 153m nginx-deploy-6cbf9498b8 0 0 0 76m nginx-deploy-74978448d5 2 2 0 7s # 再次查看历史版本信息 [root@kubernetes-master ~]# kubectl rollout history deployment nginx-deploy deployment.apps/nginx-deploy REVISION CHANGE-CAUSE 2 <none> 3 <none> 4 <none>

随着应用版本的不断增加,Kubernetes会为同一个Deployment保存很多不同的ReplicaSetDeployment 对象中spec.revisionHistoryLimit指定了 KubernetesDeployment 保留的”历史版本”个数。如果把它设置为 0,就再也不能做回滚操作了。

StatefulSet

StatefulSet 是用来管理有状态应用的工作负载 API 对象。,StatefulSet 运行一组 Pod,并为每个 Pod 保留一个稳定的标识。 这可用于管理需要持久化存储或稳定、唯一网络标识的应用。

Deployment 类似, StatefulSet 管理基于相同容器规约的一组 Pod。但和 Deployment 不同的是, StatefulSet 为它们的每个 Pod 维护了一个有粘性的 ID。这些 Pod 是基于相同的规约来创建的, 但是不能相互替换:无论怎么调度,每个 Pod 都有一个永久不变的 ID。

创建StatefulSet

创建一个StatefulSet简单的如下所示

# ------------- 第一部分:Headless Service 定义 ------------- --- # YAML分隔符:用于在单个文件中定义多个Kubernetes资源 apiVersion: v1 # 指定Kubernetes API版本,Service资源使用v1版本 kind: Service # 定义资源类型为Service(服务),用于暴露Pod网络 metadata: # 元数据部分:描述Service的基本信息 name: nginx # Service的名称,需与后续StatefulSet的serviceName字段匹配 labels: # 给Service打标签,用于资源筛选和关联 app: nginx spec: # Service的规约 ports: # 定义Service暴露的端口列表 - port: 80 # Service对外暴露的端口号 name: web # 端口名称,用于标识(需符合DNS规范) clusterIP: None # 设置clusterIP为None,创建Headless Service(无头服务),无头服务不会分配集群IP,kube-dns会为每个Pod解析出独立的DNS记录 selector: # 标签选择器:匹配带有app=nginx标签的Pod,将Service流量转发到这些Pod app: nginx --- # ------------- 第二部分:StatefulSet 定义 ------------- apiVersion: apps/v1 # StatefulSet属于apps/v1 API组(K8s 1.9+稳定版) kind: StatefulSet # 定义资源类型为StatefulSet(有状态集合),用于管理有状态应用 metadata: # 元数据部分:描述StatefulSet的基本信息 name: web # StatefulSet的名称 spec: selector: # 标签选择器:匹配符合条件的Pod,用于管理StatefulSet下的Pod matchLabels: app: nginx # 必须与.spec.template.metadata.labels中的标签完全匹配,否则无法管理Pod serviceName: "nginx" # 关联的Headless Service名称,StatefulSet依赖此Service为Pod生成固定DNS名称 replicas: 3 # 副本数 minReadySeconds: 10 # 最小就绪秒数:Pod启动后至少就绪10秒,才被视为可用 template: # Pod模板 metadata: labels: # Pod的标签,必须与.spec.selector.matchLabels匹配 app: nginx spec: terminationGracePeriodSeconds: 10 # 终止宽限期:发送终止信号后,等待10秒再强制杀死容器 containers: # 容器列表:定义Pod内运行的容器 - name: nginx image: registry.k8s.io/nginx-slim:0.24 ports: # 声明容器监听的端口 - containerPort: 80 name: web volumeMounts: # 卷挂载配置:将PVC挂载到容器内指定路径 - name: www # 卷名称,需与volumeClaimTemplates.metadata.name匹配 mountPath: /usr/share/nginx/html # 容器内挂载路径 volumeClaimTemplates: # PVC模板:为每个StatefulSet Pod自动创建PVC - metadata: name: www # PVC名称,与volumeMounts.name匹配 spec: accessModes: [ "ReadWriteOnce" ] # 访问模式:ReadWriteOnce表示只能被单个节点挂载为读写模式 storageClassName: "my-storage-class" # 指定存储类名称:需提前创建对应的StorageClass resources: # 资源请求:声明需要的存储资源大小 requests: storage: 1Gi # 请求1GiB的存储容量

因为StatefulSet涉及到持久化存储,还需要进行存储配置,创建存储类并手动创建3个PV(对应副本数为pod副本数3)

创建 storageclass.yaml 文件:

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: my-storage-class # 与StatefulSet中storageClassName完全一致 provisioner: kubernetes.io/no-provisioner # 手动创建PV,不依赖动态供给 volumeBindingMode: WaitForFirstConsumer # 延迟绑定,适配测试环境 reclaimPolicy: Delete # 删除PVC时自动删除PV(测试环境方便清理)
[root@kubernetes-master ~]# vim storageclass.yaml [root@kubernetes-master ~]# kubectl apply -f storageclass.yaml storageclass.storage.k8s.io/my-storage-class created [root@kubernetes-master ~]# kubectl get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE my-storage-class kubernetes.io/no-provisioner Delete WaitForFirstConsumer false 25s

手动创建3个PV,在所有节点上创建对应目录后应用PV

# PV1:供web-0 Pod使用 apiVersion: v1 kind: PersistentVolume metadata: name: pv-nginx-0 # 自定义名称,便于识别 spec: capacity: storage: 1Gi # 匹配PVC的1Gi请求 accessModes: - ReadWriteOnce # 匹配PVC的访问模式 persistentVolumeReclaimPolicy: Delete # 测试环境删除PVC自动删PV storageClassName: my-storage-class # 关联上面创建的存储类 hostPath: # 测试环境用主机目录(最简单,缺点:Pod调度到对应节点才可用) path: /data/nginx/pv-0 # 节点上的目录,需提前创建 type: DirectoryOrCreate # PV2:供web-1 Pod使用 --- apiVersion: v1 kind: PersistentVolume metadata: name: pv-nginx-1 spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete storageClassName: my-storage-class hostPath: path: /data/nginx/pv-1 type: DirectoryOrCreate # PV3:供web-2 Pod使用 --- apiVersion: v1 kind: PersistentVolume metadata: name: pv-nginx-2 spec: capacity: storage: 1Gi accessModes: - ReadWriteOnce persistentVolumeReclaimPolicy: Delete storageClassName: my-storage-class hostPath: path: /data/nginx/pv-2 type: DirectoryOrCreate
[root@kubernetes-master ~]# vim pv.yaml # 手动在三台节点上都创建目录并修改权限 [root@kubernetes-master ~]# mkdir -p /data/nginx/pv-0 [root@kubernetes-master ~]# mkdir -p /data/nginx/pv-1 [root@kubernetes-master ~]# mkdir -p /data/nginx/pv-2 [root@kubernetes-master ~]# chmod 777 /data/nginx/* # 创建三个PV [root@kubernetes-master ~]# kubectl apply -f pv.yaml persistentvolume/pv-nginx-0 created persistentvolume/pv-nginx-1 created persistentvolume/pv-nginx-2 created # 查看PV状态 [root@kubernetes-master ~]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv-nginx-0 1Gi RWO Delete Available my-storage-class 9s pv-nginx-1 1Gi RWO Delete Available my-storage-class 9s pv-nginx-2 1Gi RWO Delete Available my-storage-class 9s

PV创建完后,现在可以创建StatefulSet

[root@kubernetes-master ~]# kubectl apply -f statfulset-nginx.yaml service/nginx-de created statefulset.apps/web created # 验证创建状态 # kubectl get statefulset可以简写为kubectl get sts [root@kubernetes-master ~]# kubectl get statefulset NAME READY AGE web 3/3 79s [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 90s web-1 1/1 Running 0 60s web-2 1/1 Running 0 40s [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d21h # K8S自带,与本示例无关 nginx-de ClusterIP None <none> 80/TCP 102s [root@kubernetes-master ~]# kubectl get statefulset NAME READY AGE web 3/3 119s [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 2m5s web-1 1/1 Running 0 95s web-2 1/1 Running 0 75s [root@kubernetes-master ~]# kubectl get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE www-web-0 Bound pv-nginx-0 1Gi RWO my-storage-class 2m10s www-web-1 Bound pv-nginx-2 1Gi RWO my-storage-class 100s www-web-2 Bound pv-nginx-1 1Gi RWO my-storage-class 80s

水平扩展/收缩

StatefulSet的水平扩缩容也只需要修改副本数即可,与deployments一样,有两种修改办法

deployments不同的是,StatefulSet的扩容和缩容有顺序性,可以理解成栈结构(后进先出)例如将副本扩展到5个,再缩容到2个(首个pod序号0),扩容时依次创建1、2、3、4,缩容时依次删除4、3、2

# scale参数扩容 kubectl scale sts <StatefulSet_name> --replicas=2 # path参数修改副本数 kubectl patch sts <StatefulSet_name> -p '{"spec":{"replicas":3}}'

将上述示例资源缩容至1,再扩容至3

[root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 3/3 27m # 缩容至1 [root@kubernetes-master ~]# kubectl scale sts web --replicas=1 statefulset.apps/web scaled [root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 1/1 30m # 扩容至3 [root@kubernetes-master ~]# kubectl scale sts web --replicas=3 statefulset.apps/web scaled [root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 3/3 33m # 查看sts信息 [root@kubernetes-master ~]# kubectl describe sts web Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal SuccessfulDelete 6m6s statefulset-controller delete Pod web-2 in StatefulSet web successful Normal SuccessfulDelete 6m6s statefulset-controller delete Pod web-1 in StatefulSet web successful Normal SuccessfulCreate 108s (x2 over 33m) statefulset-controller create Pod web-1 in StatefulSet web successful Normal SuccessfulCreate 88s (x2 over 33m) statefulset-controller create Pod web-2 in StatefulSet web successful

滚动更新

与deployment相同,滚动更新(以更新镜像为例)也有多种方法

  • 使用set image参数更新image
  • 使用path参数实现镜像更新
  • 通过edit修改资源yaml实现更新

以下用更新sts名为web的nginx镜像为例

$ kubectl get sts NAME READY AGE web 3/3 59m # set更新镜像 kubectl set image sts web nginx=docker.io/library/nginx:latest # path更新镜像 kubectl path sts web --type='json' -p='[{"op": "replace","path": "/spec/template/spec/containers/0/image", "value": "docker.io/library/nginx:latest"}]' # edit更新镜像 kubectl edit sts web spec: template: spec: containers: - name: nginx image: docker.io/library/nginx:latest
[root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 3/3 64m # 查看pod镜像 [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:1.28 web-2: docker.io/library/nginx:1.28 # 滚动更新 [root@kubernetes-master ~]# kubectl set image sts web nginx=docker.io/library/nginx:latest statefulset.apps/web image updated # 更新验证 [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:1.28 web-2: docker.io/library/nginx:latest [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:latest web-2: docker.io/library/nginx:latest [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:latest web-1: docker.io/library/nginx:latest web-2: docker.io/library/nginx:latest

更新策略

StatefulSet 更新策略有两种

  • 默认: RollingUpdate(滚动更新),按 Pod 序号倒序更新(web-2 → web-1 → web-0),确保服务不中断
  • OnDelete(需手动删除 Pod 才会重建为新镜像)。

编辑StatefulSet资源文件,可以看到具体的策略配置

spec: updateStrategy: rollingUpdate: partition: 0 type: RollingUpdate

由于创建pod是顺序创建的,在RollingUpdate模式下,更新pod会按 Pod 倒序更新(web-2 → web-1 → web-0),确保服务不中断,这也是默认的更新策略

如果改用OnDelete模式,则表示仅当删除pod的时候,才更新pod

# 设置更新策略 [root@kubernetes-master ~]# kubectl edit sts web updateStrategy: type: OnDelete # 滚动更新,此时pod不会更新 [root@kubernetes-master ~]# kubectl set image sts web nginx=docker.io/library/nginx:latest statefulset.apps/web image updated [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:1.28 web-2: docker.io/library/nginx:1.28 # 删掉一个pod,再查看,被删掉的pod更新了 [root@kubernetes-master ~]# kubectl delete pod web-1 pod "web-1" deleted [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:latest web-2: docker.io/library/nginx:1.28

灰度发布

灰度发布(Grayscale Release),又称金丝雀发布,是一种通过逐步扩大用户覆盖范围实现平滑过渡的软件发布方式。其核心机制为将新功能先向部分用户开放,通过A/B测试收集反馈并调整问题,最终完成全量发布。该方式可降低系统升级风险,保障稳定性并完善产品功能。

利用滚动更新中的partition属性,可以实现灰度发布的效果

注:在实际生产环境中,并不会通过修改partition属性做灰度发布,现在都是通过 istio ingress 来做灰度发布

例如此时有5个pod,如果当前的partition设置为3,则滚动更新时只会更新序号 >= 3 的pod

也就是说,通过控制partition值,可以决定只更新其中一部分pod,待到更新完成确认新pod没有问题后再逐渐增大更新的pod数量,最终实现全部的pod更新

# 将web的partition值设置为2 [root@kubernetes-master ~]# kubectl edit sts web spec: updateStrategy: rollingUpdate: partition: 0 type: RollingUpdate # 再进行滚动更新 [root@kubernetes-master ~]# kubectl set image sts web nginx=docker.io/library/nginx:latest # 查看pod镜像版本,此时只有序号>=2的pod被更新 [root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 3/3 85m [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:1.28 web-2: docker.io/library/nginx:latest # 再修改partition为1,查看pod [root@kubernetes-master ~]# kubectl get pods -l app=nginx -o jsonpath='{range .items[*]}{.metadata.name}: {.spec.containers[0].image}{"\n"}{end}' web-0: docker.io/library/nginx:1.28 web-1: docker.io/library/nginx:latest web-2: docker.io/library/nginx:latest

回滚

若更新后出问题,可回滚到上一版本:

kubectl rollout undo sts web # 回滚到上一版本 kubectl rollout history sts web # 查看更新历史

删除

StatefulSet 删除时,默认仅删除 STS 控制器本身,不会自动删除其管理的 Pod 和 PVC(这是 K8s 保护有状态应用数据的设计)

级联删除

删除掉StatefulSet时默认删除掉对应的pod(--cascade=foreground),这就叫做级联删除,如果不想在删掉sts时删掉pod(非级联删除),可通过kubectl delete sts web --cascade=orphan实现。

仅删除STS

如果仅需要删除STS控制器,保留pod/pvc

# 删除名为 web 的 StatefulSet(仅删控制器,Pod/PVC 保留) kubectl delete sts web --cascade=orphan # 验证结果: [root@kubernetes-master ~]# kubectl get sts No resources found in default namespace. [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 43m web-1 1/1 Running 0 18m web-2 1/1 Running 0 21m

如果想让保留的pod被新的STS接管,可以apply以前的sts yaml,重新创建STS

[root@kubernetes-master ~]# kubectl apply -f statfulset-nginx.yaml service/nginx-de unchanged statefulset.apps/web created [root@kubernetes-master ~]# kubectl get sts NAME READY AGE web 3/3 9s [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE web-0 1/1 Running 0 46m web-1 1/1 Running 0 11s web-2 1/1 Running 0 23m

彻底删除

如果需要彻底删除STS和所有关联的资源(STS + Pod + PVC + PV + Service),需要对创建的资源逐一删除

# 1. 删除sts,默认删除pod [root@kubernetes-master ~]# kubectl delete sts web statefulset.apps "web" deleted [root@kubernetes-master ~]# kubectl get pod No resources found in default namespace. # 2. 删除Headless Service,示例中为nginx-de [root@kubernetes-master ~]# kubectl delete svc nginx-de service "nginx-de" deleted [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 2d23h # 3. 删除PVC(存储声明),需手动删除 [root@kubernetes-master ~]# kubectl delete pvc www-web-o www-web-1 www-web-2 persistentvolumeclaim "www-web-0" deleted persistentvolumeclaim "www-web-1" deleted persistentvolumeclaim "www-web-2" deleted [root@kubernetes-master ~]# kubectl get pvc No resources found in default namespace. # 4. 删除PV(持久化卷),需要手动删除 [root@kubernetes-master ~]# kubectl delete pv pv-nginx-0 pv-nginx-1 pv-nginx-2 persistentvolume "pv-nginx-0" deleted persistentvolume "pv-nginx-1" deleted persistentvolume "pv-nginx-2" deleted [root@kubernetes-master ~]# kubectl get pv No resources found # 5. 删除StorageClass [root@kubernetes-master ~]# kubectl delete sc my-storage-class storageclass.storage.k8s.io "my-storage-class" deleted [root@kubernetes-master ~]# kubectl get sc No resources found # 6. 清理各节点存储目录(/data/nginx/pv-0/1/2) rm -rf /data/nginx/pv-*

DaemonSet

如果我们需要采集多个微服务pod下的日志或者监控pod信息,就需要在每个pod创建时再部署一个副本用于收集并集中pod信息,而手动部署显然不可能,因此我们需要一个自动在创建pod时就部署副本的组件,这就是DaemonSet所解决的问题

​ DaemonSet 可以保证集群中所有的或者部分的节点都能够运行一份 Pod 副本,每当有新的节点被加入到集群时,Pod 就会在目标的节点上启动,如果节点被从集群中剔除,节点上的 Pod 也会被垃圾收集器清除;DaemonSet 的作用就像是计算机中的守护进程,它能够运行集群存储、日志收集和监控等“守护进程”,这些服务一般是集群中必备的基础服务。

​ 使用DaemonSet的一些典型用法:

  • 运行集群存储daemon(守护进程),例如在每个节点上运行Glusterd、Ceph等;
  • 在每个节点运行日志收集daemon,例如Fluentd、Logstash;
  • 在每个节点运行监控daemon,比如Prometheus Node Exporter、Collectd、Datadog代理、New Relic代理或 Ganglia gmond;

​ 一个简单的用法是,在所有的 Node 上都存在一个 DaemonSet,将被作为每种类型的 daemon 使用。 一个稍微复杂的用法可能是,对单独的每种类型的 daemon 使用多个 DaemonSet,但具有不同的标志,和/或对不同硬件类型具有不同的内存、CPU要求。

创建DaemonSet

基于 DaemonSet 部署一个 Fluentd-Elasticsearch PodFluentd-Elasticsearch是 Kubernetes 集群中核心的日志采集与转发组件,核心作用是统一采集集群所有节点的系统日志(/var/log)和容器日志(K8S默认将节点容器的日志写入到节点/var/lib/docker/containers目录下),并将日志标准化后转发到 Elasticsearch 存储 / 检索。

DaemonSet可以确保每个node在加入节点时都创建一个Fluentd-Elasticsearch Pod ,节点被剔除时销毁该pod。

apiVersion: apps/v1 kind: DaemonSet # 资源类型 metadata: # 元数据 name: fluentd-elasticsearch spec: selector: # 标签选择器,匹配要管理的pod matchLabels: # 匹配标签 name: fluentd-elasticsearch template: # pod模板,定义要在每个node上创建的pod模板 metadata: labels: # 标签,需要与上方标签选择器定义一致,这样pod才能被管理到 name: fluentd-elasticsearch spec: # pod规格 tolerations: # 容忍度(允许 Pod 调度到有指定污点的节点) - key: node-role.kubernetes.io/master # 匹配的污点键:master 节点默认的污点(禁止普通 Pod 调度) effect: NoSchedule # 容忍的污点效果:NoSchedule(允许调度到该节点) containers: # pod内的容器列表,该示例仅有一个容器 - name: fluentd-elasticsearch image: agilestacks/fluentd-elasticsearch:v1.3.0 volumeMounts: # 容器内的卷挂载 - name: varlog # 要挂载的卷名称 mountPath: /var/log # 将卷挂载到容器中的该路径 - name: varlibdockercontainers mountPath: /var/lib/docker/containers readOnly: true # 只读挂载,防止容器篡改宿主机文件 terminationGracePeriodSeconds: 30 # 优雅终止时间 volumes: # pod定义卷,此为宿主机路径挂载 - name: varlog # 名称,与volumeMounts中名称对应 hostPath: # 宿主机卷路径,该路径卷挂载到pod内 path: /var/log - name: varlibdockercontainers hostPath: path: /var/lib/docker/containers

现在,apply资源文件,查看dsDaemonSet),确认在三个node节点上都创建了fluentd-elasticsearch pod

[root@kubernetes-master ~]# kubectl get ds NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE fluentd-elasticsearch 3 3 3 3 3 <none> 71s [root@kubernetes-master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES fluentd-elasticsearch-9lcqt 1/1 Running 0 77s 10.244.2.15 kubernetes-node2 <none> <none> fluentd-elasticsearch-dh99v 1/1 Running 0 77s 10.244.1.65 kubernetes-node1 <none> <none> fluentd-elasticsearch-mlmsx 1/1 Running 0 77s 10.244.0.3 kubernetes-master <none> <none>

指定Node节点

在如上示例中,DaemonSet将会作用于所有的node,如果我们只需要DaemonSet作用在某一些node上,则需要指定Node节点

DaemonSet有三种指定Node节点的方式:

  • nodeSelector:只调度到匹配指定label的Node上

  • nodeAffinity:功能更丰富的Node选择器,比如支持集合操作

  • podAffinity:调度到满足条件的Pod所在的Node上

nodeSelector

nodeSelector模式只调度到匹配指定label的Node上,只需要给node打上标签,然后在DaemonSet中设置匹配的标签即可

spec: template: spec: nodeSelector: # nodeSelector节点选择器 test: service # 匹配该标签

先给node打上标签,然后创建DaemonSet资源,此时DaemonSet只对匹配到标签的node生效

[root@kubernetes-master ~]# kubectl label node kubernetes-node1 test=service node/kubernetes-node1 labeled [root@kubernetes-master ~]# kubectl apply -f daemonset.yaml daemonset.apps/fluentd-elasticsearch created [root@kubernetes-master ~]# [root@kubernetes-master ~]# kubectl get ds NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE fluentd-elasticsearch 1 1 1 1 1 test=service 5s [root@kubernetes-master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES fluentd-elasticsearch-4zlkk 1/1 Running 0 11s 10.244.1.66 kubernetes-node1 <none> <none>

nodeAffinity

nodeAffinity目前支持两种:requiredDuringSchedulingIgnoredDuringExecution和preferredDuringSchedulingIgnoredDuringExecution,分别代表必须满足条件和优选条件。比如下面的例子代表调度到包含标签kubernetes.io/e2e-az-name并且值为e2e-az1或e2e-az2的Node上,并且优选还带有标签another-node-label-key=another-node-label-value的Node。

apiVersion: v1 kind: Pod metadata: name: with-node-affinity spec: affinity: nodeAffinity: requiredDuringSchedulingIgnoredDuringExecution: nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/e2e-az-name operator: In values: - e2e-az1 - e2e-az2 preferredDuringSchedulingIgnoredDuringExecution: - weight: 1 preference: matchExpressions: - key: another-node-label-key operator: In values: - another-node-label-value containers: - name: with-node-affinity image: gcr.io/google_containers/pause:2.0

podAffinity示例

podAffinity基于Pod的标签来选择Node,仅调度到满足条件Pod所在的Node上,支持podAffinity和podAntiAffinity。这个功能比较绕,以下面的例子为例:

  • 如果一个“Node所在Zone中包含至少一个带有security=S1标签且运行中的Pod”,那么可以调度到该Node
  • 不调度到“包含至少一个带有security=S2标签且运行中Pod”的Node上
apiVersion: v1 kind: Pod metadata: name: with-pod-affinity spec: affinity: podAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchExpressions: - key: security operator: In values: - S1 topologyKey: failure-domain.beta.kubernetes.io/zone podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchExpressions: - key: security operator: In values: - S2 topologyKey: kubernetes.io/hostname containers: - name: with-pod-affinity image: gcr.io/google_containers/pause:2.0

滚动更新

如果节点的标签被修改,DaemonSet 将立刻向新匹配上的节点添加 Pod, 同时删除不匹配的节点上的 Pod。

你可以修改 DaemonSet 创建的 Pod。不过并非 Pod 的所有字段都可更新。 下次当某节点(即使具有相同的名称)被创建时,DaemonSet 控制器还会使用最初的模板。

您可以删除一个 DaemonSet。如果使用 kubectl 并指定 —cascade=false 选项, 则 Pod 将被保留在节点上。接下来如果创建使用相同选择算符的新 DaemonSet, 新的 DaemonSet 会收养已有的 Pod。 如果有 Pod 需要被替换,DaemonSet 会根据其 updateStrategy 来替换。

DaemonSet 有两种更新策略:

  • OnDelete: 使用 OnDelete 更新策略时,在更新 DaemonSet 模板后,只有当你手动删除老的 DaemonSet pods 之后,新的 DaemonSet Pod 才会被自动创建。跟 Kubernetes 1.6 以前的版本类似。
  • RollingUpdate: 这是默认的更新策略。使用 RollingUpdate 更新策略时,在更新 DaemonSet 模板后, 老的DaemonSet pods 将被终止,并且将以受控方式自动创建新的 DaemonSet pods。 更新期间,最多只能有DaemonSet 的一个 Pod 运行于每个节点上。

statefulset中介绍的更新策略相同,默认为RollingUpdate策略,如果想设置为策略OnDelete,则需要配置.spec.updateStrategy.typeRollingUpdate

实际上,建议使用OnDelete的更新模式,因为滚动更新会更新所有的pod,考虑到资源损耗,采用OnDelete模式只将某些需要更新的pod删除掉。

HPA

HPA的全称为Horizontal Pod Autoscaling,它可以根据当前pod资源的使用率(如CPU、磁盘、内存等),进行副本数的动态的扩容与缩容,以便减轻各个pod的压力。当pod负载达到一定的阈值后,会根据扩缩容的策略生成更多新的pod来分担压力,当pod的使用比较空闲时,在稳定空闲一段时间后,还会自动减少pod的副本数量。

  • HPA:基于CPU、内存或自定义指标触发,扩展Pod 副本数(横向扩展),适用于业务流量波动(如电商促销)的场景。
  • VPA:基于指标触发(资源不足),扩展Pod 资源配额(纵向扩展),适用于单 Pod 性能瓶颈(如内存不够)。

HPA 本身不收集指标,需依赖外部组件提供 “指标数据”,核心依赖是 Metrics Server(K8s 官方的基础指标采集组件)。

HPA工作原理

HPA基于集群中运行的应用程序资源使用情况动态调整Pod副本的数量。HPA的工作原理可以概括为以下几个步骤:

  1. 采集指标 HPA 定期(默认 15 秒,可通过 --horizontal-pod-autoscaler-sync-period 调整)从 Metrics Server 或自定义指标源(如 Prometheus)获取指标,支持 3 类指标:

    • 资源指标(Resource Metrics):K8s 内置的 CPU / 内存,基于 Pod 请求(Request)计算使用率(如 “CPU 使用率 50%”= 实际使用 / Request);
    • 容器资源指标(Container Resource Metrics):针对单个容器的 CPU / 内存(而非整个 Pod);
    • 自定义指标(Custom Metrics):业务指标(如 QPS、请求延迟、并发用户数),需依赖 Prometheus + k8s-prometheus-adapter 等组件。
  2. 计算期望副本数

    HPA 基于 “当前指标值” 和 “目标指标值”,通过公式计算期望副本数:期望副本数 = ceil(当前副本数 × (当前指标值 / 目标指标值))

    如果当前副本数 = 2,目标 CPU 使用率 = 50%,实际 CPU 使用率 = 100%,期望副本数 = 2 × (100%/50%)=4 ,则自动扩容到 4 个副本。

    关键约束(避免极端情况): 最小副本数(minReplicas):扩缩容的下限(如至少 2 个副本,避免缩到 0); 最大副本数(maxReplicas):扩缩容的上限(如最多 10 个副本,避免资源耗尽); 容忍阈值:指标波动在 ±10% 内(默认),不触发扩缩容(避免频繁调整); 冷却时间:扩容后需等待 3 分钟(默认)才能再次扩容,缩容后需等待 5 分钟(默认)才能再次缩容(避免 “震荡”)。

  3. 执行扩缩容

    HPA 通过 K8s API 调用 Deployment/StatefulSet 的 scale 接口,修改其 replicas 字段,实现 Pod 副本数的自动调整。

部署Metrics Server

Metrics Server是K8s 官方的基础指标采集组件,先确认集群中是否有该服务,如果没有,则参照如下方式部署

# 查看Metrics Server pod kubectl get pods -n kube-system | grep metrics-server
  1. 下载官方部署文件(适配 K8s 1.24+,低版本需调整):
wget https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
  1. 编辑 components.yaml,在 spec.containers.args中添加--kubelet-insecure-tls

(跳过 Kubelet 证书验证,测试环境用;生产环境需配置证书)

spec: containers: - name: metrics-server image: k8s.gcr.io/metrics-server/metrics-server:v0.6.4 # 替换为最新版本 args: - --cert-dir=/tmp - --secure-port=4443 - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname - --kubelet-use-node-status-port - --kubelet-insecure-tls # 新增这行(测试环境)
  1. 部署并验证
# 部署 kubectl apply -f components.yaml # 检查 Pod 是否运行(命名空间 kube-system) kubectl get pods -n kube-system | grep metrics-server # 验证指标是否能获取(查看 Node/Pod 指标) kubectl top node # 查看节点 CPU/内存使用 kubectl top pod # 查看 Pod CPU/内存使用

创建HPA

创建deployment资源

在创建HPA前,我们先构建一个简单的deployment文件nginx-deployment.yaml,其中必须配置requests用于限制资源大小

apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deploy spec: replicas: 2 # 初始副本数 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.21 resources: # HPA 依赖 Request 计算使用率,必须配置。 requests: # 容器启动时保障资源(最小资源) cpu: 100m # 100 毫核(0.1 CPU) memory: 128Mi limits: # 容器最大上限资源 cpu: 200m memory: 256Mi --- # 可选:创建 Service(方便外部访问压测) apiVersion: v1 kind: Service metadata: name: nginx-svc spec: selector: app: nginx ports: - port: 80 targetPort: 80 type: NodePort # 测试环境用 NodePort 暴露服务

部署该资源

[root@kubernetes-master ~]# kubectl apply -f nginx-deployment.yaml deployment.apps/nginx-deploy created service/nginx-svc created [root@kubernetes-master ~]# kubectl get deploy NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 2/2 2 2 47s [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE nginx-deploy-8559b958fd-4fzn4 1/1 Running 0 67s nginx-deploy-8559b958fd-l4svf 1/1 Running 0 67s [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5d23h nginx-svc NodePort 10.105.200.61 <none> 80:31213/TCP 89s

创建HAP资源

通过yaml创建HPA并配置基于 CPU 使用率的扩缩容规则

apiVersion: autoscaling/v2 # 注意版本:v2 支持更多指标(v1 仅支持 CPU) kind: HorizontalPodAutoscaler metadata: name: nginx-hpa spec: scaleTargetRef: # 关联的 Deployment(HPA 要控制的目标) apiVersion: apps/v1 kind: Deployment name: nginx-deploy minReplicas: 2 # 最小副本数(避免缩到 0) maxReplicas: 10 # 最大副本数(避免资源耗尽) metrics: # 扩缩容指标配置(这里用 CPU 使用率) - type: Resource resource: name: cpu target: type: Utilization # 基于“使用率”(而非绝对值) averageUtilization: 50 # 目标 CPU 使用率:50%

创建HPA并验证

[root@kubernetes-master ~]# kubectl apply -f nginx-hpa.yaml horizontalpodautoscaler.autoscaling/nginx-hpa created # 等待一段时间,hpa完成指标检查后,<unknown>变为具体数值 [root@kubernetes-master ~]# kubectl get hpa nginx-hpa -o wide NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE nginx-hpa Deployment/nginx-deploy <unknown>/50% 2 10 0 14s [root@kubernetes-master ~]# kubectl get hpa nginx-hpa -o wide NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE nginx-hpa Deployment/nginx-deploy 0%/50% 2 10 2 98s

测试HPA扩缩容

通过 busybox 容器向 Nginx 发送大量请求,触发 CPU 使用率升高,观察 HPA 是否自动扩容。

# 获取 Nginx Service 的 ClusterIP [root@kubernetes-master ~]# echo $(kubectl get svc nginx-svc -o jsonpath='{.spec.clusterIP}') 10.105.200.61 # 启动一个交互式 busybox 容器 [root@kubernetes-master ~]# kubectl run -it --rm load-generator --image=busybox /bin/sh If you don't see a command prompt, try pressing enter. # 执行压测(用 wget 循环请求 Nginx,消耗 CPU) / # while true; do wget -q -O /dev/null 10.105.200.61; done

打开一个新的窗口,每隔 10 秒查看 HPA 状态,观察HPA变化

watch kubectl get hpa nginx-hpa -o wide

预期结果:随着压测进行,TARGETS 中的 “当前 CPU 使用率” 会从 5% 上升到 50% 以上,HPA 检测到指标超标后,会逐步将 REPLICAS 从 2 扩容到 4、6…(直到 CPU 使用率降至 50% 左右)

Every 2.0s: kubectl get hpa nginx-hpa -o wide Thu Jan 8 01:42:27 2026 NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE nginx-hpa Deployment/nginx-deploy 51%/50% 2 10 3 13m

停止负载,验证HPA缩容

busybox 容器内按 Ctrl+C 停止循环请求,然后exit退出容器(容器会自动删除)

继续用 watch 查看 HPA 状态:CPU 使用率逐步下降到 50% 以下,等待 5 分钟(默认缩容冷却时间)后,HPA 会将副本数逐步缩回到 2(minReplicas),TARGETS 回到 50% 以下。

[root@kubernetes-master ~]# kubectl get hpa nginx-hpa -o wide NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE nginx-hpa Deployment/nginx-deploy 0%/50% 2 10 3 19m # 等待默认缩容时间后,hpa缩容 [root@kubernetes-master ~]# kubectl get hpa nginx-hpa -o wide NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE nginx-hpa Deployment/nginx-deploy 0%/50% 2 10 2 22m

自定义HPA行为

默认的扩缩容规则(如冷却时间、扩容速度)可能不满足生产需求,可通过 behavior 字段自定义 HPA 行为。

apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: nginx-hpa-advanced spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: nginx-deploy minReplicas: 2 maxReplicas: 10 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 50 # 自定义扩缩容行为 behavior: scaleUp: # 扩容策略 stabilizationWindowSeconds: 60 # 扩容前等待 60 秒(确认指标稳定) policies: - type: Percent # 按百分比扩容 value: 50 # 每次扩容 50% 的副本数(如当前 2 个,扩到 3 个) periodSeconds: 60 # 每 60 秒最多扩容一次 scaleDown: # 缩容策略 stabilizationWindowSeconds: 300 # 缩容前等待 300 秒(避免误缩容) policies: - type: Fixed # 按固定数量缩容 value: 1 # 每次缩容 1 个副本 periodSeconds: 120 # 每 120 秒最多缩容一次

job

在日常的工作中经常都会遇到一些需要进行批量数据处理和分析的需求,当然也会有按时间来进行调度的工作,在Kubernetes集群中为我们提供了jobcornjob两种资源对象来应对这种需求。

Job负责处理任务,即仅执行一次的任务,它保证批处理任务的一个或多个Pod成功结束。而cornjob则就是在Job上加上了时间调度。

创建job

apiVersion: batch/v1 kind: Job metadata: creationTimestamp: null name: job-test spec: template: metadata: creationTimestamp: null spec: containers: - command: - sh - -c - date ; sleep 10 image: docker.io/library/busybox:1.31.1 imagePullPolicy: IfNotPresent name: job-test restartPolicy: Never

注意Job的RestartPolicy仅支持Never和OnFailure两种,不支持Always, Job就相当于来执行一个批处理任务,执行完就结束了,如果支持Always的话会陷入了死循环了。

  • Nerver 只要任务没有完成,就会删除pod并新创建pod运行,直到job完成【会产生多个pod】
  • OnFailure 只要pod没有完成,则会重启pod,直到job完成
[root@kubernetes-master ~]# kubectl apply -f job.yaml [root@kubernetes-master ~]# kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/job-test 0/1 1s 1s NAME READY STATUS RESTARTS AGE pod/job-test-zsch7 1/1 Running 0 1s # 等待一段时间后,pod变为了Completed [root@kubernetes-master ~]# kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/job-test 0/1 13s 13s NAME READY STATUS RESTARTS AGE pod/job-test-zsch7 0/1 Completed 0 13s

job参数

修改job任务执行次数,主要有3个参数(spec下)

  • backoffLimit:如果job失败,则重试几次
  • completions:job结束需要成功运行的Pod个数,即状态为Completed的pod数(也就是job任务总次数)
  • parallelism:一次性运行N个pod,这个值不会超过completions的值。
  • activeDeadlineSeconds:pod强制完成时间,pod到该时间后会被强制删除

示例如下

apiVersion: batch/v1 kind: Job metadata: creationTimestamp: null name: job-test spec: backoffLimit: 3 completions: 8 parallelism: 3 template: metadata: creationTimestamp: null spec: containers: - command: - sh - -c - date ; sleep 10 image: docker.io/library/busybox:1.31.1 imagePullPolicy: IfNotPresent name: job-test restartPolicy: Never

创建资源

# job一共有8个任务,每次都会运行三个 [root@kubernetes-master ~]# kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/job-test 3/8 22s 22s NAME READY STATUS RESTARTS AGE pod/job-test-29f69 1/1 Running 0 9s pod/job-test-gb699 0/1 Completed 0 22s pod/job-test-gzvsg 1/1 Running 0 7s pod/job-test-nh5d4 0/1 Completed 0 22s pod/job-test-p5959 1/1 Running 0 9s pod/job-test-phmv8 0/1 Completed 0 22s # 直到最后job运行完成,所有的pod变为Completed [root@kubernetes-master ~]# kubectl get job,pod NAME COMPLETIONS DURATION AGE job.batch/job-test 8/8 40s 66s NAME READY STATUS RESTARTS AGE pod/job-test-29f69 0/1 Completed 0 53s pod/job-test-gb699 0/1 Completed 0 66s pod/job-test-gzvsg 0/1 Completed 0 51s pod/job-test-jvn7c 0/1 Completed 0 40s pod/job-test-l6h4g 0/1 Completed 0 40s pod/job-test-nh5d4 0/1 Completed 0 66s pod/job-test-p5959 0/1 Completed 0 53s pod/job-test-phmv8 0/1 Completed 0 66s

Cronjob

cronjob在Job的基础上加上了时间调度,实现:在给定的时间点运行一个任务,也可以周期性地在给定时间点运行。这个实际上和Linux中的crontab就非常类似了。

一个cronjob对象对应crontab文件中的一行,它根据配置的时间格式周期性地运行一个Job,格式和crontab也是一样的。

crontab的格式如下:

小时 要运行的命令 #第1列分钟(0~59) #第2列小时(0~23) #第3列日(1~31) #第4列月(1~12) #第5列星期(0~7)(0和7表示星期天) #第6列要运行的命令 # 不考虑具体时间的话,可以用* 【和linux的crontab一样】

创建Cronjob

apiVersion: batch/v1 kind: CronJob metadata: creationTimestamp: null name: cronjob-test spec: jobTemplate: metadata: creationTimestamp: null name: cronjob-test spec: template: metadata: creationTimestamp: null spec: containers: - command: - sh - -c - date ; sleep 10 image: docker.io/library/busybox:1.31.1 imagePullPolicy: IfNotPresent name: busybox restartPolicy: OnFailure schedule: '*/1 * * * *' # */1 即为每分钟运行1次

创建资源

[root@kubernetes-master ~]# kubectl get pod,cj NAME READY STATUS RESTARTS AGE pod/cronjob-test-29548499-78qcs 1/1 Running 0 3s NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE cronjob.batch/cronjob-test */1 * * * * False 1 3s 15s

通过kubectl get pod -w观察,每过1分钟就会创建一个pod

[root@kubernetes-master ~]# kubectl get pod,cj NAME READY STATUS RESTARTS AGE pod/cronjob-test-29548499-78qcs 0/1 Completed 0 2m13s pod/cronjob-test-29548500-kpt72 0/1 Completed 0 73s pod/cronjob-test-29548501-k7zjs 0/1 Completed 0 13s NAME SCHEDULE SUSPEND ACTIVE LAST SCHEDULE AGE cronjob.batch/cronjob-test */1 * * * * False 0 13s 2m25s

descheduler均衡pod

K8S本身缺少对pod再调度的功能,如果一个环境中新加了一个worker,现有pod不会被调度到新节点上,我们可以用descheduler实现这一功能:当集群中新增worker节点时,descheduler可以平衡worker上的副本数,把一些现有pod调度到新节点上。

环境准备

将node2设置成不可调度,创建一个deploy并设置副本数为5

# 设置不可调度 [root@kubernetes-master ~]# kubectl cordon kubernetes-node2 node/kubernetes-node2 cordoned [root@kubernetes-master ~]# kubectl get node NAME STATUS ROLES AGE VERSION kubernetes-master Ready <none> 10h v1.23.6 kubernetes-node1 Ready <none> 10h v1.23.6 kubernetes-node2 Ready,SchedulingDisabled <none> 10h v1.23.6 # 创建deploy,副本数5(yaml资源可在deployment相关章节找到,此处不赘述) [root@kubernetes-master ~]# kubectl apply -f nginx-deploy.yaml deployment.apps/nginx-deploy created [root@kubernetes-master ~]# kubectl get deploy NAME READY UP-TO-DATE AVAILABLE AGE nginx-deploy 5/5 5 5 6s

部署descheduler

你可以在GitHub - kubernetes-sigs/descheduler中查看项目详情,或者直接使用如下资源文件

--- apiVersion: batch/v1 kind: CronJob metadata: name: descheduler-cronjob namespace: kube-system spec: schedule: "*/2 * * * *" concurrencyPolicy: "Forbid" jobTemplate: spec: template: metadata: name: descheduler-pod spec: priorityClassName: system-cluster-critical containers: - name: descheduler image: registry.k8s.io/descheduler/descheduler:v0.35.0 volumeMounts: - mountPath: /policy-dir name: policy-volume command: - "/bin/descheduler" args: - "--policy-config-file" - "/policy-dir/policy.yaml" - "--v" - "3" resources: requests: cpu: "500m" memory: "256Mi" livenessProbe: failureThreshold: 3 httpGet: path: /healthz port: 10258 scheme: HTTPS initialDelaySeconds: 3 periodSeconds: 10 securityContext: allowPrivilegeEscalation: false capabilities: drop: - ALL privileged: false readOnlyRootFilesystem: true runAsNonRoot: true restartPolicy: "Never" serviceAccountName: descheduler-sa volumes: - name: policy-volume configMap: name: descheduler-policy-configmap

服务发现

Service

Endpoint和Service

Endpoints 是 K8s 的独立核心资源对象(和 Service 平级),属于纯数据存储型对象,没有任何转发和调度能力;

  • 只存储 PodIP: 容器端口的键值对列表,除此之外无任何多余信息;

  • 作为 ServicePod 之间的中间桥梁,实现解耦,因此Service不用关心 Pod 的动态变化,只需要读取Endpoints即可拿到最新的后端地址;Pod 的变化由 Endpoints 控制器同步到 Endpoints 对象中。

Service是 K8s 的核心资源,为一组功能相同的 Pod 提供固定、统一的访问地址;

  • Service 创建后会分配一个集群内唯一的虚拟 IP(ClusterIP)+ 固定的 Service 端口,这个 ClusterIPService 生命周期内永不改变;
  • 集群内的客户端(其他 Pod、节点)只需访问 ClusterIP:Service端口,就可以访问到后端的业务服务,无需关心后端 Pod 的 IP 变化、实例数量变化;
  • Service自身不存储任何 Pod 信息、不处理任何流量转发、不直接对接 Pod,它的唯一依赖就是同名的Endpoints对象。
# 查看service,简写为svc [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 8d nginx-svc NodePort 10.105.200.61 <none> 80:31213/TCP 2d23h # 查看endpoint,简写为ep [root@kubernetes-master ~]# kubectl get ep NAME ENDPOINTS AGE kubernetes 192.168.0.200:6443 8d nginx-svc 10.244.1.75:80,10.244.2.20:80 2d21h # 查看pod [root@kubernetes-master ~]# kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-deploy-8559b958fd-4fzn4 1/1 Running 1 (21m ago) 2d21h 10.244.2.20 kubernetes-node2 <none> <none> nginx-deploy-8559b958fd-l4svf 1/1 Running 1 (21m ago) 2d21h 10.244.1.75 kubernetes-node1 <none> <none>

pod、endpoint、service之间的绑定规则

  • Service 与其对应的 Endpoints 对象,必须是同名且同命名空间,跨命名空间不会关联。

  • pod需要满足两个条件才会被 Endpoints 控制器收录到 Endpoints 的 IP 清单中:Pod 标签与 Service 的 spec.selector 标签选择器匹配、Pod 为就绪状态。如果 Pod 的就绪探针失败(比如服务启动中、服务异常),该 Pod 会被立即从 Endpoints 中剔除,流量不会转发到这个 Pod 上。

  • Service 创建/删除则同名 Endpoints 自动创建/删除;Service 修改标签选择器则Endpoints 自动更新 Pod 清单;Pod 扩缩容/重建则Endpoints 自动增删 PodIP;

Service 工作原理

  1. 创建 Service 对象,API Server 持久化到 etcd

通过 kubectl create service / YAML 文件 / 编程调用 K8s API,向API Server提交 Service 创建请求,Service 的配置(包含标签选择器、端口映射等)会被持久化到 etcd中。

  1. Endpoints 控制器通过 Informer 感知 Service 变更

K8s 的Endpoints 控制器(属于 kube-controller-manager 的一个核心控制器,运行在 master 节点)内部集成了Informer 机制:

  • Informer 会实时监听 API Server 中 Service 对象的变更事件(创建、修改、删除),并将 Service 的配置缓存到本地;
  • 作用:Informer 是 K8s 的本地缓存和事件监听核心组件,避免控制器直接频繁访问 etcd,减少 etcd 的压力,同时提升控制器的响应速度。
  1. Endpoints 控制器构建并维护 Endpoints 对象

Endpoints 控制器读取 Service 的 spec.selector 标签选择器;遍历集群中同命名空间的所有 Pod,筛选出标签匹配的健康 Pod;基于这些健康 Pod,自动创建 / 更新同名的 Endpoints 对象,将这些 Pod 的 PodIP + 容器端口 写入 Endpoints;Endpoints 对象被持久化到 etcd 中,并且控制器会持续监听 Pod 的状态变化并更新Endpoints 中的 Pod 清单。

Endpoints 控制器的核心职责只有一个:维护 Service 和 Pod 的关联关系,保证 Endpoints 的准确性。

  1. kube-proxy 监听 Service/Endpoints 变更,配置节点转发规则

Service 本身不转发任何流量,真正做流量转发的是 kube-proxy,kube-proxy 运行在每一个 Node 节点上的 DaemonSet(每个节点必有一个 kube-proxy 实例)通过 Informer 机制,实时监听集群中所有 Service 和 Endpoints 的变更事件;当监听到 Service/Endpoints 变化时,kube-proxy 会在当前 Node 节点上,自动配置 iptables 或 IPVS 规则(K8s 默认 iptables,高并发场景推荐 IPVS);将访问Service 的 ClusterIP: 端口的流量,转发到 Endpoints 中的任意一个 PodIP: 容器端口上,并实现轮询 / 随机的负载均衡。

Service 的 ClusterIP 是虚拟 IP(VIP),它不是一个真实的网卡 IP,不绑定任何节点的网卡,也不会被任何进程监听,ClusterIP 的存在仅作为流量转发的目标标识,由 kube-proxy 维护其转发规则。

  1. 集群内的客户端访问 ClusterIP:Service端口,请求流量会经过以下链路到达 Pod:
  • 客户端的请求到达 Node 节点后,会被该节点的 iptables/IPVS 规则拦截;

  • 规则根据负载均衡策略(默认轮询),随机选择一个 Endpoints 中的 PodIP: 容器端口;

  • 请求流量被转发到选中的 Pod 上,Pod 的容器处理请求并返回响应;

  • 响应流量原路返回给客户端,整个请求链路完成。

创建Service

在创建service之前,还是先创建Deployment,再参照如下示例创建一个Service,当然,Service也可以写在其他的资源文件中,参考HPA中的Deployment

NodePort是在 基础的 ClusterIP Service 之上的功能扩展,当该service被创建时,K8S自动为这个 Service 分配两个访问入口

  • 分配一个固定的 ClusterIP(虚拟 IP),集群内的 Pod 可以通过 ClusterIP:80 访问服务(和普通 ClusterIP Service 一样);

  • 自动分配一个 NodePort 端口(端口号范围:30000~32767),集群外部可以通过 任意节点的IP:NodePort端口 访问服务(比如 192.168.1.100:30080)。

核心逻辑:外部请求 → 节点 IP:NodePort → 转发到 Service 的 ClusterIP:80 → 再转发到 Pod:80

apiVersion: v1 kind: Service # 资源类型为service metadata: name: nginx-svc # service名称 labels: # service自身的标签 app: service namespace: default # 命名空间,非必须 spec: selector: # 标签选择器,匹配如下标签的pod app: nginx ports: # 端口映射,监听port,转发到targetPort - port: 8080 # service自身监听端口 targetPort: 80 # 将监听转发到目标pod端口 name: web # 端口名称,非必须 protocol: TCP # 声明协议,非必须 type: NodePort # 用 NodePort 暴露服务,端口直接绑定在node上,集群每个node都会绑定该端口,测试可以该类型,生产不推荐

现在,我们创建一个service服务并测试其功能

[root@kubernetes-master ~]# kubectl apply -f service.yaml service/nginx-svc created [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9d nginx-svc NodePort 10.110.84.92 <none> 8080:32239/TCP 8s [root@kubernetes-master ~]# kubectl get svc -o wide NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9d <none> nginx-svc NodePort 10.110.84.92 <none> 8080:32239/TCP 11s app=nginx [root@kubernetes-master ~]# kubectl get ep NAME ENDPOINTS AGE kubernetes 192.168.0.200:6443 9d nginx-svc 10.244.2.22:80 2m44s

Service类型

service 的类型是一个至下而上的,逐级升高的类型,service有如下类型:

  • clusterIP类型,主要是解决pod之间,负载均衡的问题,参考基于service访问外部服务小节,配置了多个IP进行轮询

  • NodePort类型,主要是解决将外部访问内部服务的问题,参考创建Service小节,实现外部访问pod

  • LoadBalancer 类型,主要就是在NodePort的基础上,实现Node之间的负载均衡

  • ExternalName(DNS 别名服务),主要是解决内部访问外部服务的问题,K8S 1.7及以上支持,参考反向代理外部域名小节,使内部pod访问外部域名

    比如说pod访问db,如果你的pod很多,修改这个db指向就比较麻烦。ExternalName就可以类型DNS的方式,生成一个域名,帮你指向DB,这样后面你只需要修改域名指向DB的IP,就能实现快速修改

基于service访问内部服务

service创建后,外部访问可直接通过访问节点 IP:NodePort,内部访问可使用ClusterIP:Clusterport,如果是pod之间进行访问,还能通过servicename:Clusterport的形式,例如curl http://nginx-svc:8080

K8s 集群中会默认部署 coredns/kube-dns 组件(集群的 DNS 服务器),所有 Pod 启动时,都会自动配置这个 DNS 服务器的地址(写入 Pod 的 /etc/resolv.conf 文件)。这个 DNS 服务支持用Service 的名称直接解析出对应的 ClusterIP,规则如下:

  • 同命名空间:直接写 Service名称 即可解析 → 比如你在 Pod 里输入 nginx-svc,DNS 会自动解析出它的 ClusterIP 10.110.84.92

  • 跨命名空间:写 Service名称.命名空间.svc.cluster.local → 比如 nginx-svc.default.svc.cluster.local

为了验证,我们创建两个deployment,通过标记不可调度的方式,将两个pod创建在两个node上,且两个pod命名空间不同

# pod1位于ns1,node1上,pod2位于ns2,node2上 [root@kubernetes-master ~]# kubectl get pod -A -o wide NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES ns1 nginx-deploy-v1-8679b4bd58-lgljw 1/1 Running 0 7m15s app=nginx-v1,pod-template-hash=8679b4bd58 ns2 nginx-deploy-v2-5c7fb8bb7-vzxfw 1/1 Running 0 12m app=nginx-v2,pod-template-hash=5c7fb8bb7

创建两个service,分别匹配两个node

[root@kubernetes-master ~]# kubectl get all -n ns1 NAME READY STATUS RESTARTS AGE pod/nginx-deploy-v1-8679b4bd58-lgljw 1/1 Running 0 21m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/nginx-svc-v1 NodePort 10.96.129.123 <none> 8081:31416/TCP 11m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-deploy-v1 1/1 1 1 27m NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-deploy-v1-8679b4bd58 1 1 1 27m [root@kubernetes-master ~]# kubectl get all -n ns2 NAME READY STATUS RESTARTS AGE pod/nginx-deploy-v2-5c7fb8bb7-vzxfw 1/1 Running 0 27m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/nginx-svc-v2 NodePort 10.101.19.227 <none> 8082:32382/TCP 7s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/nginx-deploy-v2 1/1 1 1 27m NAME DESIRED CURRENT READY AGE replicaset.apps/nginx-deploy-v2-5c7fb8bb7 1 1 1 27m

现在,进入到nginx-svc-v1这个pod中,去访问nginx-svc-v2

[root@kubernetes-master ~]# kubectl exec -it nginx-deploy-v1-8679b4bd58-lgljw -n ns1 -- bash # 使用ClusterIP:Clusterport访问 root@nginx-deploy-v1-8679b4bd58-lgljw:/# curl http://10.101.19.227:8082 <!DOCTYPE html> <html> ......... # 使用Service 名称,跨ns访问 root@nginx-deploy-v1-8679b4bd58-lgljw:/# curl http://nginx-svc-v2.ns2.svc.cluster.local:8082 <!DOCTYPE html> <html> .........

基于service访问外部服务

有时候,pod内部也需要访问外部服务,如果外部服务出现变动,则需要在每个pod中都修改访问地址,这样显然会麻烦,我们可是用service实现外部访问,通过pod访问service,再由service访问到外部环境,后续修改只需要维护service即可

例如,内部多个pod需要访问外部服务:

与访问内部的设置不同在于:service资源文件中,不指定selector属性,此时service不会自动创建endpoint,需要自己单独创建endpoint

# 创建service资源文件 [root@kubernetes-master ~]# cat service.yaml apiVersion: v1 kind: Service # 资源类型为service metadata: name: nginx-svc-external # service名称 labels: # service自身的标签 app: service-external spec: ports: # 端口映射,监听port,转发到targetPort - port: 8080 # service自身监听端口 targetPort: 80 # 将监听转发到目标端口 name: out-web # 端口名称 protocol: TCP # 声明协议,非必须 type: ClusterIP # 创建endpoint资源文件 [root@kubernetes-master ~]# cat nginx-ep-external.yaml apiVersion: v1 kind: Endpoints metadata: labels: # ep自身标签,可以与svc不一致 app: service-ep-external name: nginx-svc-external # 必须与service一致 namespace: default # 必须与service一致 subsets: - addresses: - ip: 149.28.121.93 # 目标IP地址,即pod访问svc,会转发到该外部IP地址,例如希望pod访问www.bilibili.com,此处填写域名的对应IP - ip: 221.178.63.11 # subsets.addresses是一个数组,可填写多个IP实现负载均衡,此时 Pod 访问 Service 时,请求会被轮询转发。 ports: # 与service一致 - name: out-web # 与service一致 port: 80 protocol: TCP # 与service一致

现在,创建对应的资源文件

# 创建service资源,查看状态,此时如果get ep,会发现没有自动创建ep [root@kubernetes-master ~]# kubectl apply -f service.yaml [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9d nginx-svc-external ClusterIP 10.96.124.187 <none> 8080/TCP 23m # 创建endpoint [root@kubernetes-master ~]# kubectl apply -f nginx-ep-external.yaml [root@kubernetes-master ~]# kubectl get ep NAME ENDPOINTS AGE kubernetes 192.168.0.200:6443 9d nginx-svc-external 149.28.121.93:80 4s

验证svc与ep是否绑定,通过kubectl describe svc XXXX查看输出中的Endpoints是否有实际内容

[root@kubernetes-master ~]# kubectl describe svc nginx-svc-external Name: nginx-svc-external Namespace: default Labels: app=service-external Annotations: <none> Selector: <none> Type: ClusterIP IP Family Policy: SingleStack IP Families: IPv4 IP: 10.96.124.187 IPs: 10.96.124.187 Port: out-web 8080/TCP TargetPort: 80/TCP Endpoints: 120.78.159.117:80 Session Affinity: None Events: <none>

测试外部访问,pod内访问svc名称和端口(ClusterIP:Clusterport),endpoint会将请求转发到yaml中配置的subsets.addresses.ip

[root@kubernetes-master ~]# kubectl run -it --rm load-generator --image=busybox /bin/sh If you don't see a command prompt, try pressing enter. / # / # wget http://nginx-svc-external:8080 Connecting to nginx-svc-external:8080 (10.96.124.187:8080) saving to 'index.html' index.html 100%

反向代理外部域名

上一小节中,通过在yaml中写入外部地址实现pod的对外访问,如果需要直接使用域名访问,而不是将域名解析成地址再写入,则可以使用ExternalName类型

ExternalName 是特殊类型的 Service,它的本质是给外部域名做一个集群内的 DNS 别名,它没有端口转发的功能,也没有 Endpoints 绑定逻辑,资源创建后会在 K8s 集群的 CoreDNS 中,为这个 Service 创建一条DNS CNAME 别名记录,当 Pod 访问<svc-Name>.default.svc.cluster.local:<port>时,K8s 的 DNS 会直接把这个域名解析为配置 `externalName,然后 Pod 的请求就直接发送到目标域名上,全程没有任何端口转发 / 代理。

ExternalName 类型没有任何端口转发能力,Pod 访问 svc:port 时,会直接把请求发往 externalName域名:port,端口原样透传,因此port需要写成目标域名的目标端口

apiVersion: v1 kind: Service metadata: name: svc-externalname namespace: default labels: app: service-external spec: ports: - port: 80 # Pod访问该Service时使用的端口号,也是访问目的域名的端口号 name: web # 端口名称,非必须 protocol: TCP # 协议,默认TCP,非必须 type: ExternalName # 核心类型,声明为外部域名别名服务 externalName: www.bilibili.com # 外部服务的完整域名

创建资源文件后,查看svc信息并测试

# 查看svc信息,External Name为目标域名 [root@kubernetes-master ~]# kubectl describe svc svc-externalname Name: svc-externalname Namespace: default Labels: app=service-external Annotations: <none> Selector: <none> Type: ExternalName IP Families: <none> IP: IPs: <none> External Name: www.bilibili.com Port: web 8080/TCP TargetPort: 8080/TCP Endpoints: <none> Session Affinity: None Events: <none> # 启动一个交互式 busybox 容器 [root@kubernetes-master ~]# kubectl run -it --rm load-generator --image=busybox /bin/sh If you don't see a command prompt, try pressing enter. / # wget https://svc-externalname Connecting to svc-externalname (221.178.63.10:443) wget: note: TLS certificate validation not implemented wget: server returned error: HTTP/1.1 403 Forbidden / # # 由于目标网站反爬,所以回显403,但是svc正常工作,如果需要正确回显,可尝试: # --no-check-certificate # 关闭busybox的TLS证书校验(busybox没有内置根证书,不加会报证书错误) # --user-agent="xxx" # 模拟Chrome浏览器的请求头,绕过B站的反爬校验 # -O - # 把下载的内容输出到控制台,而不是保存为文件,方便查看结果 # 2>/dev/null # 屏蔽无关的警告信息,只显示返回的内容 / # wget --no-check-certificate --user-agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" -O - https://www.bilibili.com 2>/dev/null

Ingress

在上一章节中提到,Service对集群之外暴露服务的主要方式有两种,NotePortLoadBalancer,但这两种方式,都有一定的缺点,具体来说: NodePort 会占用很多集群机器的端口,当集群服务变多的时候,过多的端口会给k8s的运维人员带来诸多的不便;而LB的缺点是每个service需要一个LB,不仅浪费而且麻烦,并且需要kubernetes之外设备的支持;

基于这种现状,k8s提供了Ingress这种资源对象,Ingress只需要一个NodePort或者一个LB就可以满足暴露多个Service的需求;

Ingress工作原理

实际上,Ingress相当于一个7层的负载均衡器,可以理解为kubernetes对反向代理的一个抽象,它的工作原理类似于Nginx; 或者可以理解为:在Ingress里建立了诸多的映射规则,Ingress Controller通过监听这些配置规则并转化成Nginx的反向代理配置 , 然后对外部提供服务;

ingress的工作机制可以参考下图

关于Ingress,有下面两个概念需要重点理解

  • ingress是kubernetes中的一个对象,作用是定义请求如何转发到service的规则;
  • ingress controller是具体实现反向代理及负载均衡的程序,对ingress定义的规则进行解析,根据配置的规则来实现请求转发,实现方式有很多,比如Nginx, Contour, Haproxy等;

类比Nginx来说,Ingress工作原理如下 编写Ingress规则,说明哪个域名对应kubernetes集群中哪个ServiceIngressnen控制器动态感知Ingress服务规则的变化,然后生成一段对应的Nginx反向代理配置;Ingress控制器会将生成的Nginx配置写入到一个运行着的Nginx服务中,并动态更新;

到此为止,不难发现,Ingress 其实真正在工作的时候就像是充当一个Nginx在使用,内部配置了用户定义的请求转发规则;

安装ingress-nginx

通过helm安装ingress-nginx

  1. 安装helm
[root@kubernetes-master ~]# mkdir helm [root@kubernetes-master ~]# cd helm/ # 下载heml [root@kubernetes-master helm]# wget https://get.helm.sh/helm-v3.14.0-linux-amd64.tar.gz # 解压 [root@kubernetes-master helm]# tar -zxvf helm-v3.2.3-linux-amd64.tar.gz [root@kubernetes-master helm]# ls helm-v3.2.3-linux-amd64.tar.gz linux-amd64 # 将heml程序移动到/usr/local/bin [root@kubernetes-master helm]# cd linux-amd64/ [root@kubernetes-master linux-amd64]# ls helm LICENSE README.md [root@kubernetes-master linux-amd64]# cp helm /usr/local/bin/
  1. 添加helm仓库,下载ingress-nginx(版本需要与K8S版本兼容,Kubernetes 1.23.x 选用ingress-nginx 1.2.1
# 添加helm仓库 [root@kubernetes-master ~]# helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx # 更新仓库索引 [root@kubernetes-master ~]# helm repo update # 查看仓库 [root@kubernetes-master ~]# helm repo list NAME URL ingress-nginx https://kubernetes.github.io/ingress-nginx # 搜索仓库中的ingress-nginx包,CHART VERSION为helm包版本号,APP VERSION为ingress-nginx控制器内核版本 [root@kubernetes-master ~]# helm search repo ingress-nginx NAME CHART VERSION APP VERSION DESCRIPTION ingress-nginx/ingress-nginx 4.14.1 1.14.1 Ingress controller for Kubernetes using NGINX a... # 下载ingress-nginx,指定helm包版本4.14.1 [root@kubernetes-master ~]# helm pull ingress-nginx/ingress-nginx --version=4.14.1 [root@kubernetes-master ~]# ls ingress-nginx-4.14.1.tgz # 解压ingress-nginx包,建议不要解压到根目录,创建一个目录mv后解压 [root@kubernetes-master helm]# tar -xf ingress-nginx-4.14.1.tgz [root@kubernetes-master helm]# cd ingress-nginx/
  1. 配置参数,修改values.yaml(先备份再修改)
# vim先set number # 镜像地址修改为国内镜像,找到:image: ingress-nginx/controller registry: registry.cn-hangzhou.aliyuncs.com image: google_containers/nginx-ingress-controller tag: "v1.14.1" # 记住这个版本号 # digest: 注释掉下方得digest和digestChroot两行,防止校验 # digestChroot: # 搜索/kube-web 找到这行内容 image: ingress-nginx/kube-webhook-certgen # 修改成下面的内容 registry: registry.cn-hangzhou.aliyuncs.com image: google_containers/kube-webhook-certgen tag: 1.14.1 # 改成和上方得tag一样,不要引号 # digest 这个也注释掉 # 修改部署配置的 kind: DaemonSet # 搜索DaemonSet,按n匹配下一个,找到这行内容:Use a `DaemonSet` or `Deployment`,往下看 kind: DaemonSet # 原来是Deployment,把这个改为DaemonSet # 往下找nodeSelector 加入ingress: "true" # 大概在338行得位置,上一点会有terminationGracePeriodSeconds: 300 nodeSelector: kubernetes.io/os: linux ingress: "true" # 增加选择器,如果 node 上有 ingress=true 就部署 # 搜索dnsPolicy: ClusterFirst 把这个集群优先 改为下面的主机映射 dnsPolicy: ClusterFirstWithHostNet # 往下30行左右(大概107行),将hostNetwork改为true hostNetwork: true #使用本地网络 # 搜索admissionWebhooks,下方得enabled改为false admissionWebhooks: enabled: false # 原本为true,改为false,取消证书验证 # 搜索LoadBalancer,service下的type: LoadBalancer,改为ClusterIP(大概在507行) service: type: ClusterIP # 由 LoadBalancer 修改为 ClusterIP,如果服务器是云平台才用 LoadBalancer
  1. 创建namespace,安装ingress-nginx

在配置中,我们使用了hostNetwork: true,这会让ingress控制器的svc资源占用node的80和443端口

# 创建命名空间 [root@kubernetes-master ingress-nginx]# kubectl create ns ingress-nginx # 将node打上values.yaml中设置的标签,只有匹配到标签的node才会安装ingress-nginx [root@kubernetes-master ingress-nginx]# kubectl label node kubernetes-node1 ingress=true # 安装 ingress-nginx ,注意最后的点号,表示在当前目录下找values.yaml [root@kubernetes-master ingress-nginx]# helm install ingress-nginx -n ingress-nginx . # 验证安装 [root@kubernetes-master ingress-nginx]# kubectl get all -n ingress-nginx NAME READY STATUS RESTARTS AGE pod/ingress-nginx-controller-xmjkw 1/1 Running 0 109s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/ingress-nginx-controller ClusterIP 10.101.23.233 <none> 80/TCP,443/TCP 109s NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE daemonset.apps/ingress-nginx-controller 1 1 1 1 1 ingress=true,kubernetes.io/os=linux 109s # 如果需要卸载ingress-nginx组件 helm uninstall ingress-nginx -n ingress-nginx # 如果需要删除标签 kubectl label node <节点> ingress-

创建Ingress

通过部署 Ingress-nginx,我们可以轻松地将外部流量路由到不同的服务。例如,当用户访问 example.com 时,Ingress 控制器可以根据规则将流量路由到 service-aservice-b

我们可以创建一个简单的 Ingress 资源来演示这个场景:

apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: example-ingress annotations: # 注解,一些额外配置项 kubernetes.io/ingress.class: "nginx" # 配置ingress.class类型为nginx,即ingress-nginx nginx.ingress.kubernetes.io/rewrite-target: / # 将请求路径重写为指定的目标路径。 spec: rules: # 定义路由规则 - host: example.com # 指定了该 Ingress 处理的主机名为 example.com http: # 规定 HTTP 流量的路由 paths: # 定义访问路径 - path: / # 设为 /,表示处理所有路径前缀为 / 的请求 pathType: Prefix # 设置为 Prefix,表示前缀匹配 backend: # 指定服务名称和端口,流量将被路由到该后端服务。 service: name: nginx-a # 指定目标服务的名称为 nginx-a port: number: 80 # 目标服务的端口设置为 80

创建出service-a和service-b两个pod,然后创建ingress资源

# 创建两个deploy资源 [root@kubernetes-master ~]# kubectl create deployment nginx-a --image=docker.io/library/nginx:1.28 [root@kubernetes-master ~]# kubectl create deployment nginx-b --image=docker.io/library/nginx:1.28 # 为 Deployment 控制器自动创建对应的 Service 资源,Service 对外暴露 80 端口,将接收到的所有请求,转发到后端 Pod 的 80 端口上 [root@kubernetes-master ~]# kubectl expose deployment service-a --port=80 --target-port=80 --type=NodePort [root@kubernetes-master ~]# kubectl expose deployment service-b --port=80 --target-port=80 --type=NodePort # 查看service [root@kubernetes-master ~]# kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 9d nginx-a NodePort 10.96.53.108 <none> 80:30944/TCP 115s nginx-b NodePort 10.106.223.243 <none> 80:31371/TCP 45s # 创建ingress资源 [root@kubernetes-master ~]# kubectl apply -f ingress.yaml ingress.networking.k8s.io/example-ingress created [root@kubernetes-master ~]# kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE example-ingress <none> example.com 80 11s

此时,修改本地的C:\Windows\System32\drivers\etc\hosts文件,设置example.com的对应IP地址是node1节点的IP地址,因为ingress控制器部署在node1上,然后在cmd中清空dns缓存ipconfig /flushdns,使用浏览器访问example.com,即可访问到nginx页面

查看创建的ingress和controller,Ingress 资源显示的 ADDRESS 是 Ingress Controller 的 Service IP,这是因为此处使用的是 ClusterIP 类型的 Service,且 Ingress Controller 是以 DaemonSet 方式部署的。

  • Ingress 资源只是一个 “规则集合”(比如:访问 example.com 转发到哪个 Service)。真正负责转发的是 Ingress Controller(此环境里是 ingress-nginx-controller)。

  • Kubernetes 会自动将 Ingress 资源的 ADDRESS 字段设置为它所关联的 Ingress Controller 的入口地址。这意味着,所有进入该 Ingress 的流量,实际上都被转发到了这个 Service IP 上。

这种架构(DaemonSet + ClusterIP)通常配合 hostNetwork: true(使用宿主机网络)使用,真正的入口 ADDRESS 其实是 Node 的物理 IP,而不是 Ingress 资源显示的 ClusterIP。

[root@kubernetes-master ~]# kubectl get all -n ingress-nginx NAME READY STATUS RESTARTS AGE pod/ingress-nginx-controller-xmjkw 1/1 Running 0 5h41m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/ingress-nginx-controller ClusterIP 10.101.23.233 <none> 80/TCP,443/TCP 5h41m NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE daemonset.apps/ingress-nginx-controller 1 1 1 1 1 ingress=true,kubernetes.io/os=linux 5h41m [root@kubernetes-master ~]# kubectl get ingress NAME CLASS HOSTS ADDRESS PORTS AGE nginx-ingress <none> example.com 10.101.23.233 80 22m

多域名匹配

按照不同主机不同域名进行匹配

apiVersion: networking.k8s.io/v1 kind: Ingress # 资源类型为 Ingress metadata: name: nginx-ingress-mul annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/rewrite-target: / spec: rules: # ingress 规则配置,可以配置多个 - host: example.com # 域名配置,可以使用通配符 * http: paths: # 相当于 nginx 的 location 配置,可以配置多个 - pathType: Prefix # 设置为 Prefix,表示前缀匹配 backend: service: name: nginx-a # 代理到哪个 service port: number: 80 # service 的端口 path: /api/v1 # 等价于 nginx 中的 location 的路径前缀匹配 - pathType: Exact # 路径类型,Exact:精确匹配>,URL需要与path完全匹配上,且区分大小写 backend: service: name: nginx-b # 代理到哪个 service port: number: 80 # service 的端口 path: /api/v2

现在,为了验证多域名匹配,我们将pod中nginx的index.html修改下

[root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE nginx-a-5d684cf4d8-5rjxq 1/1 Running 0 68m nginx-b-78c9967d5c-4gdfs 1/1 Running 0 68m # 将index.html中的Welcome to nginx!分别换成Welcome to service-a和service-b [root@kubernetes-master ~]# kubectl exec -it nginx-a-5d684cf4d8-5rjxq -- sed -i 's#<h1>Welcome to nginx!</h1>#<h1>Welcome to service-a</h1>#g' /usr/share/nginx/html/index.html [root@kubernetes-master ~]# kubectl exec -it nginx-b-78c9967d5c-4gdfs -- sed -i 's#<h1>Welcome to nginx!</h1>#<h1>Welcome to service-b</h1>#g' /usr/share/nginx/html/index.html

打开浏览器,访问http://example.com/api/v1http://example.com/api/v2,可以分别看到显示Welcome to service-aWelcome to service-b

路径类型

Ingress 中的每个路径都需要有对应的路径类型(Path Type)。未明确设置 pathType 的路径无法通过合法性检查。当前支持的路径类型有三种:

  • ImplementationSpecific:对于这种路径类型,匹配方法取决于 IngressClass。 具体实现可以将其作为单独的 pathType 处理或者作与 PrefixExact 类型相同的处理。
  • Exact:精确匹配 URL 路径,且区分大小写。
  • Prefix:基于以 / 分隔的 URL 路径前缀匹配。匹配区分大小写, 并且对路径中各个元素逐个执行匹配操作。 路径元素指的是由 / 分隔符分隔的路径中的标签列表。 如果每个 p 都是请求路径 p 的元素前缀,则请求与路径 p 匹配。

如果路径的最后一个元素是请求路径中最后一个元素的子字符串,则不会被视为匹配 (例如:/foo/bar 匹配 /foo/bar/baz, 但不匹配 /foo/barbaz)。

关于路径类型的更多细节,请参考官方文档:Ingress | Kubernetes

配置管理

Configmap 是 k8s 中的资源对象,用于保存非机密性的配置的,数据可以用 key/value 键值对的形式保存,也可通过文件的形式保存。

Configmap

为了满足大批量的节点配置变更需求,k8s 中引入了 Configmap 资源对象,可以当成 volume 挂载到 pod 中,实现统一的配置管理。Configmap有以下几点特征

  • Configmap 是 k8s 中的资源, 相当于配置文件,可以有一个或者多个 Configmap;

  • Configmap 可以做成 Volume,k8s pod 启动之后,通过 volume 形式映射到容器内部指定目录上;

  • 容器中应用程序按照原有方式读取容器特定目录上的配置文件,在容器看来,配置文件就像是打包在容器内部特定目录,整个过程对应用没有任何侵入。

  • ConfigMap 在设计上不是用来保存大量数据的。在 ConfigMap 中保存的数据不可超过 1 MiB。

创建configmap

使用kubectl create configmap -h查看示例,构建configmap对象

资源清单

使用资源清单的方式创建configmap,配置文件多行要写|

apiVersion: v1 kind: ConfigMap metadata: name: mysql labels: app: mysql data: master.cnf: | [mysqld] log-bin log_bin_trust_function_creators=1 lower_case_table_names=1 slave.cnf: | [mysqld] super-read-only log_bin_trust_function_creators=1

创建后运行示例

[root@kubernetes-master ~]# kubectl apply -f configmap.yaml configmap/mysql created [root@kubernetes-master ~]# kubectl get cm NAME DATA AGE kube-root-ca.crt 1 34h mysql 2 8s [root@kubernetes-master ~]# kubectl describe cm mysql Name: mysql Namespace: default Labels: app=mysql Annotations: <none> Data ==== master.cnf: ---- [mysqld] log-bin log_bin_trust_function_creators=1 lower_case_table_names=1 slave.cnf: ---- [mysqld] super-read-only log_bin_trust_function_creators=1 BinaryData ==== Events: <none>

命令行创建

kubectl create configmap <name> --from-literal=<key>=<value>

# 直接在命令行中指定 configmap 参数创建,通过--from-literal 指定参数 [root@kubernetes-master ~]# kubectl create configmap nginx-config --from-literal=nginx_port=8080 --from-literal=server_name=myapp.nginx.com [root@kubernetes-master ~]# kubectl get cm NAME DATA AGE nginx-config 2 15s [root@kubernetes-master ~]# kubectl describe cm nginx-config Name: nginx-config Namespace: default Labels: <none> Annotations: <none> Data ==== nginx_port: ---- 8080 server_name: ---- myapp.nginx.com BinaryData ==== Events: <none>

通过文件指定参数

使用--from-file参数,可以将文件内容作为key对应的value,例如将nginx.conf里的内容作为server对应的value

kubectl create configmap <name> --from-file=<key>=<file_patch>

[root@kubernetes-master ~]# kubectl create configmap nginx-config --from-file=server=/root/nginx.conf [root@kubernetes-master ~]# kubectl describe cm nginx-config Name: nginx-config Namespace: default Labels: <none> Annotations: <none> Data ==== server: ---- server { server_name www.nginx.com; listen 80; root /home/nginx/www/ } BinaryData ==== Events: <none>

指定目录创建configmap

使用--from-file参数,可以将目录下的文件作为key,文件的内容作为value

kubectl create configmap <name> --from-file=<file_patch>/

[root@kubernetes-master ~]# mkdir configmap [root@kubernetes-master ~]# echo "1" > /root/configmap/file1 [root@kubernetes-master ~]# echo "2" > /root/configmap/file2 [root@kubernetes-master ~]# kubectl create configmap nginx-config --from-file=/root/configmap/ configmap/nginx-config created [root@kubernetes-master ~]# kubectl describe cm nginx-config Name: nginx-config Namespace: default Labels: <none> Annotations: <none> Data ==== file1: ---- 1 file2: ---- 2 BinaryData ==== Events: <none>

configMapKeyRef环境变量引入

创建一个存储mysql配置的configmap

[root@k8smaster ~]# vim mysql-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mysql labels: app: mysql data: log: "1" lower: "1" [root@k8smaster ~]# kubectl apply -f mysql-configmap.yaml

引用configmap中的内容

[root@k8smaster ~]# vim mysql-pod.yaml apiVersion: v1 kind: Pod metadata: name: mysql-pod spec: containers: - name: mysql image: busybox imagePullPolicy: IfNotPresent command: [ "/bin/sh", "-c", "sleep 3600" ] env: - name: log_bin #定义环境变量 log_bin valueFrom: configMapKeyRef: # 使用configMapKeyRef,将configmap的key和value引入 name: mysql #指定 configmap 的名字 key: log #指定 configmap 中的 key - name: lower #定义环境变量 lower valueFrom: configMapKeyRef: name: mysql key: lower restartPolicy: Never

此时,pod中的环境变量将会使用configmap中key对应的value

[root@k8smaster ~]# kubectl apply -f mysql-pod.yaml [root@xianchaomaster1 ~]# kubectl exec -it mysql-pod -c mysql -- /bin/sh / # printenv log_bin=1 lower=1

envfrom环境变量引入

通过envfrom直接指定configmap的名称,将configmap引入

[root@xianchaomaster1 ~]# vim mysql-pod-envfrom.yaml apiVersion: v1 kind: Pod metadata: name: mysql-pod-envfrom spec: containers: - name: mysql image: busybox imagePullPolicy: IfNotPresent command: [ "/bin/sh", "-c", "sleep 3600" ] envFrom: - configMapRef: name: mysql #指定 configmap 的名字 restartPolicy: Never

此时,configmap中的key将会被作为变量名,value作为变量值引入

#更新资源清单文件 [root@k8smaster ~]# kubectl apply -f mysql-pod-envfrom.yaml pod/mysql-pod-envfrom created [root@k8smaster ~]# kubectl exec -it mysql-pod-envfrom -- /bin/sh / # printenv lower=1 log=1 #引入到容器里了 这里的log和lower的值和mysqlconfigmap.yaml文件的值一样的

configmap作为volume挂载

创建如下configmap

# mysql-configmap.yaml apiVersion: v1 kind: ConfigMap metadata: name: mysql-config data: # MySQL 核心配置文件 my.cnf: | [mysqld] user=mysql datadir=/var/lib/mysql socket=/var/run/mysqld/mysqld.sock port=8306 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci skip-name-resolve # 关闭DNS解析,提升连接速度 [client] default-character-set=utf8mb4 # 明文存储root密码(仅示例用) root-password: "123321" other-key: "无用的配置"

configmap作为volume挂载到pod,下面以一个mysql的pod作为示例,在configmap中设置端口和密码

# mysql-pod.yaml apiVersion: v1 kind: Pod metadata: name: mysql-pod labels: app: mysql spec: containers: - name: mysql image: docker.io/library/mysql:8.0.32 imagePullPolicy: IfNotPresent # 从ConfigMap读取密码作为环境变量 env: - name: MYSQL_ROOT_PASSWORD # pod环境变量名称 valueFrom: configMapKeyRef: name: mysql-config # 从该configmap加载key对应的value,赋值到name key: root-password # 挂载ConfigMap中的配置文件 volumeMounts: # 加载数据卷 - name: mysql-config-volume # 加载的数据卷名称,在挂载数据卷中设置 mountPath: /etc/mysql/conf.d # MySQL自动加载该目录配置 readOnly: true # 只读模式 # 挂载ConfigMap卷 volumes: - name: mysql-config-volume # 设置数据卷的名称,挂载时用此名称 configMap: # 数据卷类型为configmap name: mysql-config # configmap名称 items: # 对configmap中的key进行映射,如果不指定,则将cm中所有key转换成一个同名文件 - key: "my.cnf" # configmap中的key path: "sql.cnf" # 将key转换为文件 - key: "root-password" path: "sql-password" --- # 创建Service apiVersion: v1 kind: Service metadata: name: mysql-service spec: selector: app: mysql # 匹配Pod的label ports: - port: 8306 targetPort: 8306 type: ClusterIP

应用configmap和pod,此时mysql的密码已经被启动参数修改,端口已经被挂载的volme修改,且cm的key被映射

[root@kubernetes-master ~]# kubectl apply -f mysql-configmap.yaml configmap/mysql-config created [root@kubernetes-master ~]# kubectl apply -f mysql-pod.yaml pod/mysql-pod created service/mysql-service created [root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE mysql-pod 1/1 Running 0 4s [root@kubernetes-master ~]# kubectl exec -it mysql-pod -- sh sh-4.4# ls /etc/mysql/conf.d/ sql-password sql.cnf sh-4.4# mysql -uroot -p123321 -P8306 mysql>

启动参数和运行参数

或许你已经发现了,上述配置中,既然已经通过 ConfigMap 挂载了 MySQL 的配置文件,为什么还需要用环境变量传递 root 密码?这其实是由 MySQL 官方镜像的运行机制决定,MySQL 容器的启动分为初始化阶段和运行阶段,这两个阶段依赖的配置来源是分开的:

环境变量用于 MySQL 容器的初始化阶段,第一次启动 MySQL 容器时,镜像内部的初始化脚本(docker-entrypoint.sh)会先执行,这个脚本的核心作用是:

  • 创建初始的 mysql 系统数据库
  • 设置 root 用户的密码
  • 初始化权限表、字符集等基础配置

这个初始化脚本并不会读取 /etc/mysql/conf.d 里的 my.cnf 配置,而是通过环境变量来获取关键的初始化参数,比如:

  • MYSQL_ROOT_PASSWORD:必须设置,用于初始化 root 密码(不设置的话容器会直接启动失败)
  • MYSQL_DATABASE:可选,初始化时自动创建指定数据库
  • MYSQL_USER/MYSQL_PASSWORD:可选,创建普通业务用户

挂载的 my.cnf用于 MySQL 运行阶段,当初始化完成后,MySQL 服务正式启动时,才会读取 /etc/mysql/conf.d(以及 /etc/mysql/my.cnf)里的配置文件,这些配置不会参与初始化,而且 my.cnf 里也无法设置 root 初始密码(MySQL 不支持通过配置文件设置初始密码,只能通过初始化脚本)。

如果只挂载 my.cnf 但不设置 MYSQL_ROOT_PASSWORD 环境变量,容器启动日志会报错:

[root@kubernetes-master ~]# kubectl get pod NAME READY STATUS RESTARTS AGE mysql-pod 0/1 CrashLoopBackOff 3 (38s ago) 78s [root@kubernetes-master ~]# kubectl logs -f mysql-pod 2026-02-17 15:38:25+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.32-1.el8 started. 2026-02-17 15:38:26+00:00 [Note] [Entrypoint]: Switching to dedicated user 'mysql' 2026-02-17 15:38:26+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 8.0.32-1.el8 started. 2026-02-17 15:38:26+00:00 [ERROR] [Entrypoint]: Database is uninitialized and password option is not specified You need to specify one of the following as an environment variable: - MYSQL_ROOT_PASSWORD - MYSQL_ALLOW_EMPTY_PASSWORD - MYSQL_RANDOM_ROOT_PASSWORD

只读模式

如果你去查看/etc/mysql/conf.d/的文件权限,就会看到文件权限是lrwxrwxrwx.,为什么设置了readOnly后权限还是777呢?

其实ls -l看到的 /etc/mysql/conf.d/ 下的文件是符号链接(lrwxrwxrwx),这是 K8s 动态挂载 ConfigMap 的底层机制(基于 tmpfs),文件的权限显示(777 软链接)不代表实际文件的读写权限。

readOnly: true 的作用是将整个挂载目录 /etc/mysql/conf.d 设为只读,禁止容器内对该目录下的文件进行修改 / 删除 / 创建操作,而非改变文件的权限位显示。

如果你尝试删除文件,你就会看到只读文件系统的相关提示

[root@kubernetes-master ~]# kubectl exec -it mysql-pod -- sh sh-4.4# ls -l /etc/mysql/conf.d/ total 0 lrwxrwxrwx. 1 root root 19 Feb 17 15:51 sql-password -> ..data/sql-password lrwxrwxrwx. 1 root root 14 Feb 17 15:51 sql.cnf -> ..data/sql.cnf sh-4.4# rm -f /etc/mysql/conf.d/sql-password rm: cannot remove '/etc/mysql/conf.d/sql-password': Read-only file system

热更新

如果以volume的方式将configmap挂载到pod,如果想更新configmap的配置,需要注意以下几点

  1. 默认清空下,pod中的配置也会更新,更新周期是更新时间+缓存时间
  2. subpath不会更新
  3. 如果是以变量方式引入到pod,则变量不会更新、
logs: “1”变成 log: “2” 保存退出 [root@k8smaster ~]# kubectl edit configmap mysql configmap/mysql edited [root@k8smaster ~]# kubectl exec -it mysql-pod-volume -- /bin/sh / # cat /tmp/config/log 2 #发现 log 值变成了 2,更新生效了 注意: 更新 ConfigMap 使用该 ConfigMap 挂载的 Env 不会同步更新 (环境变量不可通过configmap实时更新改变) 使用该 ConfigMap 挂载的 Volume 中的数据需要一段时间(实测大概 10 秒)才能同步更新

Secret

Secret 是 K8s 提供的敏感配置存储资源,用于存储密码、OAuth 令牌、SSH 密钥等敏感信息,相比 ConfigMap(存储非敏感配置),Secret 具备更严格的权限控制,且数据以 Base64 编码形式存储(注:Base64 是编码而非加密,需结合 RBAC / 加密配置提升安全性)。

日常开发中最常用的Secret是以下两类:

类型用途示例
Opaque通用类型,存储任意键值对(最常用)用户名、密码、自定义 Token
kubernetes.io/dockerconfigjson存储镜像仓库的拉取凭证私有镜像仓库的用户名 / 密码

base64编码规范

Secret 的 data 字段要求值为合法的 Base64 编码(长度为 4 的整数倍,仅包含字母 / 数字 /+/=/),创建前需先对明文配置编码:

若编码值不合法(如长度非 4 的整数倍、含特殊字符),创建 Secret 时会报 illegal base64 data 错误。

# 对"admin"编码(-n 避免换行符干扰) [root@kubernetes-master ~]# echo -n "admin" | base64 YWRtaW4= # 对"YWRtaW4="解码 [root@kubernetes-master ~]# echo -n "YWRtaW4=" | base64 --decode admin

创建Secret

创建 mysql-secret.yaml,包含用户名和密码两个敏感配置:

apiVersion: v1 kind: Secret metadata: name: mysql-secret namespace: default type: Opaque # 通用类型 data: username: YWRtaW4= # 对应明文 admin password: cGFzc3dvcmQ= # 对应明文 password

应用资源文件,在describe中能看到编码后的配置信息,使用base64 --decode即可解码

[root@kubernetes-master ~]# kubectl get secret mysql-secret NAME TYPE DATA AGE mysql-secret Opaque 2 62s [root@kubernetes-master ~]# kubectl describe secret mysql-secret Name: mysql-secret Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== password: 8 bytes username: 5 bytes [root@kubernetes-master ~]# kubectl get secret mysql-secret -oyaml apiVersion: v1 data: password: cGFzc3dvcmQ= username: YWRtaW4= kind: Secret metadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"v1","data":{"password":"cGFzc3dvcmQ=","username":"YWRtaW4="},"kind":"Secret","metadata":{"annotations":{},"name":"mysql-secret","namespace":"default"},"type":"Opaque"} creationTimestamp: "2026-02-18T05:02:03Z" name: mysql-secret namespace: default resourceVersion: "84748" uid: 11f99cf0-5b5c-401a-939f-a776b0fe4c38 type: Opaque [root@kubernetes-master ~]# kubectl get secret mysql-secret -o jsonpath='{.data.password}' | base64 -d

secretKeyRef环境变量引入

将 Secret 中的敏感配置作为环境变量注入 Pod 中,适用于需要通过环境变量读取配置的应用(如 Java/Go 应用)。

# mysql-sercet-pod.yaml apiVersion: v1 kind: Pod metadata: name: mysql-pod labels: app: mysql spec: containers: - name: mysql image: docker.io/library/mysql:8.0.32 imagePullPolicy: IfNotPresent # 从secret读取密码作为环境变量 env: - name: MYSQL_ROOT_PASSWORD # pod环境变量名称 valueFrom: secretKeyRef: name: mysql-secret # 关联的sercet名称 key: password # 引用secret中的password键

应用配置文件,mysql密码变量已经被引入

[root@kubernetes-master ~]# kubectl exec -it mysql-pod -- mysql -uroot -p'password' mysql>

secret作为volume挂载

apiVersion: v1 kind: Pod metadata: labels: name: secret-volume name: secret-volume-pod spec: # 定义Secret卷 volumes: - name: volumes12 secret: secretName: mysecret # 关联的Secret名称 defaultMode: 256 # 权限控制:十进制256=八进制0400(仅属主可读) items: # 按需挂载:仅挂载username键 - key: username path: my-group/my-username # 自定义挂载路径 containers: - image: wangyanglinux/myapp:v1.0 # 业务镜像 name: myapp-container # 挂载卷到容器目录 volumeMounts: - name: volumes12 # 关联上面定义的卷名称 mountPath: "/data" # 容器内挂载目录

docker仓库凭证

将docker的仓库拉取凭证存入secret,创建pod时,使用凭证去私有仓库拉取镜像

apiVersion: v1 kind: Pod metadata: name: docker-pod spec: imagePullSecrets: # 配置登录docker的secret - name: harbor-secret # 引用的secret名称 containers: - name: nginx image: 192.168.100.100:8858/opensource/nginx:latest # 私有镜像仓库,需要凭证才能登录

Subpath

在Kubernetes中,挂载存储卷是容器化应用的常见需求。然而当我们将整个卷挂载到容器中的某个目录时,可能会覆盖目标目录中已有的文件,尤其是在共享目录或有多个应用访问同一卷的场景下。为了避免这种情况,subPath字段应运而生,它允许精确指定要挂载的子目录或文件,从而避免覆盖目录中其他重要数据。

subPath是kubernetes中Pod资源volumeMounts字段的挂载选项。 subPath所定义的路径,指的是卷(Volume)内的子路径,用于将卷内subPath所对应的目录或文件,挂载到容器的挂载点,卷内subPath不存在时自动创建。 不指定此参数时,默认是将卷的根路径进行挂载

spec: containers: - image: docker.io/library/nginx:1.20.1 name: nginx volumeMounts: - name: test5 subPath: username mountPath: /etc/nginx/username

subpatch有两个常用场景:避免覆盖、文件隔离

避免覆盖

如果挂载路径是一个已存在的目录, 直接将configMap/Secret挂载在容器的路径,会覆盖掉容器路径下原有的文件,使用subpath选定configMap/Secret的指定的key-value挂载在容器中,则不会覆盖掉原目录下的其他文件

如下资源文件,会将username新增到/etc/nginx中,如果不使用subpath,则挂载目录会将/etc/nginx目录下的内容全部覆盖

apiVersion: apps/v1 kind: Deployment metadata: labels: app: test5-nginx name: test5-nginx namespace: middleware spec: replicas: 1 selector: matchLabels: app: test5-nginx template: metadata: labels: app: test5-nginx spec: containers: - image: docker.io/library/nginx:1.20.1 name: nginx volumeMounts: - name: test5 subPath: username mountPath: /etc/nginx/username volumes: - name: test5 configMap: items: - key: username path: username name: test-cfg

文件隔离

pod中含有多个容器共用一个volume,即不同容器的路径挂载在存储卷volume的子路径

apiVersion: v1 kind: Pod metadata: name: pod-subpath-test spec: containers: - name: subpath-container-1 image: docker.io/library/nginx:1.20.1 volumeMounts: - mountPath: /tmp/nginx # 容器1的挂载目录 name: subpath-vol subPath: nginxtest1 # 宿主机volume的子目录1 - name: subpath-container-2 image: docker.io/library/nginx:1.20.1 volumeMounts: - mountPath: /etc/nginx/nginxtest2 # 容器2的挂载目录 name: subpath-vol subPath: nginxtest2 # 宿主机volume的子目录2 volumes: - name: subpath-vol persistentVolumeClaim: claimName: test-subpath

配置文件不可变

如果项目上线,我们希望一些重要的配置文件在上线以后不允许修改,则可以通过configmap中的immutable: true来实现

注意:immutable: true这个修改不可逆,只能删除configmap重新创建

apiVersion: v1 kind: ConfigMap immutable: true # 设置配置文件不可变 metadata: name: mysql labels: app: mysql

设置完成后,如果再更改configmap,或者删掉immutable,则会提示

# configmaps "mysql-config" was not valid: # * immutable: Forbidden: field is immutable when `immutable` is set # * data: Forbidden: field is immutable when `immutable` is set

持久化存储

Volumes

HostPath

HostPath 就是将 Node 主机中一个实际目录挂载到 pod 中,以供容器使用,这样的设计就可以保证 pod 销毁了,但是数据依然可以存在于 Node 主机上

如下示例,会将主机的/tmp/logs挂载到pod的/var/log/nginx,即pod所在节点的目录下可以看到nginx日志

apiVersion: v1 kind: Pod metadata: name: nginx-pod namespace: default spec: containers: - name: nginx image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 volumeMounts: # 给当前容器挂载卷(将volumes定义的卷挂载到容器内) - name: hostpath-volume # 要和下方volumes的name完全一致 mountPath: /var/log/nginx # 卷挂载到容器内的目录 volumes: # 定义 Pod 可用的卷列表,供容器挂载使用 - name: hostpath-volume # 卷的名称 hostPath: # 卷的类型为hostPath(将宿主机的目录挂载到Pod) path: /tmp/logs # 宿主机上的目录路径 type: DirectoryOrCreate # 目录类型:存在则用,不存在则自动创建

其中目录类型type设置如下:

  • DirectoryOrCreate:目录存在就使用,不存在就先创建后使用

  • Directory:目录必须存在

  • FileOrCreate:文件存在就使用,不存在就先创建后使用

  • File:文件必须存在

  • Socket:unix 套接字必须存在

  • CharDevice:字符设备必须存在

  • BlockDevice:块设备必须存在

EmptyDir

与其他的类型不同,EmptyDir是一种临时存储卷,它的生命周期与 Pod 绑定,Pod 被删除时,EmptyDir 里的数据也会被清空。主要用于同一个pod中的多个容器之间共享数据,尽管 Pod 中每个容器挂载 emptyDir 卷的路径可能相同也可能不同,但是这些容器都可以读写 emptyDir 卷中相同的文件。

如果Pod中有多个容器,其中某个容器重启,不会影响emptyDir 卷中的数据。当 Pod 因为某些原因被删除时,emptyDir 卷中的数据也会永久删除。

注意:容器崩溃并不会导致 Pod 被从节点上移除,因此容器崩溃时 emptyDir 卷中的数据是安全的。

以下是一个简单示例,nginx和busybox通过EmptyDir实现共享,busybox能够读取到nginx的日志

apiVersion: v1 kind: Pod metadata: name: nginx-pod namespace: default spec: containers: - name: nginx-server image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent volumeMounts: - name: nginx-logs mountPath: /var/log/nginx # 挂载 EmptyDir 到 Nginx 日志目录 - name: log-watcher image: docker.io/library/busybox:1.31.1 imagePullPolicy: IfNotPresent command: ["/bin/sh", "-c"] args: ["tail -f /logs/access.log"] volumeMounts: # 挂载同一个 EmptyDir - name: nginx-logs mountPath: /logs volumes: # 定义 EmptyDir 卷 - name: nginx-logs emptyDir: {}

pod运行后,访问nginx,查看busy输出,读取到了对应日志信息

[root@kubernetes-master ~]# kubectl get pod -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod 2/2 Running 0 36m 10.1.22.95 kubernetes-node2 <none> <none> [root@kubernetes-master ~]# curl http://10.1.22.95 [root@kubernetes-master ~]# kubectl logs nginx-pod -c log-watcher 10.1.237.0 - - [21/Feb/2026:13:22:58 +0000] "GET / HTTP/1.1" 200 612 "-" "curl/7.29.0" "-"

NFS

NFS (Network File System,网络文件系统)是基于TCP/IP协议的应用,可以通过网络,让不同的机器、不同的操作系统可以共享彼此的文件。

NFS在文件传送或信息传送过程中依赖于RPC服务。

RPC (Remote Procedure Call,远程过程调用) 是能使客户端执行其他系统中程序的一种机制。

NFS服务器可以看作是一个FILE SERVER。它可以让客户端通过网络将远端的NFS SERVER共享目录MOUNT到自己的系统中。

安装NFS

本次示例是测试环境,直接以master节点作为NFS的服务端(作为文件共享服务器),node节点为客户端

服务端配置

  1. 安装nfs相关软件包
yum install -y nfs-utils rpcbind
  1. 创建存放数据的目录(共享目录)
mkdir -p /data/nfs-share chmod 755 /data/nfs-share # 根据需求修改 chown -R nobody:nobody /data/nfs-share # 匹配NFS默认匿名用户
  1. 修改配置文件

格式:共享目录 允许访问的网段/IP(权限参数)

10.0.0.0/8 表示允许该网段所有节点访问(根据实际集群网段调整)

cat > /etc/exports << EOF /data/nfs-share 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) EOF

参数说明:

  • rw:客户端拥有读写权限;
  • ro :客户端只读权限,根据需求可选rw或ro;
  • sync:数据同步写入磁盘(保证数据不丢失,性能略低);
  • no_root_squash:客户端 root 用户访问时,保留 root 权限(测试环境可用,生产慎用)。
  • no_subtree_check:关闭子目录检查,提升 NFS 稳定性(K8s 推荐加)
  • insecure:允许客户端从非特权端口(大于 1024)访问(K8s 环境必备);
  1. 启动nfs服务端
systemctl start rpcbind systemctl start nfs-server systemctl enable rpcbind systemctl enable nfs-server
  1. 验证是否已经共享了文件
[root@kubernetes-master data]# exportfs /data/nfs-share 10.0.0.0/8
  1. 上述检查无误后,应用配置
exportfs -arv

客户端配置(每个节点都需要配置)

  1. 安装nfs客户端,设置开机自启
yum install -y nfs-utils rpcbind systemctl start rpcbind systemctl start nfs-server systemctl enable rpcbind systemctl enable nfs-server
  1. 客户端节点访问NFS服务端
[root@kubernetes-node1 ~]# showmount -e 10.0.0.10 Export list for 10.0.0.10: /data/nfs-share 10.0.0.0/8

挂载NFS

在pod中以nfs的形式挂载volumes

如下示例中,将nfs挂载到pod中的nginx/html目录下

apiVersion: v1 kind: Pod metadata: name: nginx-pod namespace: default spec: containers: - name: nginx image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 volumeMounts: - name: nfs-volume mountPath: /usr/share/nginx/html volumes: - name: nfs-volume nfs: server: 10.0.0.10 # nfs服务端地址 path: /data/nfs-share # nfs服务端挂载路径,与showmount -e 10.0.0.10结果一致 readOnly: true # 是否只读

创建pod后,nginx的html目录实际上挂载了服务器的nfs-share,在 NFS 服务端写入测试文件直接同步到 Pod 内挂载目录

[root@kubernetes-master ~]# kubectl get pod -owide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES nginx-pod 1/1 Running 0 2m9s 10.1.22.96 kubernetes-node2 <none> <none> [root@kubernetes-master ~]# cd /data/nfs-share/ [root@kubernetes-master nfs-share]# echo "<h1>hello</h1>" > index.html [root@kubernetes-master nfs-share]# curl http://10.1.22.96 <h1>hello</h1>

PV 和 PVC

持久卷(PersistentVolume,PV)是集群中的一块存储,可以由管理员事先制备, 或者使用[存储类(Storage Class)来动态制备。 持久卷是集群资源,就像节点也是集群资源一样。PV 持久卷和普通的 Volume 一样, 也是使用卷插件来实现的,只是它们拥有独立于任何使用 PV 的 Pod 的生命周期。 此 API 对象中记述了存储的实现细节,无论其背后是 NFS、iSCSI 还是特定于云平台的存储系统。

持久卷申领(PersistentVolumeClaim,PVC) 表达的是用户对存储的请求,概念上与 Pod 类似。 Pod 会耗用节点资源,而 PVC 申领会耗用 PV 资源。Pod 可以请求特定数量的资源(CPU 和内存)。同样 PVC 申领也可以请求特定的大小和访问模式 (例如,可以挂载为 ReadWriteOnce、ReadOnlyMany、ReadWriteMany 或 ReadWriteOncePod)。

简单来说:PV是集群中的资源。 PVC是对这些资源的请求,也是对资源的索引检查。

使用方式:在 Pod 中定义一个存储卷(该存储卷类型为 PVC),定义的时候直接指定大小,PVC 必须与对应的 PV 建立关系,PVC 会根据配置的定义去 PV 申请,而 PV 是由存储空间创建出来的。PV 和 PVC 是 Kubernetes 抽象出来的一种存储资源。

生命周期

Provisioning(构建)

创建PV分为静态创建和动态创建两种方式

静态创建pv

原理:Kubernetes 管理员在集群中预先定义一些存储资源,并逻辑划分这些存储资源对象,将这些资源暴露给集群中的用户使用

动态创建pv 上述介绍的PV和PVC模式是需要运维人员先创建好PV,然后开发人员定义好PVC进行一对一的Bond,但是如果PVC请求成千上万,那么就需要创建成千上万的PV,对于运维人员来说维护成本很高,Kubernetes提供一种自动创建PV的机制,叫StorageClass,它的作用就是创建PV的模板。

原理:根据PVC的配置通过引用StorageClass(简称SC)资源触发存储卷插件动态的创建PV资源

pvc和pv绑定,才可以给pod使用,StorageClass是创建pv的模板

Binding(绑定)

当用户创建一个PVC对象后,主节点会监测新的PVC对象,并寻找与之匹配的PV卷,找到PV卷后将二者绑定在一起。

如果找不到对应的PV,则需要看PVC是否设置StorageClass来决定是否动态创建PV,如果没有配置,PVC就会一直处于未绑定状态,直到有与之匹配的PV后才会申领绑定关系

Using(使用)

pod将PVC当作存储卷来使用,集群会通过PVC找到绑定的PV,并为pod挂载该卷

pod一旦使用PVC绑定PV后,为了保护数据,避免数据丢失问题,PV对象会受到保护,在系统中无法被删除。

Releasing(释放)

Pod 释放 Volume 并删除 PVC

Recycling(回收)

当用户不再使用其存储卷时,他们可以从 API 中将 PVC 对象删除, 从而允许该资源被回收再利用。PersistentVolume 对象的回收策略告诉集群, 当其被从申领中释放时如何处理该数据卷。 目前,数据卷可以被 Retained(保留)、Recycled(回收)或 Deleted(删除)。

保留(Retain)

回收策略 Retain 使得用户可以手动回收资源。当 PersistentVolumeClaim 对象被删除时,PersistentVolume 卷仍然存在,对应的数据卷被视为”已释放(released)”。 由于卷上仍然存在这前一申领人的数据,该卷还不能用于其他申领。 管理员可以通过下面的步骤来手动回收该卷:

  1. 删除 PersistentVolume 对象。与之相关的、位于外部基础设施中的存储资产在 PV 删除之后仍然存在。
  2. 根据情况,手动清除所关联的存储资产上的数据。
  3. 手动删除所关联的存储资产。

如果你希望重用该存储资产,可以基于存储资产的定义创建新的 PersistentVolume 卷对象。

删除(Delete)

对于支持 Delete 回收策略的卷插件,删除动作会将 PersistentVolume 对象从 Kubernetes 中移除,同时也会从外部基础设施中移除所关联的存储资产。 动态制备的卷会继承其 StorageClass 中设置的回收策略, 该策略默认为 Delete。管理员需要根据用户的期望来配置 StorageClass; 否则 PV 卷被创建之后必须要被编辑或者修补。

回收(Recycle)

回收策略 Recycle 已被废弃。取而代之的建议方案是使用动态制备。以下仅作了解:

如果下层的卷插件支持,回收策略 Recycle 会在卷上执行一些基本的擦除 (rm -rf /thevolume/*)操作,之后允许该卷用于新的 PVC 申领。

不过,管理员可以使用 Kubernetes 控制器管理器命令行参数来配置一个定制的回收器(Recycler) Pod 模板。此定制的回收器 Pod 模板必须包含一个 volumes 规约。

PV

由于 PV 是集群级别的资源,即 PV 可以跨 namespace 使用,所以 PV 的 metadata 中不用配置 namespace

apiVersion: v1 kind: PersistentVolume # 描述资源对象为PV类型 metadata: name: pv001 # pv的名称 spec: capacity: # 容量配置 storage: 10Gi # pv的容量 volumeMode: Filesystem # 存储类型为文件系统,默认值 accessModes: # 访问模式 - ReadWriteOnce # 可被单节点读写 persistentVolumeReclaimPolicy: Retain # 回收策略 storageClassName: slow # 创建PV的存储类名称,需要与PVC相同 mountOptions: # 加载配置,可选项 - hard # NFS挂载的hard选项,如果NFS服务器不可达,客户端一直重试 nfs: # 连接到nfs path: /data/nfs-share/v1 # 存储路径 server: 10.0.0.10 # nfs服务地址

查看PV定义的规格:kubectl explain pv.spec

spec: nfs:(定义存储类型) path:(定义挂载卷路径) server:(定义服务器名称) accessModes:(定义访问模式,有以下三种访问模式,以列表的方式存在,也就是说可以定义多个访问模式) - ReadWriteOnce #(RWO)卷可以被一个节点以读写方式挂载。 ReadWriteOnce 访问模式也允许运行在同一节点上的多个 Pod 访问卷。 - ReadOnlyMany #(ROX)卷可以被多个节点以只读方式挂载。 - ReadWriteMany #(RWX)卷可以被多个节点以读写方式挂载。 #nfs 支持全部三种;iSCSI 不支持 ReadWriteMany(iSCSI 就是在 IP 网络上运行 SCSI 协议的一种网络存储技术);HostPath 不支持 ReadOnlyMany 和 ReadWriteMany。 capacity:(定义存储能力,一般用于设置存储空间) storage: 2Gi (指定大小) storageClassName: (自定义存储类名称,此配置用于绑定具有相同类别的PVC和PV) persistentVolumeReclaimPolicy: Retain #回收策略(Retain/Delete/Recycle) #Retain(保留):当用户删除与之绑定的PVC时候,这个PV被标记为released(PVC与PV解绑但还没有执行回收策略)且之前的数据依然保存在该PV上,但是该PV不可用,需要手动来处理这些数据并删除该PV。 #Delete(删除):删除与PV相连的后端存储资源。对于动态配置的PV来说,默认回收策略为Delete。表示当用户删除对应的PVC时,动态配置的volume将被自动删除。(只有 AWS EBS, GCE PD, Azure Disk 和 Cinder 支持) #Recycle(回收):如果用户删除PVC,则删除卷上的数据,卷不会删除。(只有 NFS 和 HostPath 支持)

PVC

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mypvc001 spec: accessModes: - ReadWriteMany resources: requests: storage: 2Gi storageClassName: slow

pod绑定静态PV

创建pv,将pvc和pv绑定,再将pod绑定pvc

配置nfs

[root@kubernetes-master ~]# mkdir -p /data/nfs-share/v{1..5} [root@kubernetes-master ~]# vim /etc/exports [root@kubernetes-master ~]# cat /etc/exports /data/nfs-share/v1 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) /data/nfs-share/v2 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) /data/nfs-share/v3 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) /data/nfs-share/v4 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) /data/nfs-share/v5 10.0.0.0/8(rw,sync,no_root_squash,no_subtree_check,insecure) [root@kubernetes-master ~]# exportfs -arv exporting 10.0.0.0/8:/data/nfs-share/v5 exporting 10.0.0.0/8:/data/nfs-share/v4 exporting 10.0.0.0/8:/data/nfs-share/v3 exporting 10.0.0.0/8:/data/nfs-share/v2 exporting 10.0.0.0/8:/data/nfs-share/v1

创建pv:volume-pv.yaml

apiVersion: v1 kind: PersistentVolume metadata: name: pv001 spec: capacity: storage: 10Gi accessModes: - ReadWriteOnce nfs: path: /data/nfs-share/v1 server: 10.0.0.10
[root@kubernetes-master ~]# kubectl apply -f volume-pv.yaml [root@kubernetes-master ~]# kubectl get pv NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE pv001 10Gi RWO Retain Available 2s

创建pvc:volume-pvc.yaml

apiVersion: v1 kind: PersistentVolumeClaim metadata: name: pvc001 spec: accessModes: - ReadWriteOnce resources: requests: storage: 2Gi
[root@kubernetes-master ~]# kubectl apply -f volume-pvc.yaml [root@kubernetes-master ~]# [root@kubernetes-master ~]# kubectl get pv,pvc NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE persistentvolume/pv001 10Gi RWO Retain Bound default/pvc001 2m NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE persistentvolumeclaim/pvc001 Bound pv001 10Gi RWO 5s

创建pod,绑定pvc

apiVersion: v1 kind: Pod metadata: name: nginx-pod namespace: default spec: containers: - name: nginx image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 volumeMounts: # 给当前容器挂载卷(将volumes定义的卷挂载到容器内) - name: v1-volume # 要和下方volumes的name完全一致 mountPath: /var/log/nginx # 卷挂载到容器内的目录 volumes: - name: v1-volume # 卷的名称 persistentVolumeClaim: # 卷的类型为PVC claimName: pvc001
[root@kubernetes-master ~]# kubectl apply -f pvc-pod.yaml pod/nginx-pod created [root@kubernetes-master ~]# ls -R /data/nfs-share/ /data/nfs-share/: v1 v2 v3 v4 v5 /data/nfs-share/v1: access.log error.log /data/nfs-share/v2: /data/nfs-share/v3: /data/nfs-share/v4: /data/nfs-share/v5:

pvc绑定pod资源之后无法直接删除,需要先删除对应的pod资源才能够删除

StorageClass

storageclass是一个存储类,k8s集群管理员通过创建storageclass可以动态生成一个存储卷供k8s用户使用。

每个StorageClass都包含字段provisioner,parameters和reclaimPolicy,当需要动态配置属于该类的PersistentVolume时使用这些字段。

StorageClass对象的名称很重要,是用户可以请求特定类的方式。 管理员在首次创建StorageClass对象时设置类的名称和其他参数,并且在创建对象后无法更新这些对象。

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: low-latency annotations: storageclass.kubernetes.io/is-default-class: "false" provisioner: csi-driver.example-vendor.example reclaimPolicy: Retain # 默认值是 Delete allowVolumeExpansion: true mountOptions: - discard # 这可能会在块存储层启用 UNMAP/TRIM volumeBindingMode: WaitForFirstConsumer parameters: guaranteedReadWriteLatency: "true" # 这是服务提供商特定的
  • Reclaim Policy(回收策略)

由存储类动态创建持久化存储卷(pv)时可以指定reclaimPolicy字段,这个字段中指定的回收策略可以是Delete或Retain(回收)。 如果在创建StorageClass对象时未指定reclaimPolicy,则默认为Delete。

  • Mount Options(挂载选项)

如果Volume Plugin不支持这个挂载选项,但是指定了,就会使provisioner创建失败

  • Volume Binding Mode(卷绑定模式)

这个字段用来说明什么时候进行卷绑定和动态配置;

默认情况下,立即模式(Immediate)表示一旦创建了PersistentVolumeClaim,就会发生卷绑定和动态配置。对于受拓扑约束且无法从群集中的所有节点全局访问的存储后端,将在不知道Pod的调度要求的情况下绑定或配置PersistentVolumes。这可能导致不可调度的Pod。

集群管理员可以通过指定WaitForFirstConsumer模式来解决此问题,该模式将延迟绑定和配置PersistentVolume,直到创建使用PersistentVolumeClaim的Pod。将根据Pod的调度约束指定的拓扑选择或配置PersistentVolumes。这些包括但不限于资源需求,节点选择器,pod亲和力和反亲和力,以及污点和容忍度。

Provisioner(制备器)

每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。 该字段必须指定。

卷插件内置制备器配置示例
AzureFileAzure File
CephFS--
FC--
FlexVolume--
iSCSI--
Local-Local
NFS-NFS
PortworxVolumePortworx Volume
RBDCeph RBD
VsphereVolumevSphere

你不限于指定此处列出的 “内置” 制备器(其名称前缀为 “kubernetes.io” 并打包在 Kubernetes 中)。 你还可以运行和指定外部制备器,这些独立的程序遵循由 Kubernetes 定义的规范。 外部供应商的作者完全可以自由决定他们的代码保存于何处、打包方式、运行方式、使用的插件(包括 Flex)等。

provisioner既可以是内部供应程序,也可以由外部供应商提供,如果是外部供应商可以参考https://github.com/kubernetes-incubator/external-storage/下提供的方法创建storageclass的provisioner,例如,NFS不提供内部配置程序,但可以使用外部配置程序。 一些外部供应商列在存储库https://github.com/kubernetes-incubator/external-storage下。

修改默认StorageClass

根据安装模式, Kubernetes 集群可能和一个被标记为默认的已有 StorageClass 一起部署。 这个默认的 StorageClass 以后将被用于动态的为没有特定存储类需求的 PersistentVolumeClaims 配置存储。

预先安装的默认 StorageClass 可能不能很好的适应你期望的工作负载;例如,它配置的存储可能太过昂贵。 如果是这样的话,你可以改变默认 StorageClass,或者完全禁用它以防止动态配置存储。

删除默认 StorageClass 可能行不通,因为它可能会被你集群中的扩展管理器自动重建。 请查阅你的安装文档中关于扩展管理器的细节,以及如何禁用单个扩展。

  1. 列出集群中的 StorageClass:kubectl get storageclass,默认 StorageClass 以 (default) 标记。

  2. 修改 StorageClass 标记,将默认改为非默认:

    默认 StorageClass 的注解 storageclass.kubernetes.io/is-default-class 设置为 true。 注解的其它任意值或者缺省值将被解释为 false

    要标记一个 StorageClass 为非默认的,你需要改变它的值为 false

    kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
  3. 标记一个 StorageClass 为默认:和前面的步骤类似,你需要添加/设置注解 storageclass.kubernetes.io/is-default-class=true

    kubectl patch storageclass <your-class-name> -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

    请注意,你可以将多个 StorageClass 标记为默认值。 如果存在多个被标记为默认的 StorageClass,对于未明确指定 storageClassNamePersistentVolumeClaim,将使用最近创建的默认 StorageClass 进行创建。 当带有指定 volumeNamePersistentVolumeClaim 被创建时,如果静态卷的 storageClassNamePersistentVolumeClaim 上的 StorageClass 不匹配, 则该 PersistentVolumeClaim 将保持在待处理状态。

动态创建NFS-PV

配置 StorageClass

检查NFS文件系统

之前已经安装过NFS,现在确认NFS运行中:

[root@kubernetes-master data]# exportfs /data/nfs-share 10.0.0.0/8

下载NFS Provisioner

NFS类型的PV并没有内置的Provisioner,需要下载GitHub - kubernetes-sigs/nfs-subdir-external-provisioner(该供给器要求 K8s≥1.14)

# 进入项目目录 cd nfs-subdir-external-provisioner/deploy # 替换RBAC和deployment中的命名空间 NAMESPACE="kube-system" sed -i'' "s/namespace:.*/namespace: $NAMESPACE/g" rbac.yaml deployment.yaml

配置RBAC权限

kubectl create -f rbac.yaml

编辑deployment

vim deployment.yaml # 找到以下片段,替换对应值 image: dyrnq/nfs-subdir-external-provisioner:v4.0.2 # 修改镜像源 imagePullPolicy: IfNotPresent # 新增拉取策略 env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 10.0.0.10 # NFS服务器IP - name: NFS_PATH value: /data/nfs-share # NFS共享路径 volumes: - name: nfs-client-root nfs: server: 10.0.0.10 # 同上 path: /data/nfs-share # 同上 # 部署deployment kubectl apply -f deployment.yaml
# 可尝试手动拉取 crictl pull docker.io/dyrnq/nfs-subdir-external-provisioner:v4.0.2

部署 StorageClass

编辑 class.yaml,确保 provisioner 名称和 deployment 一致,应用yaml创建sc资源

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: nfs-client provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # 必须和deployment的PROVISIONER_NAME一致 parameters: pathPattern: "${.PVC.name}" # 自定义路径模板,即NFS目录下创建出的子目录格式 onDelete: delete # PVC删除时自动删除NFS目录 archiveOnDelete: "false"
kubectl apply -f class.yaml

创建动态PV

创建一个测试的pod和pvc,查看是否自动创建并绑定了pv

# nginx-sc.yaml # PVC kind: PersistentVolumeClaim apiVersion: v1 metadata: name: nginx-nfs-pvc spec: storageClassName: nfs-client # 对应创建的StorageClass名称 accessModes: - ReadWriteMany # NFS支持多节点读写 resources: requests: storage: 1Gi --- # Pod apiVersion: v1 kind: Pod metadata: name: nginx-pod-nfs namespace: default spec: containers: - name: nginx image: docker.io/library/nginx:1.20.2 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 volumeMounts: - name: sc-nfs-volume mountPath: /var/log/nginx volumes: - name: sc-nfs-volume persistentVolumeClaim: claimName: nginx-nfs-pvc

验证pod和pv

[root@kubernetes-master ~]# kubectl apply -f nginx-sc.yaml persistentvolumeclaim/nginx-nfs-pvc created pod/nginx-pod-nfs created [root@kubernetes-master ~]# kubectl get pv,pvc NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE persistentvolume/pvc-4dcde448-b819-4c44-b02c-735bffe2a1e1 1Gi RWX Delete Bound default/nginx-nfs-pvc nfs-client 10s NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE persistentvolumeclaim/nginx-nfs-pvc Bound pvc-4dcde448-b819-4c44-b02c-735bffe2a1e1 1Gi RWX nfs-client 10s

高级调度

污点(Taint)

节点亲和性,是Pod的一种属性(偏好或硬性要求),它使Pod被吸引到一类特定的节点。Taint 则相反,它使节点能够排斥一类特定的 Pod。

Taint 和 Toleration 相互配合,可以用来避免 Pod 被分配到不合适的节点上。每个节点上都可以应用一个或多个 taint ,这表示对于那些不能容忍这些 taint 的 Pod,是不会被该节点接受的。如果将 toleration 应用于 Pod 上,则表示这些 Pod 可以(但不一定)被调度到具有匹配 taint 的节点上。

Node 被设置上污点之后就和 Pod 之间存在了一种相斥的关系,可以让 Node 拒绝 Pod 的调度执行,甚至将 Node 已经存在的 Pod 驱逐出去。

污点的组成形式

使用 kubectl taint 命令可以给某个 Node 节点设置污点,每个污点有一个 key 和 value 作为污点的标签,其中 value 可以为空,effect 描述污点的作用

kubectl taint node <node-name> <key>=<value>:<effect>
字段说明示例
key污点标识node-role.kubernetes.io/master
value可选值默认:true
effect排斥效果NoSchedule / PreferNoSchedule / NoExecute

当前 taint effect 支持如下三个选项

Effect含义使用场景
NoSchedule不调度新 Pod保护 Master 节点
PreferNoSchedule尽量不调度(软限制)资源紧张时的偏好设置
NoExecute不调度 + 驱逐已有 Pod节点故障、维护下线

污点的基本操作

设置污点

kubectl taint node <node-name> key1=value1:NoSchedule

查看污点

# 查看单个节点污点 kubectl describe node kubernetes-node1 | grep Taints kubectl get node kubernetes-node1 -o jsonpath='{.spec.taints}' # 查看所有节点污点 kubectl get nodes -o custom-columns=NODE:.metadata.name,TAINTS:.spec.taints

更新污点

注意:更新指的是:在污点的key和effect相同的情况下,需要更新污点的值,例如,当前有一个污点:du=no:NoSchedule

  • 打上污点:du=no:PreferNoSchedule,会生成新的污点
  • 打上污点:au=no:NoSchedule,会生成新的污点
  • 打上污点:du=yes:NoSchedule,这是更新当前污点的操作,会改变当前污点的value,不会生成新的污点
kubectl taint node kubernetes-node1 du=yes:NoSchedule --overwrite

取消污点

取消方式很简单,直接在污点命令后加-即可

# 取消某污点 kubectl taint node <node-name> key1=value1:NoSchedule- # 取消某key的所有污点 kubectl taint node <node-name> key1-

Master污点

默认状态下

<none> 节点无污点,任何 Pod 都可调度;node-role.kubernetes.io/master:NoSchedulenode-role.kubernetes.io/control-plane:NoSchedule 有污点,Pod 需添加对应 容忍:tolerations 或 去除污点:taints Pod才能进行调度;

# 去除 master 污点(K8s 1.23 及以下) kubectl taint node <master-node-name> node-role.kubernetes.io/master:NoSchedule- # 去除 control-plane 污点(K8s 1.24+) kubectl taint node <master-node-name> node-role.kubernetes.io/control-plane:NoSchedule-

生产环境示例

# 场景1:标记 GPU 专用节点 kubectl taint node gpu-node-1 hardware=gpu:NoSchedule # 场景2:标记节点维护中(驱逐所有 Pod) kubectl taint node node-2 maintenance=true:NoExecute

容忍(Tolerations)

设置了污点的 Node 将根据 taint 的 effect:NoSchedule、PreferNoSchedule、NoExecute 和 Pod 之间产生互斥的关系,Pod 将在一定程度上不会被调度到 Node 上。但我们可以在 Pod 上设置容忍(Tolerations),意思是设置了容忍的 Pod 将可以容忍污点的存在,可以被调度到存在污点Node 上。

作用:通过配置容忍,管理员可以控制Pod在具有特定污点的节点上的行为,从而实现资源的精细管理和隔离。例如,可以使用容忍来确保某些Pod只能运行在具有特定硬件或软件特性的节点上,或者防止Pod被驱逐到不受信任的节点上

容忍的语法结构

容忍定义在Pod的spec.tolerations字段中,通常包含以下几个部分:

键(Key):与污点的键相匹配。

值(Value):与污点的值相匹配。如果不指定值,Pod将容忍所有值的同名污点。

效应(Effect):与污点的效应相匹配。常见的效应包括NoSchedule、PreferNoSchedule和NoExecute。

容忍期限(TolerationSeconds)(仅对NoExecute效应有效):指定Pod在节点被赋予NoExecute污点后,能够继续在该节点上运行的时间(以秒为单位)。超过这个时间后,Pod将被驱逐。

操作符(Operator):用于指定容忍与污点的匹配方式。常见的操作符包括Equal和Exists。Equal要求键、值和效应都完全匹配,而Exists只要求键和效应匹配。

spec: tolerations: - key: "master" # 污点标识(必填,必须和污点key一致) value: "gpu" # 污点值(operator=Equal 时必填) effect: "NoSchedule" # 污点效果(operator=Equal 时必填必填,需和污点效果一致) operator: "Equal" # 操作符:Equal / Exists(必填) # tolerationSeconds: 3600 # 容忍多久后被驱逐(仅 NoExecute)

两种operator模式

Operator逻辑适用场景
Equalkey + value + effect 全匹配精确匹配特定污点
Exists只要 key 存在即匹配(无视 value)兼容多版本、通用匹配

容忍的特殊用法

仅匹配 key,无视 effect(但建议都写上),表示容忍该key的所有污点

# 仅匹配 key,无视 effect(但建议都写上) tolerations: - key: "node-role.kubernetes.io/master" operator: "Exists"

当不指定 key 值时,表示容忍所有的污点 key

tolerations: - operator: "Exists"

设置容忍时间(NoExecute 效果下,多久后被强制驱逐)

tolerations: - key: "maintenance" value: "true" effect: "NoExecute" operator: "Equal" tolerationSeconds: 3600 # 1小时后自动驱逐

cordon

cordon是针对节点的操作,可以理解成将节点设置为”不可调度”,“封锁”状态

作用:阻止新的 Pods 被调度到该节点上。当一个节点被标记为 cordon 时,已经在该节点上运行的 Pods 不会被驱逐,但新的 Pods 不会被调度到这个节点。

使用场景:通常用于节点的维护或升级,确保在维护期间不会有新的工作负载被分配到该节点上。

使用cordon指令,封锁节点,其污点状态默认为node.kubernetes.io/unschedulable:NoSchedu

# 封锁某节点 kubectl cordon <node-name> # 取消封锁 kubectl uncordon <node-name>

drain

cordon是针对节点的操作,可以理解成将节点设置为”下线”状态

作用:驱逐节点上的所有 Pods,即将它们从节点上移除并重新调度到其他可用的节点上。在执行 drain 操作时,可以指定一些选项,如忽略 DaemonSets 管理的 Pods,或者强制驱逐即使 Pods 有对应的容忍度。

使用场景:当需要对某个节点进行维护、升级或删除时,可以使用 drain 命令来确保节点上的 Pods 被安全地迁移到其他节点。

# 基本驱逐命令,会驱逐节点上的所有 Pods kubectl drain <node-name> # 忽略 DaemonSets 管理的 Pods,驱逐其他所有 Pods。 kubectl drain node01 --ignore-daemonsets=true # 强制驱逐所有 Pods(包括 DaemonSets),并删除 Pods 的本地数据。注意,使用 --force 选项可能会导致数据丢失,请确保在使用前备份重要数据 kubectl drain node01 --force --ignore-daemonsets --delete-local-data

执行 drain 命令,会自动做了两件事情:设定此 node 为不可调度状态(cordon)、evict(驱逐)了 Pod

所以恢复命令也是使用uncordon

亲和力(Affinity)

相较于nodeSelector而言,亲和性和反亲和性拓展了可以定义的约束类型。亲和性、反亲和性语言的表达能力更强,可以标明某规则是“软需求”或者“偏好”(这样调度器在无法找到匹配节点时仍然调度该 Pod),可以使用节点上(或其他拓扑域中)运行的其他 Pod 的标签来实施调度约束, 而不是只能使用节点本身的标签。这个能力让你能够定义规则允许哪些 Pod 可以被放置在一起。

确切来说Affinity包含了:nodeAffinity(节点亲和性)、podAffinity(pod 亲和性) 以及 podAntiAffinity(pod 反亲和性)。 每种亲和性调度都有软策略和硬策略两种方式:

  • 软策略preferredDuringSchedulingIgnoredDuringExecution:如果现在没有满足调度要求的节点的话,Pod 就会忽略这条规则,继续完成调度过程,说白了就是满足条件最好了,没有的话也无所谓
  • 硬策略requiredDuringSchedulingIgnoredDuringExecution:如果没有满足条件的节点的话,就不断重试直到满足条件为止,简单说就是你必须满足我的要求,不然就不干了

在 Pod spec.affinity.nodeAffinity 中定义亲和性规则

spec: template: spec: affinity: nodeAffinity: # 节点亲和性规则 requiredDuringSchedulingIgnoredDuringExecution: # 硬匹配 nodeSelectorTerms: - matchExpressions: - key: kubernetes.io/os # 匹配的key operator: In # 匹配的操作,In代表在 values: - linux # 匹配的值
operator 规则说明
In相当于key=value的形式(等于)
Notln相当于key!=value的形式(不等于)
Exists节点存在label的key为指定的值即可,不能配置values字段
DoesNotExist节点不存在label的key为指定的值即可,不能配置values字段
Gt大于value指定的值
Lt小于value指定的值

节点亲和性(nodeAffinity)

硬策略requiredDuringSchedulingIgnoredDuringExecution

下示例表示让pod强制匹配带有key=disktype,value=ssd的pod

spec: affinity: # 定义Affinity亲和力配置 nodeAffinity: # 定义节点Affinity亲和力配置 requiredDuringSchedulingIgnoredDuringExecution: # 定义硬性Affinity亲和力配置 nodeSelectorTerms: # 定义节点选择器配置 - matchExpressions: # 定义匹配节点标签键值对key=value - key: disktype # 设置节点标签键名key operator: In # 节点标签匹配方式,In等于 values: # 设置节点标签值value - ssd

软策略preferredDuringSchedulingIgnoredDuringExecution

spec: affinity: # 定义Affinity亲和力配置 nodeAffinity: # 定义节点Affinity亲和力配置 preferredDuringSchedulingIgnoredDuringExecution: # 定义软性Affinity亲和力配置 - weight: 10 # 软亲和力的权重,权重越高优先级越大,范围1-100 preference: # 软亲和力配置项 matchExpressions: # 定义匹配节点标签键值对key=value - key: disktype # 设置节点标签键名key operator: In # 节点标签匹配方式,In等于 values: # 设置节点标签值value - ssd

Pod亲和力(podAffinity)

硬策略requiredDuringSchedulingIgnoredDuringExecution

spec: affinity: # 定义Affinity亲和力配置 podAffinity: # 定义Pod Affinity亲和力配置 requiredDuringSchedulingIgnoredDuringExecution: # 定义Affinity硬性亲和力配置 - labelSelector: # 定义Pod选择器配置 matchExpressions: # 定义匹配Pod标签键值对key=value - key: disktype i # 设置Pod标签键名key operator: In # Pod标签规则匹配条件,In等于 values: # 设置Pod标签值value - ssd topologkey: kubernetes.io/hostname # 定义亲和力的范围,匹配的拓扑域的key,也就是节点上label和key,key和value相同的为同一个域。topologyke拓扑域,主要针对宿主机,相当于对宿主机进行区域的划分。用label进行判断,不同的key和不同的value是属于不同的拓扑域。kubernetes.io/hostname是K8s默认节点标签

软策略preferredDuringSchedulingIgnoredDuringExecution

spec: affinity: # 定义Affinity亲和力配置 podAffinity: # 定义节点Affinity亲和力配置 preferredDuringSchedulingIgnoredDuringExecution: # 定义软性Affinity亲和力配置 - weight: 10 # 软亲和力的权重,权重越高优先级越大,范围1-100 preference: # 软亲和力配置项 matchExpressions: # 定义匹配节点标签键值对key=value - key: disktype # 设置节点标签键名key operator: In # 节点标签匹配方式,In等于 values: # 设置节点标签值value - ssd topologkey: kubernetes.io/hostname # 定义亲和力的范围,匹配的拓扑域的key,也就是节点上label和key,key和value相同的为同一个域。topologyke拓扑域,主要针对宿主机,相当于对宿主机进行区域的划分。用label进行判断,不同的key和不同的value是属于不同的拓扑域。kubernetes.io/hostname是K8s默认节点标签

Pod反亲和力(Pod Anti-Affinity)

作用:让 Pod 避免调度到已经运行了某些特定 Pod 的节点上,例如避免两个高负载的服务运行在同一个节点上,降低故障风险。

硬策略requiredDuringSchedulingIgnoredDuringExecution

spec: affinity: # 定义Affinity亲和力配置 podAntiAffinity: # 定义Pod Affinity亲和力配置 requiredDuringSchedulingIgnoredDuringExecution: # 定义Affinity硬性亲和力配置 - labelSelector: # 定义Pod选择器配置 matchExpressions: # 定义匹配Pod标签键值对key=value - key: disktype # 设置Pod标签键名key operator: In # Pod标签规则匹配条件,In等于 values: # 设置Pod标签值value - ssd topologkey: kubernetes.io/hostname # 定义亲和力的范围,匹配的拓扑域的key,也就是节点上label和key,key和value相同的为同一个域。topologyke拓扑域,主要针对宿主机,相当于对宿主机进行区域的划分。用label进行判断,不同的key和不同的value是属于不同的拓扑域。kubernetes.io/hostname是K8s默认节点标签

软策略preferredDuringSchedulingIgnoredDuringExecution

spec: affinity: # 定义Affinity亲和力配置 podAntAffinity: # 定义Pod Affinity反亲和力配置 preferredDuringSchedulingIgnoredDuringExecution: # 定义Affinity软性反亲和力配置 - weight: 10 # 反亲和力的权重,权重越高反亲和力越大,范围1-100 podAffinityTerm: # Pod反亲和力配置项 labelSelector: # 定义Pod选择器配置 matchExpressions: # 定义匹配Pod标签键值对key=value - key: security # 设置Pod标签键名key operator: In # Pod标签规则匹配条件,In等于 values: # 设置Pod标签值value - S2 topologyKey: failure-domain.beta.kubernetes.io/zone # 定义反亲和力的范围,匹配的拓扑域的key,也就是节点上label和key,key和value相同的为同一个域。topologyke拓扑域,主要针对宿主机,相当于对宿主机进行区域的划分。用label进行判断,不同的key和不同的value是属于不同的拓扑域。