TiDB K8S 删除备份阻塞问题排查

【是否原创】是
【首发渠道】TiDB 社区

问题背景

之前阅读学习 tidb operator 备份恢复相关的代码时候,做了上面部分笔记,有下面一段总结:

在备份设置了需要删除情况下,如果删除backup crd,那么会创建 clean job清理数据,然后删除crd的操作会阻塞;如果clean job失败,那么会一直阻塞的

然后这个问题今天就遇到了!

问题描述

在 k8s 中进行 br 备份到 s3 的测试,由于 s3 参数设置有问题,导致备份失败。删除失败的备份任务,命令被卡主,backup 删除失败:

备份失败是因为 s3 参数设置有误,导致访问 s3 失败。同样的,执行删除后,创建的 clean job 也因为访问 s3 失败导致清理失败,然后删除 backup crd 就被卡主了。

$ kubectl describe backup backup-test -n tidb-test
...
    Last Transition Time:  2021-11-17T09:29:59Z
    Message:               blob (code=Unknown): RequestError: send request failed
caused by: Get http://192.168.102.96:9000/backup?list-type=2&max-keys=1000&prefix=demo%2F: dial tcp 192.168.102.96:9000: connect: connection refused
    Reason:                CleanBackupDataFailed
    Status:                True
    Type:                  Failed
...

$ kubectl logs clean-backup-test-2xz4l -n tidb-test
Create rclone.conf file.
/tidb-backup-manager clean --namespace=tidb-test --backupName=backup-test
I1117 09:39:54.889554      10 clean.go:69] start to clean backup tidb-test/backup-test
E1117 09:39:55.177011      10 manager.go:83] clean cluster tidb-test/backup-test backup s3://backup/demo failed, err: blob (code=Unknown): RequestError: send request failed
caused by: Get http://192.168.102.96:9000/backup?list-type=2&max-keys=1000&prefix=demo%2F: dial tcp 192.168.102.96:9000: connect: connection refused
error: blob (code=Unknown): RequestError: send request failed
caused by: Get http://192.168.102.96:9000/backup?list-type=2&max-keys=1000&prefix=demo%2F: dial tcp 192.168.102.96:9000: connect: connection refused

有以下现象(后面再解释原因):

  • 执行删除 backup crd 被卡住,删除 backup 失败
  • 执行删除 backup job 成功
  • 执行删除 clean job 成功,但是一会又会被创建(backup crd还是清理不了)

问题很容易复现,在 tidb operator 中设置一个 br 备份,备份到 s3 存储,在 backup crd 中故意将备份参数设置错误,导致备份失败。同时设置 .spec.cleanPolicy 为 Delete,意思是删除备份任务时候,同时清理备份文件。

