关于后端:一文读懂-Kubernetes-存储设计

3次阅读

共计 18126 个字符,预计需要花费 46 分钟才能阅读完成。

在 Docker 的设计中,容器内的文件是长期寄存的,并且随着容器的删除,容器外部的数据也会一起被清空。不过,咱们能够通过在 docker run 启动容器时,应用 –volume/-v 参数来指定挂载卷,这样就可能将容器外部的门路挂载到主机,后续在容器外部存放数据时会就被同步到被挂载的主机门路中。这样做能够保障保障即使容器被删除,保留到主机门路中的数据也依然存在。

与 Docker 通过挂载卷的形式就能够解决长久化存储问题不同,K8s 存储要面临的问题要简单的多。因为 K8s 通常会在多个主机部署节点,如果 K8s 编排的 Docker 容器解体,K8s 可能会在其余节点上从新拉起容器,这就导致原来节点主机上挂载的容器目录无奈应用。

当然也是有方法解决 K8s 容器存储的诸多限度,比方能够对存储资源做一层形象,通常大家将这层形象称为卷(Volume)。

K8s 反对的卷基本上能够分为三类:配置信息、长期存储、长久存储。

配置信息

无论何种类型的利用,都会用到配置文件或启动参数。而 K8s 将配置信息进行了形象,定义成了几种资源,次要有以下三种:

  • ConfigMap
  • Secret
  • DownwardAPI

ConfigMap

ConfigMap 卷通常以一个或多个 key: value 模式存在,次要用来保留利用的配置数据。其中 value 能够是字面量或配置文件。

不过,因为 ConfigMap 在设计上不是用来保留大量数据的,所以在 ConfigMap 中保留的数据不可超过 1 MiB(兆字节)。

ConfigMap 有两种创立形式:

  • 通过命令行创立
  • 通过 yaml 文件创建

通过命令行创立

在创立 Configmap 的时候能够通过 –from-literal 参数来指定 key: value,以下示例中 foo=bar 即为字面量模式,bar=bar.txt 为配置文件模式。

$ kubectl create configmap c1 --from-literal=foo=bar --from-literal=bar=bar.txt

bar.txt 内容如下:

baz

通过 kubectl describe 命令查看新创建的名称为 c1 的这个 Configmap 资源内容。

$ kubectl describe configmap c1
Name:         c1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
bar:
----
baz
foo:
----
bar
Events:  <none>

通过 yaml 文件创建

创立 configmap-demo.yaml 内容如下:

kind: ConfigMap
apiVersion: v1
metadata:
  name: c2
  namespace: default
data:
  foo: bar
  bar: baz

通过 kubectl apply 命令利用这个文件。

$ kubectl apply -f configmap-demo.yaml

$ kubectl get configmap c2
NAME   DATA   AGE
c2     2      11s

$ kubectl describe configmap c2
Name:         c2
Namespace:    default
Labels:       <none>
Annotations:  <none>

Data
====
foo:
----
bar
bar:
----
baz
Events:  <none>

失去的后果跟通过命令行形式创立的 Configmap 没什么两样。

应用示例

实现 Configmap 创立后,来看下如何应用。

创立好的 Configmap 有两种应用办法:

  • 通过环境变量将 Configmap 注入到容器外部
  • 通过卷挂载的形式间接将 Configmap 以文件模式挂载到容器。

通过环境变量形式援用

创立 use-configmap-env-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-configmap-env"
  namespace: default
spec:
  containers:
    - name: use-configmap-env
      image: "alpine"
      # 一次援用单个值
      env:
        - name: FOO
          valueFrom:
            configMapKeyRef:
              name: c2
              key: foo
      # 一次援用所有值
      envFrom:
        - prefix: CONFIG_  # 配置援用前缀
          configMapRef:
            name: c2
      command: ["echo", "$(FOO)", "$(CONFIG_bar)"]

能够看到咱们创立了一个名为 use-configmap-env 的 Pod,Pod 的容器将应用两种形式援用 Configmap 的内容。

第一种是指定 spec.containers.env,它能够为容器援用一个 Configmap 的 key: value 对。其中 valueFrom. configMapKeyRef 表明咱们要援用 Configmap,Configmap 的名称为 c2,援用的 key 为 foo。

第二种是指定 spec.containers.envFrom,只须要通过 configMapRef.name 指定 Configmap 的名称,它就能够一次将 Configmap 中的所有 key: value 传递给容器。其中 prefix 能够给援用的 key 后面减少对立前缀。

