2
头图

Overview

最近在维护组内K8s CSI plugin代码时,一直对其内部原理好奇,故趁机深入学习熟悉K8s CSI相关原理。
部署K8s持久化存储插件时,需要按照CSI官网说明,部署一个daemonset pod实现插件注册,该pod内容器包含 node-driver-registrar ,部署yaml类似如下:


apiVersion: apps/v1
kind: DaemonSet
metadata:
  annotations:
    deprecated.daemonset.template.generation: "7"
  name: sunnyfs-csi-share-node
  namespace: sunnyfs
spec:
  revisionHistoryLimit: 10
  selector:
    matchLabels:
      app: sunnyfs-csi-share-node
  template:
    metadata:
      labels:
        app: sunnyfs-csi-share-node
    spec:
      containers:
        - args:
            - --csi-address=/csi/sunnyfs-csi-share.sock
            - --kubelet-registration-path=/csi/sunnyfs-csi-share.sock
          env:
            - name: KUBE_NODE_NAME
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: spec.nodeName
          image: quay.io/k8scsi/csi-node-driver-registrar:v2.1.0
          imagePullPolicy: IfNotPresent
          name: node-driver-registrar
          resources:
            limits:
              cpu: "2"
              memory: 4000Mi
            requests:
              cpu: "1"
              memory: 4000Mi
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /registration
              name: registration-dir
            - mountPath: /csi
              name: socket-dir
        - args:
            - --v=5
            - --endpoint=unix:///csi/sunnyfs-csi-share/sunnyfs-csi-share.sock
            - --nodeid=$(NODE_ID)
            - --drivername=csi.sunnyfs.share.com
            - --version=v1.0.0
          env:
            - name: NODE_ID
              valueFrom:
                fieldRef:
                  apiVersion: v1
                  fieldPath: spec.nodeName
          image: sunnyfs-csi-driver:v1.0.4
          imagePullPolicy: IfNotPresent
          lifecycle:
            preStop:
              exec:
                command:
                  - /bin/sh
                  - -c
                  - rm -rf /csi/sunnyfs-csi-share.sock /registration/csi.sunnyfs.share.com-reg.sock
          name: sunnyfs-csi-driver
          resources:
            limits:
              cpu: "2"
              memory: 4000Mi
            requests:
              cpu: "1"
              memory: 4000Mi
          securityContext:
            privileged: true
          terminationMessagePath: /dev/termination-log
          terminationMessagePolicy: File
          volumeMounts:
            - mountPath: /registration
              name: registration-dir
            - mountPath: /csi
              name: socket-dir
            - mountPath: /var/lib/kubelet/pods
              mountPropagation: Bidirectional
              name: mountpoint-dir
      dnsPolicy: ClusterFirstWithHostNet
      hostNetwork: true
      imagePullSecrets:
        - name: regcred
      restartPolicy: Always
      terminationGracePeriodSeconds: 30
      tolerations:
        - operator: Exists
      volumes:
        - hostPath:
            path: /var/lib/kubelet/plugins/csi.sunnyfs.share.com
            type: DirectoryOrCreate
          name: socket-dir
        - hostPath:
            path: /var/lib/kubelet/plugins_registry
            type: Directory
          name: registration-dir
        - hostPath:
            path: /var/lib/kubelet/pods
            type: Directory
          name: mountpoint-dir
  updateStrategy:
    rollingUpdate:
      maxUnavailable: 1
    type: RollingUpdate

pod内部署了自定义的csi-plugin如sunnyfs-csi-driver,该csi-plugin后端实际存储引擎是一个自研的文件类型存储系统;和一个sidecar container node-driver-registrar ,该容器主要实现了自定义的csi-plugin的注册。

重要问题是,是如何做到csi-plugin注册的?

答案很简单:daemonset中的 node-driver-registrar 作为一个sidecar container,会被kubelet plugin-manager模块调用,
node-driver-registrar sidecar container又会去调用我们自研的csi-plugin即sunnyfs-csi-driver container。而kubelet在启动时就会往plugin-manager模块
中注册一个csi plugin handler,该handler获取sunnyfs-csi-driver container基本信息后,会做一些操作,如更新node的annotation以及创建/更新CSINode对象。

源码解析

node-driver-registrar 源码解析

