K8S Storage

概述

一般情况下,K8S中的Pod都不应该将数据持久化到Pod中,因为Pod可能被随时创建和删除(扩容或缩容),即便是StatefulSet或Operator的Pod,也都不建议在Pod里存放数据,可以将数据持久化到Host上。K8S提供了非常丰富的存储相关的功能,使得我们可以方便的让Pod访问存储设备。K8S通过Volume挂载的方式让Pod来访问存储设备,Volume与Pod绑定并与Pod有相同的生命周期,Volume在Pod中定义,而Pod中的容器只需要使用volumeMounts就可以使用在Pod中定义的Volume。

Pod可以引用的Volume包含以下几类:

  • K8S内部的资源对象:ConfigMap,Secret,DownwardAPI,Projected Volume等。
  • Node上的资源:emptyDir,hostPath。
  • 持久化存储或网络存储:CephFS,FC,CSI,iSCSI,NFS,RBD等。
  • 存储厂商提供的存储卷:ScaleIO Volumes,StorageOS,VsphereVolume等。
  • 公有云提供的存储卷:AWS EBS,AzureDisk,AzureFile,GCE PersistentDisk等。

总之,在K8S Pod中能够使用几乎所有的存储类型和方式。特别的,通过K8S CSI(Container Storage Interface),我们还可以开发自己的存储访问插件,接入到特定的存储设备中。

Node本地存储

Node本地存储包含emptyDirhostPath两种类型。emptyDir用于存储临时数据,如缓存,删除Pod的时候会自动被清理,emptyDir可以指定成Memory类型,但会被统计成容器使用的内存。hostPath用于挂载Node的某个目录,对于大部分应用来说,都不应该直接使用hostPath,因为如果Pod被调度到了其它节点,其就无法访问到之前节点的hostPath中的数据。另外,hostPath上使用的数据不会被计算到存储资源使用统计,可能出现磁盘占满而没有提醒的情况。但如果Pod只会被调度到某个Node上,那么还是可以使用hostPath。

PV/PVC

PV是Persistent Volume,即持久化卷,是K8S最常用的存储访问方式,几乎所有的外部存储都可以通过PV来访问。

PVC是Persistent Volume Claim,即持久化卷声明,通过PVC来申请对PV的使用。PV和PVC是一一对应关系,PV只有通过PVC关联后,才能被使用。Pod通过volumeMounts来关联PVC。

PV通常由管理员来创建,管理员事先分配好一定数量的PV供Pod使用,不同的存储类别(NFS,Cloud,Ceph等)最后都对应成一系列的PV。

PVC通常由Pod来创建,在需要使用存储的时候通过PVC来申请PV。

以下是PV/PVC的关系图,

下面通过NFS来介绍PV/PVC的使用,

先搭建一个双节点的K8S集群(略),参考 K8S 概述

接着在master节点上搭建NFS服务,主要命令如下,

bash 复制代码
# On Server:
sudo apt update
sudo apt install nfs-kernel-server
mkdir -p /home/sunny/nfs/root
echo "/home/sunny/nfs/root *(rw,sync,no_subtree_check)" | sudo tee -a /etc/exports
sudo exportfs -ra
sudo systemctl start nfs-kernel-server
sudo systemctl enable nfs-kernel-server

# On Client:
sudo apt install nfs-common
sudo mount 192.168.126.16:/home/sunny/nfs/root /mnt

创建PV,pv.yaml,这里会指定NFS IP和路径,

bash 复制代码
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nginx-pv
  labels:
    type: nginx-pv
spec:
  capacity:
    storage: 1Gi
  accessModes:
    - ReadWriteMany
  nfs:
    path: /home/sunny/nfs/root
    server: 192.168.126.16
bash 复制代码
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl apply -f pv.yaml
persistentvolume/nginx-pv created
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS      CLAIM   STORAGECLASS   REASON   AGE
nginx-pv   1Gi        RWX            Retain           Available                                   3s

创建PVC,pvc.yaml,

bash 复制代码
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
  namespace: default
spec:
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 1Gi
bash 复制代码
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl apply -f pvc.yaml
persistentvolumeclaim/nginx-pvc created
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl get pvc
NAME        STATUS   VOLUME     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
nginx-pvc   Bound    nginx-pv   1Gi        RWX                           2s
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl get pv
NAME       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS   REASON   AGE
nginx-pv   1Gi        RWX            Retain           Bound    default/nginx-pvc                           2m59s

可以看出,此时PVC的状态是Bound,说明其已经找到了一个与此关联的PV,而PV的状态也由之前的Available变为Bound,且CLAIM是default/nginx-pvc,先就可以在Pod里使用这个PVC了。默认PV和PVC的回收策略都是Retain,需要手动删除数据,即便PV和PVC都被删除。