Pod 的容器启动命令为 echo $(FOO) $(CONFIG_bar),能够别离打印通过 env 和 envFrom 两种形式援用的 Configmap 的内容。

# 创立 Pod
$ kubectl apply -f use-configmap-env-demo.yaml
# 通过查看 Pod 日志来察看容器外部援用 Configmap 后果
$ kubectl logs use-configmap-env
bar baz

结果表明,容器外部能够通过环境变量的形式援用到 Configmap 的内容。

通过卷挂载形式援用

创立 use-configmap-volume-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-configmap-volume"
  namespace: default
spec:
  containers:
    - name: use-configmap-volume
      image: "alpine"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: configmap-volume
          mountPath: /usr/share/tmp  # 容器挂载目录
  volumes:
    - name: configmap-volume
      configMap:
        name: c2

这里创立一个名为 use-configmap-volume 的 Pod。通过 spec.containers.volumeMounts 指定容器挂载,name 指定挂载的卷名,mountPath 指定容器外部挂载门路(也就是将 Configmap 挂载到容器外部的指定目录下)。spec.volumes 申明一个卷,而 configMap.name 表明了这个卷要援用的 Configmap 名称。

而后可通过如下命令创立 Pod 并验证容器外部是否援用到 Configmap。

# 创立 Pod
$ kubectl apply -f use-configmap-volume-demo.yaml
# 进入 Pod 容器外部
$ kubectl exec -it use-configmap-volume -- sh
# 进入容器挂载目录
/ # cd /usr/share/tmp/
# 查看挂载目录下的文件
/usr/share/tmp # ls
bar  foo
# 查看文件内容
/usr/share/tmp # cat foo
bar
/usr/share/tmp # cat bar
baz

创立实现后,通过 kubectl exec 命令能够进入容器外部。查看容器 /usr/share/tmp/ 目录,能够看到两个以 Configmap 中 key 为名称的文本文件(foo、bar),key 所对应的 value 内容即为文件内容。

以上就是两种将 Configmap 的内容注入到容器外部的形式。容器外部的利用则能够别离通过读取环境变量、文件内容的形式应用配置信息。

Secret

相熟了 Configmap 的用法,接下来看下 Secret 的应用。Secret 卷用来给 Pod 传递敏感信息,例如明码、SSH 密钥等。因为尽管 Secret 与 ConfigMap 十分相似,然而它会对存储的数据进行 base64 编码。

Secret 同样有两种创立形式:

  • 通过命令行创立
  • 通过 yaml 文件创建

通过命令行创立

Secret 除了通过 –from-literal 参数来指定 key: value,还有另一种形式。即通过 –form-file 参数间接从文件加载配置,文件名即为 key,文件内容作为 value。

# generic 参数对应 Opaque 类型,既用户定义的任意数据
$ kubectl create secret generic s1 --from-file=foo.txt

foo.txt 内容如下:

foo=bar
bar=baz

能够看到与 Configmap 不同,创立 Secret 须要指明类型。下面的示例为命令通过指定 generic 参数来创立类型为 Opaque 的 Secret,这也是 Secret 默认类型。须要留神的是除去默认类型,Secret 还反对其余类型,能够通过官网文档查看。不过初期学习阶段只应用默认类型即可,通过默认类型就可能实现其余几种类型的性能

$ kubectl describe secret s1
Name:         s1
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
foo.txt:  16 bytes

另外一点与 Configmap 不同的是,Secret 仅展现 value 的字节大小,而不间接展现数据,这是为了保留密文,也是 Secret 名称的含意。

通过 yaml 文件创建

创立 secret-demo.yaml 内容如下:

apiVersion: v1
kind: Secret
metadata:
  name: s2
  namespace: default
type: Opaque  # 默认类型
data:
  user: cm9vdAo=
  password: MTIzNDU2Cg==

通过 kubectl apply 命令利用这个文件。


$ kubectl apply -f secret-demo.yaml

$ kubectl get secret s2
NAME   TYPE     DATA   AGE
s2     Opaque   2      59s

$ kubectl describe secret s2
Name:         s2
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
password:  7 bytes
user:      5 bytes

同样可能正确创立出 Secret 资源。然而能够看到通过 yaml 文件创建 Secret 时,指定的 data 内容必须通过 base64 编码,比方咱们指定的 user 和 password 都是编码后的后果。

data:
  user: cm9vdAo=
  password: MTIzNDU2Cg==

除此外也能够应用原始字符串形式,这两种形式是等价,示例如下:

data:
  stringData:
   user: root
   password: "123456"