node-driver-registrar sidecar container代码逻辑很简单,主要做了两件事:rpc调用自研的csi-plugin插件,调用了GetPluginInfo方法,获取response.GetName即csiDriverName;
启动一个grpc server,并监听在宿主机上/var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock,供csi plugin handler来调用。

大概看下代码做的这两件事。

首先rpc调用自研的csi-plugin插件获取csiDriverName,L137-L152 :


func main() {
    // ...
    
    // 1. rpc调用自研的csi-plugin插件,调用了GetPluginInfo方法,获取response.GetName即csiDriverName
    csiConn, err := connection.Connect(*csiAddress, cmm)
    csiDriverName, err := csirpc.GetDriverName(ctx, csiConn)
    
    // Run forever
    nodeRegister(csiDriverName, addr)
}

GetDriverName 代码如下,主要rpc调用自研csi-plugin中identity server中的GetPluginInfo方法:

import (
    "github.com/container-storage-interface/spec/lib/go/csi"
)

// GetDriverName returns name of CSI driver.
func GetDriverName(ctx context.Context, conn *grpc.ClientConn) (string, error) {
    client := csi.NewIdentityClient(conn)
    req := csi.GetPluginInfoRequest{}
    rsp, err := client.GetPluginInfo(ctx, &req)
    // ...
    name := rsp.GetName()
    //...
    return name, nil
}

node-driver-registrar会先调用我们自研csi-plugin中identity server中的GetPluginInfo方法,而 CSI(Container Storage Interface) 设计文档
详细说明了,我们的csi-plugin中主要需要实现三个service: identity service, controller service和node service。其中,node service需要实现GetPluginInfo方法,返回我们自研plugin相关的基本信息,
比如我这里的identity service GetPluginInfo实现逻辑,主要返回我们自研csi plugin name:


func (ids *DefaultIdentityServer) GetPluginInfo(ctx context.Context, req *csi.GetPluginInfoRequest) (*csi.GetPluginInfoResponse, error) {
    klog.Infof("Using default GetPluginInfo")

    if ids.Driver.name == "" {
        return nil, status.Error(codes.Unavailable, "Driver name not configured")
    }

    if ids.Driver.version == "" {
        return nil, status.Error(codes.Unavailable, "Driver is missing version")
    }

    return &csi.GetPluginInfoResponse{
        Name:          ids.Driver.name,
        VendorVersion: ids.Driver.version,
    }, nil
}

然后,node-driver-registrar sidecar container就会启动一个grpc server,并监听在宿主机上/var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock 。
该rpc server遵循 kubelet plugin registration标准 ,*registrationServer service提供GetInfo和NotifyRegistrationStatus方法供客户端调用,
其实也就是被kubelet plugin manager模块调用,代码逻辑如下:

// 启动一个grpc server并监听在socket /var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock
func nodeRegister(csiDriverName, httpEndpoint string) {
    registrar := newRegistrationServer(csiDriverName, *kubeletRegistrationPath, supportedVersions)
    socketPath := buildSocketPath(csiDriverName)
    // ...
    lis, err := net.Listen("unix", socketPath)
    
    grpcServer := grpc.NewServer()
    registerapi.RegisterRegistrationServer(grpcServer, registrar)
    grpcServer.Serve(lis)
    // ...
}

// socket path为:/var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock
func buildSocketPath(csiDriverName string) string {
    return fmt.Sprintf("%s/%s-reg.sock", *pluginRegistrationPath, csiDriverName)
}

func newRegistrationServer(driverName string, endpoint string, versions []string) registerapi.RegistrationServer {
    return &registrationServer{
        driverName: driverName,
        endpoint:   endpoint,
        version:    versions,
    }
}
// GetInfo is the RPC invoked by plugin watcher
func (e registrationServer) GetInfo(ctx context.Context, req *registerapi.InfoRequest) (*registerapi.PluginInfo, error) {
    return &registerapi.PluginInfo{
        Type:              registerapi.CSIPlugin,
        Name:              e.driverName,
        Endpoint:          e.endpoint,
        SupportedVersions: e.version,
    }, nil
}
func (e registrationServer) NotifyRegistrationStatus(ctx context.Context, status *registerapi.RegistrationStatus) (*registerapi.RegistrationStatusResponse, error) {
    if !status.PluginRegistered {
        os.Exit(1)
    }
    
    return &registerapi.RegistrationStatusResponse{}, nil
}

