Kubernetes 的卷是 pod 的 一 个组成部分,并和 pod 共享相同的生命周期。

一、通过卷在容器之间共享数据

1.1. 使用 emptyDir 卷

创建一个 pod 中有两个共用同一个卷的容器:

# fortune-pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: fortune
spec:
  containers:
  - image: luksa/fortune
    name: html-generator
    volumeMounts: # 名为 html 的卷挂载在容器的 /var/htdocs 中
    - name: html
      mountPath: /var/htdocs
  - image: nginx:alpine
    name: web-server
    volumeMounts: # 名为 html 的卷挂载在容器的 /usr/share/nginx/html 中,并且只读
    - name: html
      mountPath: /usr/share/nginx/html # Nginx 服务的默认服务文件目录
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html # 一个名为 html 的单独 emptyDir 卷,挂载在上面的两个容器中
    emptyDir: {}

luksa/fortune 每隔 10 秒钟随机写入一段文字到 /var/htdocs/index.html 中;nginx:alpine 是一个 Nginx 镜像。

kubectl create -f fortune-pod.yaml

使用端口转发查看 pod 状态:

kubectl port-forward fortune 8080:80

作为卷来使用的 emptyDir 是在承载 pod 的工作节点的实际磁盘上创建的,因此其性能取决于节点的磁盘类型。我们可以通知 Kubernetes 在 tmfs 文件系统(存在内存而非硬盘)上创建 emptyDir:

volumes:
  - name: html
    emptyDir:
      medium: Memory

1.2. 使用 Git 仓库作为存储卷

gitRepo 卷本质上也是 emptyDir 卷,它通过克隆 Git 仓库并在 pod 启动时(但在创建容器之前)检出特定版本来填充数据。

apiVersion: v1
kind: Pod
metadata:
  name: gitrepo-volume-pod
spec:
  containers:
  - image: nginx:alpine
    name: web-server
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP
  volumes:
  - name: html
    gitRepo:
      repository: https://github.com/luksa/kubia-website-example.git
      revision: master
      directory: . # 将 repo 克隆到卷的根目录,如果不设置将会被克隆到 kubia-website-example 目录

二、访问工作节点文件系统上的文件

大多数 pod 应该忽略它们的主机节点,因此它们不应该访问节点文件系统上的任何文件。但是某些系统级别的 pod (切记,这些通常由 DaemonSet 管理)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes 通过 hostPath 卷实现了这一点。

hostPath 卷是我们介绍的第一种类型的持久性存储,因为gitRepo 和 emptyDir 卷的内容都会在 pod 被删除时被删除,而 hostPath 卷的内容则不会被删除。如果你正在考虑使用 hostPath 卷作为存储数据库数据的目录,请重新考虑。因为卷的内容存储在特定节点的文件系统中,所以当数据库 pod 被重新安排在另一个节点时,会找不到数据。这解释了为什么对常规 pod 使用 hostPath 卷不是一个好主意,因为这会使 pod 对预定规划的节点很敏感。

三、使用持久化存储

当运行在一个 pod 中的应用程序需要将数据保存到磁盘上,并且即使该 pod 重新调度到另一个节点时也要求具有相同的数据可用。这就不能使用到目前为止我们提到的任何卷类型,由于这些数据需要可以从任何集群节点访问,因此必须将其存储在某种类型的网络存储 (NAS) 中。

如果使用 Google Kubernetes Engine 可以使用 GCE (Google Compute Engine) 卷;如果使用 AWS EC2 可以使用 awsElasticBlockStore 卷;如果使用 Azure 可以使用 azureFile 或者 azureDisk 卷。

四、从底层存储技术解耦 pod

到目前为止,我们探索过的所有待久卷类型都要求 pod 的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持 NFS 协议的卷,开发人员必须知道 NFS 节点所在的实际服务器。这违背了 Kubernetes 的基本理念,这个理念旨在向应用程序及其开发人员隐藏真实的基础设施,使他们不必担心基础设施的具体状
态,并使应用程序可在大量云服务商和数据企业之间进行功能迁移。

4.1. 持久卷、持久卷声明

在 Kubernetes 集群中为了使应用能够正常请求存储资源,同时避免处理基础设施细节,引入了两个新的资源, 分别是持久卷(PersistentVolume 简称 PV)和持久卷声明(PersistentVolumeClaim 简称 PVC),这名字可能有点误导,因为正如在前面几节中看到的,甚至常规的 Kubernetes 卷也可以用来存储持久性数据。

