Kubernetes CSI 实现举例,你学会了吗?

云计算 云原生
通过 s3fs,用户可以像访问本地文件系统一样访问 Amazon S3 存储桶中的对象。这使得开发人员可以直接在应用程序中使用 S3 存储桶,而无需使用 Amazon S3 的 API 进行手动操作。

前言

接下去我们简单来看下一个 CSI Plugin 开源项目的实现,github 地址是:https://github.com/alibaba/open-object。

这个 CSI Plugin 实现了将 Minio 作为后端存储。以这个项目为例子,主要的考虑是:(1)它的实现不复杂,整体上的代码量就 2k 行左右,便于阅读和了解;(2)项目实现了将 Minio 作为后端存储,在这个项目基础之上,我们可以进一步扩展将阿里云 OSS 作为后端存储的实现,便于进一步的实操。(3)Minio 是对象存储,了解它的实现可以解决我心中的一个疑惑,就是我们如何将对一个文件的读写访问,转换为对 bucket 的读写请求。通过后续的了解,我们可以知道这主要是依赖了像 s3f3 这样的程序,s3f3 通过 FUSE 将一个 bucket 表现为一个文件。

从部署开始

CSIDriver 资源对象

CSIDriver 包含了部署的 CSI Plugin 有关的信息。Kubernetes AD Controller 通过该对象来决定是否需要 attach。Kubelet 通过该对象决定 mount 时是否需要传递 Pod 信息。CSIDriver 资源对象未划分命名空间。

可以了解到以下几个点:

  • OpenObject 这个 CSI 插件不需要 attach 步骤,并且在 mount 的时候需要传递 Pod 信息。
apiVersion: storage.k8s.io/v1beta1
kind: CSIDriver
metadata:
  name: object.csi.aliyun.com
spec:
  attachRequired: false
  podInfoOnMount: true
  volumeLifecycleModes:
  - Persistent

CSI Node Plugin

可以看到 Daemonset 中包含了 3 个容器:

  • init 容器,启动一个 connector,这个 connector 运行在宿主机上,监听 /etc/open-object/connector.sock。通过这个 socket 文件,connector 会收到相应的执行命令,并执行这些命令。总体上的作用相当于宿主机和容器之间的连接器。
  • driver-registrar 容器,调用 csi-plugin 的 NodeGetInfo 的接口,将 CSI Node Plugin 的信息通过 kubelet 的插件注册机制在对应节点的 kubelet 上进行注册,同时把 CSI Node Plugin 的通信地址同步给 kubelet。
  • CSI-Plugin 容器,这个容器里面运行的就是 CSI Node Plugin,也是我们实现的内容。
kind: DaemonSet
apiVersion: apps/v1
metadata:
  name: open-object
  namespace: {{ .Values.namespace }}