相对而言,我更举荐应用 base64 编码的形式。

应用示例

同 Configmap 应用形式一样,咱们也能够通过环境变量或卷挂载的形式来应用 Secret。以卷挂载形式为例。首先创立 use-secret-volume-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "use-secret-volume-demo"
  namespace: default
spec:
  containers:
    - name: use-secret-volume-demo
      image: "alpine"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: secret-volume
          mountPath: /usr/share/tmp # 容器挂载目录
  volumes:
    - name: secret-volume
      secret:
        secretName: s2

即创立一个名为 use-secret-volume-demo 的 Pod,而 Pod 的容器通过卷挂载形式援用 Secret 的内容。

# 创立 Pod
$ kubectl apply -f use-secret-volume-demo.yaml

# 进入 Pod 容器外部
$ kubectl exec -it use-secret-volume-demo -- sh
# 进入容器挂载目录
/ # cd /usr/share/tmp/
# 查看挂载目录下的文件
/usr/share/tmp # ls
password  user
# 查看文件内容
/usr/share/tmp # cat password 
123456
/usr/share/tmp # cat user 
root

能够发现被挂载到容器外部当前,Secret 的内容将变成明文存储。容器外部利用能够同应用 Configmap 一样来应用 Secret。

作为能够存储配置信息的 Configmap 和 Secret,Configmap 通常寄存一般配置,Secret 则寄存敏感配置。

值得一提的是,应用环境变量形式援用 Configmap 或 Secret,当 Configmap 或 Secret 内容变更时,容器外部援用的内容不会自动更新;应用卷挂载形式援用 Configmap 或 Secret,当 Configmap 或 Secret 内容变更时,容器外部援用的内容会自动更新。如果容器外部利用反对配置文件热加载,那么通过卷挂载对的形式援用 Configmap 或 Secret 内容将是举荐形式。

DownwardAPI

DownwardAPI 能够将 Pod 对象本身的信息注入到 Pod 所治理的容器外部。

应用示例

创立 downwardapi-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: downwardapi-volume-demo
  labels:
    app: downwardapi-volume-demo
  annotations:
    foo: bar
spec:
  containers:
    - name: downwardapi-volume-demo
      image: alpine
      command: ["sleep", "3600"]
      volumeMounts:
        - name: podinfo
          mountPath: /etc/podinfo
  volumes:
    - name: podinfo
      downwardAPI:
        items:
          # 指定援用的 labels
          - path: "labels"
            fieldRef:
              fieldPath: metadata.labels
          # 指定援用的 annotations
          - path: "annotations"
            fieldRef:
              fieldPath: metadata.annotations
# 创立 Pod
$ kubectl apply -f downwardapi-demo.yaml
pod/downwardapi-volume-demo created

# 进入 Pod 容器外部
$ kubectl exec -it downwardapi-volume-demo -- sh
# 进入容器挂载目录
/ # cd /etc/podinfo/
# 查看挂载目录下的文件
/etc/podinfo # ls
annotations  labels
# 查看文件内容
/etc/podinfo # cat annotations 
foo="bar"
kubectl.kubernetes.io/last-applied-configuration="{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{\"foo\":\"bar\"},\"labels\":{\"app\":\"downwardapi-volume-demo\"},\"name\":\"downwardapi-volume-demo\",\"namespace\":\"default\"},\"spec\":{\"containers\":[{\"command\":[\"sleep\",\"3600\"],\"image\":\"alpine\",\"name\":\"downwardapi-volume-demo\",\"volumeMounts\":[{\"mountPath\":\"/etc/podinfo\",\"name\":\"podinfo\"}]}],\"volumes\":[{\"downwardAPI\":{\"items\":[{\"fieldRef\":{\"fieldPath\":\"metadata.labels\"},\"path\":\"labels\"},{\"fieldRef\":{\"fieldPath\":\"metadata.annotations\"},\"path\":\"annotations\"}]},\"name\":\"podinfo\"}]}}\n"
kubernetes.io/config.seen="2022-03-12T13:06:50.766902000Z"
/etc/podinfo # cat labels
app="downwardapi-volume-demo"

不难发现,DownwardAPI 的应用形式同 Configmap 和 Secret 一样,都能够通过卷挂载形式挂载到容器外部当前,在容器挂载的目录下生成对应文件,用来存储 key: value。不同的是,因为 DownwardAPI 能援用的内容曾经都在以后 yaml 文件中定义好了,所以 DownwardAPI 不须要事后定义,能够间接应用。

小结

