跳到主要内容

Kubernetes 持久化数据

Kubernetes 中的 Volume 是如何设计的?

在Kubernetes 中,Pod 里包含了一组容器,这些容器是可以共享存储的,如下图所示。同时 Pod 内的容器又受制于各自的重启策略,我们需要保证容器重启不会对这些存储产生影响。因此 Kubernetes 中 Volume 的生命周期是直接和 Pod 挂钩的,而不是 Pod 内的某个容器,即 Pod 在 Volume 在。在 Pod 被删除时,才会对 Volume 进行解绑(unmount)、删除等操作。至于 Volume 中的数据是否会被删除,取决于Volume 的具体类型。

20230508162119

为了丰富可以对接的存储后端,Kubernetes 中提供了很多 volume plugin 可供使用。主要有以下几种类型:

20230508162149

如下图所示,Kubelet 内部调用相应的 plugin 实现,将外部的存储挂载到 Pod 内。类似于 CephFS、NFS 以及 awsEBS 这一类插件,是需要管理员提前在对应的存储系统中申请好的,Kubernetes 本身其实并不负责这些 Volume 的申请。

20230508162435

常见的几种内置 Volume 插件

下面介绍的这几款插件,目前依然能够照常使用,也是社区自身稳定支持的插件。但是对于一些云厂商和第三方的插件,社区已经不推荐继续使用内置的方式了,而是推荐你通过 CSI(Container Storage Interface,容器存储接口)来使用这些插件。

ConfigMap 和 Secret

首先来看 ConfigMap 和 Secret,这两类对象都可以通过 Volume 形式挂载到 Pod 内,

ConfigMap 通过键值对来存储信息,是个 namespace 级别的资源。在 kubectl 使用时,我们可以简写成 cm。

定义一个 CM 来测试 cm-demo-mix.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: cm-demo-mix # 对象名字
namespace: demo # 所在的命名空间
data: # 这是跟其他对象不太一样的地方,其他对象这里都是 spec
# 每一个键都映射到一个简单的值
player_initial_lives: "3" # 注意这里的值如果数字的话,必须用字符串来表示
ui_properties_file_name: "user-interface.properties"
# 也可以来保存多行的文本
game.properties: |
enemy.types=aliens,monsters
player.maximum-lives=5
user-interface.properties: |
color.good=purple
color.bad=yellow
allow.textmode=true

再定义一个 cm-demo-all-env.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: cm-demo-all-env
namespace: demo
data:
SPECIAL_LEVEL: very
SPECIAL_TYPE: charm

现在我们来创建这两个 ConfigMap:

$ kubectl create -f cm-demo-mix.yaml
$ kubectl create -f cm-demo-all-env.yaml
# 取得 ConfigMap 的信息
$ kubectl get cm -n demo

Pod 必须和 ConfigMap 在同一个 namespace 下面,在创建 Pod 之前,请务必保证 ConfigMap 已经存在,否则 Pod 创建会报错。

创建一个 cm-demo-pod.yaml 文件

apiVersion: v1
kind: Pod
metadata:
name: cm-demo-pod
namespace: demo
spec:
containers:
- name: demo
image: busybox:1.28
command:
- 'bin/sh'
- '-c'
- 'echo PLAYER_INITIAL_LIVES=$PLAYER_INITIAL_LIVES && sleep 10000'
# 定义环境变量
env:
- name: PLAYER_INITIAL_LIVES # 请注意这里和 ConfigMap 中的键名是不一样的
valueFrom:
configMapKeyRef:
name: cm-demo-mix # 这个值来自 ConfigMap
key: player_initial_lives # 需要取值的键
- name: UI_PROPERTIES_FILE_NAME
valueFrom:
configMapKeyRef:
name: cm-demo-mix
key: ui_properties_file_name
# 可以将 configmap 中的所有键值对都通过环境变量注入容器中
envFrom:
- configMapRef:
name: cm-demo-all-env
# 可以将 configmap 中的某个键值对注入到文件中
volumeMounts:
- name: full-config # 这里是下面定义的 volume 名字
mountPath: '/config' # 挂载的目标路径
readOnly: true
- name: part-config
mountPath: /etc/game/
readOnly: true
volumes: # 可以在 Pod 级别设置卷,然后将其挂载到 Pod 内的容器中
- name: full-config # 这是 volume 的名字
configMap:
name: cm-demo-mix # 提供你想要挂载的 ConfigMap 的名字
- name: part-config
configMap:
name: cm-demo-mix
items: # 也可以只挂载部分的配置
- key: game.properties
path: properties

