菜鸟学Kubernetes(K8s)系列——(四)关于Volume卷(PV、PVC、StorageClass等)


Kubernetes系列文章主要内容
菜鸟学Kubernetes(K8s)系列——(一)关于Pod和Namespace通过本文你将学习到:
(1)什么是Pod,为什么需要它、如何创建Pod、Pod的健康检查机制(三种探针)
(2)什么是标签、标签选择器
(3)什么是Namespace、他能做什么、如何创建它等等
菜鸟学Kubernetes(K8s)系列——(二)关于Deployment、StatefulSet、DaemonSet、Job、CronJob通过本文你将学习到:
(1)什么是Deployment,如何创建它、它的扩缩容能力是什么、自愈机制,滚动升级全过程、如何进行回滚
(2)什么是ReplicaSet、它和Deployment的关系是什么
(3)动态扩缩容能力(HPA)、蓝绿部署、金丝雀部署
(4)什么是DaemonSet/Job/CronJob、它们的功能是什么、如何创建使用等等
菜鸟学Kubernetes(K8s)系列——(三)关于Service、Ingress通过本文你将学习到:
(1)什么是Service,如何创建它、它的服务发现能力、对外暴露方式
(2)什么是NodePort类型的Service,它的使用方式、工作原理
(3)什么是Ingress,如何创建它、为什么需要Ingress
(4)什么的Ingress-Nginx、他和Nginx是什么关系、Ingress的各种功能
(5)什么是headless服务等等
菜鸟学Kubernetes(K8s)系列——(五)关于ConfigMap和Secret通过本文你将学习到:
(1)什么是ConfigMap,如何创建它、它能用来做什么事情、在实战中怎么使用ConfigMap
(2)什么是Secret,如何创建它,怎么使用它等等
菜鸟学Kubernetes(K8s)系列——(七)关于Kubernetes底层工作原理通过本文你将学习到:
(1)Kubernetes的核心组件:Etcd、Api-Server、Scheduler、Controller-Manager、Kubelet、Kube-proxy的工作方式,工作原理。
(2)Kubernetes集群中核心组件的协作方式、运行原理。
菜鸟学Kubernetes(K8s)系列——(八)关于Kubernetes的认证机制(RBAC)通过本文你将学习到:
(1)Api-Server的认证授权流程(2)什么是ServiceAccount(3)什么是Role、ClusterRole、RoleBinding、ClusterRoleBinding,如何使用他们,他们之间是如何关联协作的。等等
菜鸟学Kubernetes(K8s)系列——(番外)实现Deployment的动态扩缩容能力(HPA)通过本文你将学会实现Deployment的动态扩缩容能力(HPA)
菜鸟学Kubernetes(K8s)系列——(番外)安装Ingress-Nginx(工作原理)通过本文你将学会安装Ingress-Nginx

九、卷(Volume):将磁盘挂载到容器

前面提到过,Pod类似于一个逻辑主机,它内部的一组容器是共享一些资源的,比如CPU、RAM、网络接口等。但是当涉及到文件系统时,各个容器就是隔离的,因为大多数容器的文件系统来自容器镜像,因此默认情况下, 每个容器的文件系统与其他容器完全隔离。

我们知道,K8s中的Pod是有自愈能力的,一个Pod内的容器是有可能被重新启动的(如果出现了异常)。而在Docker中我们也学的过,容器内的数据都是临时存储的,当容器被删除,它里面的数据也会被删除。那么问题来了,一个运行的容器被重启了,那么重启后产生的新容器中该怎么恢复他原来在旧容器中产生的数据呢???

在Kubernetes中,通过定义存储卷来满足这个需求。

1、卷的应用示例

假设有一个Pod中包含了三个容器,一个容器运行了一个web服务器,该web服务器要展示的HTML页面路径位于/var/htdocs,同时他产生的日志会存储到/var/logs目录中。第二个容器运行了一个代理服务,他的作用就是用来创建HTML文件,并将它存放在/var/html中。第三个容器中运行的应用主要用来处理这个容器中/var/logs目录中的日志。
在这里插入图片描述
每个容器都有一个很明确的用途,但是如果每个容器单独使用就没多大用处了,因为他们的文件系统是完全隔离的,代理服务创建的HTML文件WebServer访问不到,WebServer产生的日志LogRotator处理不了。