ConfigMap、Secret、DownwardAPI 这三种 Volume 存在的意义不是为了保留容器中的数据,而是为了给容器传递事后定义好的数据。

长期卷

接下来咱们要关注的是长期卷,即长期存储。K8s 反对的长期存储中最罕用的就是如下两种:

  • EmptyDir
  • HostPath

长期存储卷会听从 Pod 的生命周期,与 Pod 一起创立和删除。

EmptyDir

先来看 emptyDir 如何应用。EmptyDir 相当于通过 –volume/-v 挂载时的隐式 Volume 模式应用 Docker。K8s 会在宿主机上创立一个长期目录,被挂载到容器所申明的 mountPath 目录上,即不显式的申明在宿主机上的目录。

应用示例

创立 emptydir-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "emptydir-nginx-pod"
  namespace: default
  labels:
    app: "emptydir-nginx-pod"
spec:
  containers:
    - name: html-generator
      image: "alpine:latest"
      command: ["sh", "-c"]
      args:
       - while true; do
          date > /usr/share/index.html;
          sleep 1;
         done
      volumeMounts:
        - name: html
          mountPath: /usr/share
    - name: nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          # nginx 容器 index.html 文件所在目录
          mountPath: /usr/share/nginx/html
          readOnly: true
  volumes:
    - name: html
      emptyDir: {}

这里创立一个名为 emptydir-nginx-pod 的 Pod,它蕴含两个容器 html-generator 和 nginx。html-generator 用来不停的生成 html 文件,nginx 则是用来展现 html-generator 生成的 index.html 文件的 Web 服务。

具体流程为,html-generator 不停的将以后工夫写入到 /usr/share/index.html 下,并将 /usr/share 目录挂载到名为 html 的卷中,而 nginx 容器则将 /usr/share/nginx/html 目录挂载到名为 html 的卷中,这样两个容器通过同一个卷 html 挂载到了一起。

当初通过 kubectl apply 命令利用这个文件:

# 创立 Pod
$ kubectl apply -f emptydir-demo.yaml
pod/emptydir-nginx-pod created

# 进入 Pod 容器外部
$ kubectl exec -it pod/emptydir-nginx-pod -- sh
# 查看零碎时区
/ # curl 127.0.0.1
Sun Mar 13 08:40:01 UTC 2022
/ # curl 127.0.0.1
Sun Mar 13 08:40:04 UTC 2022

依据 nginx 容器外部 curl 127.0.0.1 命令输入后果能够发现,nginx 容器 /usr/share/nginx/html/indedx.html 文件内容即为 html-generator 容器 /usr/share/index.html 文件内容。

可能实现此成果的原理是,当咱们申明卷类型为 emptyDir: {} 后,K8s 会主动在主机目录上创立一个长期目录。而后将 html-generator 容器 /usr/share/ 目录和 nginx 容器 /usr/share/nginx/html/ 同时挂载到这个长期目录上。这样两个容器的目录就可能实现数据同步。

须要留神的是,容器解体并不会导致 Pod 被从节点上移除,因而容器解体期间 emptyDir 卷中的数据是平安的。另外,emptyDir.medium 除了能够设成 {},还能够设成 Memory 示意内存挂载。

HostPath

与 emptyDir 不同,hostPath 卷能将主机节点文件系统上的文件或目录挂载到指定的 Pod 中。并且当 Pod 删除时,与之绑定的 hostPath 并不会随之删除。新创建的 Pod 挂载到上一个 Pod 应用过的 hostPath 时,原 hostPath 中的内容依然存在。但这仅限于新的 Pod 和曾经删除的 Pod 被调度到同一节点上,所以严格来讲 hostPath 依然属于长期存储。

hostPath 卷的典型利用是将主机节点上的时区通过卷挂载的形式注入到容器外部,进而保障启动的容器和主机节点工夫同步。

应用示例

创立 hostpath-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "hostpath-volume-pod"
  namespace: default
  labels:
    app: "hostpath-volume-pod"
spec:
  containers:
    - name: hostpath-volume-container
      image: "alpine:latest"
      command: ["sleep", "3600"]
      volumeMounts:
        - name: localtime
          mountPath: /etc/localtime
  volumes:
    - name: localtime
      hostPath:
        path: /usr/share/zoneinfo/Asia/Shanghai

要实现工夫同步,只须要将主机目录 /usr/share/zoneinfo/Asia/Shanghai 通过卷挂载的形式挂载到容器外部的 /etc/localtime 目录即可。

