本文主要参考(翻译)自 OpenStack 官方文档 VMState、Virtual Machine States and Transitions,以及简要说明一下其在 Nova 代码中的体现。
虚拟机状态
对于每个虚拟机而言,它都有 3 种状态,vm_state、task_state 以及 power_state。它们之间非常容易混淆。不过官方文档 VMState 简明扼要地区别了它们:
- power_state 是虚拟机在 Hypervisor 上的状态,从计算节点从下而上(bottom-up)传递,如 RUNNING
- vm_state 反应了基于 API 调用的一种稳定状态,如 ACTIVE
- task_state 反应了 API 调用过程中的过渡状态,如 SCHEDULING
power_state
power_state 应该通过调用虚拟机的 virt 驱动来获得。Hypervisor 上虚拟机的状态是最权威的,而数据库中的 power_state 只是(最近)过去状态的一个快照。这个状态会周期性更新,而且应该在每一个会影响 power_state 的任务(task)结束时更新。
如何更新
从计算节点从下而上(bottom-up)传递,覆写数据库中相应的字段。这可能会触发跟 vm_state 的一致性协调,见下文 vm_state。命名规范
取决于 Libvirt 返回的状态已废弃状态
- BLOCKED:本质上是 RUNNING
- SHUTOFF:对应到 SHUTDOWN
- FAILED:对应到 NOSTATE
vm_state
vm_state 描述的是虚拟机当前稳定(非过渡)状态。也就是说,如果没有正在执行的 API 调用(正在运行的任务),vm_state 应该和用户所期待的的一致。例如,一个好的例子是 vm_state 值为 ACTIVE,意味着虚拟机运行正常;不好的例子是 vm_state 值为 SUSPENDING(过渡状态),意味着虚拟机正在挂起并将变为已挂起(suspended)状态(注:这里是说用 SUSPENDING 这种过度状态来表示 vm_state 是不合适的,而不是说 vm_state 中有这个状态)。过渡状态属于 task_state。
如何更新
vm_state 应该只在任务结束的时候更新,即任务结束并且 task_state 变为 None。没有 API 调用,vm_state 应该保持不变。如果任务失败了,但适当处理(意义,原文为 clean up)后(如,实时迁移失败,虚拟机在原宿主机上回滚后,工作正常),vm_state 也应该保持不变。如果任务失败而且无法回滚,vm_state 变成 ERROR。命名规范
形容词跟 power_state 关系
vm_state 跟 power_state 没有一一映射的关系,它们表示的是不同的信息。你不能够由其中一个来推断出另外一个。例如,当你使用修复镜像(rescue image)修复一个虚拟机后,其 power_state 可能为 RUNNING 或者 BLOCKED,但是 vm_state 应该值为 RESCUED。如果仅仅基于 power_state,我们并不清楚 vm_state 应该为 ACTIVE 还是 RESCUED。当跟 power_state 不一致时,如何协调
首先,当有一个任务正在进行时,vm_state 和 power_state 很可能会不一致。这是因为 vm_state 只表示稳定状态,但在任务进行当中,虚拟机真正的状态其实是过渡的,而 vm_state 是过时的。
当没有任务进行时,power_state 和 vm_state 应该保持一致,除非错误或失败。这种情况要具体问题具体分析。例如:
- power_state=SHUTOFF,vm_state=ACTIVE。这很可能是关机(shutdown)命令在虚拟机内部(inside,不是指命令行)发出,所以 power_state 是正确的。这大概等同于隐式调用了 stop() API。vm_state 应该更正为 STOPPED。
- power_state=BLOCKED, vm_state=HARD_DELETED。这意味着用户已经发出了删除虚拟机的命令但不知道怎么回事失败了。我们应该试着再删一次。
- power_state=BLOCKED, vm_state=PAUSED。这意味着在调用 virt 驱动的 pause() 函数之前发生了不可预期的错误。请告诉我:在这种情形下,怎么样处理对用户最友好?设置为 ERROR 状态?
- 现在 _sync_power_states 跟正在进行的任务没有关联,可能会导致奇怪的行为。
如何从 vm_state 获取 EC2 对等的状态
EC2 状态同时包含了稳定(如 ACTIVE)和过渡状态(如 SUSPENDING)。你需要用 task_state 和 vm_state 来推导出 EC2 状态。vm_state 可选参数如下:
- INITIALIZED: 虚拟机刚从数据库中创建,但正在构建当中(building),还没构建好(built)
- ACTIVE: 正在运行
- RESCUED: 正在运行,使用修复镜像(rescue image)
- PAUSED: 暂停运行
- SUSPENDED: 挂起,但保存了内存快照(以便于恢复)
- STOPPED: 停止运行
- SOFT_DELETED: 虚拟机不在计算节点运行,但磁盘镜像保存完整,可以恢复
- HARD_DELETED: 从配额和计费角度,虚拟机不再存在。虚拟机及其磁盘镜像最终会被销毁
- RESIZED: 虚拟机在原宿主机停止运行,但在目的宿主机上继续运行。虚拟机的磁盘镜像存在于这两个宿主机当中,但大小不一样。用户需要确认调整磁盘大小(resize)或者恢复虚拟机(废弃的 task_state.RESIZE_VERIFY 和该状态意义一样)
- ERROR: 发生了不可恢复的错误。唯一允许的操作就是删除虚拟机
废弃的状态:REBUILDING、MIGRATING,REBUILDING, MIGRATING 也应该放在 task_state;SHUTOFF 也应该废弃掉,因为这个状态很费解,应该根据 shutdown_terminate 划分为 STOPPED 或 DELETED。
task_state
task_state 表示过渡状态,跟计算(compute) API 紧密关联,表明虚拟机当前正在执行的任务。处于 vm_state 的虚拟机 task_state 为 None,只有正在运行的进程有 task_state。
特殊任务:强制删除(force/hard delete)
什么时候都应该允许删除虚拟机,且删除成功。用户也因此有更多空闲配额资源,已删资源也不再计费。不幸的是,前一个任务可能卡住,导致 task_state 不能变为 None;或者 virt 驱动在删除虚拟机时卡住;或者计算节点因为网络或硬件问题不可用。所以,此时我们不应该等待 force_delete() 任务在计算节点执行并更新 vm_state 为 HARD_DELETED。相反,vm_state 应该被立即更新而无需通过计算节点。换一句话说,force_delete() 任务就是单纯的数据库操作。真正的清理工作会(数据库操作后)马上进行,但这跟 power_state 和 vm_state 之间的协调流程没什么差别,也可以周期性触发。如何更新
当确定在虚拟机上只有一个任务在进行,就可以设置 task_state。为了确保更新的原子性,在任务开始时需要生成一个唯一的 task_id(UUID 格式),并和虚拟机 ID 关联起来。如果虚拟机已经关联到一个 task_id,说明另一个任务正在执行。在任务执行期间,task_id 通过请求上下文 RequestContext 数据结构传送到计算节点(worker)。要在任务进行期间更新 task_state,必须保证 task_id 和虚拟机当前的 task_id 相匹配,否则,当前任务会被其他任务(目前只有 force delete 任务)抢占。当一个任务完成的时候,task_state 设置为 None,task_id 也会设置为 None。
因为 force delete 是目前唯一会抢占其他任务的任务,我们当前可能不需要为每一个任务添加 task_id,但我们需要检查 vm_state 是否为 HARD_DELETED 而不是 task_id 是否匹配。
我们真的需要分离开 vm_state 和 task_state 吗?
从技术上来讲,vm_state(稳定的)和 task_state(过渡的)没有交集,可以将他们结合在一起。这种分离最大的好处在于状态转移图简单得多——只需要考虑 vm_state 之间的 DFA(Deterministic finite automaton)。如果新增一个 task_state,状态转移图也不用改变。命名规范
“动词 + ing” 形式推荐用于描述 task_state,其中“动词”表示 compute API 方法(method)。在任务执行期间,task_state 应该保持不变。要表示任务的过程,应该使用一个单独的字段(field)而不是简单化的状态机。task_state 的可能值如下:
- None: no task is currently in progress
- BUILDING
- IMAGE_SNAPSHOTTING
- IMAGE_BACKINGUP
- UPDATING_PASSWORD
- PAUSING
- UNPAUSING
- SUSPENDING
- RESUMING
- DELETING
- STOPPING
- STARTING
- RESCUING
- UNRESCUING
- REBOOTING
- REBUILDING
- POWERING_ON
- POWERING_OFF
- RESIZING
- RESIZE_REVERTING
- RESIZE_CONFIRMING
- SCHEDULING
- BLOCK_DEVICE_MAPPING
- NETWORKING
- SPAWNING
- RESIZE_PREP
- RESIZE_MIGRATING
- RESIZE_MIGRATED
- RESIZE_FINISH
已废弃的值:RESIZE_VERIFY 不是一个过渡状态,而是一个稳定状态。它现在是 vm_state 中的 RESIZED 状态。
状态机
OpenStack 官方文档 Virtual Machine States and Transitions 画了一个简明的状态机图:
图片来源
另外,更加详细的的状态机图可见一个 Excel 分享。
虚拟机操作与状态关系
对于虚拟机操作及相应的状态,Virtual Machine States and Transitions 也是说的很清楚:
图片来源
另外,对一些操作所需的虚拟机状态(前提),该文档也有说明:
图片来源
最后,该文档还给出了创建虚拟机过程中虚拟机状态的变化:
图片来源
Nova 代码中的状态
在 Nova 源码 nova/compute 目录下有三个文件 vm_states.py、task_states.py 和 power_state.py,分别定义了 vm_state、task_state 和 power_state 的可能值,这里就不详细列出了,请参考源码。下边我们来看一下其他值得注意的地方。
虚拟机 status 字段
除了以上提到的 3 种 state,其实还有一个我们经常看到的虚拟机的 status 字段(nova show),其值由 vm_state 和 task_state 共同决定(status_from_state 函数,另外也说明了在某一个 vm_state 下可以有哪些 task_state 值):
1 | # nova/api/openstack/common.py |
API 中的状态检查
因为有些操作只有当虚拟机处于某种操作时才可以,所以这些操作对应的 API 在真正执行之前需要先检查一下虚拟机状态(check_instance_state 函数),如启动虚拟机 API 在被调用前需先检查虚拟机 vm_state 是否为 STOPPED:
1 | # nova/compute/api.py |
Libvirt 状态和 power_state 之间的映射关系
代码已经写的很清楚了:
1 | # nova/virt/libvirt/guest.py |