但是,如果我们在Pod中添加两个存储卷,并且在三个容器的适当路径上挂载它们,这样就能让这三个容器间协同工作了。

在Linux中是允许在文件树的任意位置挂载文件系统的,当这样做的时候,挂载的文件系统的内容在目录中是可以访问的。通过将相同的卷挂载到两个容器中,他们可以对相同的文件进行操作。

在这里插入图片描述

上面图中,Pod中存在两个卷:publicHtml和logVol。

  • publicHtml被挂载在WebServer容器的/var/htdocs/中,因为这是WebServer的服务目录。在AgentServer容器中也挂载了相同的卷,但在这个容器中挂载的路径是/var/html/,因为代理将Html文件写入/var/html/这个路径中。通过这种方式挂载这个卷,WebServer现在就能读取到AgentServer容器中产生的Html页面了。
  • logVol卷用来存放日志,此卷被挂载在WebServer容器和AgentServer容器的/var/logs/路径下。

注意,不是说同一个Pod中的所有容器都可以任意访问Pod中存在的卷。如果某个容器想访问某个卷,则需要在定义容器的时候通过volumeMounts来进行挂载声明。

  • 通过volumeMounts进行挂载声明只是用来指定容器中哪个路径要挂载,但是对外要挂在哪里是在Pod中指定的,在Pod中声明的是容器中的挂载的卷的定义。

2、卷的类型

  • 配置信息(用于将K8s部分资源和集群信息公开给Pod的特殊类型的卷,下一章会介绍)

    • Secet:对配置信息利用Base64进行编码
    • ConfigMap:对配置信息不编码
  • 非持久性存储

    • emptyDir:用来存储临时数据的简单空目录

    • hostPath:用来将目录从工作节点的文件系统挂载到Pod中

      其实他是属于半持久性存储,因为他跟被调度在哪个节点上有关。(详细看下面的介绍)

  • 持久性存储

    • 网络连接性存储
      • NAS:iSCSI、ScaleIO Volumes、FC (Fibre Channel)
      • NFS:挂载到Pod中的NFS共享卷
    • PV、PVC:一种使用预置或者动态配置的持久存储类型。(重点)
    • 分布式存储
      • GlusterFS
      • RBD (Ceph Block Device)
      • CephFS
      • Portworx Volumes
      • Quobyte Volumes
    • 云端存储
      • GCEPersistentDisk
      • AWSElasticBlockStore(AmazonWeb服务弹性块存储卷)
      • AzureFile
      • AzureDisk(Microsoft Azure磁盘卷)
      • Cinder (OpenStack block storage)
      • VsphereVolume
      • StorageOS
    • 自定义存储
      • FlexVolume

2.1 emptyDir

一个emptyDir卷对于同一个Pod中运行的容器之间共享文件特别有用。但是它也可以被单个容器用于将数据临时写入磁盘。例如在大型数据集上执行排序操作时,没有那么多内存可供使用。这时数据就需要被写到容器的文件系统中,但是将数据写入内存和写入容器的文件系统还是有区别的,甚至容器的文件系统还可能是不可写的,所以写到挂载的卷可能是唯一的选择。

在Pod中使用emptyDir卷
apiVersion: v1
kind: Pod
metadata:
  name: fortune
  labels:
    app: test
spec:
  volumes:				 # 一个名为html的单独emptyDir卷,挂载到下面的两个容器中
  - name: html			 #
    emptyDir: {}		 #
  containers:
  - image: luksa/fortune		# 第一个容器名为html-generator,运行luksa/fortune镜像
    name: html-generator			
    volumeMounts:				# 名为html的卷挂载在容器的/var/htdocs路径中
    - name: html			
      mountPath: /var/htdocs
  - image: nginx:alpine				# 第二个容器名为web-server,运行nginx:alpine镜像
    name: web-server
    volumeMounts:					# 名为html的卷挂载在容器的/usr/share/nginx/html路径中,他被设置为只读
    - name: html
      mountPath: /usr/share/nginx/html
      readOnly: true
    ports:
    - containerPort: 80
      protocol: TCP