能够应用 kubectl apply 命令利用这个文件,而后进入 Pod 容器外部应用 date 命令查看容器以后工夫。

# 创立 Pod
$ kubectl apply -f hostpath-demo.yaml
pod/hostpath-volume-pod created
# 进入 Pod 容器外部
$ kubectl exec -it hostpath-volume-pod -- sh
# 执行 date 命令输入以后工夫
/ # date
Sun Mar 13 17:00:22 CST 2022  # 上海时区 

看到输入后果为 Sun Mar 13 17:00:22 CST 2022,其中 CST 代表了上海时区,也就是主机节点的时区。如果不通过卷挂载的形式将主机时区挂载到容器外部,则容器默认时区为 UTC 时区。

小结

长期卷内容介绍了 K8s 的长期存储计划以及利用,其中 emptyDir 适用范围较少,能够当作长期缓存或者耗时工作检查点等。

须要留神的是,绝大多数 Pod 应该疏忽主机节点,不应该拜访节点上的文件系统。只管有时候 DaemonSet 可能须要拜访主机节点的文件系统,而且 hostPath 能够用来同步主机节点时区到容器,但其余状况下应用较少,特地 hostPath 的最佳实际就是尽量不应用 hostPath。

长久卷

长期卷的生命周期与 Pod 雷同,当 Pod 被删除时,K8s 会主动删除 Pod 挂载的长期卷。而当 Pod 中的利用须要将数据保留到磁盘,且即便 Pod 被调度到其余节点数据也应该存在时,咱们就须要一个真正的长久化存储了。

K8s 反对的长久卷类型十分多,以下是 v1.24 版本反对的卷类型的一部分:

  • awsElasticBlockStore – AWS 弹性块存储(EBS)
  • azureDisk – Azure Disk
  • azureFile – Azure File
  • cephfs – CephFS volume
  • csi – 容器存储接口 (CSI)
  • fc – Fibre Channel (FC) 存储
  • gcePersistentDisk – GCE 长久化盘
  • glusterfs – Glusterfs 卷
  • iscsi – iSCSI (SCSI over IP) 存储
  • local – 节点上挂载的本地存储设备
  • nfs – 网络文件系统 (NFS) 存储
  • portworxVolume – Portworx 卷
  • rbd – Rados 块设施 (RBD) 卷
  • vsphereVolume – vSphere VMDK 卷

看到这么多长久卷类型不用恐慌,因为 K8s 为了让开发者不用关怀这背地的长久化存储类型,所以对长久卷有一套独有的思维,即开发者无论应用哪种长久卷,其用法都是统一的。

K8s 长久卷设计架构如下:

Node1 和 Node2 别离代表两个工作节点,当咱们在工作节点创立 Pod 时,能够通过 spec.containers.volumeMounts 来指定容器挂载目录,通过 spec.volumes 来指定挂载卷。之前咱们用挂载卷挂载了配置信息和长期卷,而挂载长久卷也能够采纳同样的形式。每个 volumes 则指向的是下方存储集群中不同的存储类型。

为了保障高可用,咱们通常会搭建一个存储集群。通常通过 Pod 来操作存储,因为 Pod 都会部署在 Node 中,所以存储集群最好跟 Node 集群搭建在同一内网,这样速度更快。而存储集群外部能够应用任何 K8s 反对的长久化存储,如上图的 NFS、CephFS、CephRBD。

应用 NFS

长久化挂载形式与长期卷大同小异,咱们同样应用一个 Nginx 服务来进行测试。这次咱们用 NFS 存储来演示 K8s 对长久卷的反对(NFS 测试环境搭建过程能够参考文章结尾的附录局部),创立 nfs-demo.yaml 内容如下:

apiVersion: v1
kind: Pod
metadata:
  name: "nfs-nginx-pod"
  namespace: default
  labels:
    app: "nfs-nginx-pod"
spec:
  containers:
    - name: nfs-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html-volume
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html-volume
      nfs:
        server: 192.168.99.101  # 指定 nfs server 地址
        path: /nfs/data/nginx  # 目录必须存在 

将容器 index.html 所在目录 /usr/share/nginx/html/ 挂载到 NFS 服务的 /nfs/data/nginx 目录下,在 spec.volumes 配置项中指定 NFS 服务。其中 server 指明了 NFS 服务器地址,path 指明了 NFS 服务器中挂载的门路,当然这个门路必须是曾经存在的门路。而后通过 kubectl apply 命令利用这个文件。

$ kubectl apply -f nfs-demo.yaml

接下来咱们查看这个 Pod 应用 NFS 存储的后果:

在 NFS 节点中咱们筹备一个 index.html 文件,其内容为 hello nfs。

应用 curl 命令间接拜访 Pod 的 IP 地址,即可返回 Nginx 服务的 index.html 内容,后果输入为 hello nfs,证实 NFS 长久卷挂载胜利。

登入 Pod 容器,通过 df -Th 命令查看容器目录挂载信息。能够发现,容器的 /usr/share/nginx/html/ 目录被挂载到 NFS 服务的 /nfs/data/nginx 目录。

当初,当咱们执行 kubectl delete -f nfs-demo.yaml 删除 Pod 后,NFS 服务器上数据盘中的数据仍然存在,这就是长久卷。

长久卷应用痛点

尽管通过应用长久卷,能够解决长期卷数据易失落的问题。但目前长久卷的应用形式还存在以下痛点:

  • Pod 开发人员可能对存储不够理解,却要对接多种存储
  • 平安问题,有些存储可能须要账号密码,这些信息不应该裸露给 Pod

因而为了解决这些有余,K8s 又针对长久化存储形象出了三种资源 PV、PVC、StorageClass。三种资源定义如下:

  • PV 形容的是长久化存储数据卷
  • PVC 形容的是 Pod 想要应用的长久化存储属性,既存储卷申明
  • StorageClass 作用是依据 PVC 的形容,申请创立对应的 PV

PV 和 PVC 的概念能够对应编程语言中的面向对象思维,PVC 是接口,PV 是具体实现。

有了这三种资源类型后,Pod 就能够通过动态供给和动静供给这两种形式来应用长久卷。

动态供给

动态供给不波及 StorageClass,只波及到 PVC 和 PV。其应用流程图如下:

应用动态供给时,Pod 不再间接绑定长久存储,而是会绑定到 PVC 上,而后再由 PVC 跟 PV 进行绑定。这样就实现了 Pod 中的容器能够应用由 PV 真正去申请的长久化存储。

应用示例

创立 pv-demo.yaml 内容如下:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-1g
  labels:
    type: nfs
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-storage
  nfs:
    server: 192.168.99.101
    path: /nfs/data/nginx1
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-100m
  labels:
    type: nfs
spec:
  capacity:
    storage: 100m
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-storage
  nfs:
    server: 192.168.99.101
    path: /nfs/data/nginx2
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-500m
  labels:
    app: pvc-500m
spec:
  storageClassName: nfs-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 500m
---
apiVersion: v1
kind: Pod
metadata:
  name: "pv-nginx-pod"
  namespace: default
  labels:
    app: "pv-nginx-pod"
spec:
  containers:
    - name: pv-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html
      persistentVolumeClaim:
        claimName: pvc-500m

其中 yaml 文件定义了如下内容:

  • 两个 PV:申请容量别离为 1Gi、100m,通过 spec.capacity.storage 指定,并且他们通过 spec.nfs 指定了 NFS 存储服务的地址和门路。
  • 一个 PVC:申请 500m 大小的存储。
  • 一个 Pod:spec.volumes 绑定名为 pvc-500m 的 PVC,而不是间接绑定 NFS 存储服务。

通过 kubectl apply 命令利用该文件:

$ kubectl apply -f pv-demo.yaml

以上实现创立,后果查看操作则如下:

首先通过 kubectl get pod 命令查看新创建的 Pod,并通过 curl 命令拜访 Pod 的 IP 地址,失去 hello nginx1 的响应后果。

而后通过 kubectl get pvc 查看创立的 PVC:

  • STATUS 字段:标识 PVC 曾经处于绑定(Bound)状态,也就是与 PV 进行了绑定。
  • CAPACITY 字段:标识 PVC 绑定到了 1Gi 的 PV 上,只管咱们申请的 PVC 大小是 500m,但因为咱们创立的两个 PV 大小别离是 1Gi 和 100m,K8s 会帮咱们抉择满足条件的最优解。因为没有刚好等于 500m 大小的 PV 存在,而 100m 又不满足,所以 PVC 会主动与 1Gi 大小的 PV 进行绑定。

通过 kubectl get pv 来查问创立的 PV 资源,能够发现 1Gi 大小的 PV STATUS 字段为 Bound 状态。CLAIM 的值,则标识的是与之绑定的 PVC 的名字。

当初咱们登录 NFS 服务器,确认 NFS 存储上不同长久卷(PV)挂载的目录下文件内容。

能够看到,/nfs/data/nginx1 目录下的 index.html 内容为 hello nginx1,即为下面通过 curl 命令拜访 Pod 服务的响应后果。

