通过之前《kubevirt(一)虚拟化技术》和《kubevirt(二)实现原理》两篇文章,我们对kubevirt有了初步的了解,本文基于这些内容,我们来看看kubevirt虚拟机的迁移(migration)。
注:
本文内容仅限于同一个kubernetes集群内的虚拟机迁移,且本文内容基于[email protected]
虚拟机迁移一般是指因宿主机出现故障时,需要将上面运行的虚拟机迁移到其它宿主机上的过程。
在做虚拟机迁移前,首先需要考虑迁移前后宿主机的硬件资源差异性,包括宿主机架构(x86、ARM等)、宿主机cpu类型(Intel、AMD等)等因素,这部分内容需要结合具体业务场景具体分析,不在本文讨论范围内。
除去硬件资源差异,kubevirt虚拟机迁移还需要考虑以下问题:
kubevirt如何做kvm迁移?
kubevirt的kvm虚拟机是运行在pod中的,为了实现kvm迁移,kubevirt定义了一个叫作VirtualMachineInstanceMigration的CRD。用户如果想迁移kubevirt kvm,编写一个VirtualMachineInstanceMigration对象apply即可,apply后对应的controller会在其它节点创建一个新的kvm pod,之后再做切换,从而完成kvm的迁移。
迁移过程对业务的影响?
迁移可以分为冷迁移(offline migration,也叫常规迁移、离线迁移)和热迁移(live migration,也叫动态迁移、实时迁移),冷迁移在迁移之前需要将虚拟机关机,然后将虚拟机镜像和状态拷贝到目标宿主机,之后再启动;热迁移则是将虚拟机保存(save)/回复(restore),即将整个虚拟机的运行状态完整的保存下来,拷贝到其它宿主机上后快速的恢复。热迁移相比于冷迁移,对业务来说几乎无感知。
kubevirt的VirtualMachineInstanceMigration支持live migration,官方资料可参考《Live Migration in KubeVirt》
数据对迁移的限制?
这里说的数据主要考虑内存数据、系统盘/数据盘数据,系统盘和数据盘如果使用了宿主机的本地盘(如SSD),迁移后数据会丢失,云盘(包括pvc)则无影响。
如何保证kubevirt kvm pod不再调度到本机?
kubevirt的kvm pod调度由k8s调度器完成,因此为了防止新的kvm pod再次调度到本节点,可以通过给节点打污点等方法来解决。
业务方对虚拟机的ip是敏感的,如何保证迁移后虚拟机的ip不变?
kubevirt的kvm虚拟机是在pod中启动的,从而该pod和对应的虚拟机ip与k8s网络方案有关,因此可以考虑在CNI网络方案中实现kvm的固定ip。
VirtualMachineInstanceMigration(下文简写vmiMigration)是kubevirt定义的一个CRD,接下来我们从源码和流程两个角度来看看kubevirt是如何通过这个CRD实现kvm迁移的。
vmiMigration这个CRD的定义如下,从CRD的定义中可以看出,一个vmiMigration对应一台虚拟机(vmiMigration.spec.vmiName)的迁移,迁移本身是一个很复杂的过程,vmiMigration和vmi本身都有相关字段记录迁移的状态。
// staging/src/kubevirt.io/api/core/v1/types.go
type VirtualMachineInstanceMigration struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec VirtualMachineInstanceMigrationSpec `json:"spec" valid:"required"`
Status VirtualMachineInstanceMigrationStatus `json:"status,omitempty"`
}
type VirtualMachineInstanceMigrationSpec struct {
// The name of the VMI to perform the migration on. VMI must exist in the migration objects namespace
VMIName string `json:"vmiName,omitempty" valid:"required"`
}
type VirtualMachineInstanceMigrationStatus struct {
Phase VirtualMachineInstanceMigrationPhase `json:"phase,omitempty"`
Conditions []VirtualMachineInstanceMigrationCondition `json:"conditions,omitempty"`
}
vmiMigration作为一个CRD必然会有对应的controller逻辑,它的controller逻辑是放在virt-controller中的,因此我们从virt-controller的入口开始:
// cmd/virt-controller/virt-controller.go
func main() {
watch.Execute()
}
再到virt-controller的主逻辑找到vmiMigration controller的入口:
// pkg/virt-controller/watch/application.go
func Execute() {
...
app.initCommon()
...
}
func (vca *VirtControllerApp) initCommon() {
...
vca.migrationController = NewMigrationController(
vca.templateService,
vca.vmiInformer,
vca.kvPodInformer,
vca.migrationInformer,
vca.nodeInformer,
vca.persistentVolumeClaimInformer,
vca.pdbInformer,
vca.migrationPolicyInformer,
vca.vmiRecorder,
vca.clientSet,
vca.clusterConfig,
)
...
vmiMigration controller注册事件处理函数如下,它会监听vmi的add/delete/update事件、pod的add/delete/update事件、vmiMigration的add/delete/update事件以及pdb(PodDisruptionBudget)的update事件。
// pkg/virt-controller/watch/migration.go
func NewMigrationController(...) {
...
c.vmiInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addVMI,
DeleteFunc: c.deleteVMI,
UpdateFunc: c.updateVMI,
})
c.podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addPod,
DeleteFunc: c.deletePod,
UpdateFunc: c.updatePod,
})
c.migrationInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: c.addMigration,
DeleteFunc: c.deleteMigration,
UpdateFunc: c.updateMigration,
})
c.pdbInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
UpdateFunc: c.updatePDB,
})
...
}
不管上述哪种事件处理函数,都是去找到关联的vmiMigration对象然后放入队列中。我们以pod的update事件处理函数为例来看:
// pkg/virt-controller/watch/migration.go
// When a pod is updated, figure out what migration manages it and wake them
// up. If the labels of the pod have changed we need to awaken both the old
// and new migration. old and cur must be *v1.Pod types.
func (c *MigrationController) updatePod(old, cur interface{}) {
curPod := cur.(*k8sv1.Pod)
oldPod := old.(*k8sv1.Pod)
if curPod.ResourceVersion == oldPod.ResourceVersion {
// Periodic resync will send update events for all known pods.
// Two different versions of the same pod will always have different RVs.
return
}
labelChanged := !reflect.DeepEqual(curPod.Labels, oldPod.Labels)
if curPod.DeletionTimestamp != nil {
// having a pod marked for deletion is enough to count as a deletion expectation
c.deletePod(curPod)
if labelChanged {
// we don't need to check the oldPod.DeletionTimestamp because DeletionTimestamp cannot be unset.
c.deletePod(oldPod)
}
return
}
curControllerRef := c.getControllerOf(curPod)
oldControllerRef := c.getControllerOf(oldPod)
controllerRefChanged := !reflect.DeepEqual(curControllerRef, oldControllerRef)
if controllerRefChanged && oldControllerRef != nil {
// The ControllerRef was changed. Sync the old controller, if any.
if migration := c.resolveControllerRef(oldPod.Namespace, oldControllerRef); migration != nil {
c.enqueueMigration(migration)
}
}
migration := c.resolveControllerRef(curPod.Namespace, curControllerRef)
if migration == nil {
return
}
log.Log.V(4).Object(curPod).Infof("Pod updated")
c.enqueueMigration(migration) // 这里把关联的vmiMigration对象放入队列中
return
}
有了上面入队的逻辑,再来看看队列消费逻辑:
// pkg/virt-controller/watch/migration.go
func (c *MigrationController) runWorker() {
// 不断消费队列数据
for c.Execute() {
}
}
func (c *MigrationController) Execute() bool {
// 如果队列中有数据,每次从队列中取一个数据消费
key, quit := c.Queue.Get()
if quit {
return false
}
defer c.Queue.Done(key)
err := c.execute(key.(string))
if err != nil {
log.Log.Reason(err).Infof("reenqueuing Migration %v", key)
c.Queue.AddRateLimited(key)
} else {
log.Log.V(4).Infof("processed Migration %v", key)
c.Queue.Forget(key)
}
return true
}
func (c *MigrationController) execute(key string) error{
...
if needSync {
syncErr = c.sync(key, migration, vmi, targetPods)
}
err = c.updateStatus(migration, vmi, targetPods)
...
}
队列消费这边有两个重要的函数:c.sync和c.updateStatus。先看看c.sync:
// pkg/virt-controller/watch/migration.go
func (c *MigrationController) sync(...) {
...
// 根据vmiMigration的状态做不同处理
switch migration.Status.Phase {
case virtv1.MigrationPending:
if !targetPodExists {
// 如果目标pod(迁移后起来的pod)还不存在,则会根据vmi和vmi的pod生成一个新的pod
// 这个pod就是迁移后跑虚拟机的pod
} else if isPodRead(pod) {
// 如果目标pod已存在且是ready状态,检查是否热插拔(hotplug)卷
// 如果有,则会检查attachmentPods有没有,没有就创建
} else {
// 处理超时的目标pod,超时没起来超过一定时间自动删除(后续逻辑会继续尝试重建)
}
case virtv1.MigrationScheduling:
// 处理超时的目标pod,超时没起来超过一定时间自动删除
case virtv1.MigrationScheduled:
// 当目标pod存在且ready时,更新vmi的status.migrationState相关字段,并将vmi和目标pod关联起来
case virtv1.MigrationPreparingTarget, virtv1.MigrationTargetReady, virtv1.MigrationFailed:
// 如果目标pod不存在或者已经退出,更新vmi的status.migrationState相关字段,并标注该vmi的迁移结束
case virtv1.MigrationRunning:
// 如果vmiMigration的deletionTimestamp被打上(即vmiMigration被删除)
// 更新vmi的status.migrationState相关字段,并标注为abort
}
...
再看看c.updateStatus:
// pkg/virt-controller/watch/migration.go
func (c *MigrationController) updateStatus(...) {
...
if migration.IsFinal() {
// 如果迁移已经结束(不管是成功还是失败),移除vmiMigration的finalizer
} else if vmi == nil {
// 如果vmi不存在,vmiMigration.status.phase标识为failed
} else if vmi.IsFinal() {
// 如果vmi已经是结束状态(failed或succeed),vmiMigration.status.phase标识为failed
} else if podExists && podIsDown(pod) {
// 如果目标pod存在但是是failed或succeed状态,vmiMigration.status.phase标识为failed
} else if migration.TargetIsCreated() && !podExists {
// 如果vmiMigration状态中标识目标pod已创建但实际目标pod不存在,,vmiMigration.status.phase标识为failed
} else if migration.TargetIsHandedOff() && vmi.Status.MigrationState == nil {
// 如果vmiMigration状态是已结束,但vmi.status.migrationState还是空的,vmiMigration.status.phase标识为failed
} else if migration.TargetIsHandedOff() &&
vmi.Status.MigrationState != nil &&
vmi.Status.MigrationState.MigrationUID != migration.UID {
// 如果vmiMigration状态是已结束,但vmi的migration UID不是自己,vmiMigration.status.phase标识为failed
}else if vmi.Status.MigrationState != nil &&
vmi.Status.MigrationState.MigrationUID == migration.UID &&
vmi.Status.MigrationState.Failed {
// 如果vmi的migration UID是自己但是vmi migration标识为failed,vmiMigration.status.phase也标识为failed
} else if migration.DeletionTimestamp != nil && !migration.IsFinal() &&
!conditionManager.HasCondition(migration, virtv1.VirtualMachineInstanceMigrationAbortRequested) {
// 如果vmiMigration被打上删除标记,但本身状态不是终止状态,vmiMigration的condition会增加一个abort记录
} else if attachmentPodExists && podIsDown(attachmentPod) {
// 热插拔pod不正常时,vmiMigration.status.phase也标识为failed
} else {
switch migration.Status.Phase {
case virtv1.MigrationPhaseUnset:
// 如果vmiMigration状态没有设置(为空),且vmi可以迁移,则更新vmiMigration状态为Pending
case virtv1.MigrationPending:
// 如果状态为Pending,且目标pod已存在,则更新vmiMigration状态为Scheduling
case virtv1.MigrationScheduling:
// 如果状态为Scheduling,且目标pod已经ready,则更新vmiMigration状态为Scheduled
case virtv1.MigrationScheduled:
// 如果状态为Scheduled,且vmi.status.migrationState.targetNode不为空,则更新vmiMigration状态为PreparingTarget
case virtv1.MigrationPreparingTarget:
// 如果状态为PreparingTarget,且vmi.status.migrationState.targetNodeAddress不为空,则更新vmiMigration状态为TargetReady
case virtv1.MigrationTargetReady:
// 如果状态为TargetReady,且vmi.status.migrationState.StartTimestamp不为空(即已经开始迁移),则更新vmiMigration状态为Running
case virtv1.MigrationRunning:
// 如果状态是Running,且vmi.status.migrationState.completed = true,则更新vmiMigration状态为Succeed
}
}
...
}
通过代码可以发现,单纯依靠vmiMigration controller是无法完成整个迁移过程的,因为vmi.status.migrationState下的targetNode、startTimestamp、completed等字段都不是在vmiMigration controller中设置。事实上这部分数据是virt-handler设置的,virt-handler这部分逻辑从代码上不是很好讲述,因此本文略过此部分,但在下文的流程中会对virt-handler的作用做相关说明。
有了上面的源码分析,我们假设现在有一个running的kvm(即存在一个running状态的vmi和pod),此时apply一个VirtualMachineInstanceMigration会发生什么:
微信公众号卡巴斯同步发布,欢迎大家关注。