1. 存储机制概述

容器内部存储的生命周期是短暂的,会随着容器环境的销毁而被销毁,具有不稳定性。同时,如果多个容器希望共享同一份存储资源,则仅仅依赖容器本身是很难实现的。因此 Kubernetes 通过将容器应用所需的存储资源抽象为 Volume(存储卷)来解决这些问题。

Volume 在 Kubernetes 中也是一种资源,Kubernetes 提供了多种类型的 Volume 供容器应用使用。Pod 通过挂载(Mount)的方式来使用一个或多个 Volume。

Volume 可以被分为两大类:

  • 临时卷:具有与Pod相同的生命周期。
  • 持久卷:通常比Pod的生命周期更长。在Pod被销毁时,持久卷通常不会被立刻删除,而是交给用户来处理其中的数据。

2. 临时卷详解

临时卷与 Pod 具有相同的生命周期,包括为 Pod 创建的临时卷(emptyDir、Generic Ephemeral、CSI Ephemeral),以及通过 Kubernetes 的资源对象 configMap、secret、Downward API、Service Account Token、Projected Volume 为 Pod 提供数据的临时卷等。

2.1. emptyDir

这种类型的 Volume 将在 Pod 被调度到 Node 时由 kubelet 进行创建,在初始状态下其目录是空的,所以被命名为“空目录”(Empty Directory)。它与 Pod 具有相同的生命周期,当 Pod 被销毁时,emptyDir 对应的目录也会被删除。同一个 Pod 中的多个容器都可以挂载这种类型的 Volume。

常见应用场景:

  • 基于磁盘进行合并排序操作时需要的暂存空间。
  • 长时间计算任务的中间检查点文件。
  • 为某个Web服务提供的临时网站内容文件。

在默认情况下,kubele 会在 Node 的 /var/lib/kubelet/pods 目录下为 Pod 创建 emptyDir目录,这个目录的存储介质可能是本地磁盘、SSD磁盘或者网络存储设备,具体取决于环境的配置。

另外,emptyDir 可以通过 medium 字段设置存储介质为“Memory”,表示使用基于内存的文件系统。在主机重启之后,tmpfs 中的内容就会被清空。此外,写入 tmpfs 中的数据将被计入容器的内存使用量,受到容器级别内存资源上限的限制。

示例:

...
spec:
  containers:
    ...
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: {}
YAML

设置 emptyDir 为内存示例:

...
spec:
  containers:
    ...
    volumeMounts:
    - mountPath: /cache
      name: cache-volume
  volumes:
  - name: cache-volume
    emptyDir: "Memory"
    
# 在开启 SizeMemoryBackedVolumes 后可以设置使用内存上限
  volumes:
  - name: cache-volume
    emptyDir: "Memory"
    sizeLimit: 500Mi
YAML

2.2. Generic Ephemeral

2.3. CSI Ephemeral

2.4. 其他类型临时卷

3. 持久卷详解

前面介绍了临时存储,临时存储数据不能持久化。因此,Kubernetes 引人了 Persistent Volume(简称PV,持久卷)和 Persistent Volume Claim(简称PVC,持久卷申请申明)两个资源对象来实现存储持久化管理。

3.1. PV 与 PVC

我们可以将 PV 看作可用的存储资源,PVC 则是对存储资源的需求。

PV 将存储定义为一种容器应用可以使用的资源。PV 由管理员创建和配置,它与存储资源提供者的具体实现直接相关。不同提供者提供的 PV 类型包括 NFS、iSCSI、RBD 或者由 GCE 或 AWS 公有云提供的共享存储等。

如果某个 Pod 想要使用 PV,则需要通过 PVC 来完成申请,随后由 Kubernetes 完成从PVC 到 PV 的自动绑定流程,PVC 可以申请存储空间的大小(Size)和访问模式(例如ReadWriteOnce、 ReadOnlyMany 或 ReadWriteMany)。

目前 PV 和 PVC 之间是一对一绑定的关系,也就是说一个 PV 只能被一个 PVC 绑定。

3.2. StorageClass

使用PVC申请的存储空间可能仍然不满足应用对存储设备的各种需求。例如:

  • 在大规模集群场景下,管理员手动管理 PV 非常不方便。
  • 不同存储设备IO性能不同,种类不同,需要进行归类,方便 pod 使用。

为了解决上面两个问题,Kubernetes 从 v1.4 版本开始引入了一个新的资源对象StorageClass,用于标记存储资源的特性和性能,根据 PVC 的需求动态供给合适的 PV 资源。通过 StorageClass 创建的 PV 又称之为动态 PV。

3.3. PV 详解

PV 没有名称空间限制。

