Velero 备份集群数据代码解读

Velero 备份集群数据代码解读

1.介绍

Velero(以前称为Heptio Ark)可以为您提供了备份和还原Kubernetes集群资源和持久卷的能力,你可以在公有云或本地搭建的私有云环境安装Velero,可以为你提供以下能力:

  • 备份集群数据,并在集群故障的情况下进行还原;
  • 将集群资源迁移到其他集群;
  • 将您的生产集群复制到开发和测试集群;

Velero包含:

  • 在集群上运行的服务器端;
  • 在本地运行的命令行客户端;

2.工作原理

每个Velero的操作(如backup,schdule,restore)都是自定义资源,使用Kubernetes 自定义资源定义(CRD)定义并存储在 etcd中,Velero还包括处理自定义资源以执行备份,还原以及所有相关操作的控制器,可以备份或还原群集中的所有对象,也可以按类型,命名空间或标签过滤对象。

Velero是kubernetes用来灾难恢复的理想选择,也是在集群上执行系统操作(如升级)之前对应用程序状态进行快照的理想选择。

Velero 备份集群数据代码解读_第1张图片

3.代码实现

3.1 创建backup流程

在通过命令行velero backup create 创建backup的crd资源时,主要实现在velero/pkg/cli/backup包里,要想搞清楚执行该命令时具体走向那个函数,需要分析命令行的构建,接下来一起看一下velero/pkg/cli/backup包中构建命令行的函数create.NewCreateCommand,具体内容如下:

func NewCreateCommand(f client.Factory, use string) *cobra.Command {
	o := NewCreateOptions()

	c := &cobra.Command{
		Use:   use + " NAME",
		Short: "Create a backup",
		Args:  cobra.MaximumNArgs(1),
		Run: func(c *cobra.Command, args []string) {
			cmd.CheckError(o.Complete(args, f))  // 构建所需的资源
			cmd.CheckError(o.Validate(c, args, f))  // 检查参数是否正确
			cmd.CheckError(o.Run(c, f))  // 创建backup命令行具体实现的函数
		},
		Example: ...... // 构建执行命令行时的示例
	}

	o.BindFlags(c.Flags())   // 添加参数Flag
	o.BindWait(c.Flags())   
	o.BindFromSchedule(c.Flags())  // 添加从schedule创建backup相关的flag
	output.BindFlags(c.Flags())
	output.ClearOutputFlagDefault(c)

	return c  // 返回命令行对象
}

在调用NewCreateCommand方法时,会构建一个创建backup资源的子命令实例:

1.构建所需的资源,如相应的客户端
2.检查参数是否正确
3.构建创建backup命令行具体实现的函数
4.构建执行命令行时的示例
5.添加参数Flag
6.添加从schedule创建backup相关的flag
7.返回命令行对象

已经分析完构建命令行的过程,接下来看一下create.Run方法的具体实现,了解一下是如何构建Backup资源,以及如何将请求发送至kubenetes CRD API,实现如下:

func (o *CreateOptions) Run(c *cobra.Command, f client.Factory) error {
	backup, err := o.BuildBackup(f.Namespace())  // 根据命令行参数构建backup数据
    ......
    ...... // 判断是否为schedule创建的backup,以及是否需要等待backup执行完成
	_, err = o.client.VeleroV1().Backups(backup.Namespace).Create(context.TODO(), backup, metav1.CreateOptions{})  // 使用kubernetes客户端创建backup资源
	if err != nil {
		return err
	}

    ......
    ......
}

create.Run方法中,主要是构建Bakup请求参数,并向kubernetes API发起请求,以及构建等待备份完成的方法,这个会根据参数判断是否等待。

3.2 启动velero server流程

要想了解backup controller的工作原理,首先得从如何加载controller来看起,在velero中,加载所有的controller的过程具体实现在velero/pkg/cmd/server包中,在执行velero server [args]命令后,会首先根据参数加载需要的controller,默认全部加载,首先看一下命令行的构建过程,具体方法为server.NewCommand:

func NewCommand(f client.Factory) *cobra.Command {
    ......
    ...... // 初始化server启动所需的config实例和日志等其他资源

	var command = &cobra.Command{
		Use:    "server",
		Short:  "Run the velero server",
		Long:   "Run the velero server",
		Hidden: true,
		Run: func(c *cobra.Command, args []string) {  // 构建执行命令后具体执行的方法
            ......
            ......// 准备启动server所需的资源及参数
			s, err := newServer(f, config, logger) // 构建server实例
			cmd.CheckError(err)

			cmd.CheckError(s.run()) // 启动server
		},
	}

	......
    ......// 构建命令行参数

	return command // 返回命令行实例
}