在这个Pod中包含两个容器和一个挂载在两个容器中的共用的卷,但这个卷挂载在容器的不同路径上。当html-generator容器启动时,他每10s启动一次fortune命令输出到/var/htdocs/index.html文件(这是这个定制的镜像的功能)。因为卷是在/var/htdocs上挂载的,所以index.html文件被写入卷中(而不是容器的可写层(这是关于Docker的知识))。一旦web-server容器启动,他就开始为/usr/share/nginx/html目录中的任意HTML文件提供服务(这是Nginx服务的默认服务文件目录)。因为我们将卷挂载在那个确切的位置,Nginx将为运行fortune循环的容器输出的index.html文件提供服务。最终效果是:一个客户端向Pod上的80端口发送一个Http请求,将接收到当前的fortune消息作为响应。


如果想测试效果,可通过kubectl expose pod fortune --port=8888 --target-port=80 --name=testemptyDir命令将这个Pod暴露出去,然后再每十秒访问一次服务集群的8888端口,查看效果。
在这里插入图片描述

2.2 hostPath

大多数Pod不应该去访问节点文件系统上的任何文件,但是某些系统级别的Pod(由DaemonSet管理的Pod)确实需要读取节点的文件或使用节点文件系统来访问节点设备。Kubernetes通过hostPath卷实现了这一点。

hostPath卷指向节点文件系统上的特定文件或目录,在同一个节点上运行并在其hostPath卷中使用相同路径的Pod可以看到相同的文件。
在这里插入图片描述
hostPath卷是一个持久化卷,如果删除一个Pod,并且下一个Pod使用了指向主机上相同路径的hostPath卷,则新Pod将会发现上一个Pod留下的数据。(前提是下一个Pod要和上一个Pod部署在同一个节点上!)

切记,仅当需要在节点上读取或写入系统文件时才使用hostPath,切勿使用它们来持久化跨Pod的数据。

hostPath的简单应用
### 解决容器时间问题
apiVersion: v1
kind: Pod
metadata:
  name: busy-box-test
  namespace: default
spec:
  restartPolicy: OnFailure
  containers:
  - name: busy-box-test
    image: busybox
    imagePullPolicy: IfNotPresent
    volumeMounts:
    - name: date-config
      mountPath: /etc/localtime
    command: ["sleep", "60000"]
  volumes:
  - name: date-config
    hostPath:
      path: /etc/localtime

2.3 NFS

要想使用NFS,需要自己先搭建一个NFS服务。

安装NFS服务
# 在任意机器执行下面的命令

yum install -y nfs-utils
#执行命令 vi /etc/exports,创建 exports 文件,文件内容如下:
echo "/nfs/data/ *(insecure,rw,sync,no_root_squash)" > /etc/exports

# 执行以下命令,启动 nfs 服务;创建共享目录
mkdir -p /nfs/data
systemctl enable rpcbind
systemctl enable nfs-server
systemctl start rpcbind
systemctl start nfs-server
exportfs -r
#检查配置是否生效
exportfs
# 输出结果如下所示
/nfs/data /nfs/data

使用NFS类型的卷(他是完全持久性的卷,不用但是新的Pod被调度到了哪个节点上)其实和使用其他类型的卷没什么区别,只是需要改下Pod中的定义信息即可,如下:

apiVersion: v1
kind: Pod
metadata:
  name: test-nfs
  labels:
    app: test
spec:
  volumes:				 # 一个名为html的单独nfs卷,挂载到下面的容器中
  - name: html			 
    nfs:
      server: 10.170.11.8   # 指定NFS服务器的IP
      path: /nfs/some/path   # 服务器提供的路径
  containers:
  - image: luksa/fortune		# 第一个容器名为html-generator,运行luksa/fortune镜像
    name: html-generator			
    volumeMounts:				# 名为html的卷挂载在容器的/var/htdocs路径中
    - name: html			
      mountPath: /var/htdocs
  

3、PV(PersistentVolume)和PVC(PersistentVolumeClaim)

3.1 为什么要用PV和PVC?

我们发现所有的持久卷类型都要求Pod的开发人员了解集群中可用的真实网络存储的基础结构。例如,要创建支持NFS协议的卷,开发人员必须知道NFS节点所在的实际服务器。而Kubernetes的理念是向应用程序及其开发人员隐藏真实的基础设施,使他们不必担心基础设施的具体状态。(比如,开发人员要部署的应用程序到底部署在哪个节点上他根本不用关心,直接交给Kubernetes就行了。)