yaml 文件示例:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-hostpath
  labels:
    type: local
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  hostPath:
    path: '/data/test1'
    type: DirectoryOrCreate
  storageClassName: manual   # 我们是静态pv,此处声明的sc,代表设定该pv归属的类为 manual,代表静态/手动
YAML

PV 资源对象需要设置的关键配置参数如下:

  • 存储容量(capacity):存储容量用于描述存储的容量,目前仅支持对存储空间的设置(storage=xx),未来可能加人对 IOPS、吞吐率等的设置。
  • 存储卷模式(volumeMode):其中可以设置的选项包括 Filesystem (文件系统,默认值)和 Block(块设备)。
  • 访问模式(accessModes):AccessModes 是用来对 PV 进行访问模式的设置,用于描述用户应用对存储资源的访问权限,包括下面几种方式(都是针对节点级别的):
    • ReadWriteOnce(RWO):读写权限,并且只能被单个单个节点上的一个或多个 pod 挂载使用。
    • ReadOnlyMany(ROX):只读权限,可以同时在多个节点上挂载并被不同的Pod。
    • ReadWriteMany(RWX):读写权限,那么可以同时在多个节点上挂载并被不同的Pod使用。
    • ReadWriteOncePod(RWOP):可以被单个 Pod 以读写方式挂载的模式,该特性在 Kubernetes v1.22 版本中被引人。仅支持 CSI 存储卷,用于在集群中只有一个 Pod 时以读写方式使用这种模式的 PVC。
  • 存储类别(Class):PV 可以设定其存储的类别,通过 storageClassName 参数指定一个 StorageClass 资源对象的名称。具有特定类别的 PV 只能与请求了该类别的 PVC 绑定。未设定类别的 PV 则只能与不请求任何类别的 PVC 绑定。
  • 回收策略(ReclaimPolicy):回收策略通过 PV 定义中的 persistentVolumeReclaimPolicy 字段进行设置,可选项如下:
    • Retain:保留数据,当 PVC 被删除时,PV 不会自动删除。它的状态变为“Released”,但保留资源不能被其他 PVC 继续使用,直到管理员手动处理。这允许管理员手动回收资源或者检查数据。重要的数据还是推荐用该策略。
    • Delete:在这种回收策略下,PVC 被删除时,PV 以及底层的存储资源也会自动被删除。
    • Recycle(已弃用):PVC 被删除时,PV 上的数据将被简单地删除,而不会销毁 PV 本身。该策略会对 PV 进行基本清理(如删除文件内容),然后将其重新标记为Available ,使其可以被新的 PVC 再次绑定。由于安全和效能考虑,这个选项在较新的 Kubernetes 版本中已经被标记为弃用。
  • 节点亲和性(nodeAffinity):PV 可以通过设置节点亲和性来实现只能通过某些Node 访问 Volume,这可以在 PV 定义的 nodeAffinity 字段中进行设置。使用这些Volume 的 Pod 将被调度到满足亲和性要求的 Node 上。示例:
apiVersion: v1
kind: PersistentVolume
metadata:
  name: prometheus-local
  labels:
    app: prometheus
spec:
  accessModes:
    - ReadWriteOnce
  capacity:
    storage: 10Gi
  storageClassName: local-storage
  local:
    path: /data/k8s/prometheus
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - k8s-master-03-u-203
  persistentVolumeReclaimPolicy: Retain
YAML
  • PV 生命周期的各个阶段
    • Available:可用状态,还未与某个 PVC 绑定。
    • Bound:已与某个 PVC 绑定。
    • Released:与之绑定的 PVC 已被删除,但未完成资源回收,不能被其他PVC使用。
    • Failed:自动资源回收失败。

3.4. PVC 详解

受限于名称空间,PVC 与 使用该 PVC 的 Pod 需要再同一个名称空间。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-hostpath
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 8Gi
  storageClassName: manual
  selector:
    matchLabels:
      release: "stable"
    matchExpressions:
      - {key: environment, operator: In, values: [dev]}
YAML

PVC 关键参数说明:

  • 资源请求(resources):描述对存储资源的请求,通过 resources.requests.storage 字段设置需要的存储空间大小。
  • 访问模式(accessModes):PVC 也可以设置访问模式,用于描述用户应用对存储资源的访问权限。其访问模式的设置与 PV 的设置相同。
  • 存储卷模式(volumeMode):PVC 也可以设置存储卷模式,用于描述希望使用的 PV 存储卷模式,包括文件系统和块设备。PVC 设置的存储卷模式应该与 PV 存储卷模式相同,以实现绑定;如果不同,则可能出现不同的绑定结果。PV 和 PVC 在各种组合模式下是否可以绑定的结果如下表所示:(未设定默认采用 Filesystem)
