通过之前《kubevirt(一)虚拟化技术》和《kubevirt(二)实现原理》两篇文章,我们对kubevirt有了初步的了解,本文基于这些内容,我们来看看kubevirt虚拟机的迁移(migration)。
注:
本文内容仅限于同一个kubernetes集群内的虚拟机迁移,且本文内容基于kubevirt@0.49.0
前言
虚拟机迁移一般是指因宿主机出现故障时,需要将上面运行的虚拟机迁移到其它宿主机上的过程。
在做虚拟机迁移前,首先需要考虑迁移前后宿主机的硬件资源差异性,包括宿主机架构(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
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会发生什么:
文章来源:https://www.toymoban.com/news/detail-611871.html
- 初始状态,k8s中存在一个vmi和它对应的pod(src pod),即etcd中存在一个vmi对象和一个src pod对象,且node以上正常运行着这个pod;
- kubectl apply一个vmiMigration;
- apiServer收到这个请求,把vmiMigration对象存入etcd;
- virt-controller中的vmiMigration controller(后文简称migration controler)的informer机制监听到有vmiMigration创建,且状态未设置(为空),于是调apiServer接口把vmiMigration状态更新为Pending;
- apiServer更新etcd中vmiMigration对象状态为Pending;
- migration controller的informer机制监听到vmiMigration的update事件,且状态是Pending,且目标pod(dst pod)还不存在,则调apiServer接口创建dst pod;
- apiServer把dst pod对象存入etcd;
- migration controller监听到dst pod创建事件,调apiServer接口更新vmiMigration状态为Scheduling;
- apiServer更新etcd中的vmiMigration状态为Scheduling;
- k8s scheduler的informer监听到dst pod创建,通过调度算法,把dst pod的spec.nodeName设置为node2,并调apiServer接口更新pod信息;
- apiServer更新src pod的spec.nodeName字段;
- node2上的kubelet创建dst pod,并不断向apiServer更新pod状态,包括最终dst pod变成ready状态;
- apiServer接收node2上kubelet上报的dst pod状态,更新到etcd中;
- 经过一段时间后,dst pod变为ready状态,并被migration controller监听到,于是migration controller调apiServer更新vmiMigration状态为Scheduled;
- apiServer更新etcd中的vmiMigration状态为Scheduled;
- 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字段;
- apiServer更新etcd中vmi对象的status.migrationState下上述字段;
- migration controller监听到vmiMigration状态是Scheduled且vmi对象的status.migrationState.targetNode不为空,调apiServer更新vmiMigration状态为PreparingTarget;
- apiServer更新etcd中vmiMigration状态为PreparingTarget;
- 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即可。
- apiServer更新etcd中vmi的migration finalizer和targetNodeAddress、targetDirectMigrationNodePorts字段;
- migration controller监听到vmi的targetNodeAddress不为空,调apiServer接口更新vmiMigration状态为TargetReady;
- apiServer更新etcd中的vmiMigration状态为TargetReady;
- 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等字段;
- apiServer更新etcd中vmi的status.migrationState.startTimestamp等字段;
- migrationController监听到vmi的status.migrationState.startTimestamp不为空,调apiServer接口更新vmiMigration状态为Running;
- apiServer更新etcd中vmiMigration的状态为Running;
- node1上virt-handler的vmController会调virt-launcher接口检查migration是否已完成,如果已完成调apiServer接口更新vmi的status.migrationState.completed为true;
- apiServer更新etcd中vmi的status.migrationState.completed为true;
- migration监听到vmi.status.migrationState.completed为true,调apiServer更新vmiMigration状态为succeeded;
- apiServer更新etcd中vmiMigration状态为succeeded;
- node1和node2上virt-handler的vmController执行相关清理动作,包括清理proxy、删除源kvm对应的domain资源等。
微信公众号卡巴斯同步发布,欢迎大家关注。文章来源地址https://www.toymoban.com/news/detail-611871.html
到了这里,关于kubevirt(三)迁移(migration)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!