Kubevirt vmclone 重复创建bug分析

背景

kubevirt 的 clone 流程会创建底层的VirtualMachineSnapshot 以及 VirtualMachineRestore并在克隆虚机创建完成后自动删除这些tmp资源。在测试中,发现 vmsnapshot 偶尔会有残留。通过添加日志发现事情并非简单的残留,对于这个vmclone 对象,kubevirt clone-controller 创建了两个vmsnapshot 对象,其中一个vmsnapshot 在创建以后就被忽视了,后续的clone 流程仅针对另一个vmsnapshot 展开。仅处理一个而忽视另一个的原因比较直接,vmClone obj 只能通过 vmClone.Status.SnapshotName 字段来对应一个vmsnapshot。

因此问题就变成了为什么 clone-controller 会创建重复创建 vmsnapshot obj。

原因

该问题可以通过kubevirt 0.59 ~ 1.1.0 的社区版本代码复现。

syncSourceVMTargetVM 方法中可以发现只要vmClone.Status.SnapshotName为空且vmClone.Status.Phase 的phase 为UnsetSnapshotInProgress就会进入createSnapshotFromVm方法创建vmsnapshot,本问题也只需要关注vmclone的这个状态。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
	...
	switch vmClone.Status.Phase {
	case clonev1alpha1.PhaseUnset, clonev1alpha1.SnapshotInProgress:

		if vmClone.Status.SnapshotName == nil {
			_, syncInfo = ctrl.createSnapshotFromVm(vmClone, source, syncInfo)
			return syncInfo
		}

		snapshot, syncInfo = ctrl.verifySnapshotReady(vmClone, *vmClone.Status.SnapshotName, source.Namespace, syncInfo)
		if syncInfo.toReenqueue() || !syncInfo.snapshotReady {
			return syncInfo
		}

		fallthrough
		...

我们可以推断可能是由于controller 多协程并行的机制,导致有多个vmClone.Status.SnapshotName 为空的同一个 vmclone obj 被controller reconcile。

这时我们发现在vmsnapshot obj创建流程中 obj 的 name 是通过结合 vmclone 的 name 添加随机后缀在每次createSnapshotFromVm中 重新生成的。问题就在这!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//generateNameWithRandomSuffix 为vmsnapshot 添加随机后缀
func generateSnapshotName(cloneName, vmName string) string {
	return generateNameWithRandomSuffix("clone", cloneName, "snapshot", vmName)
}

func generateSnapshot(vmClone *clonev1alpha1.VirtualMachineClone, sourceVM *v1.VirtualMachine) *v1alpha1.VirtualMachineSnapshot {
	return &v1alpha1.VirtualMachineSnapshot{
		ObjectMeta: metav1.ObjectMeta{
			Name:      generateSnapshotName(vmClone.Name, sourceVM.Name),
			Namespace: sourceVM.Namespace,
			OwnerReferences: []metav1.OwnerReference{
				getCloneOwnerReference(vmClone.Name, vmClone.UID),
			},
		},
		Spec: v1alpha1.VirtualMachineSnapshotSpec{
			Source: corev1.TypedLocalObjectReference{
				Kind:     vmKind,
				Name:     sourceVM.Name,
				APIGroup: pointer.String(kubevirtApiGroup),
			},
		},
	}
}

当我们 createSnapshotFromVm 后创建的 vmsnapshot name 会放在 syncInfo 中return。接着 syncInfo 作为参数进入ctrl.updateStatus(vmClone, syncInfo) 方法。在updateStatus中如果syncInfo 中带有 snapshotName,vmclone 就会更新本身的vmClone.Status.SnapshotName。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
if isInPhase(vmClone, clonev1alpha1.SnapshotInProgress) {
		if snapshotName := syncInfo.snapshotName; snapshotName != "" {
			vmClone.Status.SnapshotName = pointer.String(snapshotName)
		}

		if syncInfo.snapshotReady {
			assignPhase(clonev1alpha1.RestoreInProgress)
		}
	}
...
...
	if !equality.Semantic.DeepEqual(vmClone.Status, origClone.Status) {
		if phaseChanged {
			log.Log.Object(vmClone).Infof("Changing phase to %s", vmClone.Status.Phase)
		}
		err := ctrl.cloneStatusUpdater.UpdateStatus(vmClone)
		if err != nil {
			return err
		}
	}

	return nil

注意在vmclone 还在更新流程中时 vmsnapshot 已经创建完成了,由于vmclone controller 的 vmsnapshotInformer 会在vmsnapshot create,delete,update 时将对应可能存在的vmclone 入队。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
...
	_, err := ctrl.vmCloneInformer.AddEventHandler(
		cache.ResourceEventHandlerFuncs{
			AddFunc:    ctrl.handleVMClone,
			UpdateFunc: func(oldObj, newObj interface{}) { ctrl.handleVMClone(newObj) },
			DeleteFunc: ctrl.handleVMClone,
		},
	)

	if err != nil {
		return nil, err
	}

	_, err = ctrl.snapshotInformer.AddEventHandler(
		cache.ResourceEventHandlerFuncs{
			AddFunc:    ctrl.handleSnapshot,
			UpdateFunc: func(oldObj, newObj interface{}) { ctrl.handleSnapshot(newObj) },
			DeleteFunc: ctrl.handleSnapshot,
		},
	)

这里就出现了时间差,当vmsnapshot 创建请求返回后,vmclone 准备update 时,kubernetes 已经成功创建了一个vmsnapshot。这时vmclone状态并未更新,此时入队的vmclone 还处在vmClone.Status.SnapshotName为空且vmClone.Status.Phase 的phase 为Unset的阶段。这时入队的vmclone 就会继续进入createSnapshotFromVm流程,直到vmclone 的update请求成功的发送给kubernetes。

解决方法

放弃随机生成名称

因此很直观的可以想到我们只要放弃生成随机后缀的vmsnapshot name 即可,一个vmclone name 对应一个vmsnapshot name。这样即使重新进入了createSnapshotFromVm 发送 create 请求也会返回alreadyExists 。将 err return 即可。 当然可以针对alreadyExists 本身进行优化,单独处理。

添加 Expectations

Expectations 是更量的方法也可以解决这个问题,在vmi vm等obj的create、update控制流程中使用了这种方法。这也是在k8s 原生组件中ReplicaSet 用来控制pod 数量的解决方法。这时属于controller 控制器内部的机制。

Expectations 对象大致如下:

type ControlleeExpectations struct {
   // Important: Since these two int64 fields are using sync/atomic, they have to be at the top of the struct due to a bug on 32-bit platforms
   // See: https://golang.org/pkg/sync/atomic/ for more information
   add       int64
   del       int64
   key       string
   timestamp time.Time
}

通常Expectations有SetExpectations、LowerExpectations等方法。这些方法会更新某个key对应的 Expectations 表明这个obj正在经历变化。而SatisfiedExpectations方法返回true 表示该obj的Expectations 已经实现,目前控制器对其进行的update or create 方法均已在k8s 集群中完成(在k8s 中该obj 已经稳定)。

如此,在执行 update 某个对象的逻辑之前,SetExpectations(key, 1, 0),表示将 add 更新为1。若update 返回err, 则将该更改撤销。

key := getVirtualMachineCloneKey(vmClone)
		ctrl.vmCloneExpectations.SetExpectations(key, 1, 0)
		err := ctrl.cloneStatusUpdater.UpdateStatus(vmClone)
		if err != nil {
			ctrl.vmCloneExpectations.LowerExpectations(key, 1, 0)
			return err
		}
// SetExpectations registers new expectations for the given controller. Forgets existing expectations.
func (r *ControllerExpectations) SetExpectations(controllerKey string, add, del int) {
	exp := &ControlleeExpectations{add: int64(add), del: int64(del), key: controllerKey, timestamp: clock.RealClock{}.Now()}
	glog.V(4).Infof("Setting expectations %#v", exp)
	if err := r.Add(exp); err != nil {
		panicWithKeyFuncMsg(err)
	}
}

仅当add 和del为<= 0 时,SatisfiedExpectations方法返回true,该控制器才会继续处理该obj。

needsSync := ctrl.snapshotExpectations.SatisfiedExpectations(key) && ctrl.vmCloneExpectations.SatisfiedExpectations(key)
	if !needsSync {
		return nil
	}

从上可知update 请求不返回err时,某 obj 的 SatisfiedExpectations 为false。同理create 、delete 操作后也会如此。因此控制器需要在update 等操作成功后将add 或delete 置为0。

这些逻辑在informer 的EventHandler中。例如当update 事件处理时,代表该obj 已经在k8s 中update成功。同理AddEventHandler、DelEventHandler 中也有类似逻辑。

func (ctrl *VMCloneController) updateVirtualMachineClone(_, curr interface{}) {
	ctrl.lowerVMCloneExpectation(curr)
	ctrl.enqueueVirtualMachineClone(curr)
}

通过Expectations 机制,保证控制器在异步多goroutine 场景下按照预期的顺序处理某个obj。

https://github.com/kubevirt/kubevirt/pull/10643

updatedupdated2024-02-142024-02-14