spec:
  selector:
    matchLabels:
      app: open-object
  template:
    metadata:
      labels:
        app: open-object
    spec:
      tolerations:
      - operator: Exists
      serviceAccount: open-object
      hostNetwork: true
      hostPID: true
      dnsPolicy: ClusterFirstWithHostNet
      initContainers:
      - name: run-connector
        image: {{ .Values.images.object.image }}:{{ .Values.images.object.tag }}
        securityContext:
          privileged: true
          capabilities:
            add: ["SYS_ADMIN"]
          allowPrivilegeEscalation: true
        command:
        - /run-connector.sh
        volumeMounts:
        - name: host-systemd-config
          mountPath: /host/usr/lib/systemd/system
        - name: host-etc
          mountPath: /host/etc/open-object
      containers:
      - name: driver-registrar
        image: {{ .Values.images.registrar.image }}:{{ .Values.images.registrar.tag }}
        args:
        - "--kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)"
        - "--v=4"
        - "--csi-address=$(ADDRESS)"
        env:
        - name: ADDRESS
          value: /csi/csi.sock
        - name: DRIVER_REG_SOCK_PATH
          value: /var/lib/kubelet/plugins/object.csi.aliyun.com/csi.sock
        - name: KUBE_NODE_NAME
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        ...
        volumeMounts:
        - name: plugin-dir
          mountPath: /csi
        - name: registration-dir
          mountPath: /registration/
      - name: csi-plugin
        securityContext:
          privileged: true
          capabilities:
            add: ["SYS_ADMIN"]
          allowPrivilegeEscalation: true
        image: {{ .Values.images.object.image }}:{{ .Values.images.object.tag }}
        args:
        - "csi"
        - "--endpoint=$(CSI_ENDPOINT)"
        - "--nodeID=$(NODE_ID)"
        - "--driver=object.csi.aliyun.com"
        env:
        - name: CSI_ENDPOINT
          value: unix:///csi/csi.sock
        - name: NODE_ID
          valueFrom:
            fieldRef:
              fieldPath: spec.nodeName
        - name: TZ
          value: Asia/Shanghai
        ...
        volumeMounts:
        - name: plugin-dir
          mountPath: /csi
        - name: pods-mount-dir
          mountPath: /var/lib/kubelet/pods
          mountPropagation: "Bidirectional"
        - name: fuse-device
          mountPath: /dev/fuse
        - name: host-etc
          mountPath: /host/etc/open-object
        - name: host-etc-os
          mountPath: /host/etc/os-release
      volumes:
      - name: registration-dir
        hostPath:
          path: /var/lib/kubelet/plugins_registry/
          type: DirectoryOrCreate
      - name: plugin-dir
        hostPath:
          path: /var/lib/kubelet/plugins/object.csi.aliyun.com
          type: DirectoryOrCreate
      - name: pods-mount-dir
        hostPath:
          path: /var/lib/kubelet/pods
          type: Directory
      - name: fuse-device
        hostPath:
          path: /dev/fuse
      - name: host-etc
        hostPath:
          path: /etc/open-object
          type: DirectoryOrCreate
      - name: host-etc-os
        hostPath:
          path: /etc/os-release
          type: File
      - name: host-systemd-config
        hostPath:
          path: /usr/lib/systemd/system
          type: DirectoryOrCreate

CSI Controller Plugin

下面这个 Deployment 部署的是 external-provisioner 组件,指定了通过哪个 socket 文件跟 CSI Controller Plugin 进行通信。但是,我们在这个 Deployment 中并没有看到 CSI Controller Plugin 组件相关的容器。这是因为对于 OpenObject 这个项目来说,它将 CSI Node Plugin 和 CSI Controller Plugin 的代码都实现在了上述提到的 csi-plugin 容器里。由于,csi-plugin 是通过 Daemonset 方式部署的,每个节点上都有,所以 external-provisioner 组件可以通过指定的 socket 文件跟 csi-plugin 容器通信。

kind: Deployment
apiVersion: apps/v1
metadata:
  name: open-object-csi-provisioner
  namespace: {{ .Values.namespace }}
  labels:
    app: open-object
    component: open-object-csi-provisioner
spec:
  selector:
    matchLabels:
      app: open-object
      component: open-object-csi-provisioner
  replicas: 1
  template:
    metadata:
      labels:
        app: open-object
        component: open-object-csi-provisioner
    spec:
      tolerations:
      - operator: Exists
        effect: NoSchedule
        key: node-role.kubernetes.io/master
      priorityClassName: system-cluster-critical
      serviceAccount: open-object
      hostNetwork: true
      dnsPolicy: ClusterFirstWithHostNet
      containers:
        - name: csi-provisioner
          image: {{ .Values.images.provisioner.image }}:{{ .Values.images.provisioner.tag }}
          args:
            - --csi-address=$(ADDRESS)
            - --volume-name-prefix=fuse
            - --extra-create-metadata=true
            - --timeout=10m
          env:
            - name: ADDRESS
              value: /var/lib/kubelet/plugins/object.csi.aliyun.com/csi.sock
            - name: TZ
              value: Asia/Shanghai
          ...
          volumeMounts:
            - name: socket-dir
              mountPath: /var/lib/kubelet/plugins/object.csi.aliyun.com
      volumes:
        - name: socket-dir
          hostPath:
            path: /var/lib/kubelet/plugins/object.csi.aliyun.com
            type: DirectoryOrCreate

