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 ®istrationServer{
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 ®isterapi.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 ®isterapi.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, ®isterapi.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对象:
总之,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 | Kubernetes 存储架构及插件使用