一文读懂 Kubernetes 存储设计

在 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 c1Name: c1Namespace: defaultLabels: <none>Annotations: <none>Data====bar:----bazfoo:----barEvents: <none>
通过 yaml 文件创建
kind: ConfigMapapiVersion: v1metadata:name: c2namespace: defaultdata:foo: barbar: baz
$ kubectl apply -f configmap-demo.yaml$ kubectl get configmap c2NAME DATA AGEc2 2 11s$ kubectl describe configmap c2Name: c2Namespace: defaultLabels: <none>Annotations: <none>Data====foo:----barbar:----bazEvents: <none>
使用示例
通过环境变量将 Configmap 注入到容器内部 通过卷挂载的方式直接将 Configmap 以文件形式挂载到容器。
通过环境变量方式引用
apiVersion: v1kind: Podmetadata:name: "use-configmap-env"namespace: defaultspec:containers:name: use-configmap-envimage: "alpine"# 一次引用单个值env:name: FOOvalueFrom:configMapKeyRef:name: c2key: foo# 一次引用所有值envFrom:prefix: CONFIG_ # 配置引用前缀configMapRef:name: c2command: ["echo", "$(FOO)", "$(CONFIG_bar)"]
# 创建 Pod$ kubectl apply -f use-configmap-env-demo.yaml# 通过查看 Pod 日志来观察容器内部引用 Configmap 结果$ kubectl logs use-configmap-envbar baz
通过卷挂载方式引用
apiVersion: v1kind: Podmetadata:name: "use-configmap-volume"namespace: defaultspec:containers:name: use-configmap-volumeimage: "alpine"command: ["sleep", "3600"]volumeMounts:name: configmap-volumemountPath: /usr/share/tmp # 容器挂载目录volumes:name: configmap-volumeconfigMap: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 # lsbar foo# 查看文件内容/usr/share/tmp # cat foobar/usr/share/tmp # cat barbaz
Secret
通过命令行创建 通过 yaml 文件创建
通过命令行创建
# generic 参数对应 Opaque 类型,既用户定义的任意数据$ kubectl create secret generic s1 --from-file=foo.txt
foo=barbar=baz
$ kubectl describe secret s1Name: s1Namespace: defaultLabels: <none>Annotations: <none>Type: OpaqueData====foo.txt: 16 bytes
通过 yaml 文件创建
apiVersion: v1kind: Secretmetadata:name: s2namespace: defaulttype: Opaque # 默认类型data:user: cm9vdAo=password: MTIzNDU2Cg==
$ kubectl apply -f secret-demo.yaml$ kubectl get secret s2NAME TYPE DATA AGEs2 Opaque 2 59s$ kubectl describe secret s2Name: s2Namespace: defaultLabels: <none>Annotations: <none>Type: OpaqueData====password: 7 bytesuser: 5 bytes
data:user: cm9vdAo=password: MTIzNDU2Cg==
data:stringData:user: rootpassword: "123456"
使用示例
apiVersion: v1kind: Podmetadata:name: "use-secret-volume-demo"namespace: defaultspec:containers:name: use-secret-volume-demoimage: "alpine"command: ["sleep", "3600"]volumeMounts:name: secret-volumemountPath: /usr/share/tmp # 容器挂载目录volumes:name: secret-volumesecret:secretName: s2
# 创建 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 # lspassword user# 查看文件内容/usr/share/tmp # cat password123456/usr/share/tmp # cat userroot
DownwardAPI
使用示例
apiVersion: v1kind: Podmetadata:name: downwardapi-volume-demolabels:app: downwardapi-volume-demoannotations:foo: barspec:containers:name: downwardapi-volume-demoimage: alpinecommand: ["sleep", "3600"]volumeMounts:name: podinfomountPath: /etc/podinfovolumes:name: podinfodownwardAPI:items:# 指定引用的 labelspath: "labels"fieldRef:fieldPath: metadata.labels# 指定引用的 annotationspath: "annotations"fieldRef:fieldPath: metadata.annotations
# 创建 Pod$ kubectl apply -f downwardapi-demo.yamlpod/downwardapi-volume-demo created# 进入 Pod 容器内部$ kubectl exec -it downwardapi-volume-demo -- sh# 进入容器挂载目录/ # cd /etc/podinfo/# 查看挂载目录下的文件/etc/podinfo # lsannotations labels# 查看文件内容/etc/podinfo # cat annotationsfoo="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 labelsapp="downwardapi-volume-demo"
小结
临时卷
EmptyDir HostPath
EmptyDir
使用示例
apiVersion: v1kind: Podmetadata:name: "emptydir-nginx-pod"namespace: defaultlabels:app: "emptydir-nginx-pod"spec:containers:name: html-generatorimage: "alpine:latest"command: ["sh", "-c"]args:while true; dodate > /usr/share/index.html;sleep 1;donevolumeMounts:name: htmlmountPath: /usr/sharename: nginximage: "nginx:latest"ports:containerPort: 80name: httpvolumeMounts:name: html# nginx 容器 index.html 文件所在目录mountPath: /usr/share/nginx/htmlreadOnly: truevolumes:name: htmlemptyDir: {}
# 创建 Pod$ kubectl apply -f emptydir-demo.yamlpod/emptydir-nginx-pod created# 进入 Pod 容器内部$ kubectl exec -it pod/emptydir-nginx-pod -- sh# 查看系统时区/ # curl 127.0.0.1Sun Mar 13 08:40:01 UTC 2022/ # curl 127.0.0.1Sun Mar 13 08:40:04 UTC 2022
HostPath
使用示例
apiVersion: v1kind: Podmetadata:name: "hostpath-volume-pod"namespace: defaultlabels:app: "hostpath-volume-pod"spec:containers:name: hostpath-volume-containerimage: "alpine:latest"command: ["sleep", "3600"]volumeMounts:name: localtimemountPath: /etc/localtimevolumes:name: localtimehostPath:path: /usr/share/zoneinfo/Asia/Shanghai
# 创建 Pod$ kubectl apply -f hostpath-demo.yamlpod/hostpath-volume-pod created# 进入 Pod 容器内部$ kubectl exec -it hostpath-volume-pod -- sh# 执行 date 命令输出当前时间/ # dateSun 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
apiVersion: v1kind: Podmetadata:name: "nfs-nginx-pod"namespace: defaultlabels:app: "nfs-nginx-pod"spec:containers:name: nfs-nginximage: "nginx:latest"ports:containerPort: 80name: httpvolumeMounts:name: html-volumemountPath: /usr/share/nginx/html/volumes:name: html-volumenfs:server: 192.168.99.101 # 指定 nfs server 地址path: /nfs/data/nginx # 目录必须存在
kubectl apply -f nfs-demo.yaml



