local-path-provisioner 源码解析

程序入口main.go

main 函数

日志模块:

logrus.SetFormatter(&logrus.TextFormatter{FullTimestamp: true})

配置命令行应用:

a := cli.NewApp()
a.Version = VERSION #设置help显示版本号为0.0.1
a.Usage = "Local Path Provisioner" #配置help显示usage内容
a.Before = func(c *cli.Context) error { #如果参数debug为true 设置启动模式为debug模式
   if c.GlobalBool("debug") {
      logrus.SetLevel(logrus.DebugLevel)
   }
   return nil
}
a.Flags = []cli.Flag{ #是否开启debug参数--debug或者-d
   cli.BoolFlag{
      Name:   "debug, d",
      Usage:  "enable debug logging level",
      EnvVar: "RANCHER_DEBUG",
   },
}
a.Commands = []cli.Command{ #程序执行入口命令,执行命令列表
   StartCmd(),
}
a.CommandNotFound = cmdNotFound #如果找不到这个命令执行cmdNotFound函数
a.OnUsageError = onUsageError   #如果使用错误执行onUsageError函数
if err := a.Run(os.Args); err != nil { #接收os.args参数启动程序
   logrus.Fatalf("Critical error: %v", err)
}

 

StartCmd函数(用于配置参数):

func StartCmd() cli.Command {
   return cli.Command{
      Name: "start",  #配置第一个参数为start的参数列表
      Flags: []cli.Flag{
         cli.StringFlag{
            Name:  FlagConfigFile,  #配置--config参数
            Usage: "Required. Provisioner configuration file.",
            Value: "",
         },
         cli.StringFlag{
            Name:   FlagProvisionerName,  #配置--provisioner-name参数 默认值rancher.io/local-path
            Usage:  "Required. Specify Provisioner name.",
            EnvVar: EnvProvisionerName,
            Value:  DefaultProvisionerName,
         },
         cli.StringFlag{
            Name:   FlagNamespace,  #配置--FlagNamespace参数 默认值为local-path-storage,
            Usage:  "Required. The namespace that Provisioner is running in",
            EnvVar: EnvNamespace,
            Value:  DefaultNamespace,
         },
         cli.StringFlag{
            Name:   FlagHelperImage,  #配置--helper-image参数 默认值为busybox 用于创建和删除目录
            Usage:  "Required. The helper image used for create/delete directories on the host",
            EnvVar: EnvHelperImage,
            Value:  DefaultHelperImage,
         },
      },
      Action: func(c *cli.Context) { #执行的动作 执行startDaemon函数
         if err := startDaemon(c); err != nil {
            logrus.Fatalf("Error starting daemon: %v", err)
         }
      },
   }
}

startDaemon函数:

stopCh := make(chan struct{}) #设置一个空结构体 stopCh
RegisterShutdownChannel(stopCh) #执行RegisterShutdownChannel函数,

RegisterShutdownChannel函数:

func RegisterShutdownChannel(done chan struct{}) {该函数的作用就是接收信号值,只接收syscall.SIGINT(2),syscall.SIGTERM(15),
   sigs := make(chan os.Signal, 1)
   signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   go func() {
      sig := <-sigs  #等待接收sigs 如果没有收到一直处于该阶段 不往下执行
      logrus.Infof("Receive %v to exit", sig)
      close(done) #关闭该程序
   }()
}
//通过集群内部配置创建 k8s 配置信息,通过 KUBERNETES_SERVICE_HOST 和 KUBERNETES_SERVICE_PORT 环境变量方式获取
//默认tokenfile  rootCAFile 在/var/run/secrets/kubernetes.io/serviceaccount/
config, err := rest.InClusterConfig()
if err != nil {
   return errors.Wrap(err, "unable to get client config")
}

kubeClient, err := clientset.NewForConfig(config)
if err != nil {
   return errors.Wrap(err, "unable to get k8s client")
}

//获取k8s版本号
serverVersion, err := kubeClient.Discovery().ServerVersion()
if err != nil {
   return errors.Wrap(err, "Cannot start Provisioner: failed to get Kubernetes server version")
}
//以下获取参数传递的值
configFile := c.String(FlagConfigFile)
if configFile == "" {
   return fmt.Errorf("invalid empty flag %v", FlagConfigFile)
}
provisionerName := c.String(FlagProvisionerName)
if provisionerName == "" {
   return fmt.Errorf("invalid empty flag %v", FlagProvisionerName)
}
namespace := c.String(FlagNamespace)
if namespace == "" {
   return fmt.Errorf("invalid empty flag %v", FlagNamespace)
}
helperImage := c.String(FlagHelperImage)
if helperImage == "" {
   return fmt.Errorf("invalid empty flag %v", FlagHelperImage)
}
//获取参数之后执行NewProvisioner函数
provisioner, err := NewProvisioner(stopCh, kubeClient, configFile, namespace, helperImage)
if err != nil {
   return err
}