创建Nginx Pod,nginx.yaml,在配置文件中引用PVC,

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
        ports:
        - containerPort: 80
      volumes:
        - name: html
          persistentVolumeClaim:
            claimName: nginx-pvc
bash 复制代码
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl apply -f nginx.yaml
deployment.apps/nginx-deployment created
sunny@xxx:~/k8s/storage/pvc_pv$ kubectl get pod -o wide
NAME                                READY   STATUS    RESTARTS   AGE   IP              NODE                     NOMINATED NODE   READINESS GATES
nginx-deployment-574dc457cf-8ds25   1/1     Running   0          53s   10.244.96.163   r05u36-nex-wvie-spr-cd   <none>           <none>
nginx-deployment-574dc457cf-fnn9z   1/1     Running   0          53s   10.244.2.220    r05u30-nex-wvie-spr-cd   <none>           <none>
nginx-deployment-574dc457cf-h4kkk   1/1     Running   0          53s   10.244.96.162   r05u36-nex-wvie-spr-cd   <none>           <none>

在NFS服务器对应的路径上创建一个index.html文件,供Nginx使用,

bash 复制代码
sunny@r05u30-nex-wvie-spr-cd:~/nfs/root$ pwd
/home/sunny/nfs/root
sunny@r05u30-nex-wvie-spr-cd:~/nfs/root$ cat index.html
Hello Nginx with PV/PVC.
sunny@r05u30-nex-wvie-spr-cd:~/nfs/root$

由于3个Nginx Pod都关联到了同一个NFS路径,所以通过任何一个Pod都能访问到相同的index.html。如果Pod扩容,新的Pod使用的也是相同的NFS路径,所以这里就非常容易的实现了数据和代码的分离,无论代码部署到哪里,无论代码如何改变,数据都是统一存储的。

bash 复制代码
sunny@xxx:~/k8s/storage/pvc_pv$ wget 10.244.2.220
--2024-03-21 01:45:21--  http://10.244.2.220/
Connecting to 10.244.2.220:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 25 [text/html]
Saving to: 'index.html'

index.html                                        100%[===========================================================================================================>]      25  --.-KB/s    in 0s

2024-03-21 01:45:21 (1.68 MB/s) - 'index.html' saved [25/25]

sunny@xxx:~/k8s/storage/pvc_pv$ cat index.html
Hello Nginx with PV/PVC.
sunny@xxx:~/k8s/storage/pvc_pv$ wget 10.244.96.163
--2024-03-21 01:47:36--  http://10.244.96.163/
Connecting to 10.244.96.163:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 25 [text/html]
Saving to: 'index.html.1'

index.html.1                                      100%[===========================================================================================================>]      25  --.-KB/s    in 0s

2024-03-21 01:47:36 (79.9 KB/s) - 'index.html.1' saved [25/25]

sunny@xxx:~/k8s/storage/pvc_pv$ cat index.html
Hello Nginx with PV/PVC.

StorageClass/PVC

PV/PVC确实能实现几乎所有存储的统一访问,但有一个问题是PV需要由管理员事先创建好,如果创建PVC的时候没有可用的PV,则PVC的状态会一直是Pending。不同的Pod可能需要不同规格和类型的PV,管理员需要创建和维护数量巨大的PV,这无疑是增加了K8S集群管理员的负担。

StorageClass可以解决上面的问题。当我们在创建PVC的时候可以指定一个StorageClass,PVC在创建过程中会根据StorageClass的描述自动创建需要的PV,不用管理员手动创建。

以下是StorageClass/PVC的关系图,

下面通过NFS来介绍StorageClass/PVC的使用,

搭建K8S和NFS参考上面内容。

StorageClass通过Provisioner来创建PV,Provisioner有很多,不同的存储类别有不同的实现,是第三方的组件。Provisioner要创建PV,意味着其能访问K8S集群,需要为其分配权限。

创建RBAC(Role Based Access Control),rbac.yaml,

bash 复制代码
apiVersion: v1
kind: Namespace
metadata:
  name: nginxns
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  namespace: nginxns
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: nfs-client-provisioner-runner
rules:
  - 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: managed-run-nfs-client-provisioner
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    namespace: nginxns
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
  namespace: nginxns
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
  namespace: nginxns
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: nginxns
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io
bash 复制代码
sunny@xxxx:~/k8s/storage/storageclass$ kubectl apply -f rbac.yaml
namespace/nginxns created
serviceaccount/nfs-client-provisioner created
clusterrole.rbac.authorization.k8s.io/nfs-client-provisioner-runner created
clusterrolebinding.rbac.authorization.k8s.io/managed-run-nfs-client-provisioner created
role.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created
rolebinding.rbac.authorization.k8s.io/leader-locking-nfs-client-provisioner created

