Velero(以前称为Heptio Ark)可以为您提供了备份和还原Kubernetes集群资源和持久卷的能力,你可以在公有云或本地搭建的私有云环境安装Velero,可以为你提供以下能力:
Velero包含:
每个Velero的操作(如backup,schdule,restore)都是自定义资源,使用Kubernetes 自定义资源定义(CRD)定义并存储在 etcd中,Velero还包括处理自定义资源以执行备份,还原以及所有相关操作的控制器,可以备份或还原群集中的所有对象,也可以按类型,命名空间或标签过滤对象。
Velero是kubernetes用来灾难恢复的理想选择,也是在集群上执行系统操作(如升级)之前对应用程序状态进行快照的理想选择。
在通过命令行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
发起请求,以及构建等待备份完成的方法,这个会根据参数判断是否等待。
要想了解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
需要做的工作。
通过查看所有的controller
的定义,发现都是继承自generic_controller.genericController
,Run
方法也定义在此结构体中,在这里可以先查看一下这个方法所做的工作:
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时关闭队列连接
从代码中可以看到,具体的处理逻辑是在runWorker
和resyncFunc
方法中实现,在这里我们主要看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的备份命令,实现的备份,由于篇幅有限,本篇文章就只介绍到这里。