NewProvisioner函数:

func NewProvisioner(stopCh chan struct{}, kubeClient *clientset.Clientset, configFile, namespace, helperImage string) (*LocalPathProvisioner, error) {
   //初始化LocalPathProvisioner结构体
   p := &LocalPathProvisioner{
      stopCh: stopCh,

      kubeClient:  kubeClient,
      namespace:   namespace,
      helperImage: helperImage,

      // config will be updated shortly by p.refreshConfig()
      config:      nil,
      configFile:  configFile,
      configData:  nil,
      configMutex: &sync.RWMutex{},
   }
   //刷新配置
   if err := p.refreshConfig(); err != nil {
      return nil, err
   }
   //监听配置变化并刷新
   p.watchAndRefreshConfig()
   return p, nil
}

refreshConfig函数:重载配置函数 加载config文件并加载到内存里

func (p *LocalPathProvisioner) refreshConfig() error {
   p.configMutex.Lock()  #写锁定
   defer p.configMutex.Unlock()  #执行完成之后解锁

   configData, err := loadConfigFile(p.configFile) #加载配置文件,解析成json格式
   if err != nil {
      return err
   }
   // no need to update
   if reflect.DeepEqual(configData, p.configData) { #对比新的配置和旧的配置 如果一样直接返回nil
      return nil
   }
   config, err := canonicalizeConfig(configData) #获取配置,设置NodePathMap的值
   if err != nil {
      return err
   }
   // only update the config if the new config file is valid
   p.configData = configData
   p.config = config

   output, err := json.Marshal(p.configData)
   if err != nil {
      return err
   }
   logrus.Debugf("Applied config: %v", string(output))

   return err
}

watchAndRefreshConfig 函数: 该函数是一个死循环 并且每30s执行重载配置函数

func (p *LocalPathProvisioner) watchAndRefreshConfig() {
   go func() {
      for {
         select {
         case <-time.Tick(ConfigFileCheckInterval): #设置配置检查间隔30s
            if err := p.refreshConfig(); err != nil { #刷新配置
               logrus.Errorf("failed to load the new config file: %v", err)
            }
         case <-p.stopCh: #等待stopCh 接收停止信号
            logrus.Infof("stop watching config file")
            return
         }
      }
   }()
}

 创建一个新的Provision控制器(主要用来生成新的Provision控制器,实例化 ProvisionController 结构,主要是为 PVC 提供 PV 操作,并初始化pv pvc storageclass的informers,当创建或者删除或者更新pv pvc storageclass时会触发enqueueClaim函数,该函数主要作用就是将pvc加入到workqueue队列,Controller 收到 WorkQueue 中的事件会根据对应的类型触发对应的回调函数):

 