创建Storage Class, storage_class.yaml,这里会定义storage class名称,在创建PVC的时候会使用,以及与之关联的provisioner,

bash 复制代码
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
  namespace: nginxns
provisioner: provisioner-nfs-storage
parameters:
  archiveOnDelete: "false"
bash 复制代码
sunny@xxx:~/k8s/storage/storageclass$ kubectl apply -f storage_class.yaml
storageclass.storage.k8s.io/managed-nfs-storage created
sunny@r05u30-nex-wvie-spr-cd:~/k8s/storage/storageclass$

创建 NFS provisioner,nfs-provisioner.yaml,这里面会指定provisioner名称,NFS地址,路径,serviceAccountName等,

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  namespace: nginxns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nfs-client-provisioner
  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: quay.io/external_storage/nfs-client-provisioner:latest
          image: gcr.io/k8s-staging-sig-storage/nfs-subdir-external-provisioner:v4.0.0
          volumeMounts:
            - name: nfs-client-root
              mountPath: /persistentvolumes
          env:
            - name: PROVISIONER_NAME
              value: provisioner-nfs-storage
            - name: NFS_SERVER
              value: 192.168.126.16
            - name: NFS_PATH
              value: /home/sunny/nfs/root
      volumes:
        - name: nfs-client-root
          nfs:
            server: 192.168.126.16
            path: /home/sunny/nfs/root
bash 复制代码
sunny@xxx:~/k8s/storage/storageclass$ kubectl apply -f nfs-provisioner.yaml
deployment.apps/nfs-client-provisioner created

创建PVC,pvc.yaml,在这里会指定storageClassName,

bash 复制代码
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: test-claim
  namespace: nginxns
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: managed-nfs-storage
  resources:
    requests:
      storage: 5Mi
bash 复制代码
sunny@xxx:~/k8s/storage/storageclass$ kubectl apply -f pvc.yaml
persistentvolumeclaim/test-claim created
sunny@xxx:~/k8s/storage/storageclass$ kubectl get pvc -n nginxns
NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS          AGE
test-claim   Bound    pvc-03905a65-efe4-4a5d-a10c-f5b50f026c4c   5Mi        RWX            managed-nfs-storage   12s
sunny@xxx:~/k8s/storage/storageclass$ kubectl get pv -n nginxns
NAME                                       CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM                STORAGECLASS          REASON   AGE
pvc-03905a65-efe4-4a5d-a10c-f5b50f026c4c   5Mi        RWX            Delete           Bound    nginxns/test-claim   managed-nfs-storage            41s

创建完PVC以后,可以看到pv也自动创建好了,且NFS根目录下也创建了相关的供PV使用的目录,在该目录中增加index.html,

bash 复制代码
sunny@xxx:~/nfs/root$ pwd
/home/sunny/nfs/root
sunny@xxx:~/nfs/root$ ll
total 0
drwxrwxrwx 3 sunny  sunny   73 Mar 21 02:30 ./
drwxrwxrwx 3 sunny  sunny   18 Mar 20 02:06 ../
drwxrwxrwx 2 nobody nogroup  6 Mar 21 02:30 nginxns-test-claim-pvc-03905a65-efe4-4a5d-a10c-f5b50f026c4c/
sunny@xxx:~/nfs/root$ cd nginxns-test-claim-pvc-03905a65-efe4-4a5d-a10c-f5b50f026c4c/
sunny@xxx:~/nfs/root/nginxns-test-claim-pvc-03905a65-efe4-4a5d-a10c-f5b50f026c4c$ cat index.html
Hello Storage Class.

创建Nginx,nginx.yaml,

bash 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment-storageclass
  namespace: nginxns
spec:
  selector:
    matchLabels:
      app: nginx-storageclass
  replicas: 3
  template:
    metadata:
      labels:
        app: nginx-storageclass
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        imagePullPolicy: IfNotPresent
        volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
        ports:
        - containerPort: 80
      volumes:
        - name: html
          persistentVolumeClaim:
            claimName: test-claim
bash 复制代码
sunny@xxx:~/k8s/storage/storageclass$ kubectl apply -f nginx.yaml
deployment.apps/nginx-deployment-storageclass created
sunny@xxx:~/k8s/storage/storageclass$ kubectl get pod -n nginxns -o wide
NAME                                             READY   STATUS    RESTARTS   AGE   IP              NODE                     NOMINATED NODE   READINESS GATES
nfs-client-provisioner-7c5d5f57b-shrd8           1/1     Running   0          37m   10.244.96.164   r05u36-nex-wvie-spr-cd   <none>           <none>
nginx-deployment-storageclass-86bb9496f8-5mpvg   1/1     Running   0          18s   10.244.2.221    r05u30-nex-wvie-spr-cd   <none>           <none>
nginx-deployment-storageclass-86bb9496f8-c8zbp   1/1     Running   0          18s   10.244.96.166   r05u36-nex-wvie-spr-cd   <none>           <none>
nginx-deployment-storageclass-86bb9496f8-z4sxm   1/1     Running   0          18s   10.244.96.165   r05u36-nex-wvie-spr-cd   <none>           <none>

