PV/PVC/SC

存储.png

PV

  • 持久化存储数据卷
    • 目录里的内容既不会因为容器的删除而被清理掉,支持pod重建
    • 也不会跟当前宿主机绑定,支持pod漂移到其他node
  • 主要定义的是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录
  • PV描述的是一个具体的volume的属性
    • 类型
    • 挂载目录
    • 远程服务地址
  • 通常由运维人员创建 ```yaml

apiVersion: v1 kind: PersistentVolume metadata: name: nfs spec: storageClassName: manual capacity: storage: 1Gi accessModes:

  1. - ReadWriteMany

nfs: server: 10.244.1.4 path: “/“


- 具体实现方式
   - 远程文件存储(NFS、GlusterFS)
   - 远程块存储(公有云提供的远程磁盘)
- 具体实现步骤(两阶段处理)
   - Attach
      - pod调度到node上,kubelet会为其准备volume目录,默认目录如下

/var/lib/kubelet/pods//volumes/kubernetes.io~/


      - 根据volume类型进行操作
         - 如果是远程块存储,需要调用远程API创建对应资源,然后进行格式化后挂在到宿主机
         - 如果是远程文件存储,可以跳过attach阶段,直接mount
```shell

# 通过lsblk命令获取磁盘设备ID
$ sudo lsblk
# 格式化成ext4格式
$ sudo mkfs.ext4 -m 0 -F -E lazy_itable_init=0,lazy_journal_init=0,discard /dev/<磁盘设备ID>
# 挂载到挂载点
$ sudo mkdir -p /var/lib/kubelet/pods/<Pod的ID>/volumes/kubernetes.io~<Volume类型>/<Volume名字>
  • Mount ```shell

$ mount -t nfs :/ /var/lib/kubelet/pods//volumes/kubernetes.io~/


- 管理方式
   - 人工创建PV StaticProvisioning
   - 自动创建PV DynamicProvisioning,主要通过SC来实现
<a name="bej0X"></a>
## PVC

- pod希望使用的持久化存储的属性,比如volume的大小、读写权限等
- 通常由开发人员创建(PVC对象或PVC模版的方式成为StatefulSet的一部分由SS控制)
```yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs
spec:
  accessModes:
    - ReadWriteMany
  storageClassName: manual
  resources:
    requests:
      storage: 1Gi

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

PV和PVC

  • 绑定
    • PV和PVC的spec字段要匹配
    • PV和PVC的storageClassName字段要一致
    • 过程
      • PVC(PersistentVolumeController)不断检查每一个PVC(PersistentVolumeClaim)是否处于Bound状态
      • 如果不是就遍历所有的、可用的PV并尝试绑定
      • 绑定具体做的就是将PV的名字写在了PVC对象的spec.volumeName字段上
      • 所以只要获取到PVC就一定能找到绑定的PV
  • 面向对象思想

    • PVC是接口,提供描述,不负责实现
    • PV是实现,提供具体的资源

      SC

  • StorageClass就是创建PV的模版,主要定义两部分内容

    • PV属性parameters(存储类型type、volume大小等)
    • 创建PV需要的插件provisioner(ceph等) ```yaml

apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: block-service provisioner: kubernetes.io/gce-pd parameters: type: pd-ssd

```yaml