pc := pvController.NewProvisionController(
   kubeClient,
   provisionerName,
   provisioner,
   serverVersion.GitVersion,

 NewProvisionController函数:

func NewProvisionController(
   client kubernetes.Interface,
   provisionerName string,
   provisioner Provisioner,
   kubeVersion string,
   options ...func(*ProvisionController) error,
) *ProvisionController {
   id, err := os.Hostname()   //获取主机名
   if err != nil {
      glog.Fatalf("Error getting hostname: %v", err)
   }
   // add a uniquifier so that two processes on the same host don't accidentally both become active
   id = id + "_" + string(uuid.NewUUID())  //生成一个随机id号
   component := provisionerName + "_" + id 

   v1.AddToScheme(scheme.Scheme)
   broadcaster := record.NewBroadcaster() //event的初始化
   broadcaster.StartLogging(glog.Infof)
   broadcaster.StartRecordingToSink(&corev1.EventSinkImpl{Interface: client.CoreV1().Events(v1.NamespaceAll)})
   eventRecorder := broadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: component})

   controller := &ProvisionController{
      client:                    client,
      provisionerName:           provisionerName,
      provisioner:               provisioner,
      kubeVersion:               utilversion.MustParseSemantic(kubeVersion),
      id:                        id,
      component:                 component,
      eventRecorder:             eventRecorder,
      resyncPeriod:              DefaultResyncPeriod,
      exponentialBackOffOnError: DefaultExponentialBackOffOnError,
      threadiness:               DefaultThreadiness,
      failedProvisionThreshold:  DefaultFailedProvisionThreshold,
      failedDeleteThreshold:     DefaultFailedDeleteThreshold,
      leaderElection:            DefaultLeaderElection,
      leaderElectionNamespace:   getInClusterNamespace(),
      leaseDuration:             DefaultLeaseDuration,
      renewDeadline:             DefaultRenewDeadline,
      retryPeriod:               DefaultRetryPeriod,
      metricsPort:               DefaultMetricsPort,
      metricsAddress:            DefaultMetricsAddress,
      metricsPath:               DefaultMetricsPath,
      addFinalizer:              DefaultAddFinalizer,
      hasRun:                    false,
      hasRunLock:                &sync.Mutex{},
   }

   for _, option := range options {
      err := option(controller)
      if err != nil {
         glog.Fatalf("Error processing controller options: %s", err)
      }
   }

   var rateLimiter workqueue.RateLimiter
   if controller.rateLimiter != nil {
      // rateLimiter set via parameter takes precedence
      rateLimiter = controller.rateLimiter
   } else if controller.exponentialBackOffOnError {
      rateLimiter = workqueue.NewMaxOfRateLimiter(
         workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 1000*time.Second),
         &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
      )
   } else {
      rateLimiter = workqueue.NewMaxOfRateLimiter(
         workqueue.NewItemExponentialFailureRateLimiter(15*time.Second, 15*time.Second),
         &workqueue.BucketRateLimiter{Limiter: rate.NewLimiter(rate.Limit(10), 100)},
      )
   }
   controller.claimQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "claims")
   controller.volumeQueue = workqueue.NewNamedRateLimitingQueue(rateLimiter, "volumes")

   informer := informers.NewSharedInformerFactory(client, controller.resyncPeriod)

   // ----------------------
   // PersistentVolumeClaims

   claimHandler := cache.ResourceEventHandlerFuncs{
      AddFunc:    func(obj interface{}) { controller.enqueueClaim(obj) },
      UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueClaim(newObj) },
      DeleteFunc: func(obj interface{}) {
         // NOOP. The claim is either in claimsInProgress and in the queue, so it will be processed as usual
         // or it's not in claimsInProgress and then we don't care
      },
   }

   if controller.claimInformer != nil {
      controller.claimInformer.AddEventHandlerWithResyncPeriod(claimHandler, controller.resyncPeriod)
   } else {
      controller.claimInformer = informer.Core().V1().PersistentVolumeClaims().Informer()
      controller.claimInformer.AddEventHandler(claimHandler)
   }
   controller.claimInformer.AddIndexers(cache.Indexers{uidIndex: func(obj interface{}) ([]string, error) {
      uid, err := getObjectUID(obj)
      if err != nil {
         return nil, err
      }
      return []string{uid}, nil
   }})
   controller.claimsIndexer = controller.claimInformer.GetIndexer()

   // -----------------
   // PersistentVolumes

   volumeHandler := cache.ResourceEventHandlerFuncs{
      AddFunc:    func(obj interface{}) { controller.enqueueVolume(obj) },
      UpdateFunc: func(oldObj, newObj interface{}) { controller.enqueueVolume(newObj) },
      DeleteFunc: func(obj interface{}) { controller.forgetVolume(obj) },
   }

   if controller.volumeInformer != nil {
      controller.volumeInformer.AddEventHandlerWithResyncPeriod(volumeHandler, controller.resyncPeriod)
   } else {
      controller.volumeInformer = informer.Core().V1().PersistentVolumes().Informer()
      controller.volumeInformer.AddEventHandler(volumeHandler)
   }
   controller.volumes = controller.volumeInformer.GetStore()

   // --------------
   // StorageClasses

   // no resource event handler needed for StorageClasses
   if controller.classInformer == nil {
      if controller.kubeVersion.AtLeast(utilversion.MustParseSemantic("v1.6.0")) {
         controller.classInformer = informer.Storage().V1().StorageClasses().Informer()
      } else {
         controller.classInformer = informer.Storage().V1beta1().StorageClasses().Informer()
      }
   }
   controller.classes = controller.classInformer.GetStore()

   if controller.createProvisionerPVLimiter != nil {
      glog.V(2).Infof("Using saving PVs to API server in background")
      controller.volumeStore = NewVolumeStoreQueue(client, controller.createProvisionerPVLimiter, controller.claimsIndexer, controller.eventRecorder)
   } else {
      if controller.createProvisionedPVBackoff == nil {
         // Use linear backoff with createProvisionedPVInterval and createProvisionedPVRetryCount by default.
         if controller.createProvisionedPVInterval == 0 {
            controller.createProvisionedPVInterval = DefaultCreateProvisionedPVInterval
         }
         if controller.createProvisionedPVRetryCount == 0 {
            controller.createProvisionedPVRetryCount = DefaultCreateProvisionedPVRetryCount
         }
         controller.createProvisionedPVBackoff = &wait.Backoff{
            Duration: controller.createProvisionedPVInterval,
            Factor:   1, // linear backoff
            Steps:    controller.createProvisionedPVRetryCount,
            //Cap:      controller.createProvisionedPVInterval,
         }
      }
      glog.V(2).Infof("Using blocking saving PVs to API server")
      controller.volumeStore = NewBackoffStore(client, controller.eventRecorder, controller.createProvisionedPVBackoff, controller)
   }

   return controller
}

