Kubernetes 存储管理

Overview

1. Dynamic Provisioner 例子与完整流程介绍

原理概述

在 Kubernetes 中,动态 provisioner 是一个实现了 Provisioner 接口的控制器,用于自动化存储卷的创建。当用户提交 PVC (PersistentVolumeClaim) 时,provisioner 根据定义的 StorageClass,自动创建相应的 PV (PersistentVolume)。这种自动化存储管理机制大大简化了卷的生命周期管理,减少了手动操作的复杂性。

流程概述

自定义动态 provisioner 的流程包括以下几个步骤:

  1. 创建自定义的 Provisioner 逻辑,负责监听 PVC 的创建事件,并生成 PV。
  2. 编写自定义的 StorageClass,使用该 Provisioner 动态创建卷。
  3. 编写控制器代码,处理卷的创建与删除。
  4. 部署自定义 provisioner 到 Kubernetes 集群中,并验证其功能。

步骤 1: 自定义 Provisioner 代码实现

https://github.com/ZhangSIming-blyq/custom-provisioner

首先,我们通过 Go 语言编写一个简单的自定义 provisioner,模拟卷的创建和删除过程。核心是自定义Provisioner结构体,实现Provision和Delete方法。Provision方法用于创建卷,Delete方法用于删除卷。

  1package main
  2
  3import (
  4	"context"
  5	"fmt"
  6	corev1 "k8s.io/api/core/v1"
  7	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
  8	"k8s.io/client-go/kubernetes"
  9	"k8s.io/client-go/rest"
 10	"k8s.io/klog"
 11	"os"
 12	"sigs.k8s.io/sig-storage-lib-external-provisioner/v7/controller"
 13)
 14
 15type customProvisioner struct {
 16	// Define any dependencies that your provisioner might need here, here I use the kubernetes client
 17	client kubernetes.Interface
 18}
 19
 20// NewCustomProvisioner creates a new instance of the custom provisioner
 21func NewCustomProvisioner(client kubernetes.Interface) controller.Provisioner {
 22	// customProvisioner needs to implement "Provision" and "Delete" methods in order to satisfy the Provisioner interface
 23	return &customProvisioner{
 24		client: client,
 25	}
 26}
 27
 28func (p *customProvisioner) Provision(options controller.ProvisionOptions) (*corev1.PersistentVolume, controller.ProvisioningState, error) {
 29	// Validate the PVC spec, 0 storage size is not allowed
 30	requestedStorage := options.PVC.Spec.Resources.Requests[corev1.ResourceStorage]
 31	if requestedStorage.IsZero() {
 32		return nil, controller.ProvisioningFinished, fmt.Errorf("requested storage size is zero")
 33	}
 34
 35	// If no access mode is specified, return an error
 36	if len(options.PVC.Spec.AccessModes) == 0 {
 37		return nil, controller.ProvisioningFinished, fmt.Errorf("access mode is not specified")
 38	}
 39
 40	// Generate a unique name for the volume using the PVC namespace and name
 41	volumeName := fmt.Sprintf("pv-%s-%s", options.PVC.Namespace, options.PVC.Name)
 42
 43	// Check if the volume already exists
 44	volumePath := "/tmp/dynamic-volumes/" + volumeName
 45	if _, err := os.Stat(volumePath); !os.IsNotExist(err) {
 46		return nil, controller.ProvisioningFinished, fmt.Errorf("volume %s already exists at %s", volumeName, volumePath)
 47	}
 48
 49	// Create the volume directory
 50	if err := os.MkdirAll(volumePath, 0755); err != nil {
 51		return nil, controller.ProvisioningFinished, fmt.Errorf("failed to create volume directory: %v", err)
 52	}
 53
 54	// Based on the above checks, we can now create the PV, HostPath is used as the volume source
 55	pv := &corev1.PersistentVolume{
 56		ObjectMeta: metav1.ObjectMeta{
 57			Name: volumeName,
 58		},
 59		Spec: corev1.PersistentVolumeSpec{
 60			Capacity: corev1.ResourceList{
 61				corev1.ResourceStorage: options.PVC.Spec.Resources.Requests[corev1.ResourceStorage],
 62			},
 63			AccessModes:                   options.PVC.Spec.AccessModes,
 64			PersistentVolumeReclaimPolicy: corev1.PersistentVolumeReclaimDelete,
 65			PersistentVolumeSource: corev1.PersistentVolumeSource{
 66				HostPath: &corev1.HostPathVolumeSource{
 67					Path: volumePath,
 68				},
 69			},
 70		},
 71	}
 72
 73	// Return the PV, ProvisioningFinished and nil error to indicate success
 74	klog.Infof("Successfully provisioned volume %s for PVC %s/%s", volumeName, options.PVC.Namespace, options.PVC.Name)
 75	return pv, controller.ProvisioningFinished, nil
 76}
 77
 78func (p *customProvisioner) Delete(volume *corev1.PersistentVolume) error {
 79	// Validate whether the volume is a HostPath volume
 80	if volume.Spec.HostPath == nil {
 81		klog.Infof("Volume %s is not a HostPath volume, skipping deletion.", volume.Name)
 82		return nil
 83	}
 84
 85	// Get the volume path
 86	volumePath := volume.Spec.HostPath.Path
 87
 88	// Check if the volume path exists
 89	if _, err := os.Stat(volumePath); os.IsNotExist(err) {
 90		klog.Infof("Volume path %s does not exist, nothing to delete.", volumePath)
 91		return nil
 92	}
 93
 94	// Delete the volume directory, using os.RemoveAll to delete the directory and its contents
 95	klog.Infof("Deleting volume %s at path %s", volume.Name, volumePath)
 96	if err := os.RemoveAll(volumePath); err != nil {
 97		klog.Errorf("Failed to delete volume %s at path %s: %v", volume.Name, volumePath, err)
 98		return err
 99	}
100
101	klog.Infof("Successfully deleted volume %s at path %s", volume.Name, volumePath)
102	return nil
103}
104
105func main() {
106	// Use "InClusterConfig" to create a new clientset
107	config, err := rest.InClusterConfig()
108	if err != nil {
109		klog.Fatalf("Failed to create in-cluster config: %v", err)
110	}
111
112	clientset, err := kubernetes.NewForConfig(config)
113	if err != nil {
114		klog.Fatalf("Failed to create clientset: %v", err)
115	}
116
117	provisioner := NewCustomProvisioner(clientset)
118
119	// Important!! Create a new ProvisionController instance and run it; Once user creates a PVC, it would find the provisioner via storageClass's field "provisioner".
120	pc := controller.NewProvisionController(clientset, "custom-provisioner", provisioner, controller.LeaderElection(false))
121	klog.Infof("Starting custom provisioner...")
122	pc.Run(context.Background())
123}

步骤 2: 部署自定义 Provisioner 到 Kubernetes

构建 Docker 镜像:

首先,我们将上述代码打包成 Docker 镜像,以下是一个简单的 Dockerfile:

1FROM golang:1.23 as builder
2WORKDIR /workspace
3COPY . .
4WORKDIR /workspace/cmd/
5RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o custom-provisioner .
6
7FROM alpine:3.14
8COPY --from=builder /workspace/cmd/custom-provisioner /custom-provisioner
9ENTRYPOINT ["/custom-provisioner"]

创建自定义 Provisioner 的 Deployment:

这里因为我们要使用HostPath的tmp目录,所以需要在Deployment中挂载/tmp目录。

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: custom-provisioner
 5spec:
 6  replicas: 1
 7  selector:
 8    matchLabels:
 9      app: custom-provisioner
10  template:
11    metadata:
12      labels:
13        app: custom-provisioner
14    spec:
15      containers:
16        - name: custom-provisioner
17          image: siming.net/sre/custom-provisioner:main_dc62f09_2024-09-29-010124
18          imagePullPolicy: IfNotPresent
19          env:
20            - name: POD_NAMESPACE
21              valueFrom:
22                fieldRef:
23                  fieldPath: metadata.namespace
24            - name: PROVISIONER_NAME
25              value: custom-provisioner
26          volumeMounts:
27            - mountPath: /tmp
28              name: tmp-dir
29      volumes:
30        - name: tmp-dir
31          hostPath:
32            path: /tmp
33            type: Directory

步骤 3: 创建 StorageClass

1apiVersion: storage.k8s.io/v1
2kind: StorageClass
3metadata:
4  name: custom-storage
5provisioner: custom-provisioner
6parameters:
7  type: custom

步骤 4: RBAC 权限配置

 1apiVersion: rbac.authorization.k8s.io/v1
 2kind: ClusterRole
 3metadata:
 4  name: custom-provisioner-role
 5rules:
 6  - apiGroups: [""]
 7    resources: ["persistentvolumes", "persistentvolumeclaims"]
 8    verbs: ["get", "list", "watch", "create", "delete", "update"]
 9  - apiGroups: ["storage.k8s.io"]
10    resources: ["storageclasses"]
11    verbs: ["get", "list", "watch"]
12  - apiGroups: [""]
13    resources: ["events"]
14    verbs: ["create", "patch"]
15
16---
17
18apiVersion: rbac.authorization.k8s.io/v1
19kind: ClusterRoleBinding
20metadata:
21  name: custom-provisioner-binding
22roleRef:
23  apiGroup: rbac.authorization.k8s.io
24  kind: ClusterRole
25  name: custom-provisioner-role
26subjects:
27  - kind: ServiceAccount
28    name: default
29    namespace: system

步骤 5: 创建 PVC 来触发 Provisioner

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

当 PVC 被创建时,Kubernetes 将触发自定义 provisioner 来创建并绑定卷。

 1# 成功部署custom-provisioner, 并且pod本地的tmp目录作为存储目录
 2kp
 3NAME                                  READY   STATUS    RESTARTS   AGE
 4controller-manager-578b69d9d4-t228b   1/1     Running   0          11d
 5custom-provisioner-58d77856f9-4m4l8   1/1     Running   0          3m19s
 6
 7# 创建pvc后查看日志
 8k logs -f custom-provisioner-58d77856f9-4m4l8
 9I0928 17:25:36.663192       1 main.go:121] Starting custom provisioner...
10I0928 17:25:36.663264       1 controller.go:810] Starting provisioner controller custom-provisioner_custom-provisioner-58d77856f9-4m4l8_6328a9e1-cdeb-4088-b8e2-c83b140429e3!
11I0928 17:25:36.764192       1 controller.go:859] Started provisioner controller custom-provisioner_custom-provisioner-58d77856f9-4m4l8_6328a9e1-cdeb-4088-b8e2-c83b140429e3!
12I0928 17:25:56.495556       1 controller.go:1413] delete "pv-system-custom-pvc": started
13I0928 17:25:56.495588       1 main.go:90] Volume path /tmp/dynamic-volumes/pv-system-custom-pvc does not exist, nothing to delete.
14I0928 17:25:56.495598       1 controller.go:1428] delete "pv-system-custom-pvc": volume deleted
15I0928 17:25:56.502579       1 controller.go:1478] delete "pv-system-custom-pvc": persistentvolume deleted
16I0928 17:25:56.502604       1 controller.go:1483] delete "pv-system-custom-pvc": succeeded
17I0928 17:26:13.279654       1 controller.go:1279] provision "system/custom-pvc" class "custom-storage": started
18I0928 17:26:13.279822       1 main.go:74] Successfully provisioned volume pv-system-custom-pvc for PVC system/custom-pvc
19I0928 17:26:13.279839       1 controller.go:1384] provision "system/custom-pvc" class "custom-storage": volume "pv-system-custom-pvc" provisioned
20I0928 17:26:13.279855       1 controller.go:1397] provision "system/custom-pvc" class "custom-storage": succeeded
21I0928 17:26:13.279891       1 volume_store.go:212] Trying to save persistentvolume "pv-system-custom-pvc"
22I0928 17:26:13.280969       1 event.go:377] Event(v1.ObjectReference{Kind:"PersistentVolumeClaim", Namespace:"system", Name:"custom-pvc", UID:"8dc69c67-609b-48aa-b5d7-ae932f91a8d7", APIVersion:"v1", ResourceVersion:"3337346", FieldPath:""}): type: 'Normal' reason: 'Provisioning' External provisioner is provisioning volume for claim "system/custom-pvc"
23I0928 17:26:13.297182       1 volume_store.go:219] persistentvolume "pv-system-custom-pvc" saved
24I0928 17:26:13.297444       1 event.go:377] Event(v1.ObjectReference{Kind:"PersistentVolumeClaim", Namespace:"system", Name:"custom-pvc", UID:"8dc69c67-609b-48aa-b5d7-ae932f91a8d7", APIVersion:"v1", ResourceVersion:"3337346", FieldPath:""}): type: 'Normal' reason: 'ProvisioningSucceeded' Successfully provisioned volume pv-system-custom-pvc
25
26# 查看pvc
27k get pvc 
28NAME         STATUS   VOLUME                 CAPACITY   ACCESS MODES   STORAGECLASS     AGE
29custom-pvc   Bound    pv-system-custom-pvc   1Gi        RWO            custom-storage   2m51s
30
31# 已经动态绑定好pv
32k get pv
33NAME                   CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM               STORAGECLASS     REASON   AGE
34pv-system-custom-pvc   1Gi        RWO            Delete           Bound    system/custom-pvc   custom-storage            2m53s
35
36# 目录也成功创建
37ls /tmp/dynamic-volumes/  
38pv-system-custom-pvc

2. StorageClass 所有字段及其功能介绍