apiVersion: ceph.rook.io/v1beta1
kind: Pool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: block-service
provisioner: ceph.rook.io/block
parameters:
  pool: replicapool
  #The value of "clusterNamespace" MUST be the same as the one in which your rook cluster exist
  clusterNamespace: rook-ceph
  • SC并不专门为了DynamicProvisioning而设计的,集群里如果没有manual的SC,k8s就会进行StaticProvisioning
  • 集群如果开启了名叫DefaultStorageClass的AdmissionPlugin,就会为PVC和PV自动添加一个默认的SC,否则PVC的SCN的值就是空字符””,也就只能和SCN是””的PV进行绑定

    本地持久化卷

    背景

  • 用户希望使用宿主机上的本地磁盘目录,而不依赖于远程存储服务来提供持久化的容器volume

  • 好处就是读写性能高于网络服务
  • k8s因此实现了LocalPersistentVolume,适用范围比较固定

    • 高优先级的系统应用
    • 需要在多个节点存储数据
    • 对IO敏感
    • 一旦节点宕机不能恢复时,数据就可能丢失,需要应用能够具备数据备份和恢复的能力
    • 典型的应用
      • 分布式数据存储MongoDB、Cassandra等
      • 分布式文件系统GlusterFS、Ceph等

        LPV难点

  • 如何把本地磁盘抽象成PV

    • 不能把宿主机上的目录当作PV使用
      • 存储行为完全不可控,随时可能被写满造成宿主机宕机
      • 不同本地目录之间缺乏最基础的IO隔离机制
    • 一个PV一块盘,应该是和宿主机根目录隔离的独立的磁盘或块设备
  • 如何保证pod始终能被正确的调度到它所请求的LPV所在的节点上

    • 常规的PV都是先调度到某个节点,然后两阶段处理
    • LPV都是运维人员提前准备好的,在调度时需要考虑LPV的分布
    • 主要是通过nodeAffinity和VolumeBindingChecker调度器过滤匹配

      LPV实践

  • 宿主机挂载并格式化一个本地磁盘

  • 宿主机挂载几个RAM Disk内存盘模拟本地磁盘 ```shell

在node-1上执行

$ mkdir /mnt/disks $ for vol in vol1 vol2 vol3; do mkdir /mnt/disks/$vol mount -t tmpfs $vol /mnt/disks/$vol done

```yaml

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:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node-1

$ kubectl create -f local-pv.yaml 
persistentvolume/example-pv created

$ kubectl get pv
NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY  STATUS      CLAIM             STORAGECLASS    REASON    AGE
example-pv   5Gi        RWO            Delete           Available                     local-storage             16s

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: local-storage
provisioner: kubernetes.io/no-provisioner # LPV不支持DynamicProvisioning,没办法动态创建PV,所以需要前面手动创建
volumeBindingMode: WaitForFirstConsumer # 延迟绑定,到第一个声明使用该PVC的Pod出现在调度器后再统一考虑

$ kubectl create -f local-sc.yaml 
storageclass.storage.k8s.io/local-storage created

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: example-local-claim
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: local-storage

$ 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

kind: Pod
apiVersion: v1
metadata:
  name: example-pv-pod
spec:
  volumes:
    - name: example-pv-storage
      persistentVolumeClaim:
       claimName: example-local-claim
  containers:
    - name: example-pv-container
      image: nginx
      ports:
        - containerPort: 80
          name: "http-server"
      volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: example-pv-storage

$ kubectl create -f local-pod.yaml 
pod/example-pv-pod created

$ kubectl get pvc
NAME                  STATUS    VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGE
example-local-claim   Bound     example-pv   5Gi        RWO            local-storage   6h


$ kubectl exec -it example-pv-pod -- /bin/sh
# cd /usr/share/nginx/html
# touch test.txt


# 在node-1上
$ ls /mnt/disks/vol1
test.txt


$ kubectl delete -f local-pod.yaml 

$ kubectl create -f local-pod.yaml 

$ kubectl exec -it example-pv-pod -- /bin/sh
# ls /usr/share/nginx/html

LPV总结

  • SS可以通过声明LPV来管理应用的存储状态
  • 删除LPV的流程
  1. 删除使用这个LPV的Pod
  2. 宿主机移除本地磁盘(umount)
  3. 删除PVC
  4. 删除PV
  • 手动操作LPV比较繁琐,提供了StaticProvisioner来帮助管理
    • 比如所有的磁盘都挂载在/mnt/disks目录下
    • 通过StaticProvisioner配置文件指定参数(上面的磁盘挂载点、SC名字等)
    • StaticProvisioner启动后通过DeamonSet自动检查每个宿主机的/mnt/disks目录,调用k8sAPI创建PV ```shell

$ kubectl get pv NAME CAPACITY ACCESSMODES RECLAIMPOLICY STATUS CLAIM STORAGECLASS REASON AGE local-pv-ce05be60 1024220Ki RWO Delete Available local-storage 26s

$ kubectl describe pv local-pv-ce05be60 Name: local-pv-ce05be60 … StorageClass: local-storage Status: Available Claim:
Reclaim Policy: Delete Access Modes: RWO Capacity: 1024220Ki NodeAffinity: Required Terms: Term 0: kubernetes.io/hostname in [node-1] Message: Source: Type: LocalVolume (a persistent volume backed by local storage on a node) Path: /mnt/disks/vol1

<a name="t2exd"></a>
# FlexVolume/CSI

- FlexVolume每一次对插件可执行文件的调用都是一次完全独立的操作,只能把信息写在宿主机上的临时文件,需要时从文件读取
- CSI更加完善、编程更友好,开发存储插件推荐方式
<a name="ZPxGw"></a>
## FlexVolume
<a name="zo8CF"></a>
### FlexVolumeNFSPV
```yaml

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-flex-nfs
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteMany
  flexVolume:
    driver: "k8s/nfs" #vendor/driver
    fsType: "nfs"
    options:
      server: "10.10.0.25" # 改成你自己的NFS服务器地址
      share: "export"

