卷 Volume
官网: https://kubernetes.io/zh-cn/docs/concepts/storage/volumes/
Container 中的文件在磁盘上是临时存放的,这给 Container 中运行的较重要的应用程序带来一些问题。问题之一是当容器崩溃时文件丢失。kubelet 会重新启动容器,但容器会以干净的状态重启。第二个问题会在同一 Pod
中运行多个容器并共享文件时出现。Kubernetes Volume 这一抽象概念能够解决这两个问题。
卷的类型
Kubernetes 支持很多类型的卷。**Pod 可以同时使用任意数目的卷类型。**临时卷类型的生命周期与 Pod 相同,但持久卷可以比 Pod 的存活期长。当 Pod 不再存在时,Kubernetes 也会销毁临时卷;不过 Kubernetes 不会销毁持久卷。对于给定 Pod 中任何类型的卷,在容器重启期间数据都不会丢失。
卷的核心是一个目录,其中可能存有数据,Pod 中的容器可以访问该目录中的数据。所采用的不同卷的类型将决定该目录如何形成的、使用何种介质保存数据以及目录中存放的内容。常用的卷类型有 ConfigMap、EmptyDir、Local、NFS、Secret 等。
ConfigMap
:可以将配置文件以键值对的形式保存到 ConfigMap 中,并且可以在 Pod 中以文件或环境变量的形式使用。ConfigMap 可以用来存储不敏感的配置信息,如应用程序的配置文件。EmptyDir
:是一个空目录,可以在 Pod 中用来存储临时数据,当 Pod 被删除时,该目录也会被删除。Local
:将本地文件系统的目录或文件映射到 Pod 中的一个 Volume 中,可以用来在 Pod 中共享文件或数据,例如:hostPath。NFS
:将网络上的一个或多个 NFS 共享目录挂载到 Pod 中的 Volume 中,可以用来在多个 Pod 之间共享数据。Secret
:将敏感信息以密文的形式保存到 Secret 中,并且可以在 Pod 中以文件或环境变量的形式使用。Secret 可以用来存储敏感信息,如用户名密码、证书等。
使用方式
使用卷时, 在 .spec.volumes
字段中设置为 Pod 提供的卷,并在 .spec.containers[*].volumeMounts
字段中声明卷在容器中的挂载位置。容器中的进程看到的文件系统视图是由它们的容器镜像的初始内容以及挂载在容器中的卷(如果定义了的话)所组成的。其中根文件系统同容器镜像的内容相吻合。任何在该文件系统下的写入操作,如果被允许的话,都会影响接下来容器中进程访问文件系统时所看到的内容。
EmptyDir
apiVersion: v1 kind: Pod metadata: name: emptydir-example spec: containers: - name: writer image: busybox command: ["/bin/sh", "-c", "echo 'Hello World!' > /data/hello.txt ; sleep 3600"] volumeMounts: - name: shared-data mountPath: /data - name: reader image: busybox command: ["/bin/sh", "-c", "cat /data/hello.txt ; sleep 3600"] volumeMounts: - name: shared-data mountPath: /data volumes: - name: shared-data emptyDir: {}
总结:emptyDir 是 Host 上创建的临时目录,其优点是能够方便地为 Pod 中的容器提供共享存储,不需要额外的配置。它不具备持久性,如果Pod 不存在了,emptyDir 也就没有了。根据这个特性,emptyDir 特别适合 Pod 中的容器需要临时共享存储空间的场景,比如前面的生产者消费者用例。
HostPath
apiVersion: v1 kind: Pod metadata: name: busybox-hostpath spec: containers: - name: busybox image: busybox command: ["/bin/sh", "-c", "echo 'hello' > /data/data.txt && sleep 3600"] volumeMounts: - name: data mountPath: /data volumes: - name: data hostPath: path: /data/hostpath
总结:如果 Pod 被销毀了,hostPath 对应的目录还是会被保留,从这一点来看,hostPath 的持久性比 emptyDir 强。不过一旦Host 崩溃,hostPath 也就无法访问了。但是这种方式也带来另外一个问题增加了 Pod 与节点的耦合。
NFS
Network FileSystem,网络文件存储系统。
apiVersion: v1 kind: Pod metadata: name: nfs-test spec: containers: - name: busybox image: busybox command: [ "/bin/sh", "-c", "while true; do sleep 3600; done" ] volumeMounts: - name: nfs-volume mountPath: /mnt/nfs volumes: - name: nfs-volume nfs: server: <NFS_SERVER_IP> path: /path/to/nfs/share
总结:相对于 emptyDir 和 hostPath,这种 Volume 类型的最大特点就是不依赖 Kubernetes,Volume 的底层基础设施由独立的存储系统管理,与 Kubernetes 集群是分离的。数据被持久化后,即使整个 Kubernetes 崩溃也不会受损。当然,运维这样的存储系统通常不是一项简单的工作,特别是对可靠性、可用性和扩展性有较高要求的时候。
PV & PVC
Volume 提供了非常好的数据持久化方案,不过在可管理性上还有不足。前面 NFS 例子来说,要使用 Volume,Pod 必须事先知道以下信息:
- 当前的 Volume 类型并明确 Volume 已经创建好。
- 必须知道 Volume 的具体地址信息。
但是 Pod 通常是由应用的开发人员维护,而 Volume 则通常是由存储系统的管理员维护。开发人员要获得上面的信息,要么询问管理员,要么自己就是管理员。这样就带来一个管理上的问题:应用开发人员和系统管理员的职责耦合在一起了。如果系统规模较小或者对于开发环境,这样的情况还可以接受,当集群规模变大,特别是对于生产环境,考虑到效率和安全性,这就成了必须要解决的问题。
Kubernetes 给出的解决方案是 Persistent Volume 和 Persistent Volume Claim
。
PersistentVolume(PV)是外部存储系统中的一块存储空间,由管理员创建和维护。与 Volume 一样,PV 具有持久性,生命周期独立于 Pod。
PersistentVolumeClaim(PVC)是对 PV 的申请(Claim)。PVC 通常由普通用户创建和维护。需要为 Pod 分配存储资源时,用户可以创建一个 PVC,指明存储资源的容量大小和访问模式(比如只读)等信息,Kubernetes 会查找并提供满足条件的 PV。有了 PersistentVolumeClaim,用户只需要告诉 Kubernetes 需要什么样的存储资源,而不必关心真正的空间从哪里分配、如何访问等底层细节信息。这些 Storage Provider 的底层信息交给管理员来处理,只有管理员才应该关心创建 PersistentVolume 的细节信息。
静态供给
事先创建好 PV 称为静态供给
创建 PV
apiVersion: v1 kind: PersistentVolume metadata: name: nfs-pv spec: capacity: storage: 1Gi # 指定容量大小 accessModes: # 访问模式 - ReadWriteMany persistentVolumeReclaimPolicy: Retain storageClassName: nfs nfs: path: /{nfs-server目录名称} server: {nfs-server IP 地址}
**accessModes:
**支持的访问模式有 3 种:
- ReadWriteOnce 表示 PV 能以 readwrite 模式挂载到单个节点,这个 PV 只能被某个节点以读写方式挂载,意味着这个 PV 只能被一个 Pod 挂载到某个节点上,并且这个 Pod 可以对这个 PV 进行读写操作。如果尝试在其他节点上挂载这个 PV,就会失败。
- ReadOnlyMany 表示 PV 能以 read-only 模式挂载到多个节点,这个 PV 能被多个节点以只读方式挂载,意味着这个 PV 可以被多个 Pod 挂载到多个节点上。
- ReadWriteMany 表示 PV 能以 read-write 模式挂载到多个节点。这个 PV 能被多个节点以读写方式挂载,意味着这个 PV 可以被多个 Pod 挂载到多个节点上。
**persistentVolumeReclaimPolicy
:**指定当 PV 的回收策略支持的策略有 3 种:
- Retain:在 PVC 被删除后,保留 PV 和其数据,手动清理 PV 中的数据。
- Delete:在 PVC 被删除后,自动删除 PV 和其数据。
- Recycle:在 PVC 被删除后,通过删除 PV 中的数据来准备 PV 以供重新使用。
值得注意的是,persistentVolumeReclaimPolicy 只适用于一些类型的 PV,如 NFS、HostPath、iSCSI 等。对于一些云平台提供的存储,如 AWS EBS 和 Azure Disk,由于底层提供商会自动处理 PV 的回收问题,因此该属性不适用。
**storageClassName
:**指定 PV 的 class 为 nfs。相当于为 PV 设置了一个分类,PVC 可以指定 class 申请相应 class 的 PV。
创建 PVC
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: nfs-pvc spec: accessModes: - ReadWriteMany resources: requests: storage: 1Gi storageClassName: nfs-pv # 通过名字进行选择 #selector: 通过标签形式 # matchLabels: # pv-name: nfs-pv
使用 PVC
apiVersion: v1 kind: Pod metadata: name: busybox-nfs spec: containers: - name: busybox image: busybox command: ["/bin/sh"] args: ["-c", "while true; do echo 'Hello NFS!' >> /data/index.html; sleep 1; done"] volumeMounts: - name: nfs-volume mountPath: /data volumes: - name: nfs-volume persistentVolumeClaim: claimName: nfs-pvc
动态供给
在前面的例子中,我们提前创建了 PV,然后通过 PVC 申请 PV 并在 Pod 中使用,这种方式叫作静态供给(Static Provision)与之对应的是动态供给(Dynamical Provision),即如果没有满足 PVC 条件的 PV,会动态创建 PV。相比静态供给,动态供给有明显的优势:不需要提前创建 PV,减少了管理员的工作量,效率高。
官网:https://kubernetes.io/zh-cn/docs/concepts/storage/storage-classes/
动态供给是通过 StorageClass 实现的,StorageClass 定义了如何创建 PV,但需要注意的是每个 StorageClass 都有一个制备器(Provisioner),用来决定使用哪个卷插件制备 PV。该字段必须指定才能实现动态创建,下面我们以 NFS 为例:
定义 NFS Provider
apiVersion: apps/v1 kind: Deployment metadata: name: nfs-client-provisioner labels: app: nfs-client-provisioner namespace: kube-system spec: replicas: 1 strategy: type: Recreate selector: matchLabels: app: nfs-client-provisioner template: metadata: labels: app: nfs-client-provisioner spec: serviceAccountName: nfs-client-provisioner containers: - name: nfs-client-provisioner image: chronolaw/nfs-subdir-external-provisioner:v4.0.2 volumeMounts: - name: nfs-client-root mountPath: /persistentvolumes env: - name: PROVISIONER_NAME value: k8s-sigs.io/nfs-subdir-external-provisioner - name: NFS_SERVER value: 10.15.0.25 # NFS 服务 IP - name: NFS_PATH value: /root/nfs/data volumes: - name: nfs-client-root nfs: server: 10.15.0.25 # NFS 服务 IP path: /root/nfs/data
定义 RBAC(配置权限)
apiVersion: v1 kind: ServiceAccount metadata: name: nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: kube-system --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: name: nfs-client-provisioner-runner rules: - apiGroups: [""] resources: ["nodes"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["persistentvolumes"] verbs: ["get", "list", "watch", "create", "delete"] - apiGroups: [""] resources: ["persistentvolumeclaims"] verbs: ["get", "list", "watch", "update"] - apiGroups: ["storage.k8s.io"] resources: ["storageclasses"] verbs: ["get", "list", "watch"] - apiGroups: [""] resources: ["events"] verbs: ["create", "update", "patch"] --- kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: run-nfs-client-provisioner subjects: - kind: ServiceAccount name: nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: kube-system roleRef: kind: ClusterRole name: nfs-client-provisioner-runner apiGroup: rbac.authorization.k8s.io --- kind: Role apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: kube-system rules: - apiGroups: [""] resources: ["endpoints"] verbs: ["get", "list", "watch", "create", "update", "patch"] --- kind: RoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: name: leader-locking-nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: kube-system subjects: - kind: ServiceAccount name: nfs-client-provisioner # replace with namespace where provisioner is deployed namespace: kube-system roleRef: kind: Role name: leader-locking-nfs-client-provisioner apiGroup: rbac.authorization.k8s.io
定义 StorageClass
apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: mysql-nfs-sc provisioner: k8s-sigs.io/nfs-subdir-external-provisioner parameters: onDelete: "remain"
使用 StorageClass 动态创建
apiVersion: apps/v1 kind: StatefulSet metadata: name: mysql labels: app: mysql spec: serviceName: mysql #headless 无头服务 保证网络标识符唯一 必须存在 replicas: 1 template: metadata: name: mysql labels: app: mysql spec: containers: - name: mysql image: mysql/mysql-server:8.0 imagePullPolicy: IfNotPresent env: - name: MYSQL_ROOT_PASSWORD value: root volumeMounts: - mountPath: /var/lib/mysql #自己容器写入数据目录 name: data #保存到指定一个变量中 变量名字就是 data ports: - containerPort: 3306 restartPolicy: Always volumeClaimTemplates: #声明动态创建数据卷模板 - metadata: name: data # 数据卷变量名称 spec: accessModes: # 访问模式 - ReadWriteMany storageClassName: mysql-nfs-sc # 使用哪个 storage class 模板存储数据 resources: requests: storage: 2G selector: matchLabels: app: mysql