根据上述的情况,我们总结下:

  • CSI Node Plugin 和 CSI Controller Plugin 都实现在同一个程序里。
  • 由于这个程序通过 Daemonset 部署到了每个节点上,因此相当于每个节点都运行着 CSI Node Plugin 和 CSI Controller Plugin 这两个插件,并且都可以通过 csi.sock 文件实现跟 CSI Node Plugin 和 CSI Controller Plugin 的通信。所以,虽然 external-provisioner 是通过 Deployment 部署的,但是它仍旧可以通过 csi.sock 文件实现跟 CSI Controller Plugin 的通信。

进入源码

代码实现上,我们重点关注 CSI 接口的实现。

初始化

首先是整个 CSI Plugin 的初始化:

  • NewFuseDriver 函数初始化了一个 FuseDriver 结构体,这个结构体包含了 endpoint 也就是 CSI Plugin 监听的 socket 文件地址。
  • ids、cs、ns 分别是 CSI Identity、CSI Controller 和 CSI Node 三类接口的具体实现。
func NewFuseDriver(nodeID, endpoint, driverName string, kubeClient *kubernetes.Clientset) (*FuseDriver, error) {
 driver := csicommon.NewCSIDriver(driverName, version.Version, nodeID)
 if driver == nil {
  klog.Fatalln("Failed to initialize CSI Driver.")
 }

 s3Driver := &FuseDriver{
  endpoint: endpoint,
  driver:   driver,
  ids:      newIdentityServer(driver),
  cs:       newControllerServer(driver),
  ns:       newNodeServer(driver),
 }
 return s3Driver, nil
}

func (s3 *FuseDriver) Run() {
 // Initialize default library driver
 s3.driver.AddControllerServiceCapabilities([]csi.ControllerServiceCapability_RPC_Type{
  csi.ControllerServiceCapability_RPC_CREATE_DELETE_VOLUME,
  csi.ControllerServiceCapability_RPC_EXPAND_VOLUME,
 })
 s3.driver.AddVolumeCapabilityAccessModes([]csi.VolumeCapability_AccessMode_Mode{
  csi.VolumeCapability_AccessMode_MULTI_NODE_MULTI_WRITER})

 s := csicommon.NewNonBlockingGRPCServer()
 s.Start(s3.endpoint, s3.ids, s3.cs, s3.ns)
 s.Wait()
}

CSI Controller 类接口的实现

先看下 CSI Controller 类接口的具体实现,主要实现的是 CreateVolume、DeleteVolume 和 ControllerExpandVolume 这三个接口。

  • CreateVolume 接口的核心实现就是在 minio 上创建一个 bucket。
  • DeleteVolume 接口的核心实现就是将 bucket 删除。
  • ControllerExpandVolume 接口的核心实现就是将 bucket 的 quota 上限提高,调整 bucket 的 metadata。
type controllerServer struct {
 kubeClinet *kubernetes.Clientset
 *csicommon.DefaultControllerServer
}

func newControllerServer(d *csicommon.CSIDriver) *controllerServer {
 cfg, err := clientcmd.BuildConfigFromFlags("", "")
 if err != nil {
  klog.Fatalf("Error building kubeconfig: %s", err.Error())
 }
 kubeClient, err := kubernetes.NewForConfig(cfg)
 if err != nil {
  klog.Fatalf("Error building kubernetes clientset: %s", err.Error())
 }
 return &controllerServer{
  DefaultControllerServer: csicommon.NewDefaultControllerServer(d),
  kubeClinet:              kubeClient,
 }
}

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

 // create volume
 return driver.CreateVolume(ctx, req)
}

func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
 ...

 return driver.DeleteVolume(ctx, req)
}

func (cs *controllerServer) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
 ...

 // expand volume
 return driver.ControllerExpandVolume(ctx, req)
}

func (driver *MinIODriver) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
 ...
 if err := driver.minioClient.CreateBucket(bucketName, capacity); err != nil {
  return &csi.CreateVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
  ...
}

