在《kubernetes CSI(上)》一文中,我们对kubernetes存储的发展有了简单的了解,并且在文中提到了CSI中负责注册的组件node-driver-registrar,本文我们基于github.com/kubernetes-csi/node-driver-registrar@v2.5.1来分析kubernetes CSI的注册过程。

回顾

  1. 我们先回顾下一个CSI进程的组成:

一个CSI进程需要实现三个grpc service:IdentityServerControllerServerNodeServer,本文需要关注的是IdentityServer和NodeServer的各一个方法:

// github.com/container-storage-interface/spec/lib/go/csi/csi.pb.go
type IdentityServer interface {GetPluginInfo(context.Context, *GetPluginInfoRequest) (*GetPluginInfoResponse, error)/*...*/
}type NodeServer interface {NodeGetInfo(context.Context, *NodeGetInfoRequest) (*NodeGetInfoResponse, error)/*...*/
}

其中:

  • GetPluginInfo:返回CSI的信息,主要是名称
  • NodeGetInfo:返回一个nodeID,用于后续更新到Node和CSINode对象中
  1. 再回顾一下node-driver-registrarCSI进程的部署:

在部署上node-driver-registrar和CSI进程一般以sidecar的形式部署在一个pod里,采用daemonSet部署的原因是因为CSI的注册是通过kubelet完成的,daemonSet可以很好的契合相关场景。

yaml示例

上面说到node-driver-registrar和CSI进程的部署,我们来看一个yaml部署示例:

kind: DaemonSet
apiVersion: apps/v1
metadata:name: csi-nfs-nodenamespace: kube-system
spec:updateStrategy:rollingUpdate:maxUnavailable: 1type: RollingUpdateselector:matchLabels:app: csi-nfs-nodetemplate:metadata:labels:app: csi-nfs-nodespec:hostNetwork: true  # original nfs connection would be broken without hostNetwork settingdnsPolicy: Default  # available values: Default, ClusterFirstWithHostNet, ClusterFirstserviceAccountName: csi-nfs-node-sanodeSelector:kubernetes.io/os: linuxtolerations:- operator: "Exists"containers:- name: node-driver-registrarimage: registry.k8s.io/sig-storage/csi-node-driver-registrar:v2.5.1args:- --v=2- --csi-address=/csi/csi.sock- --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH)env:- name: DRIVER_REG_SOCK_PATHvalue: /var/lib/kubelet/plugins/csi-nfsplugin/csi.sock- name: KUBE_NODE_NAMEvalueFrom:fieldRef:fieldPath: spec.nodeNamevolumeMounts:- name: socket-dirmountPath: /csi- name: registration-dirmountPath: /registrationresources:limits:memory: 100Mirequests:cpu: 10mmemory: 20Mi- name: nfssecurityContext:privileged: truecapabilities:add: ["SYS_ADMIN"]allowPrivilegeEscalation: trueimage: gcr.io/k8s-staging-sig-storage/nfsplugin:canaryargs:- "-v=5"- "--nodeid=$(NODE_ID)"- "--endpoint=$(CSI_ENDPOINT)"env:- name: NODE_IDvalueFrom:fieldRef:fieldPath: spec.nodeName- name: CSI_ENDPOINTvalue: unix:///csi/csi.sockports:- containerPort: 29653name: healthzprotocol: TCPimagePullPolicy: "IfNotPresent"volumeMounts:- name: socket-dirmountPath: /csi- name: pods-mount-dirmountPath: /var/lib/kubelet/podsmountPropagation: "Bidirectional"resources:limits:memory: 300Mirequests:cpu: 10mmemory: 20Mivolumes:- name: socket-dirhostPath:path: /var/lib/kubelet/plugins/csi-nfsplugintype: DirectoryOrCreate- name: pods-mount-dirhostPath:path: /var/lib/kubelet/podstype: Directory- hostPath:path: /var/lib/kubelet/plugins_registrytype: Directoryname: registration-dir

上述示例yaml中有两个容器,一个是node-driver-registrar,另一个是nfs(也就是前文说的的CSI容器),我们主要关注node-driver-registrar容器的启动参数volumeMounts配置:

  • 启动参数