.spec.cleanPolicy`:备份集群后删除备份 CR 时的备份文件清理策略。目前支持三种清理策略:

  • Retain :任何情况下,删除备份 CR 时会保留备份出的文件
  • Delete :任何情况下,删除备份 CR 时会删除备份出的文件
  • OnFailure :如果备份中失败,删除备份 CR 时会删除备份出的文件如果不配置该字段,或者配置该字段的值为上述三种以外的值,均会保留备份出的文件。值得注意的是,在 v1.1.2 以及之前版本不存在该字段,且默认在删除 CR 的同时删除备份的文件。若 v1.1.3 及之后版本的用户希望保持该行为,需要设置该字段为 Delete

备份的流程

当 backup crd 创建、变更、删除等变更后,tidb operator controller 会监听到变化,然后检查 backup crd,根据 crd 定义,operator 执行后面的备份、清理等流程(详细的流程可以翻开代码自行查看):

  1. 检查删除标志 DeletionTimestamp 是否被设置,是则执行 clean 流程
    1. 检查是否需要创建 clean job 进行清理(cleanPolicy 设置为 Delelte 的目的)
    2. 检查 clean job 是否已经存在,已经存在就返回
    3. 检查备份路径 backup.Status.BackupPath 是否为空,不为空需要清理
    4. 新建建 clean job
  2. 检查备份参数是否有问题
  3. 检查备份是否已经完成
  4. 检查备份是否已经失败
  5. 在 3、4 都不存在的情况下,检查备份任务是否被调度、执行中、准备状态
  6. 以上都不是,那就新建 backup job
    1. 创建 backup job 这里不描述细节,注意的是执行备份前会执行 backup.Status.BackupPath 设置的工作

这里如果没有没有清理完成,backup crd 的删除就会被一直卡主,这里解释了上面的第一个现象。

第二个现象:因为 DeletionTimestamp 被设置后,controller 会进入 clean 的流程,不会进行 backup job 流程的检查,所以这里删除 backup job 是可以的,不会被新建。

第三个现象:DeletionTimestamp 被设置后,进入 clean 流程,如果 clean job 被删除了,那么 contoller 就会新建 job,就出现了删也删不尽的流程。

解决方法

backup 之所以删除不成功,是因为 backup crd 的 finalizers 被设置了,关于 finalizers 可以查看 k8s 文档。

$ kubectl edit backup backup-test -n tidb-test
apiVersion: pingcap.com/v1alpha1
kind: Backup
metadata:
  creationTimestamp: "2021-11-17T09:17:53Z"
  deletionGracePeriodSeconds: 0
  deletionTimestamp: "2021-11-17T09:24:41Z"
  finalizers:
  - tidb.pingcap.com/backup-protection

因为回调函数 tidb.pingcap.com/backup-protection 没有满足条件,所以资源是不允许删除的。

看 finalizers 是如何被添加与删除的:

// UpdateBackup executes the core logic loop for a Backup.
func (c *defaultBackupControl) UpdateBackup(backup *v1alpha1.Backup) error {
	backup.SetGroupVersionKind(controller.BackupControllerKind)
	if err := c.addProtectionFinalizer(backup); err != nil {
		return err
	}

	if err := c.removeProtectionFinalizer(backup); err != nil {
		return err
	}

	return c.updateBackup(backup)
}

// addProtectionFinalizer will be called when the Backup CR is created
func (c *defaultBackupControl) addProtectionFinalizer(backup *v1alpha1.Backup) error {
	...
	if needToAddFinalizer(backup) {
		...
	}
	...
}

func (c *defaultBackupControl) removeProtectionFinalizer(backup *v1alpha1.Backup) error {
	...
	if needToRemoveFinalizer(backup) {
		...
	}
	...
}

func needToAddFinalizer(backup *v1alpha1.Backup) bool {
	return backup.DeletionTimestamp == nil && v1alpha1.IsCleanCandidate(backup) && !slice.ContainsString(backup.Finalizers, label.BackupProtectionFinalizer, nil)
}

func needToRemoveFinalizer(backup *v1alpha1.Backup) bool {
	return v1alpha1.IsCleanCandidate(backup) && isDeletionCandidate(backup) &&
		(v1alpha1.IsBackupClean(backup) || v1alpha1.NeedNotClean(backup))
}

很简单,通过 edit 将 finalizers 删除就可以了呀。

$ kubectl edit backup backup-test -n tidb-test
将 finalizers 清空删除
$ kubectl get backup -n tidb-test
No resources found in tidb-test namespace.

注意:这里采用这种方式删除能比较好的解决当前的问题。因为本身也没有备份的数据需要进行清理,所以采用了这个方案。

总结

其实这个场景,因为没有真实备份数据存在(连不上 s3),所以 clean 这里其实是可以去掉的。又由于 clean 同样失败,导致这次备份就卡在这里,相关的 crd、job、pod 资源无法完成清理,关于备份清理这里相关的同步机制稍微有点不足,比如说:

  • backup 失败的原因、失败 point,通过 backup crd status 没有较好的完成同步,导致没有正确的判断这里其实是不需要进行 clean 的(clean 也会失败)
  • 同样的是同步机制问题,backup job 创建的时候就设置了 backup.Status.BackupPath,clean 的时候通过这个来判断是否有数据需要删除。两者信息判断不一致导致决策有问题。
1赞

沙发~