StorageClass 示例 YAML

 1apiVersion: storage.k8s.io/v1
 2kind: StorageClass
 3metadata:
 4  name: custom-storage
 5provisioner: example.com/custom-provisioner
 6parameters:
 7  type: fast
 8  zone: us-east-1
 9  replication-type: none
10reclaimPolicy: Delete
11volumeBindingMode: WaitForFirstConsumer
12allowVolumeExpansion: true
13mountOptions:
14  - discard
15  - nobarrier

字段解释及控制逻辑

1. provisioner

  • 功能:

    • provisioner 字段指定了用于动态创建卷的 provisioner。它决定了 Kubernetes 如何与外部存储系统交互。不同的 provisioner 可以对接不同类型的存储系统(如 AWS EBS、GCE Persistent Disks、自定义的 provisioner)。
  • 受控组件:

    • kube-controller-manager 中的 PersistentVolume controller 负责根据该字段选择合适的 provisioner 实现,并向其发出创建卷的请求。该字段的值会与定义在集群中的 provisioner 匹配,例如 example.com/custom-provisioner

2. parameters

  • 功能:

    • parameters 字段用于向 provisioner 传递自定义参数。这些参数可以根据存储系统的特性进行定制。例如,type 参数可以定义存储类型(如高性能或标准存储),zone 参数可以定义存储卷的区域,replication-type 可以定义数据是否有复制策略。
  • 受控组件:

    • 由 provisioner 自身解析并使用这些参数,在创建 PV 时根据传递的参数设置存储卷的属性。例如,如果 provisioner 是自定义的,它需要解释 parameters 中的内容,并与存储系统交互以执行卷的创建。

3. reclaimPolicy

  • 功能:

    • 定义了当 PVC 被删除时,PV 的行为。选项包括:
      • Retain: 卷不会被删除,数据保留。
      • Delete: 卷会被删除,存储资源也会被释放。
      • Recycle: 卷会被擦除并返回到未绑定状态(在 Kubernetes 1.9 之后已废弃)。
  • 受控组件:

    • kube-controller-manager 中的 PersistentVolume controller 管理。当 PVC 释放后,controller 根据 reclaimPolicy 执行相应的删除或保留操作。

4. volumeBindingMode

  • 功能:

    • 决定了 PVC 何时绑定到 PV。选项包括:
      • Immediate: PVC 提交时立即绑定到可用的 PV。
      • WaitForFirstConsumer: 仅在 Pod 被调度时才绑定 PV。这可以避免资源分配的不平衡问题,特别适用于多可用区环境下存储和计算资源的协同调度。先调度后绑定这种延迟绑定方式在pv有多种选择的时候,先根据pod的需求选择pv,避免调度冲突(pv调度和pod调度冲突)
  • 受控组件:

    • kube-scheduler 负责在 WaitForFirstConsumer 模式下,根据 Pod 调度的节点选择合适的 PV,并执行 PVC 的绑定。在 Immediate 模式下,PVC 和 PV 的绑定则由 kube-controller-manager 中的 PersistentVolume controller 处理。

5. allowVolumeExpansion

  • 功能:

    • 当该字段设置为 true 时,允许用户动态扩展已经绑定的卷的大小。如果 PVC 需要更多存储空间,可以通过修改 PVC 的规格来触发卷扩展。
  • 受控组件:

    • kube-controller-manager 中的 ExpandController 负责处理卷扩展请求。当 PVC 被修改以请求更大的存储容量时,该 controller 会相应地对底层存储执行扩展操作,具体依赖于 storage provider 是否支持卷扩展

6. mountOptions

  • 功能:

    • 定义卷在挂载时的选项。例如,在上述示例中,discard 选项表示在卷删除时自动丢弃数据,nobarrier 选项用于提高写入性能。这些选项会影响卷的使用方式。
  • 受控组件:

    • kubelet 负责在节点上处理挂载卷的操作。kubelet 在实际挂载卷到 Pod 时,会使用 StorageClass 中定义的挂载选项。

字段与 Controller 交互表格总结

字段 作用 受控的组件
provisioner 指定动态卷 provisioner 使用哪个驱动 kube-controller-manager 中的 PersistentVolume controller
parameters 向 provisioner 提供自定义参数 由 provisioner 自身逻辑解析
reclaimPolicy 卷删除后保留、回收或删除 kube-controller-manager 中的 PersistentVolume controller
volumeBindingMode PVC 何时绑定 PV kube-scheduler (等待消费者模式) 或 kube-controller-manager (立即绑定模式)
allowVolumeExpansion 允许扩展卷 ExpandControllerkube-controller-manager 中处理
mountOptions 卷挂载时的选项 kubelet 负责挂载时的处理

3. 整体挂载流程介绍:Attach、Detach、Mount、Unmount

Kubernetes 中的存储卷挂载流程涉及两个主要组件:ControllerManagerKubelet。每个阶段都有不同的控制器负责管理卷的挂载、卸载等操作。

1. Attach(由 ControllerManager 处理)

  • 概述: 当一个 Pod 被调度到某个节点,并且该 Pod 需要使用 PersistentVolume(如 EBS 或 GCE Persistent Disk),AttachDetachController 会将卷附加(Attach)到该节点。
  • 过程:
    1. Pod 被调度到某个节点。
    2. AttachDetachController 通过 Kubernetes API 获取 PVC 的相关信息,找到相应的 PersistentVolume。
    3. 使用 Cloud Provider 或者 CSI 驱动将卷附加到节点。(调用csi的controller的controllerPublishVolume)
    4. 一旦卷附加成功,卷的状态将更新为 “Attached”,Pod 可以继续进入挂载阶段。

2. Detach(由 ControllerManager 处理)

  • 概述: 当 Pod 被删除或调度到另一个节点时,AttachDetachController 会触发卷的卸载过程,将卷从原节点分离。
  • 过程:
    1. 当 Pod 终止时,AttachDetachController 检查卷是否仍然附加在节点上。
    2. 如果卷不再使用,控制器会通过 Cloud Provider 或 CSI 驱动将卷从节点上分离。
    3. 卷状态更新为 “Detached”,资源释放。

3. Mount(由 Kubelet 处理)

  • 概述: 当卷附加到节点后,kubelet 会将卷挂载到 Pod 的容器中,这个过程通过 VolumeManager 来管理。
  • 过程:
    1. kubelet 监控到卷已附加,准备进行挂载操作。
    2. VolumeManager 负责将卷挂载到宿主机的文件系统(如 /var/lib/kubelet/pods/... 路径)。
    3. 卷挂载完成后,卷可通过容器内的目录访问。

4. Unmount(由 Kubelet 处理)

  • 概述: 当 Pod 删除时,kubelet 会将该卷从宿主机文件系统中卸载。
  • 过程:
    1. VolumeManager 检查到卷不再使用,准备卸载。
    2. kubelet 执行卸载操作,卷从宿主机文件系统中移除。
    3. 卸载完成后,卷资源释放,Pod 生命周期结束。