启动参数有三个,释义如下:

  1. v:指定容器日志级别
  2. csi-address:node-driver-registrar需要通过grpc访问CSI进程,这个参数用于指定CSI进程的unix sock文件路径。
  3. kubelet-registration-path:整个CSI功能是由node-driver-registrar向kubelet注册的,注册完成之后会直接通过unix sock访问CSI进程,因此kubelet-registration-path用于告诉kubelet,CSI进程的sock文件在宿主机上的绝对路径。kubelet-registration-path参数就是配置CSI进程的sock文件在宿主机上的绝对路径的
  • volumeMounts
  1. socket-dir:挂载宿主机的/var/lib/kubelet/plugins/csi-nfsplugin目录到容器内的/csi目录,其实就是CSI进程sock所在目录。
  2. registration-dir:kubelet启动后会基于fsnotify监听宿主机上的/var/lib/kubelet/plugins_registry目录,node-driver-registrar也是以unix sock形式提供grpc服务,因此node-driver-registrar把自己的sock文件按格式放到/var/lib/kubelet/plugins_registry目录下就会触发kubelet的注册过程。registration-dir就是用于把kubelet监听目录挂载到容器内。

源码分析

CSI注册过程涉及node-driver-registrar、kubelet和CSI进程,因不同存储的CSI实现会有不同,因此这里主要针对公共逻辑部分代码做分析,即只分析node-driver-registrar和kubelet部分代码。

node-driver-registrar源码分析
  • 启动参数

在分析main函数前,我们先看看node-driver-registrar两个非常重要的启动参数:

// node-driver-registrar/cmd/csi-node-driver-registrar/main.govar (csiAddress              = flag.String("csi-address", "/run/csi/socket", "Path of the CSI driver socket that the node-driver-registrar will connect to.")kubeletRegistrationPath = flag.String("kubelet-registration-path", "", "Path of the CSI driver socket on the Kubernetes host machine.")/*...*/
)

yaml示例章节下的启动参数对应,这里再重复解释下:

  1. csi-address:指定CSI进程的unix sock文件路径。
  2. kubelet-registration-path:CSI进程的sock文件在宿主机上的绝对路径。
  • main函数

我们再看看main函数:

// node-driver-registrar/cmd/csi-node-driver-registrar/main.gofunc main() {/*...*/csiConn, err := connection.Connect(*csiAddress, cmm)/*...*/csiDriverName, err := csirpc.GetDriverName(ctx, csiConn)/*...*/nodeRegister(csiDriverName, addr)
}

main函数里逻辑其实非常简单,首先会基于启动参数csi-address初始化一个到CSI进程的连接csiConn,之后基于该连接通过csirpc.GetDriverName拿到csiDriver的名称。

  • GetDriverName

我们来看看csirpc.GetDriverName的实现:

func GetDriverName(ctx context.Context, conn *grpc.ClientConn) (string, error) {client := csi.NewIdentityClient(conn)req := csi.GetPluginInfoRequest{}rsp, err := client.GetPluginInfo(ctx, &req)if err != nil {return "", err}name := rsp.GetName()if name == "" {return "", fmt.Errorf("driver name is empty")}return name, nil
}

在GetDriverName函数中,会先基于CSI进程的连接初始化一个连接IdentityServer service(CSI进程中的一个grpc service,前文有提到),并调用IdentityServer service的GetPluginInfo方法,从返回数据里拿到name字段作为csiDriver的名称。

  • nodeRegister

再看看nodeRegister的实现:

func buildSocketPath(csiDriverName string) string {return fmt.Sprintf("%s/%s-reg.sock", *pluginRegistrationPath, csiDriverName)
}func nodeRegister(csiDriverName, httpEndpoint string) {registrar := newRegistrationServer(csiDriverName, *kubeletRegistrationPath, supportedVersions)socketPath := buildSocketPath(csiDriverName)/*...*/lis, err := net.Listen("unix", socketPath)/*...*/grpcServer := grpc.NewServer()/*...*/// Registers kubelet plugin watcher api.registerapi.RegisterRegistrationServer(grpcServer, registrar)/*...*/// Starts serviceif err := grpcServer.Serve(lis); err != nil {klog.Errorf("Registration Server stopped serving: %v", err)os.Exit(1)}/*...*/
}