在构建命令行实例的过程中,主要有以下几个过程:

1.初始化server启动所需的config实例和日志等其他资源
2.构建执行命令后具体执行的方法
3.准备启动server所需的资源及参数
4.构建server实例
5.构建启动server实例的方法
6.返回命令行实例

在构建完成命令行实例后,当执行velero server时,会执行到实例的RUN方法,也就是上述的过程,构建server的过程这里就不再赘述,感兴趣的可自行阅读newServer方法,接下来看一下server.run方法中具体所做的工作:

func (s *server) run() error {
	signals.CancelOnShutdown(s.cancelFunc, s.logger)

	if s.config.profilerAddress != "" {
		go s.runProfiler()
	}

	// Since s.namespace, which specifies where backups/restores/schedules/etc. should live,
	// *could* be different from the namespace where the Velero server pod runs, check to make
	// sure it exists, and fail fast if it doesn't.
	if err := s.namespaceExists(s.namespace); err != nil { // 判断默认命名空间velero或指定的命名空间是否存在
		return err
	}

	if err := s.initDiscoveryHelper(); err != nil {  // 初始化k8s的discovery客户端用来查询自定义的CRD资源,另外还有dynamic和ClientSet客户端等,有兴趣可自行查找资料了解区别
		return err
	}

	if err := s.veleroResourcesExist(); err != nil {  // 判断velero工作所需的资源是否存在,
		return err
	}

	if err := s.initRestic(); err != nil {  // 初始化restic相关资源
		return err
	}

	if err := s.runControllers(s.config.defaultVolumeSnapshotLocations); err != nil { // 运行所有的controller
		return err
	}

	return nil
}

在这个方法中主要是检查相关资源是否存在,以及构建相关资源的客户端,最后才通过runControllers方法来加载需要加载的controller以及启动这些控制器:

func (s *server) runControllers(defaultVolumeSnapshotLocations map[string]string) error {
	......
    ......
	backupSyncControllerRunInfo := func() controllerRunInfo {
		backupSyncContoller := controller.NewBackupSyncController(
			s.veleroClient.VeleroV1(),
			s.mgr.GetClient(),
			s.veleroClient.VeleroV1(),
			s.sharedInformerFactory.Velero().V1().Backups().Lister(),
			s.config.backupSyncPeriod,
			s.namespace,
			s.csiSnapshotClient,
			s.kubeClient,
			s.config.defaultBackupLocation,
			newPluginManager,
			backupStoreGetter,
			s.logger,
		)

		return controllerRunInfo{
			controller: backupSyncContoller,
			numWorkers: defaultControllerWorkers,
		}
	}

	backupTracker := controller.NewBackupTracker()

	backupControllerRunInfo := func() controllerRunInfo {// 构建运行backup controller信息的函数
		backupper, err := backup.NewKubernetesBackupper(
			s.veleroClient.VeleroV1(),
			s.discoveryHelper,
			client.NewDynamicFactory(s.dynamicClient),
			podexec.NewPodCommandExecutor(s.kubeClientConfig, s.kubeClient.CoreV1().RESTClient()),
			s.resticManager,
			s.config.podVolumeOperationTimeout,
			s.config.defaultVolumesToRestic,
		)
		cmd.CheckError(err)

		backupController := controller.NewBackupController(
			s.sharedInformerFactory.Velero().V1().Backups(),
			s.veleroClient.VeleroV1(),
			s.discoveryHelper,
			backupper,
			s.logger,
			s.logLevel,
			newPluginManager,
			backupTracker,
			s.mgr.GetClient(),
			s.config.defaultBackupLocation,
			s.config.defaultVolumesToRestic,
			s.config.defaultBackupTTL,
			s.sharedInformerFactory.Velero().V1().VolumeSnapshotLocations().Lister(),
			defaultVolumeSnapshotLocations,
			s.metrics,
			s.config.formatFlag.Parse(),
			csiVSLister,
			csiVSCLister,
			backupStoreGetter,
		)

		return controllerRunInfo{
			controller: backupController,
			numWorkers: defaultControllerWorkers,
		}
	}

	......
    ......// 构建其他的controller信息

	enabledControllers := map[string]func() controllerRunInfo{
		controller.BackupSync:        backupSyncControllerRunInfo,
		controller.Backup:            backupControllerRunInfo,
		controller.Schedule:          scheduleControllerRunInfo,
		controller.GarbageCollection: gcControllerRunInfo,
		controller.BackupDeletion:    deletionControllerRunInfo,
		controller.Restore:           restoreControllerRunInfo,
		controller.ResticRepo:        resticRepoControllerRunInfo,
	}  // 默认启动所有的controller
	// Note: all runtime type controllers that can be disabled are grouped separately, below:
	......
	// Remove disabled controllers so they are not initialized. If a match is not found we want
	// to hault the system so the user knows this operation was not possible.
	if err := removeControllers(s.config.disabledControllers, enabledControllers, enabledRuntimeControllers, s.logger); err != nil {  // 移除通过命令行disable的controller
		log.Fatal(err, "unable to disable a controller")
	}

	// Instantiate the enabled controllers. This needs to be done *before*
	// the shared informer factory is started, because the controller
	// constructors add event handlers to various informers, which should
	// be done before the informers are running.
	controllers := make([]controllerRunInfo, 0, len(enabledControllers))
	for _, newController := range enabledControllers {
		controllers = append(controllers, newController())  // 通过遍历需要启动的controller信息函数列表,调用该方法,构建控制器
	}

	......
    ...... // 缓存相关数据,以及构建BackupStorageLocationReconciler和其他资源

	// TODO(2.0): presuming all controllers and resources are converted to runtime-controller
	// by v2.0, the block from this line and including the `s.mgr.Start() will be
	// deprecated, since the manager auto-starts all the caches. Until then, we need to start the
	// cache for them manually.
	for i := range controllers {
		controllerRunInfo := controllers[i]
		// Adding the controllers to the manager will register them as a (runtime-controller) runnable,
		// so the manager will ensure the cache is started and ready before all controller are started
		s.mgr.Add(managercontroller.Runnable(controllerRunInfo.controller, controllerRunInfo.numWorkers))  // 将所有controller构建成Runnable的数据类型,此数据类型定义在pkg/internal/util/managercontroller/包中,此类型只是一个闭包函数,在该函数中调用了controller.Run方法
	}

	s.logger.Info("Server starting...")

	if err := s.mgr.Start(s.ctx); err != nil { // 调用controllerManager.Start方法执行上述构建的闭包函数启动所有控制器
		s.logger.Fatal("Problem starting manager", err)
	}

	return nil
}

这个函数比较长,中间包括构建很多controller的资源,以及构建通用的控制器执行方式,所做工作如下:

1.构建运行backup controller信息的函数
2.构建其他的controller信息
3.默认所有的controller列表
4.移除通过命令行disable的controller
5.通过遍历需要启动的controller信息函数列表,调用该方法,构建控制器
6.将所有controller构建成Runnable的数据类型,此数据类型定义在pkg/internal/util/managercontroller/包中,此类型只是一个闭包函数,在该函数中调用了controller.Run方法
7.调用controllerManager.Start方法执行上述构建的闭包函数启动所有控制器

至此,server加载完所有的controller就会正常运行,下一步主要解读当执行创建命令后,会通过kubernetes CRD API创建backup资源的实例,此时controller需要做的工作。

3.3 backup controller处理流程

通过查看所有的controller的定义,发现都是继承自generic_controller.genericControllerRun方法也定义在此结构体中,在这里可以先查看一下这个方法所做的工作:

func (c *genericController) Run(ctx context.Context, numWorkers int) error {
	if c.syncHandler == nil && c.resyncFunc == nil {
		// programmer error
		panic("at least one of syncHandler or resyncFunc is required")
	}  // 判断controller的syncHandler和resyncFunc是否同时为空

	var wg sync.WaitGroup

	defer func() {
		c.logger.Info("Waiting for workers to finish their work")

		c.queue.ShutDown()

		// We have to wait here in the deferred function instead of at the bottom of the function body
		// because we have to shut down the queue in order for the workers to shut down gracefully, and
		// we want to shut down the queue via defer and not at the end of the body.
		wg.Wait()

		c.logger.Info("All workers have finished")

	}()  // 停止controller时关闭队列连接

	c.logger.Info("Starting controller")
	defer c.logger.Info("Shutting down controller")

	// only want to log about cache sync waiters if there are any
	if len(c.cacheSyncWaiters) > 0 {
		c.logger.Info("Waiting for caches to sync")
		if !cache.WaitForCacheSync(ctx.Done(), c.cacheSyncWaiters...) {
			return errors.New("timed out waiting for caches to sync")
		}
		c.logger.Info("Caches are synced")
	}

	if c.syncHandler != nil {
		wg.Add(numWorkers)
		for i := 0; i < numWorkers; i++ {
			go func() {
				wait.Until(c.runWorker, time.Second, ctx.Done()) // 通过子线程执行runWorker方法
				wg.Done()
			}()
		}
	}

	if c.resyncFunc != nil {
		if c.resyncPeriod == 0 {
			// Programmer error
			panic("non-zero resyncPeriod is required")
		}

		wg.Add(1)
		go func() {
			wait.Until(c.resyncFunc, c.resyncPeriod, ctx.Done()) // 通过子线程执行resyncFunc方法
			wg.Done()
		}()
	}

	<-ctx.Done()

	return nil
}