pc.Run(stopCh)启动该服务

 

run函数:先进行选主,leaderElection选主运行(考虑到多副本情况下),选主规则是根据endpoints(除了endpoints之外还有configmap), 主要的作用就是当设置了metricsPort 会启动一个metrics端口用于promethues收集监控数据。之后启动三个informer实例 分别是pvc pv stroageclass 

 

 

// Run starts all of this controller's control loops
func (ctrl *ProvisionController) Run(_ <-chan struct{}) {
   // TODO: arg is as of 1.12 unused. Nothing can ever be cancelled. Should
   // accept a context instead and use it instead of context.TODO(), but would
   // break API. Not urgent: realistically, users are simply passing in
   // wait.NeverStop() anyway.

   run := func(ctx context.Context) {
      glog.Infof("Starting provisioner controller %s!", ctrl.component)
      defer utilruntime.HandleCrash()
      defer ctrl.claimQueue.ShutDown()
      defer ctrl.volumeQueue.ShutDown()

      ctrl.hasRunLock.Lock()
      ctrl.hasRun = true
      ctrl.hasRunLock.Unlock()
      if ctrl.metricsPort > 0 { 。  //如果metricsPort大于0 那么就上报数据给promethues 主要用来监控数据
         prometheus.MustRegister([]prometheus.Collector{
            metrics.PersistentVolumeClaimProvisionTotal,
            metrics.PersistentVolumeClaimProvisionFailedTotal,
            metrics.PersistentVolumeClaimProvisionDurationSeconds,
            metrics.PersistentVolumeDeleteTotal,
            metrics.PersistentVolumeDeleteFailedTotal,
            metrics.PersistentVolumeDeleteDurationSeconds,
         }...)
         http.Handle(ctrl.metricsPath, promhttp.Handler())
         address := net.JoinHostPort(ctrl.metricsAddress, strconv.FormatInt(int64(ctrl.metricsPort), 10))
         glog.Infof("Starting metrics server at %s\n", address)
         go wait.Forever(func() {
            err := http.ListenAndServe(address, nil)
            if err != nil {
               glog.Errorf("Failed to listen on %s: %v", address, err)
            }
         }, 5*time.Second)
      }

      // If a external SharedInformer has been passed in, this controller
      // should not call Run again
      if !ctrl.customClaimInformer {
         go ctrl.claimInformer.Run(ctx.Done())
      }
      if !ctrl.customVolumeInformer {
         go ctrl.volumeInformer.Run(ctx.Done())
      }
      if !ctrl.customClassInformer {
         go ctrl.classInformer.Run(ctx.Done())
      }
        //从apiserver同步资源
      if !cache.WaitForCacheSync(ctx.Done(), ctrl.claimInformer.HasSynced, ctrl.volumeInformer.HasSynced, ctrl.classInformer.HasSynced) {
         return
      }
        //启动线程工作 默认4个并发(重点,用于处理pvc,pv的工作队列volumeQueue,claimQueue)
      for i := 0; i < ctrl.threadiness; i++ {
         go wait.Until(ctrl.runClaimWorker, time.Second, context.TODO().Done())
         go wait.Until(ctrl.runVolumeWorker, time.Second, context.TODO().Done())
      }

      glog.Infof("Started provisioner controller %s!", ctrl.component)

      select {}
   }

   go ctrl.volumeStore.Run(context.TODO(), DefaultThreadiness)

   if ctrl.leaderElection { //创建一个选主锁
      rl, err := resourcelock.New("endpoints",
         ctrl.leaderElectionNamespace,
         strings.Replace(ctrl.provisionerName, "/", "-", -1), //provisionerName的默认值是rancher.io/local-path
         ctrl.client.CoreV1(),
         nil,
         resourcelock.ResourceLockConfig{
            Identity:      ctrl.id,
            EventRecorder: ctrl.eventRecorder,
         })
      if err != nil {
         glog.Fatalf("Error creating lock: %v", err)
      }

      leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{
         Lock:          rl,
         LeaseDuration: ctrl.leaseDuration,
         RenewDeadline: ctrl.renewDeadline,
         RetryPeriod:   ctrl.retryPeriod,
         Callbacks: leaderelection.LeaderCallbacks{
            OnStartedLeading: run,
            OnStoppedLeading: func() {
               glog.Fatalf("leaderelection lost")
            },
         },
      })
      panic("unreachable")
   } else {
      run(context.TODO())
   }
}