结合前文yaml示例等章节内容,nodeRegister函数的主要功能就是启动一个rpc服务,并且把该rpc的sock文件放到宿主机的/var/lib/kubelet/plugins_registry/{csiDriverName}-reg.sock。这里rpc服务的定义如下:

// k8s.io/kubelet/pkg/apis/pluginregistration/v1/api.pb.go
type RegistrationServer interface {GetInfo(context.Context, *InfoRequest) (*PluginInfo, error)NotifyRegistrationStatus(context.Context, *RegistrationStatus) (*RegistrationStatusResponse, error)
}

RegistrationServer的方法释义如下:

  1. GetInfo:node-driver-registrar把sock文件放到kubelet监听目录出发kubelet的注册流程后,kubelet会基于该sock文件和rpc方式访问node-driver-registrar的GetInfo方法。GetInfo方法需要提供csiDriverName、CSI进程sock文件在宿主机上的绝对路径等信息
  2. NotifyRegistrationStatuskubelet执行注册逻辑后,会通过rpc回调node-driver-registrar的NotifyRegistrationStatus告知注册结果(成功/失败),如果注册失败node-driver-registrar进程会退出,容器会重启,相当于尝试再次注册。

到这里,node-driver-registrar组件的主要逻辑大致分析完了,接下来我们看看kubelet对应的代码逻辑。

kubelet相关代码分析
  • fsnotify

我们先看看kubelet基于fsnotify对目录的监听:

// kubernetes/pkg/kubelet/config/defaults.go
const DefaultKubeletPluginsRegistrationDirName = "plugins_registry"// kubernetes/pkg/kubelet/kubelet_getters.go
func (kl *Kubelet) getPluginsRegistrationDir() string {// kl.getRootDir() ==> /var/lib/kubeletreturn filepath.Join(kl.getRootDir(), config.DefaultKubeletPluginsRegistrationDirName)
}// kubernetes/pkg/kubelet/kubelet.go
func NewMainKubelet(...){/*...*/klet.pluginManager = pluginmanager.NewPluginManager(klet.getPluginsRegistrationDir(), /* sockDir */klet.getPluginsDir(),             /* deprecatedSockDir */kubeDeps.Recorder,)/*...*/
}// kubernetes/pkg/kubelet/pluginmanager/plugin_manager.go
func NewPluginManager(sockDir string,deprecatedSockDir string,recorder record.EventRecorder) PluginManager {asw := cache.NewActualStateOfWorld()dsw := cache.NewDesiredStateOfWorld()reconciler := reconciler.NewReconciler(operationexecutor.NewOperationExecutor(operationexecutor.NewOperationGenerator(recorder,),),loopSleepDuration,dsw,asw,)pm := &pluginManager{desiredStateOfWorldPopulator: pluginwatcher.NewWatcher(sockDir,deprecatedSockDir,dsw,),reconciler:          reconciler,desiredStateOfWorld: dsw,actualStateOfWorld:  asw,}return pm
}// kubernetes/pkg/kubelet/pluginmanager/pluginwatcher/plugin_watcher.go
func NewWatcher(sockDir string, deprecatedSockDir string, desiredStateOfWorld cache.DesiredStateOfWorld) *Watcher {return &Watcher{path:                sockDir,deprecatedPath:      deprecatedSockDir,fs:                  &utilfs.DefaultFs{},desiredStateOfWorld: desiredStateOfWorld,}
}// kubernetes/pkg/kubelet/pluginmanager/pluginwatcher/plugin_watcher.go
func (w *Watcher) Start(stopCh <-chan struct{}) error {klog.V(2).Infof("Plugin Watcher Start at %s", w.path)w.stopped = make(chan struct{})// Creating the directory to be watched if it doesn't exist yet,// and walks through the directory to discover the existing plugins.if err := w.init(); err != nil {return err}fsWatcher, err := fsnotify.NewWatcher()if err != nil {return fmt.Errorf("failed to start plugin fsWatcher, err: %v", err)}w.fsWatcher = fsWatcher// Traverse plugin dir and add filesystem watchers before starting the plugin processing goroutine.if err := w.traversePluginDir(w.path); err != nil {klog.Errorf("failed to traverse plugin socket path %q, err: %v", w.path, err)}// Traverse deprecated plugin dir, if specified.if len(w.deprecatedPath) != 0 {if err := w.traversePluginDir(w.deprecatedPath); err != nil {klog.Errorf("failed to traverse deprecated plugin socket path %q, err: %v", w.deprecatedPath, err)}}go func(fsWatcher *fsnotify.Watcher) {defer close(w.stopped)for {select {case event := <-fsWatcher.Events://TODO: Handle errors by taking corrective measuresif event.Op&fsnotify.Create == fsnotify.Create {err := w.handleCreateEvent(event)if err != nil {klog.Errorf("error %v when handling create event: %s", err, event)}} else if event.Op&fsnotify.Remove == fsnotify.Remove {w.handleDeleteEvent(event)}continuecase err := <-fsWatcher.Errors:if err != nil {klog.Errorf("fsWatcher received error: %v", err)}continuecase <-stopCh:// In case of plugin watcher being stopped by plugin manager, stop// probing the creation/deletion of plugin sockets.// Also give all pending go routines a chance to completeselect {case <-w.stopped:case <-time.After(11 * time.Second):klog.Errorf("timeout on stopping watcher")}w.fsWatcher.Close()return}}}(fsWatcher)return nil
}