在上面的这个例子中,几乎囊括了 ConfigMap 的几大使用场景:

  • 命令行参数;
  • 环境变量,可以只注入部分变量,也可以全部注入;
  • 挂载文件,可以是单个文件,也可以是所有键值对,用每个键值作为文件名。
$ kubectl create -f cm-demo-pod.yaml

创建成功后,我们 exec 到容器中看看:

$ kubectl exec -it cm-demo-pod -n demo sh

可以看到注入进去的两个环境变量

$ ls -al /config/

可以看到挂载进去的文件,里面也包含了对应的配置文件

在上面 ls -alh /config/ 后,我们看到挂载的文件中存在软链接,都指向了 ..data 目录下的文件。这样做的好处,是 kubelet 会定期同步检查已经挂载的 ConfigMap 是否是最新的,如果更新了,就是创建一个新的文件夹存放最新的内容,并同步修改 ..data 指向的软链接。

Downward API

再来看看 DownwardAPI,这是个非常有用的插件,可以帮助你获取 Pod 对象中定义的字段,比如 Pod 的标签(Labels)、Pod 的 IP 地址及 Pod 所在的命名空间(namespace)等。Downward API 有两种使用方法,既支持环境变量注入,也支持通过 Volume 挂载。

来看个 Volume 挂载的例子,如下是一个 Pod 的 yaml 文件:

# downwardapi-volume-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: downwardapi-volume-demo
namespace: demo03
labels:
zone: us-east-coast
cluster: downward-api-test-cluster1
rack: rack-123
annotations:
annotation1: "345"
annotation2: "456"
spec:
containers:
- name: volume-test-container
image: busybox:1.28
command: ["sh", "-c"]
args:
# 打印出映射到容器中的文件
- while true; do
if [[ -e /etc/podinfo/labels ]]; then
echo -en '\n\n'; cat /etc/podinfo/labels; fi;
if [[ -e /etc/podinfo/annotations ]]; then
echo -en '\n\n'; cat /etc/podinfo/annotations; fi;
sleep 5;
done;
volumeMounts:
- name: podinfo
mountPath: /etc/podinfo
volumes:
- name: podinfo
downwardAPI:
items:
- path: "labels"
fieldRef:
fieldPath: metadata.labels
- path: "annotations"
fieldRef:
fieldPath: metadata.annotations

创建这个 Pod,并通过 kubectl logs 来查看它的输出日志:

kubectl create ns demo03 
kubectl create -f downwardapi-volume-demo.yaml
kubectl logs -f downwardapi-volume-demo -n demo03

20230508163208

从上面的日志输出,我们可以看到 Downward API 可以通过 Volume 挂载到 Pod 里面,并被容器获取。

# 删掉这个 Pod
kubectl delete -f downwardapi-volume-demo.yaml

EmptyDir

在 Kubernetes 中,我们也可以使用临时存储,类似于创建一个 temp dir。我们将这种类型的插件叫作 EmptyDir,从名字就可以知道,在刚开始创建的时候,就是空的临时文件夹。在 Pod 被删除后,也一同被删除,所以并不适合保存关键数据。

EmptyDir 卷可以在多种场景中使用: 1、让不同 Container 之间共享文件和数据。例如,一个应用容器和一个 sidecar 容器可以通过 EmptyDir 共享数据。 2、作为持久化存储卷需要短暂存储数据。 3、作为缓存卷来加速任务的执行。

在使用的时候,可以参照如下的方式使用 EmptyDir:

# empty-dir-vol-demo.yaml
apiVersion: v1
kind: Pod
metadata:
name: empty-dir-vol-demo
namespace: demo03
spec:
containers:
- name: container1
image: nginx
volumeMounts:
- name: shared-data
mountPath: /data
command: ["/bin/sh", "-c", "sleep 5s && cat /data/hello.txt"]
- name: container2
image: busybox
volumeMounts:
- name: shared-data
mountPath: /data
command: ["/bin/sh", "-c", "echo 'Hello from container2' > /data/hello.txt"]
volumes:
- name: shared-data
emptyDir: {}