PV 的存储卷模式PVC 的存储卷模式是否可以绑定
FilesystemFilesystemtrue
FilesystemBlockfalse
BlockFilesystemfalse
BlockBlocktrue
  • PV 选择条件(selector):通过设置 LabelSelector ,可使 PVC 对于系统中已存在的各种 PV 进行筛选。系统将根据标签选出合适的 PV 与该 PVC 进行绑定。对于选择条件可以通过 matchLabels 和 matchExpressions 进行设置,如果两个字段都已设置,则 Selector 的逻辑将是两组条件同时满足才能完成匹配。
  • 存储类别(Class):在定义 PV C时可以设定需要的后端存储的类别(通过 storageClassName 字段进行指定),只有设置了该 Class 的 PV 才能被系统筛选出来,并与该 PVC 进行绑定。

3.4. StorageClass 详解

StorageClass 作为对存储资源的抽象定义,可对用户设置的 PVC 申请蔽后端存储的细节,这一方面减少了用户对于存储资源细节的关注,另一方面减轻了管理员手动管理 PV的工作,由系统自动完成 PV 的创建和绑定,实现动态的资源供应。

StorageClass 配置 yaml 文件示例:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: standard
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp2
reclaimPolicy: Retain
allowVolumeExpansion: true
mountOptions:
  - debug
volumeBindingMode: Immediate
YAML

StorageClass 资源关键配置:

  • 存储资源提供者(Provisioner):Provisioner 描述存储资源的提供者,用于提供具体的 PV 资源,可以将其看作后端存储驱动。
  • 回收策略(Reclaim Policy):通过动态资源供应模式创建的 PV 将继承在StorageClass 上设置的回收策略,配置字段名称为“reclaimPolicy”,可以设置的选项包括 Delete 和 Retain。如果 StorageClass 没有指定 reclaimPolicy 字段,则默认值为 Delete。
  • 是否允许存储卷扩容(Allow Volume Expansion):当 StorageClass 的 AllowVolumeExpansion 字段被设置为 true 时,表示 PV 被配置为可以扩容,系统将允许用户通过编辑增加 PVC 的存储空间自动完成 PV 的扩容。
  • 存储绑定模式(Volume Binding Mode):StorageClass 资源对象的 volumeBindingMode 字段用于控制何时将 PVC 与动态创建的 PV 绑定。目前支持的绑定模式包括:
    • Immediate:表示当一个 PersistentVolumeClaim (PVC)被创建出来时,就动态创建 PV 并进行 PVC 与 PV 的绑定操作。注意:提前绑定可能导致使用 PVC 的 Pod 调度到资源不匹配的 Node 导致 Pod 启动失败。
    • WaitForFirstConsumer:表示 PVC 与 PV 的绑定操作延迟到第一个使用 PVC 的 Pod 创建出来时再进行。系统将根据 Pod 的调度需求,在 Pod 所在的Node上创建 PV。

4. 持久化存储方案示例

4.1. hostPath

hostPath 类型的存储卷,使用的是目标节点上的本地目录。

特点:持久化数据在固定节点上,Pod 不能随意调度。

部署示例:

apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web-test
  name: web-test
spec: 
  replicas: 1
  selector: 
    matchLabels:
      app: web-test
  template:                
    metadata:
      labels:
        app: web-test
    spec:        
      nodeName: k8s-node-01-c-211 # 需要固定调度到某个节点才行,否则重启 pod 调度到别的节点了就找不到 hostPath
      containers:
        - image: nginx:1.26.2
          name: nginx
          # 挂载到容器内
          volumeMounts:
            - name: test-volume
              mountPath: /usr/share/nginx/html    
      volumes:
        - name: test-volume
          hostPath:
            path: /test-data   # 宿主机目录
            # type: Directory    # 表示该目录必须存在
            type:  DirectoryOrCreate  # 表示目录不存在则会自动创建
YAML

测试:

# 在挂载目录创建测试文件
echo wwwwww > index.html

# 访问 pod ip
curl 10.244.2.62
# 显示结果
wwwwww
ShellScript

4.2. Local PV

在 hostPath 的基础上,k8s 依靠 pv 与 pv c实现了一个新特性叫 Local PV。

注意:Local pv 对应的存储介质应该是一块额外挂载到宿主机上的磁盘或者块设备,避免与宿主机文件共用一个磁盘。否则会导致磁盘空间管理不可控。

创建 PV:

# local-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: local-pv
spec:
  capacity:
    storage: 1Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /local-pv-data # k8s-node-01-c-211节点上的目录,该目录强烈建议挂载一块单独的磁盘,该目录需要手动创建
  nodeAffinity:
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - key: kubernetes.io/hostname
              operator: In
              values:
                - k8s-node-01-c-211