Pod 磁盘挂载具体流程:

  1. 用户创建 Pod 并指定 PVC。
  2. Attach: AttachDetachController 将卷附加到节点。
  3. Mount: kubelet 将卷挂载到节点,并将其映射到 Pod 的容器中。
  4. Unmount: 当 Pod 终止时,kubelet 执行卷的卸载操作。
  5. Detach: AttachDetachController 将卷从节点分离。

要完整地理解 CSI(Container Storage Interface),我们可以通过编写一个简单的 CSI 驱动来演示其工作原理。这个例子将帮助你从基础层面理解 CSI 的各个组件:CSI IdentityCSI ControllerCSI Node

4. CSI 驱动自主实现

https://github.com/ZhangSIming-blyq/hostpathcsi

组件介绍

csi

一个 CSI 驱动包括三个主要部分:

  • CSI Identity:提供驱动信息,如名称、版本和功能。
  • CSI Controller:管理卷的生命周期(创建、删除、扩展等)。
  • CSI Node:负责将卷挂载到节点或 Pod 中。

kubernetes原生提供3个外部组件来与CSI驱动交互:

  • csi-attacher:负责将卷附加到节点。
  • csi-provisioner:负责创建和删除卷。
  • csi-driver-registrar:负责注册 CSI 驱动。

前两个要作为sidecar和csi-controller一起部署,后者作为sidecar和csi-node一起部署。都使用socket通信。

代码组织结构

 1.
 2├── LICENSE
 3├── Makefile
 4├── README.md
 5├── cmd
 6│   └── main.go
 7├── deploy
 8│   ├── Dockerfile
 9│   ├── csi-controller.yaml
10│   ├── csi-node.yaml
11│   ├── pvc.yaml
12│   └── sc.yaml
13├── go.mod
14├── go.sum
15└── pkg
16    └── hostpathcsi
17        ├── controller.go
18        ├── identity.go
19        └── node.go

1. 创建 PVC 并找到 CSI 插件处理逻辑

当用户创建一个 PVC(Persistent Volume Claim)时,Kubernetes 会根据 PVC 所指定的 StorageClass 找到对应的 CSI 插件。

StorageClass 的作用:

  • StorageClass 定义了如何动态创建存储卷。其关键字段是 provisioner,指定了由哪个 CSI 驱动来处理存储卷的创建。
  • 例如,provisioner 的值为 hostpath.csi.k8s.io,Kubernetes 会根据这个名称找到已注册的 CSI 驱动,并通过它来完成存储卷的操作。

CSI 驱动注册:

  • 在 Kubernetes 中,CSI 驱动通过 CSIDriver 对象进行注册。CSIDriver 对象保存了驱动的元数据信息,Kubernetes 通过它与 CSI 驱动进行交互。
  • 例如,csi-node-driver-registrar 是一个负责在节点上注册 CSI 驱动的组件,它确保 Kubernetes 能识别并与节点上的 CSI 驱动通信。

当 PVC 创建后,StorageClass 会告诉 Kubernetes 使用哪个 CSI 驱动来处理该卷的创建逻辑。

 1apiVersion: v1
 2kind: PersistentVolumeClaim
 3metadata:
 4  name: custom-pvc
 5spec:
 6  accessModes:
 7    - ReadWriteOnce
 8  resources:
 9    requests:
10      storage: 1Gi
11  storageClassName: custom-csi-sc  # 使用之前定义的 StorageClass
12
13---
14apiVersion: storage.k8s.io/v1
15kind: StorageClass
16metadata:
17  name: custom-csi-sc
18provisioner: hostpath.csi.k8s.io  # 注意:这里的 provisioner 名字必须和你在 CSI 驱动中的名称一致
19volumeBindingMode: Immediate       # 表示 PVC 立即绑定
20reclaimPolicy: Delete              # PVC 删除时删除卷

2. 初始化grpc服务器并上面说的三个csi服务

 1package main
 2
 3import (
 4	"github.com/ZhangSIming-blyq/hostpathcsi/pkg/hostpathcsi"
 5	"log"
 6	"net"
 7	"os"
 8
 9	"github.com/container-storage-interface/spec/lib/go/csi"
10	"google.golang.org/grpc"
11)
12
13func main() {
14	// 先删除已存在的 socket 文件,这是因为 kubelet 会在 /var/lib/kubelet/plugins/hostpath.csi.k8s.io/ 目录下创建一个 socket 文件
15	// 先删除 socket 文件是为了确保新的进程可以绑定到同样的 socket 地址,避免因为旧的 socket 文件存在导致绑定失败或进程崩溃。
16	// Unix Socket 适用于本地进程间通信,效率更高,安全性好,适用于 CSI 驱动和 Kubelet 的通信场景。
17	// IP 地址(TCP/IP Socket) 适用于跨主机的进程通信,主要用于需要远程通信的场景。
18	socket := "/var/lib/kubelet/plugins/hostpath.csi.k8s.io/csi.sock"
19	if err := os.RemoveAll(socket); err != nil {
20		log.Fatalf("failed to remove existing socket: %v", err)
21	}
22
23	listener, err := net.Listen("unix", socket)
24	if err != nil {
25		log.Fatalf("failed to listen on socket: %v", err)
26	}
27
28	server := grpc.NewServer()
29	// 这里需要把三个服务注册到 gRPC 服务器上
30	csi.RegisterIdentityServer(server, &hostpathcsi.IdentityServer{})
31	csi.RegisterControllerServer(server, &hostpathcsi.ControllerServer{})
32	csi.RegisterNodeServer(server, &hostpathcsi.NodeServer{})
33
34	log.Println("Starting CSI driver...")
35	// 启动 gRPC 服务器
36	if err := server.Serve(listener); err != nil {
37		log.Fatalf("failed to serve: %v", err)
38	}
39}

identity是用于返回csi插件的详细信息,具备的能力等。

 1// pkg/hostpathcsi/identity.go
 2package hostpathcsi
 3
 4import (
 5	"context"
 6	csi "github.com/container-storage-interface/spec/lib/go/csi"
 7	"k8s.io/klog"
 8)
 9