持久卷使用痛点
Pod 开发人员可能对存储不够了解,却要对接多种存储 安全问题,有些存储可能需要账号密码,这些信息不应该暴露给 Pod
PV 描述的是持久化存储数据卷 PVC 描述的是 Pod 想要使用的持久化存储属性,既存储卷申明 StorageClass 作用是根据 PVC 的描述,申请创建对应的 PV
静态供应

使用示例
apiVersion: v1kind: PersistentVolumemetadata:name: nfs-pv-1glabels:type: nfsspec:capacity:storage: 1GiaccessModes:ReadWriteOncestorageClassName: nfs-storagenfs:server: 192.168.99.101path: /nfs/data/nginx1---apiVersion: v1kind: PersistentVolumemetadata:name: nfs-pv-100mlabels:type: nfsspec:capacity:storage: 100maccessModes:ReadWriteOncestorageClassName: nfs-storagenfs:server: 192.168.99.101path: /nfs/data/nginx2---apiVersion: v1kind: PersistentVolumeClaimmetadata:name: pvc-500mlabels:app: pvc-500mspec:storageClassName: nfs-storageaccessModes:ReadWriteOnceresources:requests:storage: 500m---apiVersion: v1kind: Podmetadata:name: "pv-nginx-pod"namespace: defaultlabels:app: "pv-nginx-pod"spec:containers:name: pv-nginximage: "nginx:latest"ports:containerPort: 80name: httpvolumeMounts:name: htmlmountPath: /usr/share/nginx/html/volumes:name: htmlpersistentVolumeClaim:claimName: pvc-500m
两个 PV:申请容量分别为 1Gi 、100m ,通过 spec.capacity.storage 指定,并且他们通过 spec.nfs 指定了 NFS 存储服务的地址和路径。 一个 PVC :申请 500m 大小的存储。 一个 Pod:spec.volumes 绑定名为 pvc-500m 的 PVC,而不是直接绑定 NFS 存储服务。
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
apiVersion: storage.K8s.io/v1kind: StorageClassmetadata:annotations:storageclass.kubernetes.io/is-default-class: "true"...
使用示例
apiVersion: storage.K8s.io/v1kind: StorageClassmetadata:name: nfs-storageprovisioner: K8s-sigs.io/nfs-subdir-external-provisionerparameters:archiveOnDelete: "true"