当集群用户需要在其 pod 中使用持久化存储时,他们首先创建持久卷声明清单,指定所需要的最低容量要求和访问模式,然后用户将持久卷声明清单提交给 Kubernetes API 服务器,Kubernetes 将找到可匹配的持久卷并将其绑定到持久卷声明。

4.1.1. 创建持久卷

本文使用的依然是 Minikube:

# mongodb-pv-hostpath.yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mongodb-pv
spec:
  capacity: 
    storage: 1Gi # 定义 PersistentVolume de 大小
  accessModes: # 可以被单个客户端挂在为读写模式或者被多个客户端挂在为只读模式
    - ReadWriteOnce
    - ReadOnlyMany
  persistentVolumeReclaimPolicy: Retain # 当声明被释放后,PersistentVolume 将会被保留(不清理和删除)
  hostPath:
    path: /tmp/mongodb

kubectl create -f mongodb-pv-hostpath.yaml

查看:

kubectl get pv

注意:持久卷不属于任何命名空间,它跟节点一样是集群层面的资源。

4.1.2. 创建持久卷声明

假设现在需要部署一个需要持久化存储的 pod,将要用到之前创建的持久卷,但是不能直接在 pod 内使用,需要先声明一个。声明一个持久卷和创建一个 pod 是相对独立的过程,因为即使 pod 被重新调度(重新调度意味着先前的 pod 被删除并且创建了一个新的 pod),我们也希望通过相同的持久卷声明来确保可用。

# mongodb-pvc.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc # 声明的名称,稍后将声明当作 pod 的卷使用时需要用到
spec:
  resources:
    requests:
      storage: 1Gi # 申请 1 GiB 的存储空间
  accessModes:
  - ReadWriteOnce # 允许单个客户端访问(同时支持读取和写入操作)
  storageClassName: "" # 将空字符串指定为存储类名可确保 PVC 绑定到预先配置的 PV, 而不是动态配置新的 PV

kubectl create -f mongodb-pvc.yaml

查看:

kubectl get pvc

可以看到 PVC 已经与持久卷的 mongodb-pv 绑定。

访问模式:

  • RWO: ReadWriteOnce - 仅允许单个节点挂载读写
  • ROX: ReadOnlyMany - 允许多个节点挂载只读
  • RWX: ReadWriteMany - 允许多个节点挂载读写

访问模式涉及可以同时使用卷的工作节点的数量而非 pod 的数量。

持久卷是集群范围的,因此不能在特定的命名空间中创建,但是持久卷声明又只能在特定的命名空间创建,所以持久卷和持久卷声明只能被同一命名空间内的 pod 创建使用。

4.1.3. 在 pod 中使用持久卷声明

# mongodb-pod-pvc.yaml

apiVersion: v1
kind: Pod
metadata:
  name: mongodb 
spec:
  containers:
  - image: mongo
    name: mongodb
    volumeMounts:
    - name: mongodb-data
      mountPath: /data/db
    ports:
    - containerPort: 27017
      protocol: TCP
  volumes:
  - name: mongodb-data
    persistentVolumeClaim:
      claimName: mongodb-pvc # 在 pod 卷中通过名称引用持久卷声明

五、持久卷的动态卷配置

5.1. 通过 StorageClass 资源定义可用存储类型

# storageclass-fast-hostpath.yaml

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast
provisioner: k8s.io/minikube-hostpath # 用户配置持久卷的卷插件
parameters:
  type: pd-ssd

kubectl create -f storageclass-fast-hostpath.yaml

5.2. 请求持久卷声明中的存储类

创建一个请求特定存储类的 PVC 定义

# mongodb-pvc-dp.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc-fast 
spec:
  storageClassName: fast # 请求自定义存储类
  resources:
    requests:
      storage: 100Mi
  accessModes:
    - ReadWriteOnce

kubectl create -f mongodb-pvc-dp.yaml

查看:

kubectl get pvc mongodb-pvc-fast

VOLUME 列显示了与此声明绑定的持久卷,执行命令,可以看到自动创建了一个新的 PV:

kubectl get pv

5.3. 不指定存储类的动态配置

列出存储类:

kubectl get sc # sc 是 storageclass 的简写

查看默认存储类:

kubectl get sc standard -o yaml

创建一个没有指定存储类别的持久卷声明:

# mongodb-pvc-dp-nostorageclass.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mongodb-pvc2 
spec:
  resources:
    requests:
      storage: 100Mi
  accessModes:
    - ReadWriteOnce
kubectl create -f mongodb-pvc-dp-nostorageclass.yaml

查看:

kubectl get pvc mongodb-pvc2