TiDB K8S 定时备份状态异常问题排查

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

问题场景

在进行 tidb operator 定时备份测试环境中,配置了使用 br 定时备份到 s3 的测试。定时备份 backupschedule crd 关键参数是这样的:

  maxReservedTime: 1h
  schedule: '*/10 * * * *'

代表每 10 分钟进行一次定时备份,备份数据保留时长为 1 小时。

使用的 k8s 环境跑了很多测试应用,悲催的是遇到了 k8s 的一个 bug 问题,导致 pod 创建失败。

pod 的错误信息如下:

Warning  FailedCreatePodContainer  2s (x144754 over 29d)  kubelet, 192.168.10.10  unable to ensure pod container exists: failed to create container for [kubepods besteffort pod12685337-1956-464c-9050-4b8551567ea2] : mkdir /sys/fs/cgroup/memory/kubepods/besteffort/pod12685337-1956-464c-9050-4b8551567ea2: cannot allocate memory

针对这个 k8s 问题,pingcap 还发过一个博客文档来修复这个问题:诊断修复 TiDB Operator 在 K8s 测试中遇到的 Linux 内核问题

具体的 k8s 环境、修复方法暂且不提,重点是因为这个问题,导致已有的备份 clean pod ContainerCreating,新的备份 backup pod ContainerCreating。

backupschedule 执行删除已有的 backup 没有完成删除,导致过期,新建的因为处于 ContainerCreating 可以完成删除,然后新的在定时创建,过期删除,已有的都处于无法删除状态。

定时备份流程

  1. 检查是否需要开始下一次备份,下面情况都需要

    • bs.Status.LastBackup 为空

    • bs.Status.LastBackup 已经完成、被调度、失败

  2. 计算下次备份的时间,根据已有的备份,计算是否需要执行下次备份,返回需要执行定时备份的时间点

  3. 删除上次备份的 backup job

  4. 创建 backup crd

    • 设置 bs.Status.LastBackup = backup.GetName()
    • 设置 bs.Status.LastBackupTime = &metav1.Time{Time: *scheduledTime}
    • 设置 bs.Status.AllBackupCleanTime = nil
  5. 清理过期备份

    1. 如果设置最大保留时间,根据最大保留时间进行清理
    2. 如果设置最大保留副本,根据副本数进行清理
    3. 检查如果所有的 backup 都被清理,那么 resetLastBackup
      • bs.Status.LastBackupTime = nil
      • bs.Status.LastBackup = “”
      • bs.Status.AllBackupCleanTime = &metav1.Time{Time: bm.now()}

上面第 2 步计算下次备份时间点源码:

// getLastScheduledTime return the newest time need to be scheduled according last backup time.
// the return time is not before now and return nil if there's no such time.
func getLastScheduledTime(bs *v1alpha1.BackupSchedule, nowFn nowFn) (*time.Time, error) {
	...
	var earliestTime time.Time
	if bs.Status.LastBackupTime != nil {
		earliestTime = bs.Status.LastBackupTime.Time
	} else if bs.Status.AllBackupCleanTime != nil {
		...
		earliestTime = bs.Status.AllBackupCleanTime.Time
	} else {
		...
		earliestTime = bs.ObjectMeta.CreationTimestamp.Time
	}
	
	...
	var scheduledTimes []time.Time
	for t := sched.Next(earliestTime); !t.After(now); t = sched.Next(t) {
		scheduledTimes = append(scheduledTimes, t)
		...
			return nil, nil
		}
	}
	...
	scheduledTime := scheduledTimes[len(scheduledTimes)-1]
	return &scheduledTime, nil

}

从代码上看 earliestTime 作为计算备份的上次时间点,取值根据情况依次可能从下面三个值选取:

  • bs.Status.LastBackupTime.Time
  • bs.Status.AllBackupCleanTime.Time
  • bs.ObjectMeta.CreationTimestamp.Time

在上面第 4 步中,创建 backup 后会设置 bs.Status.LastBackup 等。

清理备份函数如下:

func (bm *backupScheduleManager) backupGCByMaxReservedTime(bs *v1alpha1.BackupSchedule) {
	...
	backupsList, err := bm.getBackupList(bs)
	if err != nil {
		klog.Errorf("backupGCByMaxReservedTime, err: %s", err)
		return
	}
	
	var deleteCount int
	for _, backup := range backupsList {
		if backup.CreationTimestamp.Add(reservedTime).After(bm.now()) {
			continue
		}
		// delete the expired backup
		if err := bm.deps.BackupControl.DeleteBackup(backup); err != nil {
			...
			return
		}
		deleteCount += 1
		...
	}
	
	if deleteCount == len(backupsList) {
		// All backups have been deleted, so the last backup information in the backupSchedule should be reset
		bm.resetLastBackup(bs)
	}

}

func (bm *backupScheduleManager) resetLastBackup(bs *v1alpha1.BackupSchedule) {
	bs.Status.LastBackupTime = nil
	bs.Status.LastBackup = ""
	bs.Status.AllBackupCleanTime = &metav1.Time{Time: bm.now()}
}

也就是说如果 deleteCount == len(backupsList) 那么进行 resetLastBackup。

func (bm *backupScheduleManager) getBackupList(bs *v1alpha1.BackupSchedule) ([]*v1alpha1.Backup, error) {
	...
	backupLabels := label.NewBackupSchedule().Instance(bsName).BackupSchedule(bsName)
	selector, err := backupLabels.Selector()
	...
	backupsList, err := bm.deps.BackupLister.Backups(ns).List(selector)
	...
	return backupsList, nil
}

注意这里 list backup 是从 k8s 缓存里读,如果缓存没有更新及时,新创建的 backup 这里没有 list 出来。

问题产生的原因

  1. 由于测试环境中所有旧的 backup 都超过保留时间,那么都需要被删除
  2. 从 k8s list 出 backup 时候,没有查询刚创建的最新的 backup
  3. 导致所有的 list 的 backup 都需要执行删除操作,达到 deleteCount == len(backupsList) 那么进行 resetLastBackup
  4. 出现异常状态,新创建的 backup 是存在的,但是 backupschedule crd 的 status 中 bs.Status.LastBackup = “”
  5. 下次备份的 earliestTime 选取有偏差

修复方法

等缓存更新后再 list backup、不通过缓存去查询最近创建的 backup、或者增加 resetLastBackup 触发的判断条件应该都可以。