# 28 pv,pvc,storageClass这到底是说的啥

# PV

  1. PV:持久化存储数据卷,定义的是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录

    apiVersion: v1
    kind: PersistentVolume
    metadata:
      name: nfs
    spec:
      storageClassName: manual
      capacity:
        storage: 1Gi
      accessModes:
        - ReadWriteMany
      nfs:
        server: 10.2.1.4
        path: "/"
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
  2. PVC:描述的是Pod希望使用的持久化存储的属性

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: nfs
    spec:
      accessModes:
        - ReadWriteMany
      storageClassName: manual
      resources:
        requests:
          storage: 1Gi
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    绑定:

    • PV和PVC的spec字段,比如存储大小必须满足PVC的要求
    • storageClassName必须一样
  3. 声明使用

    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        role: web-frontend
    spec:
      containers:
      - name: web
        image: nginx
        ports:
        - name: web
          containerPort: 80
        volumeMounts:
        - name: nfs
          mountPath: "/usr/share/nginx/html"
      volumes:
      - name: nfs
        persistentVolumeClaim:
          claimName: nfs
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
  4. 大多数的情况下,持久化的Volume的实现,依赖一个远程存储服务,如NFS、GlusterFS,远程块存储,比如公有云提供的远程磁盘等

  5. 持久化是指容器在这个目录里写入的文件,都会保存在远程存储中,从而使这个目录有了持久性

  6. 默认情况下,目录是宿主机的一个目录:/var/lib/kubelet/pods/<Pod ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>

    • 第一阶段:attach 如果是远程块存储,如GCE提供的远程磁盘服务,先调用GOOGLE的API,将它提供的PersistentDisk挂载到Pod所在的宿主机上,相当于执行gcloud compute instances attach-disk <虚拟机名字> --disk <远程磁盘名字>
    • 第二阶段:mount 将磁盘设备格式化并挂载到Volume宿主机目录的操作,一般称为Mount
    • 第一阶段提供可用的参数是nodeName,宿主机的名字,第二阶段提供的是dir,即Volume的宿主机目录
    • 容器中挂载这个Volume,`docker run -v /var/lib/kubelet/pods//volumes/kubernetes.io~<Volume类型>/<Volume名字>:/<容器的目标目录> 我的镜像 ...
  7. 反向操作

    • Unmount
    • Dettach

# 维护

  1. 第一阶段:Volume Controller负责,名字AttachDetachController,不断检查每个Pod对应的PV,和这个Pod所在宿主机之间的挂载情况,从而决定,是否需要对这个PV进行Attach或Dettach操作 内置的控制器,kube-controller-manager的一部分,运行在master节点上
  2. 第二阶段:必须发生在Pod对应的宿主机上,它是kubelet组件的一部分,名字VolumeManagerReconciler,是一个独立于kubelet主循环的Goroutine

kubelet主要设计的一个原则:主控制循环绝对不可以被block

# StorageClass

k8s为我们提供了一套自动创建PV的机制,Dynamic Provisioning,前面人工管理的方式叫Static Provisioning

StorageClass是创建PV的模板

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: block-service
provisioner: kubernets.io/gce-pd # 供应者
parameters:
  type: pd-ssd
1
2
3
4
5
6
7

# 29 PV PVC是否多此一举

Local Persistent Volume的设计难点

  1. 如何抽象成PV 绝不能把一个宿主机的目录当作PV使用,本地目录的存储行为完全不可控,磁盘可能被其它应用写满,造成整个宿主机宕机。而且不同的本地目录也没有办法做IO隔离机制 所以一个Local Persistent Volume的存储介质一定是一块额外挂载在宿主机上的磁盘或设备。不应该是和宿主机根目录使用的主硬盘。一个PV一块盘
  2. 调度器如何保证Pod始终能被正确调度到所请求的Local Persistent Volume所在的节点上呢 常规PV都是先调度Pod到节点上,然后再通过两阶段处理来执久化这台机器上的Volume目录,进而完成Volume目录与容器的绑定挂载 调度时考虑Volume分布:调度器必须知道所有节点和Local Persistent Volume对应关联关系,然后根据这个来调度Pod。调度器中VolumeBindingChecker过滤条件专门负责

所以在开始使用“Local Persistent Volume“之前,首先在集群中需要配置好磁盘或志设备

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example-pv
spec:
  capacity:
    storage: 5Gi
  volumeMode: Filesystem
  accessModes:
  - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-storage
  local:
    path: /mnt/disks/vol1
  nodeAffinity:
    required:
      nodeSelecotrTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
           - node-1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kubectl create -f local-pv.yaml
kubectl get pv
1
2

最佳实践,要创建一个StorageClass来描述这个PV,延迟绑定,推迟到调度的时候

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-storage
provisioner: kubernates.io/no-provisioner # 供应者
# 告诉VolumeController,不要现在进行绑定(设置PVC的VolumeName字段)将原本实时发生的PVC和PV的绑定过程,延迟到Pod第一次调度时在调度器中进行,从而保证了这个绑定结果不会影响Pod的正常 调度
volumeBindingMode: WaitForFirstConsumer 
1
2
3
4
5
6
7
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: example-local-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-storage # Volume Cotrller看到它时,不会为它进行绑定操作
1
2
3
4
5
6
7
8
9
10
11

# 限制

  1. 创建PV的操作不可省略,不支持Dynamic Provisioning,没有办法在创建pvc时创建pv
  2. WaitForFirstConsumer属性,延迟绑定,推迟到调度的时候

具体实现时,调度器实际维护了一个与Volume Controller类似的控制循环,专门负责为“延迟绑定”的PV和PVC进行绑定操作

$ kubectl create -f local-pvc.yaml 
persistentvolumeclaim/example-local-claim created

$ kubectl get pvc
NAME                  STATUS    VOLUME    CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Pending                                       local-storage   7s
1
2
3
4
5
6

创建后仍处理Pending状态,也就是等待绑定的状态

apiVersion: v1
kind: Pod
metadata:
  name: example-pv-pod
spec:
  containers:
  - name: example-pv-container
    image: nginx
    ports:
    - containerPort: 80
      name: http-server
    volumeMounts:
    - mountPath: /usr/share/nginx/html
      name: example-pv-storage
  volumes:
  - name: example-pv-storage
    persistentVolumeClaim:
      claimName: example-local-claim # 定义了PVC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 清理策略

  1. 删除使用这个PV的Pod
  2. 从宿主机移除本地磁盘
  3. 删除pvc
  4. 删除pv

# 简化策略

Static Provisioner:启动后,它会通过DaemonSet,自动检查每个宿主机的/mnt/disks目录,然后调用api为这些目录的每一个挂载,创建一个对应的pv出来

# 编写自己的存储插件

# 方式1:Flexvolume

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-flex-nfs
spec:
  capacity:
    storage: 10Gi
  accessModes:
  - ReadWriteMany
  flexVolume:
    driver: k8s/nfs # 插件名,提供商+存储驱动,会使用k8s~nfs做为插件名
    fsType: nfs
    options:
      server: 10.10.0.25 
      share: export
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

命令/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>操作的参数:init、mount、unmount、attach以用户dettach等 缺点

  • 一不支持Dynamic Provisioning(即:为每个PVC自动创建PV及对应的Volume),除非专门写一个External Provisioner
  • 挂载信息无法在解挂载时直接使用

# 方式2 CSI(Container Storage Interface)

**设计思想:把Provision阶段,及k8s里的一部分存储管理的功能,从主干代码中剥离,做成了几个单独的组件。**组件通过Watch API监听k8s中与存储有关的事件变化,比如pvc创建,来执行具体的存储管理动作。

把插件职责从'两阶段处理‘, 扩展成Provision 创建磁盘 Attach 挂载到虚拟机 Mount 格式化后挂载到宿主机上 三个阶段

0000008

持久卷类型

  1. PV
  2. CSI Volume

三个独立的外部组件(External Components)对应从k8s中剥离出来的存储管理功能,Mount阶段不是它的职责,而是在Kubelet的VolumeManagerReconciler控制循环调用

  1. **Driver Registrar:**需要请求CSI Identity来获取插件信息

  2. **External Provisioner:**负责Provision阶段。

    监听API Server里的PVC对象。当一个PVC创建时,它会调用CSI Controller的CreateVolume方法创建PV

  3. **External Attacher:**负责Attach阶段。

    监听API Server里VolumeAttachment对象的变化。此对象是k8s中确认一个Volume可进行Attach附件的重要标志 一旦发现VolumeAttachment对象,它就会调用CSI Controller的ControllerPublish方法,完成对应Volume的Attach阶段

CSI三个服务

  1. CSI Identity:注册插件到 Driver Registrar,负责对外暴露插件本身的信息
  2. CSI Controller:定义对CSI Volume(对应PV)的管理接口。它们无需要在宿主机上进行,而是属于Volume Controller的逻辑,属于Master节点的一部分
  3. CSI Node:在宿主机上执行的操作

流程:

  1. AttachDetachContrller需要执行Attach操作时,会执行pkg/volume/csi目录中,创建一个VolumeAttachment对象,进而触发External Attacher调用CSI Contrller服务的ControllerPublisherVolume方法
  2. VolumeManagerReconciler需要Mount操作时,它实际上也会到png/volume/csi目录中,直接向CSI Node服务发起NodePublishVolume方法的请求

# 31 CSI插件编写指南

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: db-block-storage
  namespace: kubee-system
  annotations:
    # 作为默认的持久化存储提供者
    storageclass.kubernetes.io/is-default-class: true
# 使用com.digitalocean.csi.dobs的csi插件处理这个SC在的所有操作
provisioner: com.digitalocean.csi.dobs
1
2
3
4
5
6
7
8
9
10

代码结构

tree $GOPATH/src/github.com/digitalocean/csi-digitalocean/driver  
$GOPATH/src/github.com/digitalocean/csi-digitalocean/driver 
├── controller.go
├── driver.go
├── identity.go
├── mounter.go
└── node.go
1
2
3
4
5
6
7

接口定义地址:https://github.com/container-storage-interface/spec/blob/master/csi.proto

在定义 DaemonSet Pod 的时候,我们需要把宿主机的 /var/lib/kubelet 以 Volume 的方式挂载进 CSI 插件容器的同名目录下,然后设置这个 Volume 的 mountPropagation=Bidirectional,即开启双向挂载传播,从而将容器在这个目录下进行的挂载操作“传播”给宿主机,反之亦然。

上次更新: : 7 months ago