总之,node-driver-registrar sidecar container 主要代码逻辑很简单,先调用我们自研的csi-plugin获取csiDriverName,然后在/var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock 启动一个grpc server,并按照kubelet plugin registration标准
提供了registrationServer供kubelet plugin manager实现rpc调用。

接下来关键就是kubelet plugin manager是如何rpc调用node-driver-registrar sidecar container的?

kubelet plugin manager 源码解析

kubelet组件在启动时,会实例化 pluginManager 对象,这里的socket dir就是 /var/lib/kubelet/plugins_registry/ 目录:

const (
    DefaultKubeletPluginsRegistrationDirName = "plugins_registry"
)

klet.pluginManager = pluginmanager.NewPluginManager(
        klet.getPluginsRegistrationDir(), /* sockDir */
        kubeDeps.Recorder,
    )

func (kl *Kubelet) getPluginsRegistrationDir() string {
    return filepath.Join(kl.getRootDir(), config.DefaultKubeletPluginsRegistrationDirName)
}

同时还会注册一个CSIPlugin type的csi.RegistrationHandler{}对象,并启动pluginManager对象,代码见 L1385-L1391


// Adding Registration Callback function for CSI Driver
kl.pluginManager.AddHandler(pluginwatcherapi.CSIPlugin, plugincache.PluginHandler(csi.PluginHandler))

// Start the plugin manager
klog.V(4).Infof("starting plugin manager")
go kl.pluginManager.Run(kl.sourcesReady, wait.NeverStop)

pluginmanager package 模块代码尽管比较多,但实际上主要就实现了两个逻辑。

plugin watcher

pluginmanager模块plugin watcher对象来 recursively watch /var/lib/kubelet/plugins_registry socket dir,而该对象实际上使用 github.com/fsnotify/fsnotify 包来实现该功能。
如果该socket dir增加或删除一个socket file,都会写入desiredStateOfWorld缓存对象的 socketFileToInfo map[string]PluginInfo 中,看下主要的watch socket dir代码,代码见 L50-L98


func (w *Watcher) Start(stopCh <-chan struct{}) error {
    // ...
    fsWatcher, err := fsnotify.NewWatcher()
    w.fsWatcher = fsWatcher
    // 去watch socket dir
    if err := w.traversePluginDir(w.path); err != nil {
        klog.Errorf("failed to traverse plugin socket path %q, err: %v", w.path, err)
    }

    // 启动一个goroutine去watch socket dir中,socket文件的增加和删除
    go func(fsWatcher *fsnotify.Watcher) {
        defer close(w.stopped)
        for {
            select {
            case event := <-fsWatcher.Events:
                if event.Op&fsnotify.Create == fsnotify.Create {
                    err := w.handleCreateEvent(event)
                } else if event.Op&fsnotify.Remove == fsnotify.Remove {
                    w.handleDeleteEvent(event)
                }
                continue
            case err := <-fsWatcher.Errors: 
                // ...
                continue
            case <-stopCh:
                // ...
                return
            }
        }
    }(fsWatcher)

    return nil
}

当我们daemonset部署node-driver-registrar sidecar container时,/var/lib/kubelet/plugins_registry socket dir中会多一个socket file ${csiDriverName}-reg.sock,
这时plugin watcher对象会把数据写入desiredStateOfWorld缓存中,供第二个逻辑reconcile使用

reconciler

该reconciler就是一个定时任务,每 rc.loopSleepDuration 运行一次,L84-L90


func (rc *reconciler) Run(stopCh <-chan struct{}) {
    wait.Until(func() {
        rc.reconcile()
    },
        rc.loopSleepDuration,
        stopCh)
}

每一次调谐,会去diff下两个缓存map对象:desiredStateOfWorld和actualStateOfWorld。desiredStateOfWorld是期望状态,actualStateOfWorld是实际状态。
如果一个plugin在actualStateOfWorld缓存中但不在desiredStateOfWorld中(表示plugin已经被删除了),或者尽管在desiredStateOfWorld中但是plugin.Timestamp不匹配(表示plugin重新注册更新了),
则需要从desiredStateOfWorld缓存中删除并注销插件DeRegisterPlugin;如果一个plugin在desiredStateOfWorld中但不在actualStateOfWorld缓存中,说明是新建的plugin,需要添加到desiredStateOfWorld缓存中并注册插件RegisterPlugin。
看下调谐主要逻辑 L110-L164