10// IdentityServer 注意因为要作为csi.ControllerServer的实现,所以需要实现csi.ControllerServer的所有方法
11type IdentityServer struct {
12	csi.UnimplementedIdentityServer
13}
14
15// GetPluginInfo 的作用是返回插件的信息,包括插件的名称和版本号
16func (s *IdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
17	klog.Infof("Received GetPluginInfo request")
18
19	return &csi.GetPluginInfoResponse{
20		// csi要求插件的名称必顫是域名的逆序,这里使用了hostpath.csi.k8s.io
21		Name:          "hostpath.csi.k8s.io",
22		VendorVersion: "v1.0.0",
23	}, nil
24}
25
26// GetPluginCapabilities 的作用是返回插件的能力,这里只返回了 ControllerService 的能力; 也就是说,这个插件只实现了 ControllerService
27func (s *IdentityServer) GetPluginCapabilities(ctx context.Context, req *csi.GetPluginCapabilitiesRequest) (*csi.GetPluginCapabilitiesResponse, error) {
28	// 什么是ControllerService能力呢?ControllerService是CSI规范中的一个服务,它负责管理卷的生命周期,包括创建、删除、扩容等操作
29	klog.Infof("Received GetPluginCapabilities request")
30
31	return &csi.GetPluginCapabilitiesResponse{
32		Capabilities: []*csi.PluginCapability{
33			{
34				Type: &csi.PluginCapability_Service_{
35					Service: &csi.PluginCapability_Service{
36						Type: csi.PluginCapability_Service_CONTROLLER_SERVICE,
37					},
38				},
39			},
40		},
41	}, nil
42}
43
44func (s *IdentityServer) Probe(ctx context.Context, req *csi.ProbeRequest) (*csi.ProbeResponse, error) {
45	klog.Infof("Received Probe request")
46
47	return &csi.ProbeResponse{}, nil
48}

3. external-provisioner 监听并调用 CSI 插件的 CreateVolumeControllerPublishVolume

external-provisioner 的作用:

  • external-provisioner 是一个外部组件,它监听集群中 PVC 的创建请求。它会根据 PVC 中引用的 StorageClass 和对应的 provisioner 字段,找到相关的 CSI 驱动。
  • external-provisioner 发现 PVC 时,会向 CSI 控制器发出 CreateVolume 请求。

CreateVolume 调用:

  • CreateVolume 方法是 CSI Controller 侧的一个接口,负责在存储系统中创建卷。这是 PVC 动态分配卷的关键步骤。
  • CreateVolume 的实现通常会在底层存储系统中实际分配卷,并返回卷的 ID 和其他相关元数据信息给 Kubernetes。

ControllerPublishVolume 调用:

  • 卷创建后,ControllerPublishVolume 负责将卷附加到指定的节点上。这通常是一个“模拟的”附加操作,特别是在 HostPath 这样的驱动中,它可能不涉及实际的物理附加,而是将卷关联到节点上。

PV 与 PVC 的绑定:

  • Kubernetes 的 PV(PersistentVolume)控制器会自动将创建好的卷绑定到 PVC 上,完成 PVC 和 PV 的关联。
  • external-provisioner 调用 CreateVolumeControllerPublishVolume 成功后,Kubernetes 会创建 PV,并将其绑定到相应的 PVC,完成存储卷的动态分配。
 1// pkg/hostpathcsi/controller.go
 2// Package hostpathcsi Description: 这个服务主要实现的是Volume管理流程中的"Provision阶段"和"Attach阶段"的功能。
 3package hostpathcsi
 4
 5import (
 6	"context"
 7	"fmt"
 8	csi "github.com/container-storage-interface/spec/lib/go/csi"
 9	"k8s.io/klog"
10	"os"
11)
12
13// ControllerServer 用于实现 ControllerService
14type ControllerServer struct {
15	// 继承默认的 ControllerServer
16	csi.ControllerServer
17}
18
19// CreateVolume 用于创建卷, 具体的创建"远程"真的数据卷出来
20func (s *ControllerServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
21	klog.Infof("Received CreateVolume request for %s", req.Name)
22
23	// 模拟 HostPath 卷的创建
24	volumePath := "/tmp/csi/hostpath/" + req.Name
25	if err := os.MkdirAll(volumePath, 0755); err != nil {
26		return nil, fmt.Errorf("failed to create volume directory: %v", err)
27	}
28
29	return &csi.CreateVolumeResponse{
30		Volume: &csi.Volume{
31			VolumeId:      req.Name,
32			CapacityBytes: req.CapacityRange.RequiredBytes,
33			VolumeContext: req.Parameters,
34		},
35	}, nil
36}
37
38// DeleteVolume 用于删除卷, 具体的删除"远程"真的数据卷
39func (s *ControllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
40	klog.Infof("Received DeleteVolume request for %s", req.VolumeId)
41
42	volumePath := "/tmp/csi/hostpath/" + req.VolumeId
43	if err := os.RemoveAll(volumePath); err != nil {
44		return nil, fmt.Errorf("failed to delete volume directory: %v", err)
45	}
46
47	return &csi.DeleteVolumeResponse{}, nil
48}
49
50// ControllerPublishVolume 用于发布卷, 这个是Attach阶段的功能
51func (s *ControllerServer) ControllerPublishVolume(ctx context.Context, req *csi.ControllerPublishVolumeRequest) (*csi.ControllerPublishVolumeResponse, error) {
52	// 在 HostPath 场景中,通常不需要 Controller 发布卷,因为它是本地存储
53	return nil, fmt.Errorf("ControllerPublishVolume is not supported")
54}
55
56// ControllerUnpublishVolume 用于取消发布卷, 这个是Detach阶段的功能
57func (s *ControllerServer) ControllerUnpublishVolume(ctx context.Context, req *csi.ControllerUnpublishVolumeRequest) (*csi.ControllerUnpublishVolumeResponse, error) {
58	return nil, fmt.Errorf("ControllerUnpublishVolume is not supported")
59}
60
61// ControllerGetCapabilities 返回 Controller 的功能
62func (s *ControllerServer) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) {
63	klog.Infof("Received ControllerGetCapabilities request")
64	capabilities := []*csi.ControllerServiceCapability{
65		{
66			Type: &csi.ControllerServiceCapability_Rpc{
67				Rpc: &csi.ControllerServiceCapability_RPC{
68					Type: csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
69				},
70			},
71		},
72	}
73	return &csi.ControllerGetCapabilitiesResponse{Capabilities: capabilities}, nil
74}

4. Pod 使用 PVC 触发 NodePublishVolume 挂载操作

当一个 Pod 使用 PVC 时,Kubernetes 会调度该 Pod 到合适的节点,并触发 CSI 节点组件执行挂载操作。

NodePublishVolume 的作用:

  • 当 Pod 被调度到节点并使用 PVC 时,kubelet 会与 csi-node 组件通信,触发 NodePublishVolume 操作。
  • NodePublishVolume 负责将卷从存储系统挂载到节点的文件系统上,这样 Pod 就可以访问该存储卷。

挂载流程:

  1. Kubernetes 调度 Pod 到合适的节点,该节点的 kubelet 负责协调卷的挂载。
  2. kubelet 会通过 CSI 驱动发出 NodePublishVolume 请求,要求将卷挂载到节点上的指定目录(如 /var/lib/kubelet 下的路径)。
  3. NodePublishVolume 成功完成后,卷挂载到节点,Pod 可以使用该卷进行读写操作。