Run方法中主要做了一下几步:

1.判断controller的syncHandler和resyncFunc是否同时为空
2.通过子线程执行runWorker方法
3.通过子线程执行resyncFunc方法
4.停止controller时关闭队列连接

从代码中可以看到,具体的处理逻辑是在runWorkerresyncFunc方法中实现,在这里我们主要看runWorker,在这个里面直接循环调用了processNextWorkItem方法:

func (c *genericController) processNextWorkItem() bool {
	key, quit := c.queue.Get()  // 从队列中取出新增的对象key
	if quit {
		return false
	}
	// always call done on this item, since if it fails we'll add
	// it back with rate-limiting below
	defer c.queue.Done(key)

	err := c.syncHandler(key.(string)) // 调用syncHandler处理新增的对象
	if err == nil {
		// If you had no error, tell the queue to stop tracking history for your key. This will reset
		// things like failure counts for per-item rate limiting.
		c.queue.Forget(key)
		return true
	}

	c.logger.WithError(err).WithField("key", key).Error("Error in syncHandler, re-adding item to queue")
	// we had an error processing the item so add it back
	// into the queue for re-processing with rate-limiting
	c.queue.AddRateLimited(key)

	return true
}

这个方法中内容如下:

1.从队列中取出新增的对象key
2.调用syncHandler处理新增的对象

在构建backup controller的方法NewBackupController中,具体定义了syncHandler的实现是processBackup方法,resyncFunc的实现是resync方法,所以可以直接查看processBackup方法,来了解具体的备份实现:

func (c *backupController) processBackup(key string) error {
	log := c.logger.WithField("key", key)

	log.Debug("Running processBackup")
	ns, name, err := cache.SplitMetaNamespaceKey(key) // 从key中拆分出命名空间和备份对象的名称
	......
	original, err := c.lister.Backups(ns).Get(name) // 获取kubenetes CRD API接收到的请求数据
	......
	switch original.Status.Phase {
	case "", velerov1api.BackupPhaseNew:// 当状态为空或者New时,表示为新增的对象,需要进行处理
		// only process new backups
	default:
		return nil
	}

	log.Debug("Preparing backup request")
	request := c.prepareBackupRequest(original) // 构建备份的请求信息,包括api版本、过期时间、通过restic默认备份的卷信息、资源和命名空间是否包含在被排除的列表中

	......
	// update status
	updatedBackup, err := patchBackup(original, request.Backup, c.client) // 更新backup信息
	......
    ......
	// execution & upload of backup
	if err := c.runBackup(request); err != nil { // 调用runBackup方法处理备份请求
		// even though runBackup sets the backup's phase prior
		// to uploading artifacts to object storage, we have to
		// check for an error again here and update the phase if
		// one is found, because there could've been an error
		// while uploading artifacts to object storage, which would
		// result in the backup being Failed.
		log.WithError(err).Error("backup failed")
		request.Status.Phase = velerov1api.BackupPhaseFailed
	}  
    ......
	if _, err := patchBackup(original, request.Backup, c.client); err != nil { //修改备份结果
		log.WithError(err).Error("error updating backup's final status")
	}

	return nil
}

此方法中主要实现以下步骤:

1.从key中拆分出命名空间和备份对象的名称
2.获取kubenetes CRD API接收到的请求数据
3.判断当状态为空或者New时,表示为新增的对象,需要进行处理
4.构建备份的请求信息,包括api版本、过期时间、通过restic默认备份的卷信息、资源和命名空间是否包含在被排除的列表中
5.更新backup信息
6.调用runBackup方法处理备份请求
7.修改备份状态