func (rc *reconciler) reconcile() {

    // diff下actualStateOfWorld和desiredStateOfWorld,判断是否需要从desiredStateOfWorld缓存中删除并注销插件DeRegisterPlugin
    for _, registeredPlugin := range rc.actualStateOfWorld.GetRegisteredPlugins() {
        if !rc.desiredStateOfWorld.PluginExists(registeredPlugin.SocketPath) {
            unregisterPlugin = true
        } else {
            for _, dswPlugin := range rc.desiredStateOfWorld.GetPluginsToRegister() {
                if dswPlugin.SocketPath == registeredPlugin.SocketPath && dswPlugin.Timestamp != registeredPlugin.Timestamp {
                    unregisterPlugin = true
                    break
                }
            }
        }
        if unregisterPlugin {
            err := rc.UnregisterPlugin(registeredPlugin, rc.actualStateOfWorld)
        }
    }

    // diff下desiredStateOfWorld和actualStateOfWorld,查是否需要添加到desiredStateOfWorld缓存中并注册插件RegisterPlugin
    for _, pluginToRegister := range rc.desiredStateOfWorld.GetPluginsToRegister() {
        if !rc.actualStateOfWorld.PluginExistsWithCorrectTimestamp(pluginToRegister) {
            err := rc.RegisterPlugin(pluginToRegister.SocketPath, pluginToRegister.Timestamp, rc.getHandlers(), rc.actualStateOfWorld)
        }
    }
}

这里主要查看一个新建的plugin的注册逻辑,reconciler对象会rpc调用node-driver-registrar sidecar container中rpc server提供的的GetInfo。
然后根据返回字段的type,从最开始注册的pluginHandlers中查找对应的handler,这里就是上文说的CSIPlugin type的csi.RegistrationHandler{}对象,并调用该对象的
ValidatePlugin和RegisterPlugin来注册插件,这里的注册插件其实就是设置node annotation和创建/更新CSINode对象。最后rpc调用NotifyRegistrationStatus告知注册结果。

看下注册插件相关代码,L74-L134


// 与/var/lib/kubelet/plugins_registry/${csiDriverName}-reg.sock建立grpc通信
func dial(unixSocketPath string, timeout time.Duration) (registerapi.RegistrationClient, *grpc.ClientConn, error) {
    // ...
    c, err := grpc.DialContext(ctx, unixSocketPath, grpc.WithInsecure(), grpc.WithBlock())
    return registerapi.NewRegistrationClient(c), c, nil
}

func (og *operationGenerator) GenerateRegisterPluginFunc(/*...*/) func() error {
    registerPluginFunc := func() error {
        client, conn, err := dial(socketPath, dialTimeoutDuration)
        // 调用node-driver-registrar sidecar container中rpc server提供的的GetInfo
        infoResp, err := client.GetInfo(ctx, &registerapi.InfoRequest{})
        // 这里handler就是上文说的CSIPlugin type的csi.RegistrationHandler{}对象
        handler, ok := pluginHandlers[infoResp.Type]
        // 调用handler.ValidatePlugin
        if err := handler.ValidatePlugin(infoResp.Name, infoResp.Endpoint, infoResp.SupportedVersions); err != nil {
        }
        // 加入actualStateOfWorldUpdater缓存
        err = actualStateOfWorldUpdater.AddPlugin(cache.PluginInfo{
            SocketPath: socketPath,
            Timestamp:  timestamp,
            Handler:    handler,
            Name:       infoResp.Name,
        })
        // 这是最关键逻辑,调用handler.RegisterPlugin注册插件
        // 这里的infoResp.Endpoint是我们自研的csi-plugin监听的socket path
        if err := handler.RegisterPlugin(infoResp.Name, infoResp.Endpoint, infoResp.SupportedVersions); err != nil {
            return og.notifyPlugin(client, false, fmt.Sprintf("RegisterPlugin error -- plugin registration failed with err: %v", err))
        }
        // ...
    }
    return registerPluginFunc
}

总之,kubelet plugin manager模块代码逻辑比较清晰简单,主要两个逻辑:通过plugin watcher对象去watch socket dir即/var/lib/kubelet/plugins_registry,把plugin数据放入
desiredStateOfWorld缓存中;reconcile调谐desiredStateOfWorld和actualStateOfWorld缓存,调用node-driver-registrar获取plugin info,根据该plugin info查找plugin handler,
然后调用plugin handler来注册插件RegisterPlugin,plugin handler会根据传入的csi-plugin监听的socket path,直接和我们自研的csi-plugin通信(其实node-driver-registrar起到中介作用,传递
我们自研csi-plugin grpc server监听的socket path这个关键信息)。