插件执行的钩子

  • init
  • attach
  • dettach
  • mount
  • unmount

    插件执行流程

  • demo 👉 链接 ```shell

kubelet —> pkg/volume/flexvolume.SetUpAt() —> /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount

<a name="jnNlt"></a>
## CSI

- 把插件的职责从两阶段处理,扩展成Provision、Attach、Mount三个阶段
   - Provision等价于创建磁盘
   - Attach等价于挂载磁盘到虚拟机
   - Mount等价于将磁盘格式化后挂载在Volume的宿主机目录上
<a name="9oAp3"></a>
### 存储插件原理
![CSI原理图.png](https://cdn.nlark.com/yuque/0/2020/png/1491874/1602299852621-b8a76fe8-0569-4f01-9c70-5d6b975dff30.png#align=left&display=inline&height=946&margin=%5Bobject%20Object%5D&name=CSI%E5%8E%9F%E7%90%86%E5%9B%BE.png&originHeight=946&originWidth=1212&size=61143&status=done&style=none&width=1212)
<a name="iUtpN"></a>
### 设计思想

- 把provision阶段以及k8s里的一部分存储管理功能,从主干代码剥离出来,做成几个单独组件
- 这些组件通过watch API监听k8s里与存储相关的事件变化(pvc创建)来执行具体的存储管理动作
- 这些管理动作(attach/mount)就是通过调用CSI插件来完成的
- 一个CSI插件只有一个二进制文件,但会以gRPC方式对外提供三个服务gRPC Service
   - CSI Identity
   - CSI Controller
   - CSI Node

![设计思想图.png](https://cdn.nlark.com/yuque/0/2020/png/1491874/1602300155289-c8012c28-d360-4cef-bce7-60b3c827fae5.png#align=left&display=inline&height=941&margin=%5Bobject%20Object%5D&name=%E8%AE%BE%E8%AE%A1%E6%80%9D%E6%83%B3%E5%9B%BE.png&originHeight=941&originWidth=1880&size=122870&status=done&style=none&width=1880)

- ExternalComponents
   - DriverRegistrar
      - 负责将插件注册到kubelet里面(把可执行文件放在插件目录下)
      - 请求CSI的Identity服务来获取插件信息
   - ExternalProvisioner
      - 负责provision阶段
      - 监听APIServer里的PVC对象,当一个PVC被创建时就会调用CSI Controller的CreateVolume方法创建PV
      - CSI插件是独立于k8s之外的,不会直接使用k8s定义的PV,而是会自己定义一个单独的Volume类型CSI Volume
   - ExternalAttacher
      - 负责attach阶段
      - 监听APIServer里的VolumeAttachment对象的变化,确定一个volume是否可以进入attach阶段
      - 一旦出现了VolumeAttachment对象,Attacher就会调用CSIController服务的ControllerPublish方法完成所对应的volume的attach阶段
- Volume的Mount阶段不属于ExternalComponents的职责
   - kubelet检测到需要执行mount操作的时候,会通过pkg/volume/csi包直接调用CSI Node服务完成mount
- 在实际使用CSI插件时将三个ExternalComponents作为sidecar容器和CSI插件放在同一个Pod中提高效率
- CustomComponents
   - CSI Identity
      - 负责对外暴露这个插件本身的信息
   - CSI Controller
      - 定义的是对CSI Volume(对应k8s里的PV)的管理接口
         - 创建和删除 CSI Volume
         - 对CSI Volume进行Attach和Dettach(Publish/Unpublish)
         - 对CSI Volume进行Snapshot
      - 这些接口都无需在宿主机上进行,而是属于k8s里VolumeController的逻辑,属于master的一部分
      - CSI Controller服务的调用者并不是k8s,而是ExternalProvisioner和ExternalAttacher,这2个组件分别通过监听PVC和VolumeAttachment对象来跟k8s进行协作
   - CSI Node
      - CSI Volume需要在宿主机上执行的操作都定义在CSI Node服务里
      - Mount阶段在CSI Node里的接口是由NodeStageVolume和NodePublishVolume两个接口共同实现

<a name="dTuHr"></a>
# CSI插件编写指南

- 选择了DigitalOcean的块存储服务(BlockStorage)<br />
<a name="vRkGn"></a>
## 使用插件
```yaml

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: do-block-storage
  namespace: kube-system
  annotations:
    storageclass.kubernetes.io/is-default-class: "true" #使用这个SC作为默认的持久化存储的提供者