到此长久卷实现应用,咱们总结下整个长久卷应用流程。首先创立一个 Pod,Pod 的 spec.volumes 中绑定 PVC。这里的 PVC 只是一个存储申明,代表咱们的 Pod 须要什么样的长久化存储,它不须要表明 NFS 服务地址,也不须要明确要和哪个 PV 进行绑定,只是创立出这个 PVC 即可。接着咱们创立两个 PV,PV 也没有明确指出要与哪个 PVC 进行绑定,只须要指出它的大小和 NFS 存储服务地址即可。此时 K8s 会主动帮咱们进行 PVC 和 PV 的绑定,这样 Pod 就和 PV 产生了分割,也就能够拜访长久化存储了。

其余

仔细的你可能曾经发现,前文提到动态供给不波及 StorageClass,然而在定义 PVC 和 PV 的 yaml 文件时,还是都为其指定了 spec.storageClassName 值为 nfs-storage。因为这是一个便于管理的操作形式,只有具备雷同 StorageClass 的 PVC 和 PV 才能够进行绑定,这个字段标识了长久卷的拜访模式。在 K8s 长久化中反对四种拜访模式:

  • RWO – ReadWriteOnce —— 卷能够被一个节点以读写形式挂载
  • ROX – ReadOnlyMany —— 卷能够被多个节点以只读形式挂载
  • RWX – ReadWriteMany —— 卷能够被多个节点以读写形式挂载
  • RWOP – ReadWriteOncePod —— 卷能够被单个 Pod 以读写形式挂载(K8s 1.22 以上版本)

只有具备雷同读写模式的 PVC 和 PV 才能够进行绑定。

当初咱们来持续试验,通过命令 kubectl delete pod pv-nginx-pod 删除 Pod,再次查看 PVC 和 PV 状态。

