Kubernetes 存储管理
Overview
1. Dynamic Provisioner 例子与完整流程介绍
原理概述
在 Kubernetes 中,动态 provisioner 是一个实现了 Provisioner
接口的控制器,用于自动化存储卷的创建。当用户提交 PVC (PersistentVolumeClaim) 时,provisioner 根据定义的 StorageClass,自动创建相应的 PV (PersistentVolume)。这种自动化存储管理机制大大简化了卷的生命周期管理,减少了手动操作的复杂性。
流程概述
自定义动态 provisioner 的流程包括以下几个步骤:
- 创建自定义的 Provisioner 逻辑,负责监听 PVC 的创建事件,并生成 PV。
- 编写自定义的 StorageClass,使用该 Provisioner 动态创建卷。
- 编写控制器代码,处理卷的创建与删除。
- 部署自定义 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
中的内容,并与存储系统交互以执行卷的创建。
- 由 provisioner 自身解析并使用这些参数,在创建 PV 时根据传递的参数设置存储卷的属性。例如,如果 provisioner 是自定义的,它需要解释
3. reclaimPolicy
-
功能:
- 定义了当 PVC 被删除时,PV 的行为。选项包括:
Retain
: 卷不会被删除,数据保留。Delete
: 卷会被删除,存储资源也会被释放。Recycle
: 卷会被擦除并返回到未绑定状态(在 Kubernetes 1.9 之后已废弃)。
- 定义了当 PVC 被删除时,PV 的行为。选项包括:
-
受控组件:
- 由
kube-controller-manager
中的 PersistentVolume controller 管理。当 PVC 释放后,controller 根据reclaimPolicy
执行相应的删除或保留操作。
- 由
4. volumeBindingMode
-
功能:
- 决定了 PVC 何时绑定到 PV。选项包括:
Immediate
: PVC 提交时立即绑定到可用的 PV。WaitForFirstConsumer
: 仅在 Pod 被调度时才绑定 PV。这可以避免资源分配的不平衡问题,特别适用于多可用区环境下存储和计算资源的协同调度。先调度后绑定这种延迟绑定方式在pv有多种选择的时候,先根据pod的需求选择pv,避免调度冲突(pv调度和pod调度冲突)
- 决定了 PVC 何时绑定到 PV。选项包括:
-
受控组件:
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 |
允许扩展卷 | ExpandController 在 kube-controller-manager 中处理 |
mountOptions |
卷挂载时的选项 | kubelet 负责挂载时的处理 |
3. 整体挂载流程介绍:Attach、Detach、Mount、Unmount
Kubernetes 中的存储卷挂载流程涉及两个主要组件:ControllerManager 和 Kubelet。每个阶段都有不同的控制器负责管理卷的挂载、卸载等操作。
1. Attach(由 ControllerManager 处理)
- 概述: 当一个 Pod 被调度到某个节点,并且该 Pod 需要使用 PersistentVolume(如 EBS 或 GCE Persistent Disk),
AttachDetachController
会将卷附加(Attach)到该节点。 - 过程:
- Pod 被调度到某个节点。
AttachDetachController
通过 Kubernetes API 获取 PVC 的相关信息,找到相应的 PersistentVolume。- 使用 Cloud Provider 或者 CSI 驱动将卷附加到节点。(调用csi的controller的controllerPublishVolume)
- 一旦卷附加成功,卷的状态将更新为 “Attached”,Pod 可以继续进入挂载阶段。
2. Detach(由 ControllerManager 处理)
- 概述: 当 Pod 被删除或调度到另一个节点时,
AttachDetachController
会触发卷的卸载过程,将卷从原节点分离。 - 过程:
- 当 Pod 终止时,
AttachDetachController
检查卷是否仍然附加在节点上。 - 如果卷不再使用,控制器会通过 Cloud Provider 或 CSI 驱动将卷从节点上分离。
- 卷状态更新为 “Detached”,资源释放。
- 当 Pod 终止时,
3. Mount(由 Kubelet 处理)
- 概述: 当卷附加到节点后,
kubelet
会将卷挂载到 Pod 的容器中,这个过程通过VolumeManager
来管理。 - 过程:
kubelet
监控到卷已附加,准备进行挂载操作。VolumeManager
负责将卷挂载到宿主机的文件系统(如/var/lib/kubelet/pods/...
路径)。- 卷挂载完成后,卷可通过容器内的目录访问。
4. Unmount(由 Kubelet 处理)
- 概述: 当 Pod 删除时,
kubelet
会将该卷从宿主机文件系统中卸载。 - 过程:
VolumeManager
检查到卷不再使用,准备卸载。kubelet
执行卸载操作,卷从宿主机文件系统中移除。- 卸载完成后,卷资源释放,Pod 生命周期结束。
Pod 磁盘挂载具体流程:
- 用户创建 Pod 并指定 PVC。
- Attach:
AttachDetachController
将卷附加到节点。 - Mount:
kubelet
将卷挂载到节点,并将其映射到 Pod 的容器中。 - Unmount: 当 Pod 终止时,
kubelet
执行卷的卸载操作。 - Detach:
AttachDetachController
将卷从节点分离。
要完整地理解 CSI(Container Storage Interface),我们可以通过编写一个简单的 CSI 驱动来演示其工作原理。这个例子将帮助你从基础层面理解 CSI 的各个组件:CSI Identity、CSI Controller 和 CSI Node。
4. CSI 驱动自主实现
https://github.com/ZhangSIming-blyq/hostpathcsi
组件介绍
一个 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 插件的 CreateVolume
和 ControllerPublishVolume
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
调用CreateVolume
和ControllerPublishVolume
成功后,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 就可以访问该存储卷。
挂载流程:
- Kubernetes 调度 Pod 到合适的节点,该节点的
kubelet
负责协调卷的挂载。 kubelet
会通过 CSI 驱动发出NodePublishVolume
请求,要求将卷挂载到节点上的指定目录(如/var/lib/kubelet
下的路径)。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
整体流程总结:
-
PVC 创建:
- 用户创建 PVC 并指定
StorageClass
。StorageClass
的provisioner
字段指定了哪个 CSI 驱动负责处理卷。 external-provisioner
监听 PVC 事件,并通过provisioner
字段找到相应的 CSI 插件。
- 用户创建 PVC 并指定
-
卷的创建和附加:
external-provisioner
调用csi-controller
的CreateVolume
方法,创建存储卷。ControllerPublishVolume
将卷附加到节点。
-
Pod 使用 PVC 触发卷挂载:
- 当 Pod 使用 PVC 时,
kubelet
通过NodePublishVolume
请求将卷挂载到节点。 - Pod 可以在挂载成功后使用该卷进行数据读写。
- 当 Pod 使用 PVC 时,
-
卸载与删除:
- 当 Pod 停止或删除时,
NodeUnpublishVolume
卸载卷。 - 当 PVC 被删除时,
DeleteVolume
删除卷,并释放底层存储资源。
- 当 Pod 停止或删除时,
这个过程描述了从 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-ebs ,nfs-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。