接下来关键逻辑就是csi.RegistrationHandler{}对象是如何注册插件的?

csi.RegistrationHandler 源码解析

csi.RegistrationHandler{}对象注册插件逻辑,主要就是更新node annotation和创建/更新CSINode对象,这里可以看下代码逻辑 L112-L154

import (
    csipbv1 "github.com/container-storage-interface/spec/lib/go/csi"
)

func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, versions []string) error {
    // ...
    // 与我们自研的csi-plugin建立grpc通信,并调用csi-plugin中node service中的NodeGetInfo()获得相关数据,供更新node annotation和创建CSINode对象使用
    csi, err := newCsiDriverClient(csiDriverName(pluginName))
    driverNodeID, maxVolumePerNode, accessibleTopology, err := csi.NodeGetInfo(ctx)
    // ...
    // 这里是主要逻辑:更新node annotation和创建/更新CSINode对象
    err = nim.InstallCSIDriver(pluginName, driverNodeID, maxVolumePerNode, accessibleTopology)
    // ...
    return nil
}

// 与我们自研的csi-plugin建立grpc通信,创建一个grpc client
func newCsiDriverClient(driverName csiDriverName) (*csiDriverClient, error) {
    // ...
    nodeV1ClientCreator := newV1NodeClient
    return &csiDriverClient{
        driverName:          driverName,
        addr:                csiAddr(existingDriver.endpoint),
        nodeV1ClientCreator: nodeV1ClientCreator,
    }, nil
}
// 这里调用csipbv1.NewNodeClient(conn)创建一个grpc client
// CSI标准文档可以参见该仓库的 https://github.com/container-storage-interface/spec/blob/master/spec.md#rpc-interface
func newV1NodeClient(addr csiAddr) (nodeClient csipbv1.NodeClient, closer io.Closer, err error) {
    var conn *grpc.ClientConn
    conn, err = newGrpcConn(addr)
    nodeClient = csipbv1.NewNodeClient(conn)
    return nodeClient, conn, nil
}
func newGrpcConn(addr csiAddr) (*grpc.ClientConn, error) {
    network := "unix"
    return grpc.Dial(string(addr), /*...*/)
}

以上代码中,主要包含两个逻辑:更新node annotation;创建更新CSINode对象。
更新node annotation逻辑很简单,主要是往当前Node中增加一个annotation csi.volume.kubernetes.io/nodeid:{"$csiDriverName":"$driverNodeID"} ,$csiDriverName是之前rpc调用node-driver-registrar sidecar container获得的,
$driverNodeID是直接rpc调用我们自定义csi-plugin的node service NodeGetInfo获得的,代码可见 L237-L273

然后是往apiserver中创建/更新CSINode对象,创建CSINode对象逻辑可见 CreateCSINode ,更新CSINode对象逻辑可见 installDriverToCSINode
就可以通过kubectl查看CSINode对象:

csinodes.png

总之,csi.RegistrationHandler{}对象注册插件其实主要就是更新了node annotation和创建/更新该plugin相应的CSINode对象。

总结

本文主要学习了CSI Plugin注册机制相关原理逻辑,涉及的主要组件包括:由node-driver-registrar sidecar container和我们自研的csi-plugin组成的daemonset pod,以及
kubelet plugin manager模块框架包,和csi plugin handler模块。其中,kubelet plugin manager模块框架包是一个桥梁,会rpc调用node-driver-registrar sidecar container获取
我们自研csi-plugin相关信息如监听的rpc socket地址,然后调用csi plugin handler模块并传入csi-plugin rpc socket地址, 与csi-plugin直接rpc通信,
实现更新node annotation和创建/更新CSINode对象等相关业务逻辑。

这样,通过以上几个组件模块共同作用,我们自研的一个csi-plugin就注册进来了。
但是,我们自研的csi-plugin提供了create/delete volume等核心功能,又是如何工作的呢?后续有空再更新。

参考文献

一文读懂 K8s 持久化存储流程

从零开始入门 K8s | Kubernetes 存储架构及插件使用

Kubernetes Container Storage Interface (CSI) Documentation

node-driver-registrar


lx1036
3.1k 声望923 粉丝

为五斗米折腰