在上面的例子中,我们创建了一个名为 shared-data 的 EmptyDir 卷,并在两个容器 container1 和 container2 中都挂载了该卷。

在 container2 中,我们向 /data/hello.txt 写入了一条消息。

kubectl describe pod empty-dir-vol-demo -n demo03
kubectl logs empty-dir-vol-demo -n demo03

20230508170224

一般来说,EmptyDir 可以用来做一些临时存储,比如为耗时较长的计算任务存储中间结果或者作为共享卷为同一个 Pod 内的容器提供数据等等。

除此之外,我们也可以将 emptyDir.medium 字段设置为 “Memory”,来挂载 tmpfs (一种基于内存的文件系统)类型的 EmptyDir。比如下面这个例子:

apiVersion: v1
kind: Pod
metadata:
name: empty-dir-vol-memory-demo
namespace: demo
spec:
containers:
- image: busybox:1.28
imagePullPolicy: IfNotPresent
name: myvolumes-container
command: ['sh', '-c', 'echo container is Running ; df -h ; sleep 3600']
volumeMounts:
- mountPath: /demo
name: demo-volume
volumes:
- name: demo-volume
emptyDir:
medium: Memory

HostPath

HostPath 它和 EmptyDir 一样,都是利用宿主机的存储为容器分配资源。但是两者有个很大的区别,就是 HostPath 中的数据并不会随着 Pod 被删除而删除,而是会持久地存放在该节点上。

使用 HostPath 非常方便,既不需要依赖外部的存储系统,也不需要复杂的配置,还能持续存储数据。但是这里我要提醒你避免滥用:

1、避免通过容器恶意修改宿主机上的文件内容; 2、避免容器恶意占用宿主机上的存储资源而打爆宿主机; 3、要考虑到 Pod 自身的声明周期,而且 Pod 是会“漂移”重新“长”到别的节点上的,所以要避免过度依赖本地的存储。

同时使用的时候也需要额外注意,因为 Hostpath 中定义的路径是宿主机上真实的绝对路径,那么就会存在同一节点上的多个 Pod 共用一个 Hostpath 的情形,比如同一工作负载的不同实例调度到同一节点上,这会造成数据混乱,读写异常。这个时候我们就需要额外设置一些调度策略,避免这种情况发生。

下面是一个使用 HostPath 的例子:

apiVersion: v1
kind: Pod
metadata:
name: hostpath-demo
namespace: demo
spec:
containers:
- image: nginx:1.19.2
name: container-demo
volumeMounts:
- mountPath: /test-pd
name: hostpath-volume
volumes:
- name: hostpath-volume
hostPath:
path: /data # 对应宿主机上的绝对路径
type: Directory # 可选字段,默认是 Directory

在上面的例子中,我们要注意 hostpath.type 这个可以缺省的字段。为了保证后向兼容性,默认值是 Directory。目前这个字段还支持 DirectoryOrCreate、FileOrCreate 、File 、Socket 、CharDevice 和 BlockDevice,具体含义参考 官网

这个 type 可以帮助你做一些预检查,比如你期望挂载的是单个文件,如果检测到挂载路径是个目录,这个时候就会报异常,这样可以有效地避免一些误配置。

社区使用的 CSI

一开始,上述这些云厂商以及第三方的卷插件(volume plugin),都是直接内置在 Kubernetes 代码库中进行开发的,目前代码库中包含 20 多个插件。但这种方式带来了很多问题。

  1. 这些插件对 Kubernetes 代码本身的稳定性以及安全性引入了很多未知的风险,一个很小的 Bug 都有可能导致集群受到攻击或者无法工作。
  2. 这些插件的维护和 Kubernetes 的正常迭代紧密耦合在一起,一起打包和编译。即便是某个单一插件出现了 Bug,都需要通过升级 Kubernetes 的版本来修复。
  3. 社区需要维护所有的 volume plugin,并且要经过完整的测试验证流程,来保证可用性,这给社区的正常迭代平添了很多麻烦。
  4. 各个卷插件依赖的包也都要算作 Kubernetes 项目的一部分,这会让 Kubernetes 的依赖变得臃肿。
  5. 开发者被迫要将这些插件代码进行开源。