kind: PersistentVolumeClaimapiVersion: v1metadata:name: test-claimspec:storageClassName: nfs-storageaccessModes:ReadWriteOnceresources:requests:storage: 1Mi---apiVersion: v1kind: Podmetadata:name: "test-nginx-pod"namespace: defaultlabels:app: "test-nginx-pod"spec:containers:name: test-nginximage: "nginx:latest"ports:containerPort: 80name: httpvolumeMounts:name: htmlmountPath: /usr/share/nginx/html/volumes:name: htmlpersistentVolumeClaim:claimName: test-claim
$ kubectl apply -f nfs-provisioner-demo.yamlpersistentvolumeclaim/test-claim createdpod/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 rpcbindsystemctl enable nfs-serversystemctl start rpcbindsystemctl start nfs-server# 重载使配置生效exportfs -r# 检查配置是否生效exportfs# 输出结果如下所示# /nfs/data *
Client 节点
# 关闭防火墙systemctl stop firewalldsystemctl disable firewalld# 安装 nfs 工具yum install -y nfs-utils# 挂载 nfs 服务器上的共享目录到本机路径 /root/nfsmountmkdir /root/nfsmountmount -t nfs 192.168.99.101:/nfs/data /root/nfsmount
快 来 找 又 小 拍

推 荐 阅 读 


设为星标

更新不错过




设为星标

更新不错过
[广告]赞助链接:
关注数据与安全,洞悉企业级服务市场:https://www.ijiandao.com/
让资讯触达的更精准有趣:https://www.0xu.cn/
关注KnowSafe微信公众号随时掌握互联网精彩
- VeOmni – 字节跳动开源的全模态PyTorch原生训练框架
- 宝塔面板SSL证书安装指南
- 微软在Microsoft Edge中添加视频录制功能 暂不支持音频
- 微信推出这个送礼功能 到底好不好用
- 智能搜索、精准定位,微软全新升级 OneDrive 文件搜索体验
- 华为云发布代码检查服务;微软向其美国雇员提供“无限制”休假时间;付费版 ChatGPT|极客头条
- 你身上有哪些隐藏标签?
- 中国移动携手高通实现业内首次基于5G切片的端边协同分离渲染无界XR技术演示
- 诸子云沙龙系列活动 | 2021.9.25北京.综合场
- 新生黑客组织整合三大勒索软件,声明不会攻击特定行业
- Spring Boot 高效入门实战
- 报名倒计时!2020京麒大会 物联网安全攻防实战训练营