重要提示

  • 只有当 PVC 被 Pod 使用时,才会触发 NodePublishVolume。如果 PVC 没有被任何 Pod 使用,该方法不会被调用。
  1// pkg/hostpathcsi/node.go
  2// Package hostpathcsi Description: 这个服务主要实现的是Volume管理流程中的"NodePublishVolume阶段"和"NodeUnpublishVolume阶段"的功能。
  3// 对应Mount和Unmount操作
  4package hostpathcsi
  5
  6import (
  7	"context"
  8	"fmt"
  9	csi "github.com/container-storage-interface/spec/lib/go/csi"
 10	"k8s.io/klog"
 11	"os"
 12	"path/filepath"
 13)
 14
 15type NodeServer struct {
 16	csi.NodeServer
 17}
 18
 19func (s *NodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
 20	klog.Infof("Received NodePublishVolume request for %s", req.VolumeId)
 21
 22	targetPath := req.TargetPath
 23	sourcePath := "/tmp/csi/hostpath/" + req.VolumeId
 24
 25	// 检查源路径是否存在
 26	if _, err := os.Stat(sourcePath); os.IsNotExist(err) {
 27		return nil, fmt.Errorf("source path %s does not exist", sourcePath)
 28	}
 29
 30	// 检查目标路径的父目录是否存在,若不存在则创建
 31	parentDir := filepath.Dir(targetPath)
 32	if err := os.MkdirAll(parentDir, 0755); err != nil {
 33		return nil, fmt.Errorf("failed to create parent directory %s: %v", parentDir, err)
 34	}
 35
 36	// 检查目标路径是否存在
 37	if fi, err := os.Lstat(targetPath); err == nil {
 38		// 如果目标路径已经是符号链接,检查它是否指向正确的源路径
 39		if fi.Mode()&os.ModeSymlink != 0 {
 40			existingSource, err := os.Readlink(targetPath)
 41			if err == nil && existingSource == sourcePath {
 42				klog.Infof("Target path %s already linked to correct source %s, skipping creation.", targetPath, sourcePath)
 43				return &csi.NodePublishVolumeResponse{}, nil
 44			}
 45			klog.Infof("Target path %s is a symlink but points to %s, removing it.", targetPath, existingSource)
 46		} else {
 47			klog.Infof("Target path %s exists but is not a symlink, removing it.", targetPath)
 48		}
 49		// 删除现有的文件或目录,避免冲突
 50		if err := os.RemoveAll(targetPath); err != nil {
 51			return nil, fmt.Errorf("failed to remove existing target path %s: %v", targetPath, err)
 52		}
 53	}
 54
 55	// 创建软链接
 56	if err := os.Symlink(sourcePath, targetPath); err != nil {
 57		return nil, fmt.Errorf("failed to create symlink from %s to %s: %v", sourcePath, targetPath, err)
 58	}
 59
 60	klog.Infof("Volume %s successfully mounted to %s", sourcePath, targetPath)
 61	return &csi.NodePublishVolumeResponse{}, nil
 62}
 63
 64func (s *NodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
 65	klog.Infof("Received NodeUnpublishVolume request for %s", req.VolumeId)
 66
 67	targetPath := req.TargetPath
 68
 69	// 检查目标路径是否存在且是软链接
 70	if fi, err := os.Lstat(targetPath); err == nil {
 71		if fi.Mode()&os.ModeSymlink != 0 {
 72			klog.Infof("Target path %s is a symlink, removing it.", targetPath)
 73			if err := os.RemoveAll(targetPath); err != nil {
 74				return nil, fmt.Errorf("failed to remove symlink at target path %s: %v", targetPath, err)
 75			}
 76			klog.Infof("Successfully removed symlink at %s", targetPath)
 77		} else {
 78			klog.Infof("Target path %s is not a symlink, skipping removal.", targetPath)
 79		}
 80	} else if os.IsNotExist(err) {
 81		klog.Infof("Target path %s does not exist, skipping unpublish.", targetPath)
 82	} else {
 83		return nil, fmt.Errorf("error checking target path %s: %v", targetPath, err)
 84	}
 85
 86	return &csi.NodeUnpublishVolumeResponse{}, nil
 87}
 88
 89func (s *NodeServer) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) (*csi.NodeGetInfoResponse, error) {
 90	klog.Infof("Received NodeGetInfo request")
 91
 92	// 获取node的主机名
 93	nodeID := "node1"
 94
 95	// 可选:假如你支持Topologies,可以添加相关信息
 96	topology := &csi.Topology{
 97		Segments: map[string]string{
 98			"topology.hostpath.csi/node": nodeID,
 99		},
100	}
101
102	return &csi.NodeGetInfoResponse{
103		NodeId:             nodeID,   // 返回节点ID
104		AccessibleTopology: topology, // 返回可访问拓扑信息
105	}, nil
106}
107
108// NodeGetCapabilities 返回该节点的能力信息
109func (s *NodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
110	klog.Infof("Received NodeGetCapabilities request")
111
112	// 返回节点的能力信息,不包含 STAGE_UNSTAGE_VOLUME,表示跳过这个阶段
113	capabilities := []*csi.NodeServiceCapability{
114		{
115			Type: &csi.NodeServiceCapability_Rpc{
116				Rpc: &csi.NodeServiceCapability_RPC{
117					// 不包含 STAGE_UNSTAGE_VOLUME,跳过该能力
118					Type: csi.NodeServiceCapability_RPC_UNKNOWN, // 表示无特定能力
119				},
120			},
121		},
122	}
123
124	return &csi.NodeGetCapabilitiesResponse{
125		Capabilities: capabilities,
126	}, nil
127}
128
129// NodeStageVolume 空实现,用于跳过该操作
130func (s *NodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
131	klog.Infof("Received NodeStageVolume request but this operation is not needed, skipping.")
132	return &csi.NodeStageVolumeResponse{}, nil
133}
134
135// NodeUnstageVolume 空实现,用于跳过该操作
136func (s *NodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
137	klog.Infof("Received NodeUnstageVolume request but this operation is not needed, skipping.")
138	return &csi.NodeUnstageVolumeResponse{}, nil
139}

5. 部署文件

我们使用statefulset来保证csi-controller拓扑状态的稳定性,因为他严格按照顺序更新pod,只有前一个pod停止并且删除后才会创建启动下一个pod;同时要做好RBAC。