工作流程:

runClaimWorker -> processNextClaimWorkItem -> syncClaimHandler -> syncClaim -> provisionClaimOperation ->Provision

runVolumeWorker -> processNextVolumeWorkItem -> syncVolumeHandler -> syncVolume ->deleteVolumeOperation

 

runClaimWorker工作流程:

processNextClaimWorkItem:判断pvc是否完成创建

syncClaimHandler: 从informer的缓存中获取对象,然后调用syncClaim

syncClaim: 通过shouldProvision判断是否需要生成pv ,kubernetes版本大于1.5 如果pvc的Annotations存在"volume.beta.kubernetes.io/storage-provisioner",并且provisionerName的值rancher.io/local-path 返回true

如果是1.4版本以下的Annotations v1.BetaStorageClassAnnotation 并且StorageClassName 存在 并且可以通过getStorageClass获取到StorageClass

如果返回true 执行provisionClaimOperation函数

provisionClaimOperation: 主要是执行Provision函数,该函数需要自己编写 如下

,主要实现创建pvc以及通过创建pod挂载对应目录之后创建对应目录,最后将PersistentVolume对象返回provisionClaimOperation函数,之后绑定pv与pvc,通过绑定ClaimRef值,整体逻辑就是先创建pvc之后通过创建pod在对应node下创建文件夹之后创建pv。

func (p *LocalPathProvisioner) Provision(opts pvController.ProvisionOptions) (*v1.PersistentVolume, error) {
   pvc := opts.PVC
   if pvc.Spec.Selector != nil {
      return nil, fmt.Errorf("claim.Spec.Selector is not supported")
   }
   for _, accessMode := range pvc.Spec.AccessModes {
      if accessMode != v1.ReadWriteOnce {
         return nil, fmt.Errorf("Only support ReadWriteOnce access mode")
      }
   }
   node := opts.SelectedNode
   if opts.SelectedNode == nil {
      return nil, fmt.Errorf("configuration error, no node was specified")
   }

   basePath, err := p.getRandomPathOnNode(node.Name)
   if err != nil {
      return nil, err
   }

   name := opts.PVName
   folderName := strings.Join([]string{name, opts.PVC.Namespace, opts.PVC.Name}, "_")

   path := filepath.Join(basePath, folderName)
   logrus.Infof("Creating volume %v at %v:%v", name, node.Name, path)

   createCmdsForPath := []string{
      "mkdir",
      "-m", "0777",
      "-p",
   }
   if err := p.createHelperPod(ActionTypeCreate, createCmdsForPath, name, path, node.Name); err != nil {
      return nil, err
   }

   fs := v1.PersistentVolumeFilesystem
   hostPathType := v1.HostPathDirectoryOrCreate
   return &v1.PersistentVolume{
      ObjectMeta: metav1.ObjectMeta{
         Name: name,
      },
      Spec: v1.PersistentVolumeSpec{
         PersistentVolumeReclaimPolicy: *opts.StorageClass.ReclaimPolicy,
         AccessModes:                   pvc.Spec.AccessModes,
         VolumeMode:                    &fs,
         Capacity: v1.ResourceList{
            v1.ResourceName(v1.ResourceStorage): pvc.Spec.Resources.Requests[v1.ResourceName(v1.ResourceStorage)],
         },
         PersistentVolumeSource: v1.PersistentVolumeSource{
            HostPath: &v1.HostPathVolumeSource{
               Path: path,
               Type: &hostPathType,
            },
         },
         NodeAffinity: &v1.VolumeNodeAffinity{
            Required: &v1.NodeSelector{
               NodeSelectorTerms: []v1.NodeSelectorTerm{
                  {
                     MatchExpressions: []v1.NodeSelectorRequirement{
                        {
                           Key:      KeyNode,
                           Operator: v1.NodeSelectorOpIn,
                           Values: []string{
                              node.Name,
                           },
                        },
                     },
                  },
               },
            },
         },
      },
   }, nil
}

从这里面我们可以知道如何在在不同节点 设置不同的volume目录。我们只需要在配置文件里设置node对应的path就可以里

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值