为此,社区早在 v1.2 版本就开始尝试用 FlexVolume 插件来解决,在 v1.8 版本 GA,并停止接收任何新增的内置 volume plugin 了。用户需要遵循 FlexVolume 约定的接口规范,自己开发可执行的程序,比如二进制程序、Shell脚本等,以命令行参数作为输入,并返回 JSON 格式的结果,这样 Kubelet 就可以通过 exec 的方式调用用户的插件程序了,如下图所示。

20230508171312

这种方式方便了各个插件的开发、更新、维护和升级,同时也和 Kubernetes 进行了解耦。在使用的时候,需要用户提前将这些二进制的文件放到各个节点上指定的目录里面(默认是 /usr/libexec/kubernetes/kubelet-plugins/volume/exec/),方便 Kubelet 可以动态发现和调用。

持久卷 Persistent Volume

持久化数据需要考虑的问题

在 Kubernetes 中持久化数据需要考虑的问题:

  1. 共享 Volume。目前 Pod 内的 Volume 其实跟 Pod 是存在静态的一一绑定关系,即生命周期绑定。这导致不同 Pod 之间无法共享 Volume。
  2. 复用 Volume 中的数据。当 Pod 由于某种原因失败,被工作负载控制器删除重新创建后,我们需要能够复用 Volume 中的旧数据。
  3. Volume 自身的一些强关联诉求。对于有状态工作负载 StatefulSet 来说,当其管理的 Pod 由于所在的宿主机出现一些硬件或软件问题,比如磁盘损坏、kernel 异常等,Pod 重新“长”到别的节点上,这时该如何保证 Volume 和 Pod 之间强关联的关系?
  4. Volume 功能及语义扩展,比如容量大小、标签信息、扩缩容等。

针对以上的问题,Kubernetes 中引入了一个专门的对象 Persistent Volume(简称 PV),将计算和存储进行分离,可以使用不同的控制器来分别管理。

PV 和 PVC 的概念

通过 PV,各个服务也可以和 Pod 自身的生命周期进行解耦。一个 PV 可以被几个 Pod 同时使用,即使 Pod 被删除后,PV 这个对象依然存在,其他新的 Pod 依然可以复用。

为了更好地描述这种关联绑定关系,易于使用,并且屏蔽更多用户并不关心的细节参数(比如 PV 由谁提供、创建在哪个 zone/region、怎么去访问到,等等),通过一个抽象对象 Persistent Volume Claim(PVC)来使用 PV。

可以把 PV 理解成是对实际的物理存储资源描述,PVC 是便于使用的抽象 API。在 Kubernetes 中,使用时都是在 Pod 中通过PVC 的方式来使用 PV 的,见下图。

20230508172201

PVC 作为声明式请求,指定了需要的 PV 的属性(例如存储空间、访问模式等)和数量等信息。PVC 对象一旦被创建并与 PV 绑定,它就可以被 Pod 使用。Pod 可以在其定义文件中声明需要使用哪个 PVC,然后容器就可以像使用本地存储一样使用 PVC。因此,通过 PVC,容器无需关心真正使用的具体存储后端是什么,从而提高了灵活性。

在 Kubernetes 中,创建 PV(PV Provision) 有两种方式,即静态和动态,如下图所示。

20230508172757

持久卷的单位

storage 的单位是什么

在 Kubernetes 中,存储资源的单位为 Gi,即 Gibibytes(2^30 Bytes)。其他常见的存储单位包括 Mi(Mebibytes,2^20 Bytes)、Ti(Tebibytes,2^40 Bytes)等,它们的换算关系如下:

1 Gi = 1024 Mi 1 Ti = 1024 Gi

如何创建静态 PV

静态 PV(Static PV),管理员通过手动的方式在后端存储平台上创建好对应的 Volume,然后通过 PV 定义到 Kubernetes 中去。开发者通过 PVC 来使用。我们来看个 HostPath 类型的 PV 例子:

apiVersion: v1
kind: PersistentVolume
metadata:
name: task-pv-volume # pv 的名字
labels: # pv 的一些label
type: local
spec:
storageClassName: manual
capacity: # 该 pv 的容量
storage: 10Gi
accessModes: # 该 pv 的接入模式
- ReadWriteOnce
hostPath: # 该 pv 使用的 hostpath 类型,还支持通过 CSI 接入其他 plugin
path: "/mnt/data"

这里,我们定义了一个名为 task-pv-volume的 PV,PV 是集群的资源,并不属于某个 namespace。其中 storageClassName 这个字段是某个 StorageClass 对象的名字。我们会在下一段动态 PV 中讲解 StorageClass 的作用。

这里头 accessMode 可以指定该 PV 的几种访问 挂载方式

  • ReadWriteOnce(RWO)表示该卷只可以以读写方式挂载到一个 Pod 内;
  • ReadOnlyMany( ROX)表示该卷可以挂载到多个节点上,并被多个 Pod 以只读方式挂载;
  • ReadWriteMany(RWX)表示卷可以被多个节点以读写方式挂载供多个 Pod 同时使用。

注意一个 PV 只能有一种访问挂载模式。不同的 volume plugin 支持的 accessMode 并不相同,在使用的时候 参照官方的这个表格 进行选择

创建好后查看这个 PV:

$ kubectl get pv task-pv-volume
NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE
task-pv-volume 10Gi RWO Retain Available manual 4s

可以看到,这个 PV 的状态为 Available(可用),这里还看到上面 kubectl get 的输出里面有个 ReclaimPolicy 字段,该字段表明对 PV 的回收策略,默认是 Retain,即 PV 使用完后数据保留,需要由管理员手动清理数据。

除了 Retain 外,还支持如下策略:

  • Recycle,即回收,这个时候会清除 PV 中的数据;
  • Delete,即删除,这个策略常在云服务商的存储服务中使用到,比如 AWS EBS。

再创建一个 PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: task-pv-claim
namespace: dmeo
spec:
storageClassName: manual
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi

创建好了以后,Kubernetes 会为 PVC 匹配满足条件的 PV。在 PVC 里面指定 storageClassName 为 manua,这个时候就只会去匹配 storageClassName 同样为 manual 的 PV。一旦发现合适的 PV 后,就可以绑定到该 PV 上。

PVC 是 namespace 级别的资源,我们来创建看看:

$ kubectl get pvc -n demo
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
task-pv-claim Bound task-pv-volume 10Gi RWO manual 9s

我们可以看到 这个 PVC 已经和我们上面的 PV 绑定起来了。我们再来查看下 task-pv-volume 这个 PV 对象,可以看到它的状态也从 Available 变成了 Bound。

$ kubectl get pv task-pv-volume
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE
task-pv-volume 10Gi RWO Retain Bound default/task-pv-claim manual 2m12s

PV 一般会有如下五种状态:

  • Pending 表示目前该 PV 在后端存储系统中还没创建完成;
  • Available 即闲置可用状态,这个时候还没有被绑定到任何 PVC 上;
  • Bound 就像上面例子里似的,这个时候已经绑定到某个 PVC 上了;
  • Released 表示已经绑定的 PVC 已经被删掉了,但资源还未被回收掉;
  • Failed 表示回收失败。

同样,对于 PVC 来说,也有如下三种状态:

  • Pending 表示还未绑定任何 PV;
  • Bound 表示已经和某个 PV 进行了绑定;
  • Lost 表示关联的 PV 失联。

下面来看看,如何在 Pod 中使用静态的 PV。看如下的例子:

apiVersion: v1
kind: Pod
metadata:
name: task-pv-pod
namespace: demo
spec:
volumes:
- name: task-pv-storage
persistentVolumeClaim:
claimName: task-pv-claim
containers:
- name: task-pv-container
image: nginx:1.14.2
ports:
- containerPort: 80
name: "http-server"
volumeMounts:
- mountPath: "/usr/share/nginx/html"
name: task-pv-storage

创建完成以后:

$ kubectl get pod task-pv-pod -n demo
NAME READY STATUS RESTARTS AGE
task-pv-pod 1/1 Running 1 82s
$ kubectl exec -it task-pv-pod -n demo -- /bin/bash
root@task-pv-pod:/# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 40G 5.0G 33G 14% /
tmpfs 64M 0 64M 0% /dev
tmpfs 996M 0 996M 0% /sys/fs/cgroup
/dev/vda1 40G 5.0G 33G 14% /etc/hosts
shm 64M 0 64M 0% /dev/shm
overlay 996M 4.0M 992M 1% /usr/share/nginx/html
tmpfs 996M 12K 996M 1% /run/secrets/kubernetes.io/serviceaccount
tmpfs 996M 0 996M 0% /proc/acpi
tmpfs 996M 0 996M 0% /sys/firmware

可以看到,PV 已经正确挂载到 Pod 内。

静态 PV 最大的问题就是使用起来不够方便,都是管理员提前创建好一批指定规格的 PV,无法做到按需创建。使用过程中,经常会遇到由于资源大小不匹配,规格不对等,造成 PVC 无法绑定 PV 的情况。同时还会造成资源浪费,比如一个只需要 1G 空间的 Pod,绑定了 10G 的 PV。

这些问题,都可以通过动态 PV 来解决。

StorageClass 是什么

在 Kubernetes 中,StorageClass 是定义存储配置的对象,用于将不同的存储技术抽象为 Kubernetes 管理的统一接口。StorageClass 可以定义不同的存储类别,例如本地存储、云存储、分布式存储等,同时可以指定不同的存储特性,例如性能、可靠性、持久化等级、数据保护等。

使用 StorageClass,Kubernetes 集群管理员可以为应用程序和服务提供不同的存储策略,以满足不同的应用程序和服务的存储需求。例如,可以根据数据的重要性和访问模式选择不同的存储类别和特性,以确保数据的安全和高效性。

在创建 PVC(PersistentVolumeClaim)时,可以指定所需的 StorageClass。

当 PVC 被创建后,Kubernetes 会根据 StorageClass 自动创建一个 PV(PersistentVolume),并将其绑定到 PVC 上。然后,PVC 可以被挂载到 Pod 中,以提供持久化存储服务。

本地存储的插件

确认 Kubernetes 集群中是否已经安装了支持本地存储的插件,例如 local-path-provisioner。

sudo kubectl get storageclass

如下图这种就是已经安装的

20230509144708

如果集群中没有安装本地存储插件,可以通过 Helm 等工具来安装。以 local-path-provisioner 为例,可以使用以下命令:

helm repo add local-path https://charts.local-path.dev
helm install local-path-provisioner local-path/local-path-provisioner

安装完成后,再执行上述步骤,就可以确认本地存储插件已经安装了。

怎么样创建动态的 PV

上面已经介绍了,所谓的动态 PV 其实就是通过创建一个指定了 StorageClass 的 PVC 去借用这个 StorageClass 的力量去创建动态的内容,具体的实现还得看这个插件的能力,如下介绍一个创建本地的动态 PV 的例子

1、确认 Kubernetes 集群中是否已经安装了支持本地存储的插件,例如 local-path-provisioner。

2、创建一个 PVC 配置文件,例如 my-pvc.yaml,指定 StorageClass 为 local-path,并且指定所需的存储容量和访问模式等。例如:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: my-pvc
spec:
storageClassName: local-path
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi

该配置文件表示需要创建一个名为 my-pvc 的 PVC,使用 local-path StorageClass,请求 10GB 的存储空间,并且只支持单节点读写。

3、执行以下命令创建 PVC:

kubectl apply -f my-pvc.yaml

如果本地存储插件已经安装并且运行正常,PVC 就会自动创建并绑定到一个本地 PV 上。

4、创建一个 Pod 配置文件,例如 my-pod.yaml,并在其中引用上一步创建的 PVC。例如:

kind: Pod
apiVersion: v1
metadata:
name: my-pod
spec:
containers:
- name: my-container
image: redis
volumeMounts:
- name: my-volume
mountPath: /data
volumes:
- name: my-volume
persistentVolumeClaim:
claimName: my-pvc

该配置文件表示需要创建一个名为 my-pod 的 Pod,挂载一个名为 my-volume 的持久化存储卷,使用 my-pvc PVC。

如果一切正常,Pod 就会被创建,并且会自动挂载上一步创建的 PVC。在 Pod 中访问 /data 目录就相当于访问本地磁盘上的持久化数据。