provisioner: com.digitalocean.csi.dobs # 通过CSI Identity提供插件信息
  • CSI体系中DynamicProvisioning的实现方式
    • 有了这个SC,ExternalProvisioner就会为集群中新出现的PVC自动创建出PV
    • 然后调用CSI插件创建出这个PV对应的Volume

      编写插件

      ```shell

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


- driver.go里定义gRPC Server
```go

// Run starts the CSI plugin by communication over the given endpoint
func (d *Driver) Run() error {
 ...

 listener, err := net.Listen(u.Scheme, addr)
 ...

 d.srv = grpc.NewServer(grpc.UnaryInterceptor(errHandler))
 csi.RegisterIdentityServer(d.srv, d)
 csi.RegisterControllerServer(d.srv, d)
 csi.RegisterNodeServer(d.srv, d)

 d.ready = true // we're now ready to go!
 ...
 return d.srv.Serve(listener)
}
  • CSI Identity服务中最重要的接口是GetPluginInfo,返回的就是这个插件的名字和版本号
    • GetPluginCapabilities接口返回插件的能力
      • 比如不实现Provision阶段和Attach阶段的NFS插件就通过此接口返回不支持
    • Probe接口被k8s调用来检查CSI插件是否正常工作
      • 编写插件时设置一个Ready标志
      • 当插件的gRPC Server停止的时候把这个标志设置为false作为健康检查 ```go

func (d Driver) GetPluginInfo(ctx context.Context, req csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) { resp := &csi.GetPluginInfoResponse{ Name: driverName, // SS里声明的CSI插件名字,遵守反向DNS格式 VendorVersion: version, } … }


- CSI Controller服务主要实现Provision和Attach
   - Provision 调用者是ExternalProvisoner
      - CreateVolume接口
      - DeleteVolume接口
```go

func (d *Driver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
 ...

 volumeReq := &godo.VolumeCreateRequest{
  Region:        d.region,
  Name:          volumeName,
  Description:   createdByDO,
  SizeGigaBytes: size / GB,
 }

 ...

 vol, _, err := d.doClient.Storage.CreateVolume(ctx, volumeReq)

 ...

 resp := &csi.CreateVolumeResponse{
  Volume: &csi.Volume{
   Id:            vol.ID,
   CapacityBytes: size,
   AccessibleTopology: []*csi.Topology{
    {
     Segments: map[string]string{
      "region": d.region,
     },
    },
   },
  },
 }

 return resp, nil
}
  • Attach 调用者是ExternalAttacher
    • ControllerPublishVolume
    • ControllerUnpublishVolume ```go

func (d Driver) ControllerPublishVolume(ctx context.Context, req csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) { …

dropletID, err := strconv.Atoi(req.NodeId)

// check if volume exist before trying to attach it _, resp, err := d.doClient.Storage.GetVolume(ctx, req.VolumeId)

// check if droplet exist before trying to attach the volume to the droplet _, resp, err = d.doClient.Droplets.Get(ctx, dropletID)

action, resp, err := d.doClient.StorageActions.Attach(ctx, req.VolumeId, dropletID)

if action != nil { ll.Info(“waiting until volume is attached”) if err := d.waitAction(ctx, req.VolumeId, action.ID); err != nil { return nil, err } }

ll.Info(“volume is attached”) return &csi.ControllerPublishVolumeResponse{}, nil }


- CSI Node服务主要实现Mount 
   - 两个接口
      - NodeStageVolume 格式化volume在宿主机上对应的存储设备,然后挂载到一个临时目录Staging上
      - NodePublishVolume 将Staging目录绑定挂载到volume对应的宿主机目录上
   - kubelet的VolumeManagerReconciler控制循环中分别对应这两个的实现
      - MountDevice
      - SetUp
   - 对于文件系统类型的存储服务(NFS/GlusterFS)来说,并没有一个对应的磁盘设备存在于宿主机上,所以在VolumeManagerReconciler控制循环中会跳过MountDevice的操作而直接进行SetUp
```go