所以,理想情况下是,在Kubernetes上部署应用程序的开发人员不需要知道底层使用的技术细节(应用程序部署在哪台机器、存储技术等)。这些是交给集群管理员去考虑的。开发人员需要一定数量的持久化存储为应用持久化数据时,只需要向Kubernetes申请就行了(PV和PVC的概念就诞生了!),就像在创建Pod时可以请求CPU、内存和其他机器资源一样。集群管理员可以对集群进行配置,让其可以为应用程序提供所需的服务。

3.2 PV和PVC的协作原理

在这里插入图片描述
首先由集群管理员创建持久卷PV,在创建时可以指定这个PV的大小和所支持的访问模式。当开发者(集群用户)需要在他的Pod中添加特定的卷时,他们首先需要创建持久卷声明PVC清单,指定所需要的最低容量要求和访问模式,然后这个PVC会交给K8s,K8s将找到可匹配的持久卷并将其绑定到持久卷声明。

3.3 创建PV

接下来我们分别担任管理员的角色,创建一个PV。然后在承担开发者的角色,首先声明PVC,然后在Pod中使用。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: test-pv-volume
  labels:
    type: nfs
spec:
  storageClassName: my-nfs-storage
  capacity:
    storage: 10Gi       ### 定义PV的大小
  accessModes:          ### 指定访问模式
    - ReadWriteOnce     ### 允许单个客户端访问(同时支持读取和写入操作)
  persistentVolumeReclaimPolicy: Retain    ###  当声明被释放后,PV将会被保留(不清理和删除)
  nfs:      ### PV指定支持之前创建的NFS服务
    server: 10.170.11.8
    path: "/nfs/data/haha"

3.4 创建PVC

马上我们要部署一个需要持久化存储的Pod,将要用到之前创建的持久卷,但是不能直接在Pod内使用,需要先声明一个PVC。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
  namespace: default
  labels:
    app: nginx-pvc
spec:
  storageClassName: ""  ## 存储类的名字,后面会讲到他的使用方式。
  accessModes:		 ### 指定访问模式
  - ReadWriteOnce
  resources:
    requests:
      storage: 50m    ### 表示需要申请50m的磁盘空间

上面这个PVC表示:请求50m的存储空间和ReadWriteOnce访问模式。

当我们将这个PVC创建好以后,Kubernetes就会找到适合的PV并将其和该PVC进行绑定。注意:PV的容量必须是大于PVC申请的容量的,而且卷的访问模式(PVC中指定的)也必须包含声明中指定的访问模式(PV中指定的)。

3.5 在Pod中使用PVC

要在Pod中使用PV,则需要在Pod的卷定义中引用PVC,如下:

apiVersion: v1
kind: Pod
metadata:
  name: "nginx-pvc"
  namespace: default    ### 需要和PVC在同一命名空间
  labels:
    app: "nginx-pvc"
spec:
  volumes:
  - name: html
    persistentVolumeClaim:    ### 指定PVC
      claimName:  nginx-pvc  ### PVC的名字
      
  containers:
  - name: nginx-pvc
    image: "nginx"
    ports:
    - containerPort:  80
      name:  http
    volumeMounts:
    - name: html
      mountPath: /usr/share/nginx/html
  restartPolicy: Always

当PV被Pod使用了他就不能再被分配给别人了,而且绑定了Pod的PVC,而且Pod正在运行,PVC是不能被删除的,PVC会一直等到Pod删除了后再去删除

注意:

当我们删除Pod和他引用的PVC,然后再重新创建一个新的PVC,这时这个PVC是不能被绑定到刚刚被释放的那个PV的,因为这个PV已经被使用过,所以它可能还包含前一个声明人的数据。所以如果集群管理员还没来得及清理那个不再被使用PV,那么他就不应该将这个PV绑定到PVC中。

3.6 PV和PVC的状态

PVC的状态
  • Released:表示PV释放了和PVC的关联关系,绑定不存在。新的不同PVC不能重新绑定上来
  • Available:表示PV可用。可以和任意PVC进行绑定
PV的状态
  • Available:表示PV是一个空闲资源,尚未绑定到任何PVC;任意人都能继续使用
  • Bound:表示该PV已经绑定到某PVC;别人不能再用
  • Released:所绑定的PVC已被删除,但是资源尚未被集群回收;PVC被删除了,PV没有被回收
    • 需要管理员确认这个PV没用了,手动删除,然后再重新创建出这个PV
  • Failed:卷的自动回收操作失败