上述代码片段描述了kubelet监听/var/lib/kubelet/plugin_registry目录,当该目录下有文件创建或者文件删除的时候,会执行w.handlerCreateEvent/w.handleDeleteEvent函数,这两个函数仅仅就把node-driver-registrar的sock文件从内存中一个叫desiredStateOfWorld的对象中加入或移除。难道注册过程仅仅如此?答案是否定的。

  • 注册

其实kubelet在初始化完pluginManager后,会执行pluginManager.Run方法,该方法如下:

// kubernetes/pkg/kubelet/pluginmanager/plugin_manager.go
func (pm *pluginManager) Run(sourcesReady config.SourcesReady, stopCh <-chan struct{}) {/*...*/go pm.reconciler.Run(stopCh)/*...*/
}// kubernetes/pkg/kubelet/pluginmanager/reconciler/reconciler.go
func (rc *reconciler) Run(stopCh <-chan struct{}) {wait.Until(func() {rc.reconcile()},rc.loopSleepDuration,stopCh)
}// kubernetes/pkg/kubelet/pluginmanager/reconciler/reconciler.go
func (rc *reconciler) reconcile() {// Unregisterations are triggered before registrations// Ensure plugins that should be unregistered are unregistered.for _, registeredPlugin := range rc.actualStateOfWorld.GetRegisteredPlugins() {unregisterPlugin := falseif !rc.desiredStateOfWorld.PluginExists(registeredPlugin.SocketPath) {unregisterPlugin = true} else {// We also need to unregister the plugins that exist in both actual state of world// and desired state of world cache, but the timestamps don't match.// Iterate through desired state of world plugins and see if there's any plugin// with the same socket path but different timestamp.for _, dswPlugin := range rc.desiredStateOfWorld.GetPluginsToRegister() {if dswPlugin.SocketPath == registeredPlugin.SocketPath && dswPlugin.Timestamp != registeredPlugin.Timestamp {klog.V(5).Infof(registeredPlugin.GenerateMsgDetailed("An updated version of plugin has been found, unregistering the plugin first before reregistering", ""))unregisterPlugin = truebreak}}}if unregisterPlugin {klog.V(5).Infof(registeredPlugin.GenerateMsgDetailed("Starting operationExecutor.UnregisterPlugin", ""))err := rc.operationExecutor.UnregisterPlugin(registeredPlugin.SocketPath, rc.getHandlers(), rc.actualStateOfWorld)if err != nil &&!goroutinemap.IsAlreadyExists(err) &&!exponentialbackoff.IsExponentialBackoff(err) {// Ignore goroutinemap.IsAlreadyExists and exponentialbackoff.IsExponentialBackoff errors, they are expected.// Log all other errors.klog.Errorf(registeredPlugin.GenerateErrorDetailed("operationExecutor.UnregisterPlugin failed", err).Error())}if err == nil {klog.V(1).Infof(registeredPlugin.GenerateMsgDetailed("operationExecutor.UnregisterPlugin started", ""))}}}// Ensure plugins that should be registered are registeredfor _, pluginToRegister := range rc.desiredStateOfWorld.GetPluginsToRegister() {if !rc.actualStateOfWorld.PluginExistsWithCorrectTimestamp(pluginToRegister) {klog.V(5).Infof(pluginToRegister.GenerateMsgDetailed("Starting operationExecutor.RegisterPlugin", ""))err := rc.operationExecutor.RegisterPlugin(pluginToRegister.SocketPath, pluginToRegister.FoundInDeprecatedDir, pluginToRegister.Timestamp, rc.getHandlers(), rc.actualStateOfWorld)if err != nil &&!goroutinemap.IsAlreadyExists(err) &&!exponentialbackoff.IsExponentialBackoff(err) {// Ignore goroutinemap.IsAlreadyExists and exponentialbackoff.IsExponentialBackoff errors, they are expected.klog.Errorf(pluginToRegister.GenerateErrorDetailed("operationExecutor.RegisterPlugin failed", err).Error())}if err == nil {klog.V(1).Infof(pluginToRegister.GenerateMsgDetailed("operationExecutor.RegisterPlugin started", ""))}}}
}