func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
 ...

 vol, resp, err := d.doClient.Storage.GetVolume(ctx, req.VolumeId)

 ...

 source := getDiskSource(vol.Name)
 target := req.StagingTargetPath

 ...

 if !formatted {
  ll.Info("formatting the volume for staging")
  if err := d.mounter.Format(source, fsType); err != nil {
   return nil, status.Error(codes.Internal, err.Error())
  }
 } else {
  ll.Info("source device is already formatted")
 }

...

 if !mounted {
  if err := d.mounter.Mount(source, target, fsType, options...); err != nil {
   return nil, status.Error(codes.Internal, err.Error())
  }
 } else {
  ll.Info("source device is already mounted to the target path")
 }

 ...
 return &csi.NodeStageVolumeResponse{}, nil
}


func (d *Driver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
 ...
 source := req.StagingTargetPath
 target := req.TargetPath

 mnt := req.VolumeCapability.GetMount()
 options := mnt.MountFlag
    ...

 if !mounted {
  ll.Info("mounting the volume")
  if err := d.mounter.Mount(source, target, fsType, options...); err != nil {
   return nil, status.Error(codes.Internal, err.Error())
  }
 } else {
  ll.Info("volume is already mounted")
 }

 return &csi.NodePublishVolumeResponse{}, nil
}

部署插件

  • 首先创建一个DigitalOcean Client授权需要的Secret对象 ```yaml

apiVersion: v1 kind: Secret metadata: name: digitalocean namespace: kube-system stringData: access-token: “a05dd2f26b9b9ac2asdasREPLACEME_123cb5d1ec17513e06da”


- 部署CSI
```shell

$ kubectl apply -f https://raw.githubusercontent.com/digitalocean/csi-digitalocean/master/deploy/kubernetes/releases/csi-digitalocean-v0.2.0.yaml

kind: DaemonSet
apiVersion: apps/v1beta2
metadata:
  name: csi-do-node
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app: csi-do-node
  template:
    metadata:
      labels:
        app: csi-do-node
        role: csi-do
    spec:
      serviceAccount: csi-do-node-sa
      hostNetwork: true
      containers:
        - name: driver-registrar
          image: quay.io/k8scsi/driver-registrar:v0.3.0
          ...
        - name: csi-do-plugin
          image: digitalocean/do-csi-plugin:v0.2.0
          args :
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--token=$(DIGITALOCEAN_ACCESS_TOKEN)"
            - "--url=$(DIGITALOCEAN_API_URL)"
          env:
            - name: CSI_ENDPOINT
              value: unix:///csi/csi.sock
            - name: DIGITALOCEAN_API_URL
              value: https://api.digitalocean.com/
            - name: DIGITALOCEAN_ACCESS_TOKEN
              valueFrom:
                secretKeyRef:
                  name: digitalocean
                  key: access-token
          imagePullPolicy: "Always"
          securityContext:
            privileged: true
            capabilities:
              add: ["SYS_ADMIN"]
            allowPrivilegeEscalation: true
          volumeMounts:
            - name: plugin-dir
              mountPath: /csi
            - name: pods-mount-dir
              mountPath: /var/lib/kubelet
              mountPropagation: "Bidirectional"
            - name: device-dir
              mountPath: /dev
      volumes:
        - name: plugin-dir
          hostPath:
            path: /var/lib/kubelet/plugins/com.digitalocean.csi.dobs
            type: DirectoryOrCreate
        - name: pods-mount-dir
          hostPath:
            path: /var/lib/kubelet
            type: Directory
        - name: device-dir
          hostPath:
            path: /dev