下面是runBackup的内容:

func (c *backupController) runBackup(backup *pkgbackup.Request) error {
	c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Setting up backup log")
    ......
    ......
	backupStore, err := c.backupStoreGetter.Get(backup.StorageLocation, pluginManager, backupLog) // 构建操作对象存储的结构体的实例
    ......
    pluginManager := c.newPluginManager(backupLog) // 初始化插件管理工具
    ......
	exists, err := backupStore.BackupExists(backup.StorageLocation.Spec.StorageType.ObjectStorage.Bucket, backup.Name) // 检查备份是否已经存在
	......
    ......
	if err := c.backupper.Backup(backupLog, backup, backupFile, actions, pluginManager); err != nil {  // 调用备份工具的Backup方法备份具体的资源
		fatalErrs = append(fatalErrs, err)
	}

	// Empty slices here so that they can be passed in to the persistBackup call later, regardless of whether or not CSI's enabled.
	// This way, we only make the Lister call if the feature flag's on.
	var volumeSnapshots []*snapshotv1beta1api.VolumeSnapshot
	var volumeSnapshotContents []*snapshotv1beta1api.VolumeSnapshotContent
	if features.IsEnabled(velerov1api.CSIFeatureFlag) { // 如果CSIFeatureFlag为true,则查询snapshot和snapshotcontent信息
		selector := label.NewSelectorForBackup(backup.Name)

		// Listers are wrapped in a nil check out of caution, since they may not be populated based on the
		// EnableCSI feature flag. This is more to guard against programmer error, as they shouldn't be nil
		// when EnableCSI is on.
		if c.volumeSnapshotLister != nil {
			volumeSnapshots, err = c.volumeSnapshotLister.List(selector)
			if err != nil {
				backupLog.Error(err)
			}
		}

		if c.volumeSnapshotContentLister != nil {
			volumeSnapshotContents, err = c.volumeSnapshotContentLister.List(selector)
			if err != nil {
				backupLog.Error(err)
			}
		}
	}

	// Mark completion timestamp before serializing and uploading.
	// Otherwise, the JSON file in object storage has a CompletionTimestamp of 'null'.
	backup.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()}

	backup.Status.VolumeSnapshotsAttempted = len(backup.VolumeSnapshots)
	for _, snap := range backup.VolumeSnapshots { // 
		if snap.Status.Phase == volume.SnapshotPhaseCompleted { 
			backup.Status.VolumeSnapshotsCompleted++ // 记录快照已经执行完成的卷数量
		}
	}

	......
    ......

	if errs := persistBackup(backup, backupFile, logFile, backupStore, c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)), volumeSnapshots, volumeSnapshotContents); len(errs) > 0 { // 记录此次备份资源以及快照等信息到对象存储
		fatalErrs = append(fatalErrs, errs...)
	}

	c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup completed")

	// if we return a non-nil error, the calling function will update
	// the backup's phase to Failed.
	return kerrors.NewAggregate(fatalErrs)
}

在这个方法里,主要的步骤有以下几步:

1.构建操作对象存储的结构体的实例
2.初始化插件管理工具
3.检查备份是否已经存在
4.调用备份工具的Backup方法备份具体的资源
5.如果CSIFeatureFlag为true,则查询snapshot和snapshotcontent信息
6.记录此次备份资源以及快照等信息到对象存储

napshots, volumeSnapshotContents); len(errs) > 0 { // 记录此次备份资源以及快照等信息到对象存储
fatalErrs = append(fatalErrs, errs…)
}

c.logger.WithField(Backup, kubeutil.NamespaceAndName(backup)).Info("Backup completed")

// if we return a non-nil error, the calling function will update
// the backup's phase to Failed.
return kerrors.NewAggregate(fatalErrs)

}


在这个方法里,主要的步骤有以下几步:

1.构建操作对象存储的结构体的实例
2.初始化插件管理工具
3.检查备份是否已经存在
4.调用备份工具的Backup方法备份具体的资源
5.如果CSIFeatureFlag为true,则查询snapshot和snapshotcontent信息
6.记录此次备份资源以及快照等信息到对象存储


具体备份kubernetes的资源还在`backup`的包中实现着,通过将本节点所有的卷挂载到restic的pod中,调用restic的备份命令,实现的备份,由于篇幅有限,本篇文章就只介绍到这里。

你可能感兴趣的:(k8s,kubernetes,go)