kubevirt(三)迁移(migration)

通过之前《kubevirt(一)虚拟化技术》《kubevirt(二)实现原理》两篇文章,我们对kubevirt有了初步的了解,本文基于这些内容,我们来看看kubevirt虚拟机的迁移(migration)。

注:

本文内容仅限于同一个kubernetes集群内的虚拟机迁移,且本文内容基于kubevirt@0.49.0

前言

虚拟机迁移一般是指因宿主机出现故障时,需要将上面运行的虚拟机迁移到其它宿主机上的过程。

在做虚拟机迁移前,首先需要考虑迁移前后宿主机的硬件资源差异性,包括宿主机架构(x86、ARM等)、宿主机cpu类型(Intel、AMD等)等因素,这部分内容需要结合具体业务场景具体分析,不在本文讨论范围内。

除去硬件资源差异,kubevirt虚拟机迁移还需要考虑以下问题:

  1. kubevirt如何做kvm迁移?

kubevirt的kvm虚拟机是运行在pod中的,为了实现kvm迁移,kubevirt定义了一个叫作VirtualMachineInstanceMigration的CRD。用户如果想迁移kubevirt kvm,编写一个VirtualMachineInstanceMigration对象apply即可,apply后对应的controller会在其它节点创建一个新的kvm pod,之后再做切换,从而完成kvm的迁移。

  1. 迁移过程对业务的影响?

迁移可以分为冷迁移(offline migration,也叫常规迁移、离线迁移)和热迁移(live migration,也叫动态迁移、实时迁移),冷迁移在迁移之前需要将虚拟机关机,然后将虚拟机镜像和状态拷贝到目标宿主机,之后再启动;热迁移则是将虚拟机保存(save)/回复(restore),即将整个虚拟机的运行状态完整的保存下来,拷贝到其它宿主机上后快速的恢复。热迁移相比于冷迁移,对业务来说几乎无感知。

kubevirt的VirtualMachineInstanceMigration支持live migration,官方资料可参考《Live Migration in KubeVirt》

  1. 数据对迁移的限制?

这里说的数据主要考虑内存数据、系统盘/数据盘数据,系统盘和数据盘如果使用了宿主机的本地盘(如SSD),迁移后数据会丢失,云盘(包括pvc)则无影响。

  1. 如何保证kubevirt kvm pod不再调度到本机?

kubevirt的kvm pod调度由k8s调度器完成,因此为了防止新的kvm pod再次调度到本节点,可以通过给节点打污点等方法来解决。

  1. 业务方对虚拟机的ip是敏感的,如何保证迁移后虚拟机的ip不变?

kubevirt的kvm虚拟机是在pod中启动的,从而该pod和对应的虚拟机ip与k8s网络方案有关,因此可以考虑在CNI网络方案中实现kvm的固定ip。

VirtualMachineInstanceMigration

VirtualMachineInstanceMigration(下文简写vmiMigration)是kubevirt定义的一个CRD,接下来我们从源码和流程两个角度来看看kubevirt是如何通过这个CRD实现kvm迁移的。

vmiMigration源码分析

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会发生什么:

在这里插入图片描述

  1. 初始状态,k8s中存在一个vmi和它对应的pod(src pod),即etcd中存在一个vmi对象和一个src pod对象,且node以上正常运行着这个pod;
  2. kubectl apply一个vmiMigration;
  3. apiServer收到这个请求,把vmiMigration对象存入etcd;
  4. virt-controller中的vmiMigration controller(后文简称migration controler)的informer机制监听到有vmiMigration创建,且状态未设置(为空),于是调apiServer接口把vmiMigration状态更新为Pending;
  5. apiServer更新etcd中vmiMigration对象状态为Pending;
  6. migration controller的informer机制监听到vmiMigration的update事件,且状态是Pending,且目标pod(dst pod)还不存在,则调apiServer接口创建dst pod;
  7. apiServer把dst pod对象存入etcd;
  8. migration controller监听到dst pod创建事件,调apiServer接口更新vmiMigration状态为Scheduling;
  9. apiServer更新etcd中的vmiMigration状态为Scheduling;
  10. k8s scheduler的informer监听到dst pod创建,通过调度算法,把dst pod的spec.nodeName设置为node2,并调apiServer接口更新pod信息;
  11. apiServer更新src pod的spec.nodeName字段;
  12. node2上的kubelet创建dst pod,并不断向apiServer更新pod状态,包括最终dst pod变成ready状态;
  13. apiServer接收node2上kubelet上报的dst pod状态,更新到etcd中;
  14. 经过一段时间后,dst pod变为ready状态,并被migration controller监听到,于是migration controller调apiServer更新vmiMigration状态为Scheduled;
  15. apiServer更新etcd中的vmiMigration状态为Scheduled;
  16. migration controller监听到vmiMigration状态变为Scheduled,查找集群中的migration policy(kubevirt的另一个CRD资源),并调apiServer更新vmi(注意这里是vmi而不是vmiMigration)status.migrationState下的migrationUID(更新为vmiMigration的uid)、targetNode(即node2)、sourceNode(即node1)、targetPodName(dst pod名称)、migrationPolicyName和migrationConfiguration字段;
  17. apiServer更新etcd中vmi对象的status.migrationState下上述字段;
  18. migration controller监听到vmiMigration状态是Scheduled且vmi对象的status.migrationState.targetNode不为空,调apiServer更新vmiMigration状态为PreparingTarget;
  19. apiServer更新etcd中vmiMigration状态为PreparingTarget;
  20. node2上的virt-handler会先通过grpc调用virt-launcher方法拿到一些数据,然后基于这些数据启动一个dst migration proxy,用于后续迁移时数据传输;同理node1上的virt-handler也会起个src migration proxy,这两个proxy之间的通信可以配置另外的网卡,防止迁移流量影响k8s本身网络;之后node2上virt-handler的vmController调apiServer接口给vmi打上migration的finalizer,同时更新vmi.status.migrationState下的targetNodeAddress和targetDirectMigrationNodePorts。virt-handler是以daemonSet+hostNetwork形式部署的,所以targetNodeAddress其实只需要virt-handler取其pod ip即可。
  21. apiServer更新etcd中vmi的migration finalizer和targetNodeAddress、targetDirectMigrationNodePorts字段;
  22. migration controller监听到vmi的targetNodeAddress不为空,调apiServer接口更新vmiMigration状态为TargetReady;
  23. apiServer更新etcd中的vmiMigration状态为TargetReady;
  24. node1上virt-handler的vmController会在第19步后,通过unix sock的方式调用源pod virt-launcher的grpc接口,virt-launchert调用libvirt开始异步执行migration,如果是nonroot,uri为qemu+unix:///session?socket={源proxy unix socket文件};否则uri为qemu+unix:///system?socket={源proxy unix socket文件}。最后如果node1上virt-handler发现migration已经开始了,则调apiServer接口更新vmi的status.migrationState.startTimestamp等字段;
  25. apiServer更新etcd中vmi的status.migrationState.startTimestamp等字段;
  26. migrationController监听到vmi的status.migrationState.startTimestamp不为空,调apiServer接口更新vmiMigration状态为Running;
  27. apiServer更新etcd中vmiMigration的状态为Running;
  28. node1上virt-handler的vmController会调virt-launcher接口检查migration是否已完成,如果已完成调apiServer接口更新vmi的status.migrationState.completed为true;
  29. apiServer更新etcd中vmi的status.migrationState.completed为true;
  30. migration监听到vmi.status.migrationState.completed为true,调apiServer更新vmiMigration状态为succeeded;
  31. apiServer更新etcd中vmiMigration状态为succeeded;
  32. node1和node2上virt-handler的vmController执行相关清理动作,包括清理proxy、删除源kvm对应的domain资源等。

微信公众号卡巴斯同步发布,欢迎大家关注。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,关于Android中使用Room数据库迁移(Migration)的问题,我可以给你一些简单的解释和示例。 在Android中,Room是一个SQLite数据库对象映射库,它提供了一种简单的方法来处理SQLite数据库。当你需要修改数据库结构时,你需要使用Room数据库迁移(Migration)来保持数据的完整性。 下面是一个简单的Room数据库迁移(Migration)的示例: 首先,定义一个新的实体类,例如: ``` @Entity(tableName = "users") public class User { @PrimaryKey public int id; @ColumnInfo(name = "user_name") public String name; public String email; } ``` 然后,在你的数据库类中,添加一个新的版本和对应的迁移代码,例如: ``` @Database(entities = {User.class}, version = 2) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); // Add migration code here static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { database.execSQL("ALTER TABLE users ADD COLUMN last_name TEXT"); } }; } ``` 在上面的代码中,我们定义了一个新的版本2,并添加了一个新的实体类User,同时定义了一个名为MIGRATION_1_2的迁移对象。迁移对象中重写了migrate()方法,在该方法中执行了数据库的修改操作,例如添加了一个新的last_name列。 最后,在你的应用程序中,使用以下代码创建或打开数据库: ``` AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name") .addMigrations(AppDatabase.MIGRATION_1_2) .build(); ``` 在上面的代码中,我们使用addMigrations()方法将MIGRATION_1_2迁移对象添加到数据库的创建中。 通过以上步骤,你就可以使用Room数据库迁移(Migration)来修改数据库结构,同时保持数据的完整性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值