---
kind: StatefulSet
apiVersion: apps/v1beta1
metadata:
  name: csi-do-controller
  namespace: kube-system
spec:
  serviceName: "csi-do"
  replicas: 1
  template:
    metadata:
      labels:
        app: csi-do-controller
        role: csi-do
    spec:
      serviceAccount: csi-do-controller-sa
      containers:
        - name: csi-provisioner
          image: quay.io/k8scsi/csi-provisioner:v0.3.0
          ...
        - name: csi-attacher
          image: quay.io/k8scsi/csi-attacher:v0.3.0
          ...
        - name: csi-do-plugin
          image: digitalocean/do-csi-plugin:v0.2.0
          args :
            - "--endpoint=$(CSI_ENDPOINT)"
            - "--token=$(DIGITALOCEAN_ACCESS_TOKEN)"
            - "--url=$(DIGITALOCEAN_API_URL)"
          env:
            - name: CSI_ENDPOINT
              value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
            - name: DIGITALOCEAN_API_URL
              value: https://api.digitalocean.com/
            - name: DIGITALOCEAN_ACCESS_TOKEN
              valueFrom:
                secretKeyRef:
                  name: digitalocean
                  key: access-token
          imagePullPolicy: "Always"
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/csi/sockets/pluginproxy/
      volumes:
        - name: socket-dir
          emptyDir: {}

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: csi-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: do-block-storage

部署原则

  • 通过DeamonSet在每个节点上都启动一个CSI插件,来为kubelet提供CSI Node服务
    • CSI Node服务需要被kubelet调用,所以需要一对一部署
    • 在DaemonSet定义里除了CSI插件,还以sidecar方式运行着driver-registrar,向kubelet注册插件
    • CSI Node服务在Mount阶段执行的挂载实际是发生在容器的mount namespace里,而我们希望挂载到宿主机的/var/lib/kubelet目录下,所以在定义DaemonSet Pod时需要把宿主机的/var/lib/kubelet 以Volume方式挂载进CSI插件容器的同名目录下,然后设置mountPropagation=Bidirectional,开启双向挂载传播,将容器在这个目录留下的挂载操作“传播”给宿主机,反之亦然
  • 通过SS在任意一个节点上再启动一个CSI插件,为ExternalComponents提供CSI Controller服务
    • 作为CSI Controller服务的调用者,ExternalProvisioner和ExternalAttacher这两个外部组件需要以sidecar方式和这次部署的CSI插件定义在同一个Pod中
    • 用SS来保证拓扑状态的稳定性
    • 将replicas设置为1来确保Pod被删除重建的时候永远有且只有一个CSI插件的Pod在集群中

      运作流程

  1. 集群部署CSI存储插件
  2. 用户创建PVC后,前面部署的StatefulSet里的ExternalProvisioner容器就会监听PVC的诞生,然后调用同一个Pod里的CSI插件的CSI Controller服务的CreateVolume方法创建PV
  3. 运行在Master节点上的VolumeController通过PersistentVolumeController控制循环发现新创建出来的PV和PVC并发现是同一个StorageClass就会将其Bound
  4. 用户创建了一个声明使用上述PVC的Pod,并把Pod调度到NodeA上,这时VolumeController的AttachDetachController控制循环发现PVC对应的Volume需要被 attach 到NodeA上,所以AttachDetachController就会创建一个VolumeAttachment对象,携带了NodeA和待处理的Volume名字
  5. SS里的ExternalAttacher容器监听到这个VolumenAttachment对象的诞生,就会使用这个对象里的NodeA和Volume名字调用同一个Pod里的CSI插件的CSI Controller服务的ControllerPublishVolume方法完成 attach
  6. 运行在NodeA上的kubelet通过VolumeManagerReconciler控制循环发现当前宿主机上有一个volume对应的存储设备已经被attach到了某个目录下,于是kubelet调用同一台宿主机上的CSI插件的CSI Node服务的NodeStageVolume和NodePublishVolume方法完成 mount