从上图能够看到,Pod 删除后 PVC 和 PV 还在,这阐明 Pod 删除并不影响 PVC 的存在。而当 PVC 删除时 PV 是否删除,则能够通过设置回收策略来决定。PV 回收策略(pv.spec.persistentVolumeReclaimPolicy)有三种:

  • Retain —— 手动回收,也就是说删除 PVC 后,PV 仍然存在,须要管理员手动进行删除
  • Recycle —— 根本擦除 (相当于 rm -rf /*)(新版已废除不倡议应用,倡议应用动静供给)
  • Delete —— 删除 PV,即级联删除

当初通过命令 kubectl delete pvc pvc-500m 删除 PVC,查看 PV 状态。

能够看到 PV 仍然存在,其 STATUS 曾经变成 Released,此状态下的 PV 无奈再次绑定到 PVC,须要由管理员手动删除,这是由回收策略决定的。

留神:绑定了 Pod 的 PVC,如果 Pod 正在运行中,PVC 无奈删除。

动态供给的有余

咱们一起体验了动态供给的流程,尽管比间接在 Pod 中绑定 NFS 服务更加清晰,但动态供给仍然存在有余。

  • 首先会造成资源节约,如下面示例中,PVC 申请 500m,而没有刚好等于 500m 的 PV 存在,这 K8s 会将 1Gi 的 PV 与之绑定
  • 还有一个致命的问题,如果以后没有满足条件的 PV 存在,则这 PVC 始终无奈绑定到 PV 处于 Pending 状态,Pod 也将无奈启动,所以就须要管理员提前创立好大量 PV 来期待新创建的 PVC 与之绑定,或者管理员时刻监控是否有满足 PVC 的 PV 存在,如果不存在则马上进行创立,这显然是无奈承受的

动静供给

因为动态供给存在有余,K8s 推出一种更加不便的长久卷应用形式,即动静供给。动静供的应外围组件就是 StorageClass——存储类。StorageClass 次要作用有两个:

  • 一是资源分组,咱们下面应用动态供给时指定 StorageClass 的目前就是对资源进行分组,便于管理
  • 二是 StorageClass 可能帮咱们依据 PVC 申请的资源,主动创立出新的 PV,这个性能是 StorageClass 中 provisioner 存储插件帮咱们来做的。

其应用流程图如下:

相较于动态供给,动静供给在 PVC 和 PV 之间减少了存储类。这次 PV 并不需要提前创立好,只有咱们申请了 PVC 并且绑定了有 provisioner 性能的 StorageClass,StorageClass 会帮咱们主动创立 PV 并与 PVC 进行绑定。

咱们能够依据提供的长久化存储类型,别离创立对应的 StorageClass,比方:

  • nfs-storage
  • cephfs-storage
  • rbd-storage

也能够设置一个默认 StorageClass,通过在创立 StorageClass 资源时指定对应的 annotations 实现:

apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
...

当创立 PVC 时不指定 spec.storageClassName,这个 PVC 就会应用默认 StorageClass。

应用示例

依然应用 NFS 来作为长久化存储。

首先须要有一个可能反对主动创立 PV 的 provisioner,这能够在 GitHub 中找到一些开源的实现。示例应用 nfs-subdir-external-provisioner 这个存储插件,具体装置办法非常简单,只须要通过 kubectl apply 命令利用它提供的几个 yaml 文件即可。实现存储插件装置后,能够创立如下 StorageClass:

apiVersion: storage.K8s.io/v1
kind: StorageClass
metadata:
  name: nfs-storage
provisioner: K8s-sigs.io/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "true"

这个 StorageClass 指定了 provisioner 为咱们装置好的 K8s-sigs.io/nfs-subdir-external-provisioner。
Provisioner 实质上也是一个 Pod,能够通过 kubectl get pod 来查看。指定了 provisioner 的 StorageClass 就有了主动创立 PV 的能力,因为 Pod 可能主动创立 PV。

创立好 provisioner 和 StorageClass 就能够进行动静供给的试验了。首先创立 nfs-provisioner-demo.yaml 内容如下:

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
spec:
  storageClassName: nfs-storage
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Mi
---
apiVersion: v1
kind: Pod
metadata:
  name: "test-nginx-pod"
  namespace: default
  labels:
    app: "test-nginx-pod"
spec:
  containers:
    - name: test-nginx
      image: "nginx:latest"
      ports:
        - containerPort: 80
          name: http
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html/
  volumes:
    - name: html
      persistentVolumeClaim:
        claimName: test-claim

这里咱们只定义了一个 PVC 和一个 Pod,并没有定义 PV。其中 PVC 的 spec.storageClassName 指定为下面创立好的 StorageClass nfs-storage,而后只须要通过 kubectl apply 命令来创立出 PVC 和 Pod 即可:

$ kubectl apply -f nfs-provisioner-demo.yaml
persistentvolumeclaim/test-claim created
pod/test-nginx-pod created

当初查看 PV、PVC 和 Pod,能够看到 PV 曾经被主动创立进去了,并且它们之间实现了绑定关系。

而后登录 NFS 服务,给近程挂载的卷写入 hello nfs 数据。

在 K8s 侧,就能够应用 curl 命令验证挂载的正确性了。

此时如果你通过 kubectl delete -f nfs-provisioner-demo.yaml 删除 Pod 和 PVC,PV 也会跟着删除,因为 PV 的删除策略是 Delete。不过删除后 NFS 卷中的数据还在,只不过被归档成了以 archived 结尾的目录。这是 K8s-sigs.io/nfs-subdir-external-provisioner 这个存储插件所实现的性能,这就是存储插件的弱小。

实现全副操作后,咱们能够发现通过定义指定了 provisioner 的 StorageClass,不仅实现了 PV 的自动化创立,甚至实现了数据删除时主动归档的性能,这就是 K8s 动静供给存储设计的精妙。也能够说动静供给是长久化存储最佳实际。

附录:NFS 试验环境搭建

NFS 全称 Network File System,是一种分布式存储,它可能通过局域网实现不同主机间目录共享。

以下为 NFS 的架构图:由一个 Server 节点和两个 Client 节点组成。

上面列出 NFS 在 Centos 零碎中的搭建过程。

Server 节点

# 装置 nfs 工具
yum install -y nfs-utils

# 创立 NFS 目录
mkdir -p /nfs/data/

# 创立 exports 文件,* 示意所有网络上的 IP 都能够拜访
echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports

# 启动 rpc 近程绑定性能、NFS 服务性能
systemctl enable rpcbind
systemctl enable nfs-server
systemctl start rpcbind
systemctl start nfs-server

# 重载使配置失效
exportfs -r
# 查看配置是否失效
exportfs
# 输入后果如下所示
# /nfs/data      

Client 节点

# 敞开防火墙
systemctl stop firewalld
systemctl disable firewalld

# 装置 nfs 工具
yum install -y nfs-utils

# 挂载 nfs 服务器上的共享目录到本机门路 /root/nfsmount
mkdir /root/nfsmount
mount -t nfs 192.168.99.101:/nfs/data /root/nfsmount
正文完
 0