可以看出,kubelet是专门起了一个协程(goroutine)周期性地处理注册过程,确定某个CSI是否真正的需要向apiServer“注册”是根据actualStateOfWorld和前文提到的desiredStateOfWorld两者数据比较来确定是注册还是移除(取消注册)。如果需要注册,则会执行operationExecutor.RegisterPlugin;如果需要取消注册,则执行operationExecutor.UnregisterPlugin。

先看一下注册的最终实现(部分代码逻辑涉及go语法,这里直接给出关键步骤):

// kubernetes/pkg/kubelet/pluginmanager/operationexecutor/operation_generator.go
func (og *operationGenerator) GenerateRegisterPluginFunc(socketPath string,foundInDeprecatedDir bool,timestamp time.Time,pluginHandlers map[string]cache.PluginHandler,actualStateOfWorldUpdater ActualStateOfWorldUpdater) func() error {registerPluginFunc := func() error {client, conn, err := dial(socketPath, dialTimeoutDuration)/*…*/infoResp, err := client.GetInfo(ctx, &registerapi.InfoRequest{})if err != nil {return fmt.Errorf("RegisterPlugin error -- failed to get plugin info using RPC GetInfo at socket %s, err: %v", socketPath, err)}handler, ok := pluginHandlers[infoResp.Type]/*…*/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))}// Notify is called after register to guarantee that even if notify throws an error Register will always be called after validateif err := og.notifyPlugin(client, true, ""); err != nil {return fmt.Errorf("RegisterPlugin error -- failed to send registration status at socket %s, err: %v", socketPath, err)}return nil}return registerPluginFunc
}// kubernetes/pkg/kubelet/pluginmanager/operationexecutor/operation_generator.go
func (og *operationGenerator) notifyPlugin(client registerapi.RegistrationClient, registered bool, errStr string) error {ctx, cancel := context.WithTimeout(context.Background(), notifyTimeoutDuration)defer cancel()status := &registerapi.RegistrationStatus{PluginRegistered: registered,Error:            errStr,}if _, err := client.NotifyRegistrationStatus(ctx, status); err != nil {return errors.Wrap(err, errStr)}if errStr != "" {return errors.New(errStr)}return nil
}

这段代码有两个需要关注的地方:

  1. client.GetInfo和client.NotifyRegistrationStatus对应的是node-driver-register grpc的两个方法;
  2. handler.RegisterPlugin是真正的注册过程

handler.RegisterPlugin逻辑如下:

// kubernetes/pkg/volume/csi/csi_plugin.go
func (h *RegistrationHandler) RegisterPlugin(pluginName string, endpoint string, versions []string) error {/*…*/csi, err := newCsiDriverClient(csiDriverName(pluginName))/*…*/driverNodeID, maxVolumePerNode, accessibleTopology, err := csi.NodeGetInfo(ctx)if err != nil {if unregErr := unregisterDriver(pluginName); unregErr != nil {klog.Error(log("registrationHandler.RegisterPlugin failed to unregister plugin due to previous error: %v", unregErr))}return err}err = nim.InstallCSIDriver(pluginName, driverNodeID, maxVolumePerNode, accessibleTopology)if err != nil {if unregErr := unregisterDriver(pluginName); unregErr != nil {klog.Error(log("registrationHandler.RegisterPlugin failed to unregister plugin due to previous error: %v", unregErr))}return err}return nil
}// kubernetes/pkg/volume/csi/csi_plugin.go
func unregisterDriver(driverName string) error {csiDrivers.Delete(driverName)if err := nim.UninstallCSIDriver(driverName); err != nil {return errors.New(log("Error uninstalling CSI driver: %v", err))}return nil
}// kubernetes/pkg/volume/csi/nodeinfomanager/nodeinfomanager.go
func (nim *nodeInfoManager) InstallCSIDriver(driverName string, driverNodeID string, maxAttachLimit int64, topology map[string]string) error {if driverNodeID == "" {return fmt.Errorf("error adding CSI driver node info: driverNodeID must not be empty")}nodeUpdateFuncs := []nodeUpdateFunc{updateNodeIDInNode(driverName, driverNodeID),}if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) {nodeUpdateFuncs = append(nodeUpdateFuncs, updateTopologyLabels(topology))}err := nim.updateNode(nodeUpdateFuncs...)if err != nil {return fmt.Errorf("error updating Node object with CSI driver node info: %v", err)}if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) {err = nim.updateCSINode(driverName, driverNodeID, maxAttachLimit, topology)if err != nil {return fmt.Errorf("error updating CSINode object with CSI driver node info: %v", err)}}return nil
}// kubernetes/pkg/volume/csi/nodeinfomanager/nodeinfomanager.go
func (nim *nodeInfoManager) UninstallCSIDriver(driverName string) error {if utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) {err := nim.uninstallDriverFromCSINode(driverName)if err != nil {return fmt.Errorf("error uninstalling CSI driver from CSINode object %v", err)}}err := nim.updateNode(removeMaxAttachLimit(driverName),removeNodeIDFromNode(driverName),)if err != nil {return fmt.Errorf("error removing CSI driver node info from Node object %v", err)}return nil
}

通过上述代码,我们可以看出kubelet注册其实是做了两个事情:

  1. 更新对应node对象的annotation,写入csiDriverName
  2. 创建或更新CSINode对象

总结

前面我们从node-driver-registrar和kubelet代码分析了CSI的注册过程,我们可以总结出下图:

相关的步骤释义如下:

  1. kubelet启动后基于fsnotify监听/var/lib/kubelet/plugin_registry目录;
  2. node-driver-registrar启动通过启动参数中配置的CSI进程的sock文件,调CSI进程的GetPluginInfo方法获取CSI插件名称;
  3. node-driver-registrar启动后在/var/lib/kubelet/plugin_registry目录下创建自己的sock文件{csiName}-reg.sock
  4. kubelet的watcher监听到/var/lib/kubelet/plugin_registry目录下有sock文件创建,把该sock文件信息存入内存中的desiredStateOfWorld对象中;
  5. kubelet中有个reconciler协程周期性的检查desiredStateOfWorld对象和actualStateOfWorld对象中的数据差异,发现有新的CSI插件需要执行注册过程;
  6. reconciler通过/var/lib/kubelet/plugin_registry/{csiName}-reg.sock,调用node-driver-registrar下的GetInfo方法获取CSI插件的名称CSI进程的sock文件路径等信息;
  7. reconciler通过上一步拿到的CSI进程sock文件,调用CSI进程下NodeGetInfo方法获取一些数据用于后续的Node和CSINode对象;
  8. 组装数据调apiServer接口更新本节点对应的Node对象的annotation;
  9. 组装数据调apiServer接口创建/更新对应的CSINode对象;
  10. reconciler通过/var/lib/kubelet/plugin_registry/{csiName}-reg.sock,调用node-driver-registrar的NotifyRegistrationStatus方法,告知其注册结果。

微信公众号卡巴斯同步发布,欢迎大家关注。