YAML

创建目录:

mkdir /local-pv-data
ShellScript

接下了不能直接创建 PVC 与 PV 绑定,如果上述 PV 创建了多个且亲和不同的节点,则直接创建的 PVC 会随机选择一个 PV 绑定。再创建 Pod 后使用该 PVC 的 Pod 会调度到绑定的 PV 亲和的节点上,如何该节点资源不足,则 Pod 会启动失败。需要通过 storageClass 对象实现延迟绑定。

创建 storageClass:

# local-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner  # 指定为 no-provisioner 代表我们是手动创建的 PV
volumeBindingMode: WaitForFirstConsumer
YAML

创建 PVC:

# local-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: local-pvc
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
  storageClassName: local-storage
YAML

创建 Pod:

# pod-demo.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: web-test
  name: web-test
spec: 
  replicas: 1
  selector: 
    matchLabels:
      app: web-test             
  template:                
    metadata:
      labels:
        app: web-test
    spec:        
      containers:
        - image: nginx:1.26.2
          resources: 
            requests:
              cpu: 1
              memory: 1.5Gi
          name: nginx
          # 挂载到容器内
          volumeMounts:
            - name: test-volume
              mountPath: /usr/share/nginx/html 
      # PVC声明     
      volumes:
        - name: test-volume
          persistentVolumeClaim:
            claimName: local-pvc
YAML

上面的方案只能手动创建 PV ,下面推荐一个自动创建 PV 的插件:

由 Rancher 开发的 local-path-provisioner 这个存储方案允许 Kubernetes 用户在每个节点上使用本地存储,对外提供一个 storageclass 默认名为 local-path。

下载地址:https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

4.3. NFS 共享存储

4.3.1. 手动创建 PV

在存储节点安装 nfs 服务端:

# 内网环境可以直接关闭防火墙
systemctl stop firewalld.service
systemctl disable firewalld.service

# 安装
yum install -y nfs-utils rpcbind

# 创建数据共享目录
mkdir -p /data/nfs
chmod 755 /data/nfs

# 配置共享目录
cat >  /etc/exports <<EOF
/data/nfs *(rw,sync,no_root_squash)
EOF

# *:表示任何人都有权限连接,当然也可以是一个网段,一个 IP,也可以是域名
# rw:读写的权限
# sync:表示文件同时写入硬盘和内存
# no_root_squash:当登录 NFS 主机使用共享目录的使用者是 root 时,其权限将被转换成为匿名使用者,通常它的 UID 与 GID,都会变成 nobody 身份

# 启动nfs服务
systemctl start rpcbind.service
systemctl enable rpcbind
systemctl status rpcbind
systemctl start nfs
systemctl enable nfs
systemctl status nfs
ShellScript

k8s 集群节点上安装客户端:

sudo yum install -y nfs-utils 

sudo apt install -y nfs-common
ShellScript

创建基于 nfs 的 PV:

# nfs-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv
spec:
  storageClassName: manual
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  nfs:
    path: /data/nfs       # 指定 nfs 的挂载目录
    server: 192.168.2.50  # 指定 nfs 服务地址
YAML

创建 PVC:

# nfs-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
   requests:
     storage: 1Gi
YAML

测试 Pod:

# nfs-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-volumes
spec:
  volumes:
    - name: nfs
      persistentVolumeClaim:
        claimName: nfs-pvc
  containers:
    - name: web
      image: nginx:1.26.2
      ports:
        - name: web
          containerPort: 80
      volumeMounts:
        - name: nfs
          subPath: test-volumes
          mountPath: '/usr/share/nginx/html'
YAML

4.3.2. 动态创建 PV

插件官网:https://github.com/kubernetes-sigs/nfs-subdir-external-provisioner

安装:

helm repo add nfs-subdir-external-provisioner https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

# 镜像地址可以更换为自己的
helm upgrade --install nfs-subdir-external-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner --set nfs.server=192.168.2.50 --set nfs.path=/data/nfs --set storageClass.defaultClass=true --set image.repository=crpi-sozjkv641zbs4m9x.cn-shenzhen.personal.cr.aliyuncs.com/pingk-k8s-test/nfs-subdir-external-provisioner --set image.tag=v4.0.2 -n kube-system

# 详细的安装可以通过 values.yaml 文件配置安装
ShellScript

常用 StorageClass 配置命令:

# 关闭默认
kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}' 

# 设置为默认
kubectl patch storageclass nfs-client -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}' 

# StorageClass 回收策略不支持修改,只能重新创建
ShellScript