PV/PVC/SC
PV
- 持久化存储数据卷
- 目录里的内容既不会因为容器的删除而被清理掉,支持pod重建
- 也不会跟当前宿主机绑定,支持pod漂移到其他node
- 主要定义的是一个持久化存储在宿主机上的目录,比如一个NFS的挂载目录
- PV描述的是一个具体的volume的属性
- 类型
- 挂载目录
- 远程服务地址
- 通常由运维人员创建 ```yaml
apiVersion: v1 kind: PersistentVolume metadata: name: nfs spec: storageClassName: manual capacity: storage: 1Gi accessModes:
- ReadWriteMany
nfs: server: 10.244.1.4 path: “/“
- 具体实现方式
- 远程文件存储(NFS、GlusterFS)
- 远程块存储(公有云提供的远程磁盘)
- 具体实现步骤(两阶段处理)
- Attach
- pod调度到node上,kubelet会为其准备volume目录,默认目录如下
/var/lib/kubelet/pods/
- 根据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
- 管理方式
- 人工创建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
面向对象思想
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,适用范围比较固定
如何把本地磁盘抽象成PV
- 不能把宿主机上的目录当作PV使用
- 存储行为完全不可控,随时可能被写满造成宿主机宕机
- 不同本地目录之间缺乏最基础的IO隔离机制
- 一个PV一块盘,应该是和宿主机根目录隔离的独立的磁盘或块设备
- 不能把宿主机上的目录当作PV使用
如何保证pod始终能被正确的调度到它所请求的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的流程
- 删除使用这个LPV的Pod
- 宿主机移除本地磁盘(umount)
- 删除PVC
- 删除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
-
插件执行流程
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的实现方式
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
- GetPluginCapabilities接口返回插件的能力
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存储插件
- 用户创建PVC后,前面部署的StatefulSet里的ExternalProvisioner容器就会监听PVC的诞生,然后调用同一个Pod里的CSI插件的CSI Controller服务的CreateVolume方法创建PV
- 运行在Master节点上的VolumeController通过PersistentVolumeController控制循环发现新创建出来的PV和PVC并发现是同一个StorageClass就会将其Bound
- 用户创建了一个声明使用上述PVC的Pod,并把Pod调度到NodeA上,这时VolumeController的AttachDetachController控制循环发现PVC对应的Volume需要被 attach 到NodeA上,所以AttachDetachController就会创建一个VolumeAttachment对象,携带了NodeA和待处理的Volume名字
- SS里的ExternalAttacher容器监听到这个VolumenAttachment对象的诞生,就会使用这个对象里的NodeA和Volume名字调用同一个Pod里的CSI插件的CSI Controller服务的ControllerPublishVolume方法完成 attach
- 运行在NodeA上的kubelet通过VolumeManagerReconciler控制循环发现当前宿主机上有一个volume对应的存储设备已经被attach到了某个目录下,于是kubelet调用同一台宿主机上的CSI插件的CSI Node服务的NodeStageVolume和NodePublishVolume方法完成 mount