kubernetes CSI(中)相关推荐

  1. 阿里云Kubernetes CSI实践—NAS动态存储卷使用

    1. 前言 NAS存储盘能将nfs(网络文件系统)挂载到你的Pod中,阿里云Kubernetes CSI支持静态存储卷挂载和动态存储卷挂载2种方式, 在静态存储卷挂载的方式中,通常需要手动编辑和创建一 ...

  2. Kubernetes CSI 介绍及使用

    CSI 介绍及使用 和 Flexvolume 类似,CSI 也是为第三方存储提供数据卷实现的抽象接口. 有了 Flexvolume,为何还要 CSI 呢? Flexvolume 只是给 kuberne ...

  3. kubernetes CSI(上)

    随着应用容器化的趋势,越来越多的应用部署到了kubernetes平台,同时日益复杂的业务场景,也使得kubernetes需要支持越来越多类别的存储.kubernete对存储的支持,大致可以分为三个历程 ...

  4. kubernetes CSI(下)

    前面几篇文章分别介绍了dynamic provisioning.CSI接口定义和CSI插件的注册等内容,这篇文章基于这些内容,尝试实现一个NFS的CSI,这个CSI主要包含注册.dynamic pro ...

  5. 在Kubernetes Pod中使用Service Account访问API Server

    2019独角兽企业重金招聘Python工程师标准>>> 在Kubernetes Pod中使用Service Account访问API Server 博客分类: Kubernetes ...

  6. ceph rbd mysql_如何在 Kubernetes 环境中搭建 MySQL(三):使用 PVC 挂接 RBD

    MySQL in Kubernetes MySQL 中的数据是关键信息,是有状态的,不可能随着 MySQL pod 的销毁而被销毁,所以数据必须要外接到一个可靠的存储系统中,目前已经有了 Ceph 系 ...

  7. Kubernetes CSI(一):介绍

    容器存储接口(CSI)是用于将任意块和文件存储系统暴露给诸如Kubernetes之类的容器编排系统(CO)上的容器化工作负载的标准.使用CSI的第三方存储提供商可以编写和部署在Kubernetes中公 ...

  8. Kubernetes对象中的PersistentVolume、PersistentVolumeClaim和StorageClass的概念关系

    Kubernetes容器要持久化数据,离不开volume,k8s的volume和Docker原生概念中的volume有一些差别,不过本次不讲这个,本次要明确的是k8s持久化数据用到的几个对象Persi ...

  9. ASP.NET Core在Azure Kubernetes Service中的部署和管理

    目标 部署:掌握将aspnetcore程序成功发布到Azure Kubernetes Service(AKS)上 管理:掌握将AKS上的aspnetcore程序扩容.更新版本 准备工作 注册 Azur ...

最新文章

  1. Linux下出现Read-only file system的解决办法
  2. Starting MySQL.... ERROR! The server quit without updating PID file
  3. 在计算机系统中引入通道结构,第5-6章习题讲解.doc
  4. android so文件不混淆_Android studio 混淆打包时如何忽略依赖库中的第三方.so文件...
  5. 精仿B站播放器外加弹幕库源码-带后台
  6. C-Free 5.0注册码分享
  7. 【PTA】 统计素数并求和
  8. 免费好用的的在线代码IDE网站,支持python
  9. excel午晚加班考勤统计(excel快速计算午多少个和晚多少个)
  10. wordpress简约淘客主题风格附详细实例教程源码
  11. 时差怎么理解_英国与中国的时差为什么隔8小时(英国与中国的时差解读)
  12. 探索跨平台应用开发的最佳实践
  13. Adaptive调度器
  14. 用免费邮箱,做你的网络资料“寄存器”
  15. 删除maven仓库中的lastUpdated
  16. CentOS 7安装Mongodb并使用Robo 3T远程测试连接
  17. windows安装Rocket因为JAVAHOME空格导致找不到加载类问题
  18. 如何搭建一个超级好用的JavaWeb框架?
  19. 制造业管理系统如何帮助企业做好物料编码管理?
  20. 2019-2020 10th BSUIR Open Programming Championship. Semifinal 补题

热门文章

  1. 数据备份形式主要方式解读大全
  2. js 10进制 16进制互相转换
  3. 群晖设置流量控制后进不去系统
  4. 开会时重点总是记不全,职场前辈教我用这几个录音转文字方法
  5. 基于80x86的导弹打飞机游戏
  6. linux系统浏览器安装失败怎么办,360浏览器Linux版测试期已过并删除配置文件也不能安装使用的解决...
  7. 关于编程与数学的名言金句——字字珠玑
  8. 画图工具-mini画板
  9. 当微信小程序邂逅知识问答,订单就按耐不住了!
  10. 三种联邦学习的简单介绍