StatefulSet 使用 PVC

在 Kubernetes 中,可以使用 StatefulSet 来管理有状态的应用程序,例如数据库集群。与 Deployment 不同,StatefulSet 中的每个 Pod 都有唯一的标识符,即它们的名称,这些名称可以基于固定的命名约定自动创建。这使得它们可以更容易地进行有序的操作,例如扩展和缩小,因为它们的名称不会随机生成和更改。

当使用 StatefulSet 部署应用程序时,可以使用 PVC(Persistent Volume Claims)来管理 Pod 中的数据。PVC 充当 Pod 和存储之间的中介,并确保数据存储持久化,即使 Pod 进程终止或迁移。

在 StatefulSet 中,可以使用 volumeClaimTemplates 字段来定义 PVC 模板,以便每个 Pod 在创建时自动创建 PVC。以下是一个 StatefulSet 的 YAML 配置文件示例,其中使用 PVC 存储 Redis 数据库的数据:

apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-cluster
replicas: 3
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
containers:
- name: redis
image: redis
volumeMounts:
- name: data
mountPath: /data
ports:
- containerPort: 6379
volumes:
- name: data
persistentVolumeClaim:
claimName: redis-data
volumeClaimTemplates:
- metadata:
name: redis-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
storageClassName: local-path

在上述配置文件中,volumeClaimTemplates 字段定义了一个 PVC 模板,名称为 redis-data。在每个 Pod 中,定义了一个名为 data 的卷,并将其挂载到 /data 路径上。persistentVolumeClaim 字段用于指定要使用的 PVC,这里使用了 redis-data,它会根据 volumeClaimTemplates 中的定义自动创建。在这个例子中,使用了 local-path 存储类作为 PVC 的存储后端,该存储类已经配置为使用本地存储插件。

当创建 StatefulSet 时,Kubernetes 会自动创建三个 Pod,每个 Pod 都会自动创建一个 redis-data PVC。如果 Pod 被删除或重新创建,Kubernetes 会自动重新绑定相应的 PVC,确保 Pod 中的数据不会丢失。

例:MySQL 数据持久化存储

首先,需要创建一个 Kubernetes Deployment 和一个 PersistentVolumeClaim 对象,用于创建 MySQL Pod 和动态数据卷。

# mysql-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: mysql
spec:
replicas: 1
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:latest
env:
- name: MYSQL_ROOT_PASSWORD
# 如果不是要 secret 中的 password,可以直接使用 value 注意 value 得是字符串
# value: "123456"
# 使用 secret 中的 password
valueFrom:
secretKeyRef:
name: mysql-secret
key: password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-persistent-storage
mountPath: /var/lib/mysql
volumes:
- name: mysql-persistent-storage
persistentVolumeClaim:
claimName: mysql-pvc
# mysql-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
提示

resources.requests.storage 是 Kubernetes 中用来请求指定 Pod 或 PersistentVolumeClaim (PVC) 所需存储资源的一种方式。它是 Pod 或 PVC 中定义的一组资源限制(Resource Limits)中的一部分,通常用于指定需要分配给 Pod 或 PVC 的磁盘存储空间的最小值。

在上面的 YAML 文件中,我们创建了一个名为 mysql 的 Deployment,它使用 MySQL 官方镜像创建一个容器。我们将 MySQL 的数据目录 /var/lib/mysql 挂载到名为 mysql-persistent-storage 的动态数据卷上。

此外,我们还创建了一个 PersistentVolumeClaim 对象,它请求 1GB 的存储空间。这个 PVC 可以根据需要动态地分配存储资源,然后与 Deployment 中的 Pod 进行绑定。

kubectl apply -f mysql-deployment.yaml
kubectl apply -f mysql-pvc.yaml

# 查看 pod 和 pvc
kubectl get pod
kubectl get pvc
kubectl get pv

20230509110746

20230509110945

可以看到默认的回收策略是 Delete,这样当 PVC 被删除时,对应的 PV 也会被删除。这样就会导致数据丢失,所以我们需要修改回收策略。

如果要修改回收策略,找到 spec 字段,然后找到 persistentVolumeReclaimPolicy 字段。如果该字段不存在,则需要添加它。将该字段的值设置为 Retain。例如:

spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: standard
persistentVolumeReclaimPolicy: Retain # 将回收策略设置为 Retain

保存并关闭编辑器。Kubernetes 将自动更新 PVC 对象,并将回收策略设置为 Retain。

注意,当将 PVC 的回收策略设置为 Retain 时,需要手动回收与 PV 相关联的存储资源。可以使用以下命令删除 PV 和相关的存储资源:

kubectl delete pv <pv-name>

请注意,删除 PV 将不会删除与之相关的 PVC。如果您希望删除 PVC 和 PV,需要分别使用 kubectl delete pvckubectl delete pv 命令。

Secret 存储密码

我们还使用了一个名为 mysql-secret 的 Secret 对象来存储 MySQL 的根密码。

以下是一个示例 MySQL Secret 的 YAML 文件:

apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
mysql-user: <base64-encoded-username>
mysql-password: <base64-encoded-password>

在上面的 YAML 文件中,需要将 <base64-encoded-username><base64-encoded-password> 替换为经过 Base64 编码的 MySQL 用户名和密码。

可以使用以下命令来生成 Base64 编码的字符串:

echo -n 'your-username' | base64
echo -n 'your-password' | base64

将生成的编码字符串替换到 YAML 文件中的 <base64-encoded-username><base64-encoded-password> 字段中,如下所示:

apiVersion: v1
kind: Secret
metadata:
name: mysql-secret
type: Opaque
data:
mysql-user: bXl1c2Vy
mysql-password: bXlwYXNzd29yZA==

保存上面的 YAML 文件并使用 kubectl apply -f 命令将其部署到 Kubernetes 集群中。之后,您可以在 MySQL 部署 YAML 文件中引用 mysql-secret,以获取 MySQL 访问凭据。

会挂载在物理机的哪个目录

在 Kubernetes 中,当创建一个 PersistentVolumeClaim (PVC) 时,它并不直接映射到物理机的目录。相反,PVC 会在 Kubernetes 集群中请求一定数量的存储空间,并由 Kubernetes 集群中的存储插件提供支持。

根据 Kubernetes 集群配置和存储插件,PVC 可能会映射到不同的存储后端。例如,PVC 可能会映射到物理机的本地存储、网络存储(例如 NFS 或 iSCSI)或云存储(例如 AWS EBS 或 Azure Disk)。

如果想查看 PVC 是否已经成功地绑定到一个 PersistentVolume(PV)上,并且 PV 是否已经成功地绑定到物理存储上,可以使用以下命令:

kubectl get pvc
kubectl get pv

其中,kubectl get pvc 命令将返回所有 PVC 的状态,包括它们是否已绑定到 PV 上。kubectl get pv 命令将返回所有 PV 的状态,包括它们是否已绑定到物理存储上。

如果 PVC 已经绑定到一个 PV 上,可以使用以下命令查看 PV 的详细信息,包括 PV 的存储类型和存储路径:

kubectl describe pv <pv-name>

其中,<pv-name> 是要查看详细信息的 PV 的名称。在输出中,可以查找 Source: 字段,以查看 PV 的存储类型和存储路径。

20230509111335

需要注意的是,PV 的存储路径是相对于存储插件的。例如,在本地存储插件中,PV 的存储路径可能是物理机上的目录路径,而在网络存储插件中,PV 的存储路径可能是网络存储服务器上的路径。

访问 MySQL

上面的创建的 MySQL 使用的是 Deployment,所以我们需要再创建一个 Service 来访问它

# mysql-service.yaml
apiVersion: v1
kind: Service
metadata:
name: mysql-service
labels:
app: mysql
spec:
ports:
- name: mysql
port: 3306
targetPort: 3306
selector:
app: mysql
type: ClusterIP

请注意,在此示例中,我们将服务类型设置为 ClusterIP,这将创建一个内部服务,只能在 Kubernetes 群集内部访问。如果要从 Kubernetes 集群外部访问 MySQL,请将服务类型设置为 LoadBalancer。

获取服务的 IP 地址和端口号。可以使用以下命令获取服务的 IP 地址和端口号:

kubectl get service mysql-service

20230509113545

20230509113530

Reference