在 Docker 的设计中,容器内的文件是临时存放的,并且随着容器的删除,容器内部的数据也会一同被清空。不过,我们可以通过在 docker run 启动容器时,使用 --volume/-v 参数来指定挂载卷,这样就能够将容器内部的路径挂载到主机,后续在容器内部存放数据时会就被同步到被挂载的主机路径中。这样做可以保证保证即便容器被删除,保存到主机路径中的数据也仍然存在。
与 Docker 通过挂载卷的方式就可以解决持久化存储问题不同,K8s 存储要面临的问题要复杂的多。因为 K8s 通常会在多个主机部署节点,如果 K8s 编排的 Docker 容器崩溃,K8s 可能会在其他节点上重新拉起容器,这就导致原来节点主机上挂载的容器目录无法使用。
当然也是有办法解决 K8s 容器存储的诸多限制,比如可以对存储资源做一层抽象,通常大家将这层抽象称为卷(Volume)。
K8s 支持的卷基本上可以分为三类:配置信息、临时存储、持久存储。
配置信息
ConfigMap Secret DownwardAPI
ConfigMap
通过命令行创建 通过 yaml 文件创建
通过命令行创建
$ kubectl create configmap c1 --from-literal=foo=bar --from-literal=bar=bar.txt
baz
$ kubectl describe configmap c1 Name: c1 Namespace: default Labels: <none> Annotations: <none> Data ==== bar: ---- baz foo: ---- bar Events: <none>
通过 yaml 文件创建
kind: ConfigMap apiVersion: v1 metadata: name: c2 namespace: default data: foo: bar bar: baz
$ 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 以文件形式挂载到容器。
通过环境变量方式引用
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)"]
# 创建 Pod $ kubectl apply -f use-configmap-env-demo.yaml # 通过查看 Pod 日志来观察容器内部引用 Configmap 结果 $ kubectl logs use-configmap-env bar baz
通过卷挂载方式引用
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
# 创建 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
Secret
通过命令行创建 通过 yaml 文件创建
通过命令行创建
# generic 参数对应 Opaque 类型,既用户定义的任意数据 $ kubectl create secret generic s1 --from-file=foo.txt
foo=bar bar=baz
$ kubectl describe secret s1 Name: s1 Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== foo.txt: 16 bytes
通过 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"
使用示例
同 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
DownwardAPI
使用示例
创建 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 不需要预先定义,可以直接使用。
小结
临时卷
EmptyDir HostPath
EmptyDir
使用示例
创建 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: {}
现在通过 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
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
可以使用 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 # 上海时区
小结
持久卷
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 卷
使用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 开发人员可能对存储不够了解,却要对接多种存储 安全问题,有些存储可能需要账号密码,这些信息不应该暴露给 Pod
PV 描述的是持久化存储数据卷 PVC 描述的是 Pod 想要使用的持久化存储属性,既存储卷申明 StorageClass 作用是根据 PVC 的描述,申请创建对应的 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
两个 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
STATUS 字段:标识 PVC 已经处于绑定(Bound)状态,也就是与 PV 进行了绑定。 CAPACITY 字段:标识 PVC 绑定到了 1Gi 的 PV 上,尽管我们申请的 PVC 大小是 500m ,但由于我们创建的两个 PV 大小分别是 1Gi 和 100m ,K8s 会帮我们选择满足条件的最优解。因为没有刚好等于 500m 大小的 PV 存在,而 100m 又不满足,所以 PVC 会自动与 1Gi 大小的 PV 进行绑定。
其他
RWO - ReadWriteOnce —— 卷可以被一个节点以读写方式挂载 ROX - ReadOnlyMany —— 卷可以被多个节点以只读方式挂载 RWX - ReadWriteMany —— 卷可以被多个节点以读写方式挂载 RWOP - ReadWriteOncePod —— 卷可以被单个 Pod 以读写方式挂载( K8s 1.22 以上版本)
Retain —— 手动回收,也就是说删除 PVC 后,PV 依然存在,需要管理员手动进行删除 Recycle —— 基本擦除 (相当于 rm -rf /*)(新版已废弃不建议使用,建议使用动态供应) Delete —— 删除 PV,即级联删除
静态供应的不足
我们一起体验了静态供应的流程,虽然比直接在 Pod 中绑定 NFS 服务更加清晰,但静态供应依然存在不足。
首先会造成资源浪费,如上面示例中,PVC 申请 500m,而没有刚好等于 500m 的 PV 存在,这 K8s 会将 1Gi 的 PV 与之绑定 还有一个致命的问题,如果当前没有满足条件的 PV 存在,则这 PVC 一直无法绑定到 PV 处于 Pending 状态,Pod 也将无法启动,所以就需要管理员提前创建好大量 PV 来等待新创建的 PVC 与之绑定,或者管理员时刻监控是否有满足 PVC 的 PV 存在,如果不存在则马上进行创建,这显然是无法接受的
动态供应
一是资源分组,我们上面使用静态供应时指定 StorageClass 的目前就是对资源进行分组,便于管理 二是 StorageClass 能够帮我们根据 PVC 请求的资源,自动创建出新的 PV,这个功能是 StorageClass 中 provisioner 存储插件帮我们来做的。
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" ...
使用示例
首先需要有一个能够支持自动创建 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"
创建好 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
附录:NFS 实验环境搭建
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