官方文档: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-version 命令查询
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

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)时重启容器
  • 创建 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 端口(暴露到宿主机 node2 节点的 80 端口上),因为安装了 flannel ,master 节点能够和 node2 通过隧道 flannel.1 通信。所以在 kubernetes-mastercurl pod 中的 nginx,流量请求为:

kubernetes-master --( flannel.1 )--> node2 --( 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:
    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:
    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:
    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
  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 次即判定为存活探测失败
      periodSeconds: 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 
  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 次即判定为存活探测失败
      periodSeconds: 3 # 探测间隔时间
      successThreshold: 1 # http 请求成功 1 次即判定为存活探测成功
      timeoutSeconds: 5 # 请求超时时间
    readinessProbe: # 就绪探针配置
      httpGet:
        path: /wellcome.html
        port: 80
      periodSeconds: 5 # 每隔 5 秒进行一个探测
      failureThreshold: 3 # 请求失败 3 次即判定为存活探测失败
      periodSeconds: 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 是在终止信号发送后触发。

# 资源调度

# 标签和选择器

在 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:latest # 镜像
        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 秒最多缩容一次

# 服务发现

# 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 的容器处理请求并返回响应;

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

![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \service.png)

# 创建 Service

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

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 访问外部服务](### 基于 service 访问外部服务) 小节,配置了多个 IP 进行轮询

  • NodePort 类型,主要是解决将外部访问内部服务的问题,参考 [创建 Service](### 创建 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 的工作机制可以参考下图

![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \ingress_1.png)

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

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

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

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

![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \ingress_2.png)

# 安装 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 sed -i's#<h1>Welcome to nginx!</h1>#<h1>Welcome to service-a</h1>#g' /usr/share/nginx/html/index.html
sed: -e expression #1, char 3: unterminated `s' command
command terminated with exit code 1
[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 主机上

![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \HostPath.png)

如下示例,会将主机的 /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 管理员在集群中预先定义一些存储资源,并逻辑划分这些存储资源对象,将这些资源暴露给集群中的用户使用

![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \static_PV.png)

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

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

pvc 和 pv 绑定,才可以给 pod 使用,StorageClass 是创建 pv 的模板
![](C:\Users\Amadues\Desktop\K8S\K8S 实操 \storage_PV.jpeg)

# 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 资源才能够删除