func (driver *MinIODriver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) (*csi.DeleteVolumeResponse, error) {
 ...
 if err := driver.minioClient.DeleteBucket(bucketName); err != nil {
  return &csi.DeleteVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
 ...
}

func (driver *MinIODriver) ControllerExpandVolume(ctx context.Context, req *csi.ControllerExpandVolumeRequest) (*csi.ControllerExpandVolumeResponse, error) {
 ...
 capacity := req.GetCapacityRange().RequiredBytes
 if DefaultFeatureGate.Enabled(Quota) {
  if err := driver.minioClient.SetBucketQuota(bucketName, capacity, madmin.HardQuota); err != nil {
   return &csi.ControllerExpandVolumeResponse{}, status.Error(codes.Internal, err.Error())
  }
 }

 bucketMap, err := driver.minioClient.GetBucketMetadata(bucketName)
 if err != nil {
  return &csi.ControllerExpandVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
 bucketMap[MetaDataCapacity] = strconv.FormatInt(capacity, 10)
 if err = driver.minioClient.SetBucketMetadata(bucketName, bucketMap); err != nil {
  return &csi.ControllerExpandVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }

 return &csi.ControllerExpandVolumeResponse{CapacityBytes: capacity, NodeExpansionRequired: false}, nil
}

CSI Node 类接口的实现

接下去看一下 CSI Node 类接口的具体实现,主要实现的是 NodePublishVolume、NodeUnpublishVolume 这两个接口。NodeStageVolume 和 NodeUnstageVolume 这两个接口并没有相应的逻辑,也就是说不存在多个 Pod 共享一个 volume 的情况。

  • NodePublishVolume 接口会调用 MinIODriver 的 NodePublishVolume 接口。在这个接口中会封装好的 s3fs 执行命令,并将执行命令发送给 connector,connector 在宿主机上执行相应的命令。这个命令的主要作用是将 minio 的 bucket 挂载到 target path 上,也就是 /var/lib/kubelet/pods/${pod uid}/volumes/kubernetes.io****~${CSI Plugin Name}/${PV name} 上。
  • NodeUnpublishVolume 接口会调用 FuseUmount 方法 unmount 掉 target path 上的挂载。代码继续跟踪下去,它是在容器内部就执行了 unmount 方法。为什么在容器内部执行 unmount 方法就可以了呢?这是因为在部署这个容器的时候,把宿主机的 /var/lib/kubelet/pods/ 目录就挂载到了容器的 /var/lib/kubelet/pods/ 上。
type nodeServer struct {
 *csicommon.DefaultNodeServer
}

func newNodeServer(d *csicommon.CSIDriver) *nodeServer {
 return &nodeServer{
  DefaultNodeServer: csicommon.NewDefaultNodeServer(d),
 }
}

func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
 volumeID := req.GetVolumeId()
 targetPath := req.GetTargetPath()
  ...
  
 return driver.NodePublishVolume(ctx, req)
}

func (ns *nodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublishVolumeRequest) (*csi.NodeUnpublishVolumeResponse, error) {
 volumeID := req.GetVolumeId()
 targetPath := req.GetTargetPath()
  ...
  
 if err := common.FuseUmount(targetPath); err != nil {
  return &csi.NodeUnpublishVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
 klog.Infof("s3: mountpoint %s has been unmounted.", targetPath)

 return &csi.NodeUnpublishVolumeResponse{}, nil
}

func (ns *nodeServer) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRequest) (*csi.NodeStageVolumeResponse, error) {
 return &csi.NodeStageVolumeResponse{}, nil
}

func (ns *nodeServer) NodeUnstageVolume(ctx context.Context, req *csi.NodeUnstageVolumeRequest) (*csi.NodeUnstageVolumeResponse, error) {
 return &csi.NodeUnstageVolumeResponse{}, nil
}

// NodeGetCapabilities returns the supported capabilities of the node server
func (ns *nodeServer) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) {
 ...
}

func (ns *nodeServer) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolumeRequest) (*csi.NodeExpandVolumeResponse, error) {
 return &csi.NodeExpandVolumeResponse{}, status.Error(codes.Unimplemented, "NodeExpandVolume is not implemented")
}