访问Pod,

bash 复制代码
sunny@xxx:~/k8s/storage/storageclass$ wget 10.244.2.221
--2024-03-21 03:06:12--  http://10.244.2.221/
Connecting to 10.244.2.221:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 21 [text/html]
Saving to: 'index.html'

index.html                                        100%[===========================================================================================================>]      21  --.-KB/s    in 0s

2024-03-21 03:06:12 (53.6 KB/s) - 'index.html' saved [21/21]

sunny@xxx:~/k8s/storage/storageclass$ cat index.html
Hello Storage Class.

StorageClass确实能自动创建PV,减少管理员准备PV的工作,但是我们也发现StorageClass的配置多了不少,需要定义RBAC,StorageClass等资源对象,需要创建provisioner这个额外的Pod等。所以,如果在一些复杂场景下需要频繁创建和维护PV,我们可以使用StorageClass + PVC的模来使用存储,如果在一些简单的场景下只需要一些固定的PV,我们可以使用PV + PVC的模式来使用存储。

CSI

在 Kubernetes 中,存储插件的开发主要有以下几种方式:

  1. CSI插件:Container Storage Interface (CSI) 是 Kubernetes 的标准插件接口,是全新的插件方案,插件和驱动调用通过grpc协议,功能丰富,支持存储卷动态提供、快速、动态扩容等等。
  2. FlexVolume插件:FlexVolume 是 Kubernetes 的早期存储插件接口之一,它提供了一个简单的接口,但局限性却很大,用于将存储驱动程序接入到 Kubernetes 中。通过实现 FlexVolume 接口,可以将各种存储系统接入到 Kubernetes 集群中,包括 NFS、GlusterFS、Ceph 等等。
  3. in-tree插件:in-tree 存储插件是 Kubernetes 的早期存储插件接口之一,它将存储驱动程序嵌入到 Kubernetes 主体代码库中。in-tree 插件可以实现对本地存储、NFS、iSCSI 等存储系统的支持。不过,由于 in-tree 插件需要嵌入到 Kubernetes 主体代码库中,因此对于插件开发者而言,维护成本较高,并且需要适应 Kubernetes 主体代码库的版本变化。

CSI 全称 Container Storage Interface,是容器编排系统(CO)如k8s等扩展容器存储的一种实现方式,基于gRPC实现,是当前主流的存储扩展方式。为什么会使用CSI?首先,CSI 可以满足不同编排系统的需求,除了k8s,还可以比如 Mesos,Swarm。其次,CSI 是容器化部署,可以减少环境依赖,增强安全性,丰富插件的功能。CSI 的设计思想,把插件的职责从之前讲的 "两阶段处理",扩展成了 Provision、Attach 和 Mount 三个阶段。

CSI 主要包含两个部分:CSI Controller Server 与 CSI Node Server。

  • Controller Server 是控制端的功能,负责将卷与具体节点进行配置,每个集群中只需要有一个Controller提供服务。
  • Node Server 负责k8s负载节点上的卷配置,每个节点都有一个Node提供服务。

CSI部署架构,

参考:

CSI规范

k8s-编写CSI插件

CSI 插件开发简介

相关推荐
蜜獾云5 分钟前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
年薪丰厚1 小时前
如何在K8S集群中查看和操作Pod内的文件?
docker·云原生·容器·kubernetes·k8s·container
zhangj11251 小时前
K8S Ingress 服务配置步骤说明
云原生·容器·kubernetes
岁月变迁呀1 小时前
kubeadm搭建k8s集群
云原生·容器·kubernetes
墨水\\1 小时前
二进制部署k8s
云原生·容器·kubernetes
Source、1 小时前
k8s-metrics-server
云原生·容器·kubernetes
上海运维Q先生1 小时前
面试题整理15----K8s常见的网络插件有哪些
运维·网络·kubernetes
颜淡慕潇1 小时前
【K8S问题系列 |19 】如何解决 Pod 无法挂载 PVC问题
后端·云原生·容器·kubernetes
大熊程序猿4 小时前
K8s证书过期
云原生·容器·kubernetes
摸鱼也很难7 小时前
Docker 镜像加速和配置的分享 && 云服务器搭建beef-xss
运维·docker·容器