对于csi-node,因为要和本地的kubelet交互,我们使用daemonset来保证每个节点都有一个pod。通信使用socket,要挂载到/var/lib/kubelet/plugins/hostpath.csi.k8s.io/目录下。对于容器内完成挂载链接的操作要设置mountPropagation: Bidirectional来保证双边的可见性。

  1apiVersion: apps/v1
  2kind: StatefulSet
  3metadata:
  4  name: csi-controller
  5  namespace: kube-system
  6spec:
  7  serviceName: "csi-controller"
  8  replicas: 1
  9  selector:
 10    matchLabels:
 11      app: csi-controller
 12  template:
 13    metadata:
 14      labels:
 15        app: csi-controller
 16    spec:
 17      serviceAccountName: csi-controller-sa
 18      containers:
 19        - name: csi-controller
 20          securityContext:
 21            privileged: true
 22          image: siming.net/sre/custom-csi:main_de5c0f9_2024-10-08-231124
 23          imagePullPolicy: IfNotPresent
 24          volumeMounts:
 25            - name: socket-dir
 26              mountPath: /var/lib/kubelet/plugins/hostpath.csi.k8s.io/
 27              mountPropagation: Bidirectional
 28            - name: pods-dir  # 挂载 /var/lib/kubelet/pods 目录
 29              mountPath: /var/lib/kubelet/pods
 30              mountPropagation: Bidirectional
 31            - name: tmp-dir  # 挂载 /tmp 目录
 32              mountPath: /tmp
 33              mountPropagation: Bidirectional
 34
 35        - name: external-provisioner
 36          image: quay.io/k8scsi/csi-provisioner:v2.0.0
 37          args:
 38            - "--csi-address=/csi/csi.sock"
 39            - "--leader-election=true"
 40          volumeMounts:
 41            - name: socket-dir
 42              mountPath: /csi
 43
 44        - name: external-attacher
 45          image: quay.io/k8scsi/csi-attacher:v3.0.0
 46          args:
 47            - "--csi-address=/csi/csi.sock"
 48            - "--leader-election=true"
 49          volumeMounts:
 50            - name: socket-dir
 51              mountPath: /csi
 52
 53      volumes:
 54        - name: socket-dir
 55          hostPath:
 56            path: /var/lib/kubelet/plugins/hostpath.csi.k8s.io/
 57            type: DirectoryOrCreate
 58        - name: pods-dir  # 宿主机 /var/lib/kubelet/pods 目录挂载
 59          hostPath:
 60            path: /var/lib/kubelet/pods
 61            type: Directory
 62        - name: volumes-dir  # 宿主机 /var/lib/kubelet/volumes 目录挂载
 63          hostPath:
 64            path: /var/lib/kubelet/volumes
 65            type: Directory
 66        - name: tmp-dir  # 宿主机 /tmp 目录挂载
 67          hostPath:
 68            path: /tmp
 69            type: Directory
 70
 71---
 72
 73apiVersion: v1
 74kind: ServiceAccount
 75metadata:
 76  name: csi-controller-sa
 77  namespace: kube-system
 78
 79---
 80
 81apiVersion: rbac.authorization.k8s.io/v1
 82kind: ClusterRole
 83metadata:
 84  name: csi-controller-role
 85rules:
 86  - apiGroups: [""]
 87    resources: ["persistentvolumeclaims", "persistentvolumes", "nodes"]
 88    verbs: ["get", "list", "watch", "update", "create", "delete"]
 89  - apiGroups: ["storage.k8s.io"]
 90    resources: ["storageclasses", "volumeattachments", "csinodes"]
 91    verbs: ["get", "list", "watch", "update", "create"]
 92  - apiGroups: ["storage.k8s.io"]
 93    resources: ["volumeattachments/status"]  # 增加对 volumeattachments/status 的 patch 权限
 94    verbs: ["patch"]
 95  - apiGroups: [""]
 96    resources: ["events"]
 97    verbs: ["create", "patch"]
 98  - apiGroups: ["coordination.k8s.io"]
 99    resources: ["leases"]
100    verbs: ["get", "watch", "list", "update", "patch", "create"]
101
102---
103
104apiVersion: rbac.authorization.k8s.io/v1
105kind: ClusterRoleBinding
106metadata:
107  name: csi-controller-binding
108roleRef:
109  apiGroup: rbac.authorization.k8s.io
110  kind: ClusterRole
111  name: csi-controller-role
112subjects:
113  - kind: ServiceAccount
114    name: csi-controller-sa
115    namespace: kube-system
116
117---
118apiVersion: apps/v1
119kind: DaemonSet
120metadata:
121  name: csi-node
122  namespace: kube-system
123  labels:
124    app: csi-node
125spec:
126  selector:
127    matchLabels:
128      app: csi-node
129  template:
130    metadata:
131      labels:
132        app: csi-node
133    spec:
134      serviceAccountName: csi-node-sa
135      containers:
136        - name: csi-node
137          securityContext:
138            privileged: true
139          image: siming.net/sre/custom-csi:main_de5c0f9_2024-10-08-170548
140          volumeMounts:
141            - name: plugin-dir
142              mountPath: /var/lib/kubelet/plugins/hostpath.csi.k8s.io/
143              mountPropagation: Bidirectional
144            - name: pods-mount-dir
145              mountPath: /var/lib/kubelet/pods
146              mountPropagation: HostToContainer
147            - name: tmp-dir  # 挂载 /tmp 目录
148              mountPath: /tmp
149              mountPropagation: Bidirectional
150
151        - name: csi-driver-registrar
152          image: quay.io/k8scsi/csi-node-driver-registrar:v2.0.0
153          securityContext:
154            privileged: true
155          args:
156            - "--csi-address=/csi/csi.sock"
157            - "--kubelet-registration-path=/var/lib/kubelet/plugins/hostpath.csi.k8s.io/csi.sock"
158          volumeMounts:
159            - name: plugin-dir
160              mountPath: /csi
161            - name: registration-dir
162              mountPath: /registration
163
164      volumes:
165        - name: plugin-dir
166          hostPath:
167            path: /var/lib/kubelet/plugins/hostpath.csi.k8s.io/
168            type: DirectoryOrCreate
169        - name: pods-mount-dir
170          hostPath:
171            path: /var/lib/kubelet/pods
172            type: DirectoryOrCreate
173        - name: registration-dir
174          hostPath:
175            path: /var/lib/kubelet/plugins_registry/
176            type: DirectoryOrCreate
177        - name: tmp-dir  # 宿主机 /tmp 目录挂载
178          hostPath:
179            path: /tmp
180            type: Directory

6. 卷的卸载和资源的销毁

卸载卷(NodeUnpublishVolume):

  • 当 Pod 被删除或停止时,kubelet 会请求卸载卷,触发 NodeUnpublishVolume 方法。
  • NodeUnpublishVolume 负责从节点文件系统中卸载卷,并删除挂载路径上的符号链接或执行卸载命令。

删除存储卷(DeleteVolume):

  • 当用户删除 PVC 后,Kubernetes 会调用 CSI 驱动的 DeleteVolume 方法,删除后端存储中的卷。
  • DeleteVolume 的作用是释放底层存储资源并删除与该卷相关的元数据。
  • external-provisioner 在监听到 PVC 删除后,会与 CSI Controller 交互,通过 DeleteVolume 完成存储卷的回收和删除操作。

反向流程:

  • 当 Pod 停止使用 PVC 时,Kubernetes 会逐步执行卷的卸载过程,通过 NodeUnpublishVolume 完成从节点的卸载。
  • 当 PVC 被删除时,external-provisioner 调用 DeleteVolume 来释放存储资源。