// NodeGetVolumeStats used for csi metrics
func (ns *nodeServer) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) {
 return nil, nil
}

// NodePublishVolume 中的 driver.NodePublishVolume
func (driver *MinIODriver) NodePublishVolume(ctx context.Context, req *csi.NodePublishVolumeRequest) (*csi.NodePublishVolumeResponse, error) {
 pv, err := driver.kubeClinet.CoreV1().PersistentVolumes().Get(ctx, req.GetVolumeId(), metav1.GetOptions{})
 if err != nil {
  return &csi.NodePublishVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
 bucketName := pv.Spec.CSI.VolumeAttributes[ParamBucketNameTag]
 targetPath := req.GetTargetPath()

 notMnt, err := checkMount(targetPath)
 if err != nil {
  return &csi.NodePublishVolumeResponse{}, status.Error(codes.Internal, err.Error())
 }
 if !notMnt {
  return &csi.NodePublishVolumeResponse{}, nil
 }

 if err := S3FSMount(driver.Endpoint, bucketName, targetPath, driver.AK, driver.SK); err != nil {
  return &csi.NodePublishVolumeResponse{}, err
 }

 klog.Infof("s3: bucket %s successfully mounted to %s", bucketName, targetPath)

 return &csi.NodePublishVolumeResponse{}, nil
}

// NodeUnpublishVolume 中的 common.FuseUmount
func FuseUmount(path string) error {
 if err := mount.New("").Unmount(path); err != nil {
  return err
 }
 // as fuse quits immediately, we will try to wait until the process is done
 process, err := findFuseMountProcess(path)
 if err != nil {
  klog.Errorf("Error getting PID of fuse mount: %s", err)
  return nil
 }
 if process == nil {
  klog.Warningf("Unable to find PID of fuse mount %s, it must have finished already", path)
  return nil
 }
 klog.Infof("Found fuse pid %v of mount %s, checking if it still runs", process.Pid, path)
 return waitForProcess(process, 1)
}

扩展认识

上文提到了 s3f3,s3fs 是一个开源的用户态文件系统,它允许将 Amazon S3 bucket 挂载到 Linux 系统中,使其表现为本地文件系统。

通过 s3fs,用户可以像访问本地文件系统一样访问 Amazon S3 存储桶中的对象。这使得开发人员可以直接在应用程序中使用 S3 存储桶,而无需使用 Amazon S3 的 API 进行手动操作。s3fs 提供了对 Amazon S3 存储桶的标准文件系统操作,例如读取、写入、复制、移动和删除文件。它还支持文件权限、目录结构和符号链接等常见的文件系统功能。由于 Amazon S3 和 minio 的 API 是兼容的,因此也可以用于 minio。

s3f3 github 地址:https://github.com/s3fs-fuse/s3fs-fuse

责任编辑:武晓燕 来源: 多选参数
相关推荐

2022-06-16 07:50:35

数据结构链表

2022-07-26 08:03:27

Kubernetes节点磁盘

2024-01-30 18:29:29

微服务架构Ingress

2024-01-19 08:25:38

死锁Java通信

2024-02-04 00:00:00

Effect数据组件

2023-01-10 08:43:15

定义DDD架构

2023-07-26 13:11:21

ChatGPT平台工具

2024-01-02 12:05:26

Java并发编程

2023-08-01 12:51:18

WebGPT机器学习模型

2024-08-12 08:12:38

2024-03-12 08:37:32

asyncawaitJavaScript

2023-12-07 12:29:49

Nginx负载均衡策略

2024-01-26 06:05:16

KuberneteseBPF网络

2023-01-30 09:01:54

图表指南图形化

2024-07-31 08:39:45

Git命令暂存区

2024-05-06 00:00:00

InnoDBView隔离

2024-08-06 09:47:57

2023-12-12 08:02:10

2022-07-08 09:27:48

CSSIFC模型

2023-10-10 11:04:11

Rust难点内存
点赞
收藏

51CTO技术栈公众号