3.7 PV回收策略:

我们可以通过persistentVolumeReclaimPolicy属性指定PV的回收策略

  • Retain(默认规则)——手动回收。PVC删除,PV分文不动,需要自己手动删除
    • 即使Pod以及PVC删除。但是PV会记住上次是和哪个PVC建立的绑定关系
    • 只要下次Pod继续使用这个PVC,依然能重新建立连接
  • Recycle——基本擦除 (rm -rf /thevolume/*):删除PVC时同时清除PV关联的卷里面的内容
    • 这种方式,PV可以被不同的PVC和Pod反复使用。
  • Delete——删除PVC,同时删除PV
    • 目前,仅 NFS 和 HostPath 支持回收(Recycle)。 AWS EBS、GCE PD、Azure Disk 和 Cinder 卷都支持删除(Delete)。

4、动态配置持久卷

学习了上面的技术后,我们发现这个PV总是需要有个集群管理员来进行创建,能不能不要人来做这件事呢?可以,K8s可以通过动态配置持久卷来自动执行此任务。

集群管理员只需要创建一个持久卷配置,并定义一个或多个StorageClass资源,从而让用户选择他们想要的持久卷类型而不是仅仅只是创建持久卷。用户可以在其持久卷声明中引用StorageClass,而配置程序在配置持久存储时将采用这一点。

4.1 通过StorageClass定义可用存储类型

在用户创建PVC之前,管理员需要创建一个或多个StorageClass资源,然后集群自动根据PVC创建出对应PV进行使用。

## 创建了一个存储类
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: my-nfs-storage
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner   ### 指定用于动态配置PV的卷插件
#### provisioner指定一个供应商的名字。  必须匹配 k8s-deployment 的 env PROVISIONER_NAME的值
parameters:
  archiveOnDelete: "false"   ### 该参数会传递给provisioner

StorageClass资源指定当PVC请求此StorageClass时应该使用哪个动态配置供应商来创建PV。

4.2 安装NFS的动态配置能力

## 创建了一个存储类
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: managed-nfs-storage
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner #### provisioner指定一个供应商的名字。  必须匹配 下面Deployment中指定的env的PROVISIONER_NAME的值
parameters:
  archiveOnDelete: "false"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nfs-client-provisioner
  labels:
    app: nfs-client-provisioner
  namespace: default
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: registry.cn-hangzhou.aliyuncs.com/lfy_k8s_images/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  ### 这个值在创建StorageClass时会用到
            - name: NFS_SERVER
              value: 10.170.11.8 ## 指定自己nfs服务器地址
            - name: NFS_PATH  
              value: /nfs/data  ## nfs服务器共享的目录
      volumes:
        - name: nfs-client-root
          nfs:
            server: 10.170.11.8
            path: /nfs/data
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nfs-client-provisioner
  # replace with namespace where provisioner is deployed
  namespace: default
---
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: default
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: default
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: default
subjects:
  - kind: ServiceAccount
    name: nfs-client-provisioner
    # replace with namespace where provisioner is deployed
    namespace: default
roleRef:
  kind: Role
  name: leader-locking-nfs-client-provisioner
  apiGroup: rbac.authorization.k8s.io

4.3 使用StorageClass

创建StorageClass资源后,用户可以在其PVC中按名称引用StorageClass。

创建PVC,使用StorageClass

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nginx-pvc
  namespace: default
  labels:
    app: nginx-pvc
spec:
  storageClassName:  my-nfs-storage  ## 该PVC请求自定义存储类
  accessModes:		 
  - ReadWriteOnce
  resources:
    requests:
      storage: 50m    

在创建这个PVC时,PV会由指定的StorageClass资源中引用的provisioner创建。

集群管理员可以创建具有不同性能或其他特性的多个StorageClass,然后开发人员再在PVC中指明他想要的StorageClass。

这样看来其实StorageClass就是将PV按照不同特性分了个组。

4.4 注意

当我们在创建PVC时如果不明确的指定storageClassName属性,那么他会使用一个默认的StorageClass!如果我们就是想让这个PVC和我们自己手动创建的PV进行绑定,那么只需要将storageClassName的值填为 “” (空字符串)就行。

4.5 动态配置的流程图

在这里插入图片描述

未完,待续>>>

参考:Kubernetes in Action

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