7. 部署查看效果

 1# 模拟创建
 2k apply -f pvc.yaml
 3persistentvolumeclaim/custom-pvc created
 4
 5k apply -f demo.yaml 
 6pod/my-pod-1 created
 7
 8k get pvc
 9NAME         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS    AGE
10custom-pvc   Bound    pvc-dce4da52-28cf-4514-854e-545df5a31a29   1Gi        RWO            custom-csi-sc   5s
11
12 sudo touch /tmp/csi/hostpath/pvc-dce4da52-28cf-4514-854e-545df5a31a29/testfile               
13
14kp
15NAME                                      READY   STATUS      RESTARTS   AGE
16calico-kube-controllers-5b564d9b7-5lbrn   1/1     Running     0          33d
17canal-lxk8w                               2/2     Running     0          33d
18coredns-54cc789d79-mrbpb                  1/1     Running     0          33d
19coredns-autoscaler-6ff6bf758-hrxmh        1/1     Running     0          33d
20csi-controller-0                          3/3     Running     0          10h
21csi-node-wmsq7                            2/2     Running     0          16h
22metrics-server-657c74b5d8-jjxzd           1/1     Running     0          33d
23my-pod-1                                  1/1     Running     0          17s
24rke-coredns-addon-deploy-job-rmw2r        0/1     Completed   0          33d
25rke-ingress-controller-deploy-job-2z4d6   0/1     Completed   0          33d
26rke-metrics-addon-deploy-job-9bs7c        0/1     Completed   0          33d
27rke-network-plugin-deploy-job-2pzvs       0/1     Completed   0          33d
28
29k exec -it my-pod-1 ls /mnt/data   
30kubectl exec [POD] [COMMAND] is DEPRECATED and will be removed in a future version. Use kubectl exec [POD] -- [COMMAND] instead.
31testfile
32
33# 模拟删除
34kdel -f demo.yaml   
35warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
36pod "my-pod-1" force deleted
37
38kdel -f pvc.yaml  
39warning: Immediate deletion does not wait for confirmation that the running resource has been terminated. The resource may continue to run on the cluster indefinitely.
40persistentvolumeclaim "custom-pvc" force deleted
41
42ls /tmp/csi/hostpath 

整体流程总结:

  1. PVC 创建

    • 用户创建 PVC 并指定 StorageClassStorageClassprovisioner 字段指定了哪个 CSI 驱动负责处理卷。
    • external-provisioner 监听 PVC 事件,并通过 provisioner 字段找到相应的 CSI 插件。
  2. 卷的创建和附加

    • external-provisioner 调用 csi-controllerCreateVolume 方法,创建存储卷。
    • ControllerPublishVolume 将卷附加到节点。
  3. Pod 使用 PVC 触发卷挂载

    • 当 Pod 使用 PVC 时,kubelet 通过 NodePublishVolume 请求将卷挂载到节点。
    • Pod 可以在挂载成功后使用该卷进行数据读写。
  4. 卸载与删除

    • 当 Pod 停止或删除时,NodeUnpublishVolume 卸载卷。
    • 当 PVC 被删除时,DeleteVolume 删除卷,并释放底层存储资源。

这个过程描述了从 PVC 的创建、卷的动态分配、Pod 使用卷,再到卷的卸载和删除的完整生命周期。

5. Provisioner 和 Csi的区别

分类 Provisioner CSI (Container Storage Interface)
定义 Provisioner 是负责动态或静态分配存储卷的组件。可以是基于 CSI 也可以不是。 CSI 是一种标准接口,定义了 Kubernetes 如何与存储系统交互,通常用于动态卷管理。
工作机制 Provisioner 监听 PVC 事件,动态或静态创建 PV,绑定 PVC。可以是 CSI 驱动的 external-provisioner,也可以是传统非 CSI 的 Provisioner。 CSI 通过标准接口与 Kubernetes 交互,提供卷的创建、挂载、卸载等功能,实际操作由存储插件实现。
通用性 Provisioner 可以是非 CSI 方案,例如基于传统存储系统的动态分配机制。也可以支持静态预创建的 PV。 CSI 是专门为容器环境设计的通用存储接口,支持任何遵循 CSI 标准的存储系统。
工作方式 Provisioner 可以通过 StorageClass 中的 provisioner 字段指定,动态分配存储卷,不一定依赖 CSI,可以是特定存储厂商的原生方案。 CSI 提供标准化接口,负责与 Kubernetes API 交互,执行卷的创建、挂载、卸载等,存储厂商通过实现 CSI 驱动来提供具体功能。
功能 Provisioner 负责创建、管理 PV 和 PVC,提供了动态卷分配的能力,支持通过插件或内置机制实现(例如 kubernetes.io/aws-ebs)。 CSI 提供标准的 API 规范,允许 Kubernetes 与不同存储系统交互,完成卷管理。
外部组件 external-provisioner 是典型的 CSI-based Provisioner,也有非 CSI 的动态 Provisioner 通过特定的 API 与 Kubernetes 交互。 CSI 是存储接口规范,具体的存储实现依赖不同的 CSI 驱动,负责处理存储的实际操作。
动态 vs 静态 动态 Provisioner 动态创建卷,静态 Provisioner 允许预创建卷并手动分配给 PVC。 CSI 一般用于动态存储卷分配,但也可以通过 PV 实现静态卷分配。
示例 1. 动态:kubernetes.io/aws-ebsnfs-client 动态创建 PV。
2. 静态:手动创建 PV,绑定 PVC。
HostPath CSI、AWS EBS CSI、NFS CSI 等,可以通过 CSI 驱动创建和管理存储卷。
适用范围 适用于传统存储系统和非容器化存储,支持 Kubernetes 的动态存储分配。 适用于容器化环境,支持各种存储类型,标准化接口确保跨平台和跨供应商的存储兼容。
主要职责 1. 监听 PVC 创建请求
2. 调用 API 或存储系统接口创建 PV,绑定 PVC。
1. 提供跨存储供应商标准接口
2. 提供容器化环境中的存储卷管理操作。
核心接口/方法 - 动态:Provision
- 静态:手动创建 PV 并绑定 PVC
- NodePublishVolume
- ControllerPublishVolume
- CreateVolume
优势 动态 Provisioner 提供了非 CSI 环境下的动态存储卷管理。 CSI 通过标准化接口,支持广泛的存储系统和供应商,具有高度扩展性。
典型场景 动态:AWS EBS、GCE PD 等云供应商存储卷,NFS 动态卷客户端。
静态:预分配卷,管理员手动操作。
适用于支持 CSI 的存储系统,HostPath、GlusterFS、AWS EBS 等支持 CSI 的存储。

CSI 需要与 Provisioner 结合使用,但 Provisioner 不一定需要依赖 CSI。