velero的常见问题之:备份恢复任务卡住很久是怎么回事?
前言
在velero的使用过程中,有些人可能会碰到这样的问题:有时候一个备份或者恢复任务出现了问题,比如配置错误或者遇到了环境问题导致任务有error
,做不下去,这时候一般就把当前velero的Backup
或者Restore
的CR给删掉了,然后创建一个新的备份或者恢复。但是这个新的任务却迟迟没有反应,CR的Status
部分一直是空的,velero也不给任何提示这个CR是怎么了,到底怎么回事呢?这篇文章就对这个问题来一探究竟。
问题解析
这个问题可以从几个方面来展开。首先,从velero的部署方式应该可以看到velero处理备份恢复的入口,到底是一个还是可以扩展的。其次,所有的request
都是以CR的形式给velero的,而CR对应着controller的实现,要分析这个问题,绕不开velero的controller实现逻辑。另外,备份或者恢复request
属于可能会持续时间比较长的任务,但是一定会有超时的设置,找到超时相关的代码也可能对这个问题有帮助。最后,velero全局应该有一些默认参数的定义,比如超时的值等。
velero server
velero的deployment和daemonset的入口以及提供的服务,可以从部署的deployment和daemonset来找线索。如果用kubectl exec
进入velero的pod,用ps
命令可以看到,名为velero-xxxx
的pod,里面的服务是这样启动的:
nobody 1 0 0 Sep27 ? 00:35:06 /velero server --features=EnableCSI
同样可以看到,名为restic-xxxx
的pod,里面的服务启动如下:
root 1 0 0 Sep27 ? 00:09:35 /velero restic server --features=EnableCSI
通过velero的代码进行简单的分析,可以看到pkg/cmd/velero/velero.go
这个是velero的命令入口,里面引用了pkg/cmd/server
和pkg/cmd/cli/restic
的命令实现。
其中,pkg/cmd/server/server.go
实际上对应了/velero server
这个命令及子命令的执行,而pkg/cmd/cli/restic/server.go
对应的是/velero restic server
这个命令及子命令的执行。
进一步去看pkg/cmd/cli/restic/server.go::runControllers()
以及pkg/cmd/cli/restic/server.go::run()
的实现,可以看到前者启动了BackupController
、RestoreController
等来处理备份恢复等请求;而后者启动的是PodVolumeBackupController
和PodVolumeRestoreController
来通过restic处理数据的拷贝。
从这一节可以看出,velero的server是处理备份恢复请求的入口,通过deployment来部署,并且replica=1(deployment的默认值),不能随集群节点扩展。
Generic Controller
通过velero的controller实现代码可以看到,velero实现了一个通用的controller,而其他所有的controller都是基于这个controller来实现的。这个通用controller的代码在pkg/controller/generic_controller.go
,它的实现其实和controller-runtime
中的controller实现大同小异(见pkg/internal/controller/controller.go),下面是generic controller的部分代码:
func (c *genericController) Run(ctx context.Context, numWorkers int) error {
...
if c.syncHandler != nil {
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go func() {
wait.Until(c.runWorker, time.Second, ctx.Done())
wg.Done()
}()
}
}
...
<-ctx.Done()
return nil
}
其中,这里一个关键的for循环中,会用goroutine方式执行runWorker
这个函数numWorkers
次,numWorker
这个参数我们在后面去寻找,这里先看runWorker
的实现:
func (c *genericController) runWorker() {
for c.processNextWorkItem() {
}
}
func (c *genericController) processNextWorkItem() bool {
key, quit := c.queue.Get()
if quit {
return false
}
defer c.queue.Done(key)
err := c.syncHandler(key.(string))
if err == nil {
c.queue.Forget(key)
return true
}
c.logger.WithError(err).WithField("key", key).Error("Error in syncHandler, re-adding item to queue")
c.queue.AddRateLimited(key)
return true
}
可以看到,这里主要就是for循环执行processNextWorkItem
。processNextWorkItem
的主要逻辑是把要处理的request从workqueue(一般都是用限速队列)中拿出来,然后执行syncHandler
,其实就对应每个controller实现的Reconcile
函数。直到这个syncHandler
执行完,才会执行下一个。
如果去看velero的BackupController或者RestoreController的syncHandler
实现,可以看到,要么当前处理的备份或者恢复的request全部完成,要么出现error,否则不会return。而一个备份或恢复request,可能是一个比较长的过程,因为可能会涉及到restic来传输若干个PV的数据,整个的时间会被多个因素制约。
通过上面分析可以看到,每一个备份或者恢复的request在controller的每个goroutine都是顺序执行的。并行性关键在于参数numWorker
的值。
备份的超时处理
pkg/backup/backup.go
的Backup
函数是备份的主处理函数,下面是部分涉及到超时的代码:
ctx, cancelFunc := context.WithTimeout(context.Background(), podVolumeTimeout)
defer cancelFunc()
var resticBackupper restic.Backupper
if kb.resticBackupperFactory != nil {
resticBackupper, err = kb.resticBackupperFactory.NewBackupper(ctx, backupRequest.Backup)
if err != nil {
return errors.WithStack(err)
}
}
可以看到,这里velero用了golang context库的WithTimeout
来得到一个设置了超时的context。超时的具体的值会在下一节讲到,这里主要看这个ctx的处理。ctx被用来创建了一个restic的backupper对象,可以预计,这个超时的设置主要是为了对restic拷贝数据量比较大的PV时来定义的。如果数据量太大,而网络带宽不稳定,则拷贝的时间本身就比较长,这个超时的值必须足够大,否则还没传输完就超时强行结束了。
默认参数
pkg/cmd/server/server.go
定义了很多默认的参数,其中有两个参数与上面的分析相关:
defaultPodVolumeOperationTimeout = 240 * time.Minute
defaultControllerWorkers = 1
defaultPodVolumeOperationTimeout
就是超时的默认值,是4小时。而defaultControllerWorkers
就是generic controller的并行数量。因此,对于每一个velero的controller,每次执行queue里的一个work,并且这个work的超时设置是4小时。
问题结论
总结一下上面的几个因素:
- velero的server是一个deployment,并且replica=1,里面运行着备份、恢复等controller
- 每一个controller执行一个任务结束或者出现error才会返回,执行下一个
- 每一个controller的并行执行能力是1
- 涉及到restic文件拷贝的work的超时设置是4小时
对于开头的问题,到这里就比较清楚了。当一个备份或者恢复任务开始时,这个任务对于特定的controller来说是串行的。也就是说前一个备份没有做完的话,后面一个备份任务即使创建出来,也不会处理,直到前一个顺利完成,或者超时。如果第一个任务遇到问题,即使删掉任务的CR,这个任务的状态还在内存中,并且velero的controller reconcile逻辑是直到做完或者超时,否则不会return,并不会因为CR的删掉而结束任务。因此,即使删掉任务,也不会立即处理下一个任务,要等这个任务超时才行(因为CR被删了,没法顺利完成了)。
应变方法
遇到这个问题时,一个简单的应变方法就是:删掉velero deployment的pod,造成pod重启,重启之后内存中的任务就没有了,下一个任务就开始了。
总结
这个问题实际上涉及到了velero比较核心的一个问题,就是velero的扩展性。现在这样的设计,对于某一类型的任务来说,都是串行的。如果用户想在同一时刻启动多个备份任务,那就要考虑到这个限制,对要备份的namespace和PV进行合理的安排。值得注意的是,velero社区已经对这个问题有了一个比较好的解决方案,预计在v1.8会发布,到时候我们再来看velero是如何设计解决这个问题的。