开门见山。这一篇继续"每天5分钟玩转 OpenStack"系列的运维/虚拟化主题,聊一个几乎所有用过 OpenStack 的人都点过的按钮:Launch(启动实例)和 Shut Off(关闭实例)。别觉得这两个操作简单,我在生产环境里见过不少因为对这两个动作理解不透而翻车的案例——比如实例状态显示 SHUTOFF 但底层进程还在跑,比如宿主机挂了之后 SHUTOFF 实例死活起不来,再比如任务状态卡在 powering-off 一整天没动静。这篇不打算只讲"点哪个按钮",而是把 Nova 在这两个动作背后的状态流转、libvirt 层的真实行为、资源释放逻辑以及排障手段一次讲透。适合刚入门 OpenStack 的运维新人,也适合管过几套云、但被冷启动和强制关机坑过的人。
1. 先把概念对齐:Launch 和 Shut Off 在 OpenStack 里到底指什么
1.1 从实例的生命周期状态说起
OpenStack 里一台云主机的生命周期,远没有"开机-关机-删除"这么简单。在 Nova 的数据库里,每个实例同时拥有好几套状态,最常被运维关注的是这三个维度:
vm_state:实例生命周期的大阶段,比如 building(创建中)、active(运行中)、stopped(已关机)、shelved(已搁置)、error(故障)。task_state:当前正在执行的异步任务,比如 spawning(正在创建)、powering-off(正在关机)、powering-on(正在开机)、resizing(正在调整规格)。没有任务时是None。power_state:来自虚拟化层(通常是 libvirt)的实际电源状态,用数字表示,常见值是 1(RUNNING)和 4(SHUTDOWN),0 表示未知。
而我们常说的 Launch 和 Shut Off,对应的分别是 nova 的 os-start 和 os-stop 这两个 API action。当一个实例执行了 stop 操作之后,vm_state 变成 stopped,task_state 回到 None,对外展示的 status 就是 SHUTOFF。当这个实例再次执行 start 操作,vm_state 变回 active,status 变成 ACTIVE。
这里有一个很多人容易忽略的细节:在 OpenStack 的语境里,"Launch" 有时候也指从镜像或卷创建一个新实例并启动起来,但这篇我们更聚焦在"对一台已经存在、当前处于 SHUTOFF 状态的实例执行启动动作"。标题既然把 Launch 和 Shut Off 并列拿出来说,核心就是这两个状态之间的来回切换,以及切换过程中 Nova 和底层虚拟化到底做了哪些事。
1.2 为什么这两个操作值得单独拿出来讲
老实说,在 Horizon 界面上点 Shut Off Instance 只需要两下确认,点 Start Instance 也就一下。但生产环境里,这对操作是"看起来越简单、越容易出幺蛾子"的典型。
我从几个真实场景说起。
遇到过有人批量关机几十台测试机省资源,结果第二天开机的时候,其中几台卡在 powering-off 状态,无论怎么点 Start 都没反应。也遇到过某个计算节点宕机后,上面的 SHUTOFF 实例全部无法在其他节点启动,排障排到最后才发现,冷启动和热迁移的调度逻辑根本不是一回事。还有一个更隐蔽的坑:Nova 数据库里显示实例已经 SHUTOFF,但跑到宿主机上用 virsh list 一看,QEMU 进程还活着,网络端口也被占着,莫名其妙就出现了 IP 冲突。
这些事故的共同点,就是对"关机"背后的机制理解不足。你以为是简单地把虚拟机电源断掉,实际上 Nova 要处理优雅关机超时、强制断电、状态持久化、资源配额回收、网络端口解绑等一系列问题。要把这些讲清楚,就得先看 Nova 的状态机是怎么设计的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 背后的原理:一次 Shut Off 到底发生了什么
2.1 从 API 到 libvirt 的调用链路
一次完整的关机操作,客户端(命令行或 Horizon)发出的请求会沿着一条链路层层传递:
- 客户端发送
POST /v2.1/servers/{instance_id}/action,请求体是{"os-stop": null}。 nova-api接收请求,做完身份认证、项目配额校验、实例状态合法性检查之后,通过 RPC 把任务交给nova-conductor。nova-conductor再通过 RPC 把任务投递给该实例所在宿主机上的nova-compute。nova-compute执行stop_instance方法,先把数据库里的task_state更新为powering-off,然后调用虚拟化驱动的power_off方法。- 对于 KVM 环境,libvirt 驱动会找到这个实例对应的 domain,发送 ACPI 关机信号给 guest 操作系统。
- 等 guest 自己把操作系统关掉之后,libvirt 里 domain 状态变为 shut off,Nova 再把数据库里的
vm_state更新为stopped,task_state清空。
这里需要特别强调第 4 步的时序:Nova 先记录任务状态,再真正去操作底层虚拟化。所以如果你在任何时刻看到实例的 task_state 不是 None,说明仍有一个操作在过程中,这时候不要强行再发一个 start 或 stop,否则会触发 "unexpected task state" 之类的冲突错误。
2.2 优雅关机与"超时强杀"
很多人以为 Shut Off 就是把虚拟机的电源断掉,其实 Nova 默认走的是优雅关机流程,而不是直接断电。libvirt 驱动在收到 stop 请求后,会向 guest 发送 ACPI shutdown 信号,让 guest 里的操作系统自己执行关机脚本、刷盘、卸载文件系统。这个过程跟我们在物理机上点"开始菜单-关机"是一回事。
问题来了:如果 guest 里的操作系统卡住了,或者根本没有装 ACPI 驱动,或者内核直接 panic 了,那 ACPI 信号发过去也没人响应。为了不让关机操作无限期挂起,Nova 设计了一个超时强杀机制。在 nova.conf 的 [libvirt] 配置组里,有一个参数叫 shutdown_timeout,默认值是 60 秒。Nova 发送 ACPI 信号之后会轮询等待,如果超过这个时间 guest 还没有真正关掉,就直接调用 libvirt 的 destroy 方法,把 QEMU 进程强制杀掉。
这里有一个运维要记住的实践结论:优雅关机失败后,最终形态跟强制关机没有区别。所以如果你的实例上面跑着数据库或者有大量未落盘的缓存数据,别指望 OpenStack 的 stop 操作能帮你"软着陆",必要的业务侧停服和备份还是得自己做。我曾经在生产环境里为了省事,直接对一台跑着旧版 MySQL 的实例执行 openstack server stop,结果 guest 因为磁盘 IO 卡住,60 秒超时后被强杀,重启后 InnoDB 恢复日志跑了大半天才起来。这锅不怨 OpenStack,怨我没提前在业务侧做优雅停服。
2.3 Start 的完整链路和资源恢复
启动一个 SHUTOFF 实例,链路跟关机是对称的:os-start → API → conductor → compute → libvirt 驱动把磁盘和 XML 定义重新加载,domain.create() 把虚拟机跑起来。启动成功后,vm_state 变成 active,power_state 变成 1(RUNNING)。
但有一个运维必须知道的重点:SHUTOFF 实例的启动,默认是"回原宿主机"的,而不是重新调度到任意一台有空闲资源的机器。原因也很直白,关机时实例的本地磁盘(ephemeral disk)和 XML 定义都留在原来的宿主机上,除非你用了共享存储或纯 Cinder Volume 后端,否则别的机器根本没有这个实例的磁盘,自然起不来。Nova 在处理 start 时也会走一遍调度器,但因为 placement 服务里该实例的资源占用仍然锚定在原宿主机上,所以绝大部分情况下调度结果就是原来的那台机器。
理解了这个机制,你就能解释一个经典故障:某计算节点宕机后,它上面所有 SHUTOFF 实例都无法启动,报错 NoValidHost。不是因为你集群里没资源了,而是因为那些实例的"家"已经失联了。后面我会在常见问题部分专门展开。
2.4 跟 Suspend、Shelve 的关键差异
刚接触 OpenStack 的人经常把 Shut Off、Suspend、Shelve 三个操作混为一谈,因为它们表面上都是"让虚拟机停下来"。但从运维角度看,这三个动作对资源的占用、数据的保存方式、恢复的速度,差别非常大。
| 操作 | 对外状态 | CPU/内存是否释放 | 磁盘是否保留在宿主机 | 恢复速度 | 适合场景 |
|---|---|---|---|---|---|
| Shut Off(关机) | SHUTOFF | 释放(QEMU 进程退出) | 保留 | 相当于冷启动,较慢 | 长时间不用但不想删除 |
| Suspend(挂起) | SUSPENDED | 不释放(QEMU 进程还在,只是暂停) | 保留 | 很快,相当于从暂停点恢复 | 短时间暂停,希望秒级恢复 |
| Shelve(搁置) | SHELVED / SHELVED_OFFLOADED | 释放 | 不保留,磁盘打包上传到 Glance | 很慢,需要重新解包 | 长期不用,又想节省磁盘配额 |
其中 Shelve 有一个变种叫 Shelve Offload,会把实例的磁盘和内存状态打包上传到 Glance,然后从宿主机上彻底清理掉。这是三者里唯一一个能把宿主机磁盘空间也释放出来的方案。
我在实际运维中见过有人为了"释放内存给别的实例",把一批实例 Suspend 掉,结果内存压根没释放,因为 Suspend 在 KVM 下的实现只是 virDomainSuspend,QEMU 进程还占着内存。要真正释放 CPU 和内存,要么 Shut Off,要么 Shelve Offload。这个认知偏差在资源吃紧的集群里会带来完全不同的结果。
3. 实操演示:命令行和 Horizon 两种姿势
3.1 动手前的环境确认
在敲命令之前,先把实例和宿主机状态摸清楚。我习惯用三条命令做前置检查:
bash复制# 查看实例的完整状态,重点关注 status、task_state、vm_state、power_state
openstack server show 7a2d6f1e-9c3b-4a8f-b5e2-1f0d3c9a7b21
# 查看宿主机层面的资源使用情况
openstack hypervisor show compute-node-03
# 在宿主机上看 domain 的真实状态(需要 SSH 到计算节点)
virsh list --all | grep 7a2d6f1e-9c3b-4a8f-b5e2-1f0d3c9a7b21
openstack server show 的输出里,如果 status 是 ACTIVE、task_state 是 None、power_state 是 Running,说明实例处于正常可操作的状态。这个时候你执行 stop 才有意义;如果 status 已经是 SHUTOFF 或者 task_state 不是 None,就要先搞明白它为什么处于那个状态,而不是盲目补一个操作。
另外提醒一句:尽量用实例 UUID 而不是名称做操作,因为项目之间实例名称可以重复,用名称操作很容易误伤别的项目的机器。
3.2 命令行执行 Shut Off 和 Launch
命令行关机很简单:
bash复制# 优雅关机
openstack server stop 7a2d6f1e-9c3b-4a8f-b5e2-1f0d3c9a7b21
这个命令默认就是走前面说的 ACPI 优雅关机流程,Nova 会等待 [libvirt] shutdown_timeout 配置的时间,超时才强杀。如果你想给 guest 更多时间慢慢关,可以在宿主机上的 nova.conf 里调大这个值:
ini复制[libvirt]
shutdown_timeout = 120
注意这个参数在 compute 节点上,改完要重启 nova-compute 服务。另外,如果 guest 里根本没装 ACPI 驱动,你给它再长时间也没用,这种情况可以直接考虑用 openstack server stop --hard 之类的方式?遗憾的是,现代 OpenStack 的 API 已经没有独立的 force-stop 接口了,早期版本里那个 os-forceStop 因为太容易搞坏数据,早被移除。现在唯一的做法就是靠超时强杀,或者你先在 guest 里手动执行 poweroff。
启动一个 SHUTOFF 实例,用 start 命令:
bash复制# 启动已关机的实例
openstack server start 7a2d6f1e-9c3b-4a8f-b5e2-1f0d3c9a7b21
执行完之后,可以用下面的命令轮询状态,直到 task_state 变为 None、status 变为 ACTIVE:
bash复制watch -n 5 "openstack server show 7a2d6f1e-9c3b-4a8f-b5e2-1f0d3c9a7b21 -c status -c task_state -c power_state"
如果一切正常,你会看到 status 从 SHUTOFF 变成 ACTIVE,power_state 从 Shutdown 变成 Running。
老版本环境里可能还在用 nova 客户端,对应命令是 nova stop <server> 和 nova start <server>,逻辑完全一样,只是客户端入口不同。现在很多新部署已经默认用 openstack 命令了,但历史环境里两套命令并存很常见,运维要能同时认出来。
3.3 Horizon 面板上的操作路径
如果你负责的云平台给业务方开了 Horizon 自助界面,那用户侧的操作路径通常是:
- 进入 项目(Project)→ 计算(Compute)→ 实例(Instances)。
- 在实例列表右侧的操作列里,找到下拉菜单或按钮区域的 "Shut Off Instance"。
- 点击后弹窗确认,注意看提示内容,它会明确告诉你"关闭实例"不等同于"删除实例"。
- 等实例状态列从 ACTIVE 变成 SHUTOFF。
- 需要启动时,再点 "Start Instance",状态列变回 ACTIVE。
这里面有个体验上的小坑:Horizon 的实例列表刷新不是实时的,而且布局在不同版本里差异很大。有些版本把 Shut Off 放在下拉菜单里,有些版本直接露出一排按钮。如果用户找不到按钮,也可以让他直接切到命令行操作。我个人更推荐普通用户用面板、运维用命令行,因为命令行能看到 task_state 等更底层的字段,排障方便很多。
3.4 操作后的验证与资源观察
关机后再去看宿主机,能很明显地看到资源变化。比如你用 openstack hypervisor show compute-node-03,对比关机前后的 free_ram_mb、free_disk_gb、vcpus_used 这几个字段,会发现 vcpus_used 和已用内存会降下来,因为 QEMU 进程退出后,vCPU 和内存都还给宿主机了。
但是注意,磁盘空间不会释放。SHUTOFF 实例的 ephemeral disk 还占着宿主机本地磁盘,所以如果你想通过关机来腾磁盘空间,那是做不到的。只有 Shelve Offload 或者删除实例才能真正把本地磁盘空间吐出来。
网络侧也有一个值得观察的点:实例关机后,它绑定的 neutron port 会从 UP 变成 DOWN,但 IP 地址依然被这个 port 占着。这意味着,只要实例不删除,它的固定 IP 就不会被别人抢走,重启后 IP 保持不变。这个特性对生产环境很重要,因为很多业务系统的白名单、域名解析都跟 IP 强绑定。
4. 运维视角:怎么用才不容易出事
4.1 什么场景该用 Shut Off,什么场景不该用
从资源管理的角度,我把常见场景做了一个分类,方便大家对照:
- 开发测试环境定期回收:晚上关机、早上开机,这种场景非常适合 Shut Off,因为比 Shelve 快,比删除安全。
- 生产实例长期停用但保留数据:如果只是停用一两周,Shut Off 可以;如果要停用几个月,我更建议做成镜像或 Shelve Offload,避免实例一直占着宿主机磁盘和固定 IP。
- 宿主机要维护但不想迁移:如果实例数据都在本地盘,关机维护宿主机是可以的,但你必须清楚一点:宿主机如果彻底坏了,这个实例很难在其他机器上原地复活。重要实例还是优先做在线迁移或至少保障数据有备份。
- 需要快速恢复的场景:别用 Shut Off,用 Suspend 更合适,前提是你能接受内存不释放。
我见过最典型的误用场景,是把 Shut Off 当成"安全的暂停键",以为随时可以无损恢复。实际上冷启动过程里 guest 要重新跑一遍完整的系统初始化,数据库要重放日志,应用要重新连接中间件,整个过程的时间和中间可能出现的异常,都比 Suspend 多得多。所以"快"和"省"往往是矛盾的,做决策前先把恢复速度的要求想清楚。
4.2 别踩的坑:本地盘、配额、IP 绑定
关机和删除最大的区别是,关机后实例的定义还在,配额也依然被占用。这里很多人有误解,以为实例关了,vCPU 和内存配额就该还回来了。在 OpenStack 的配额模型里,一个处于 SHUTOFF 状态的实例仍然占用 flavor 对应的 vCPU、内存和磁盘配额,因为实例记录还在,只是底层进程停了。这就是为什么有些用户把一堆实例关机后,想再创建新实例却依然报配额不足。
另一个坑是本地盘数据的"假安全"。只要宿主机不宕机,SHUTOFF 实例的本地盘数据就安然无恙。但宿主机一旦损坏,本地盘数据就可能跟着完蛋。市面上不少私有云项目为了让用户觉得"关机很安全",会给 SHUTOFF 实例做延迟迁移或者数据冗余,但在原生 OpenStack 里并没有这种默认保证。数据安全靠的是你提前做的备份策略,不靠关机这个动作。
还有 IP 绑定问题。SHUTOFF 实例的 port 还在,浮动 IP 的绑定关系也还在。如果你希望把一个浮动 IP 腾出来给别的实例用,必须先解绑,或者直接删除实例。只关机不删实例,浮动 IP 是腾不出来的。
4.3 批量管理 SHUTOFF 实例的小技巧
生产环境里的实例动辄几百台,逐台点击肯定不现实。我整理几个常用的批量操作思路。
找出所有处于 SHUTOFF 状态的实例:
bash复制# 按状态过滤
openstack server list --status SHUTOFF --all-projects
# 只看某个项目的
openstack server list --status SHUTOFF --project <project-id>
批量启停可以用 shell 循环,但一定要加保护逻辑。比如批量关闭指定项目下所有 ACTIVE 实例:
bash复制openstack server list --project <project-id> --status ACTIVE -f value -c ID | while read id; do
echo "stopping $id"
openstack server stop "$id"
done
这里有三个实操建议:第一,先导出实例清单备份,防止误操作;第二,加 -f value -c ID 只取 ID,避免把名称里的空格带进来;第三,循环里加 sleep,避免瞬间把 API 打满。批量启动同理,把 stop 换成 start,状态过滤改成 SHUTOFF 即可。
我还习惯在批量操作前列出目标实例的清单让第二个人复核。这个习惯救过我一次——曾经有个脚本因为正则写错,差点把线上核心业务实例一起关机,幸亏在复核环节拦下来了。运维的严谨,往往体现在这种不起眼的小流程里。
5. 常见问题与排查实录
5.1 实例一直卡在 powering-off / powering-on
这是群里被问得最多的一个问题。现象是执行 stop 之后,task_state 长期停留在 powering-off,实例既没有变成 SHUTOFF,也没有回到 ACTIVE。排查步骤如下:
- 先确认 nova-compute 服务是否正常:登录对应宿主机,执行
systemctl status nova-compute,看服务有没有在跑,有没有反复重启。 - 再看日志:
tail -n 200 /var/log/nova/nova-compute.log,搜索实例 UUID,重点看有没有超时、libvirt 连接失败之类的关键字。 - 用
virsh list --all直接看 domain 的实际状态:如果 domain 已经不存在,说明底层其实已经关掉了,只是 Nova 没来得及更新数据库;如果 domain 还显示 running,说明关机的执行阶段卡住了。
如果是 Nova 数据库状态和底层不一致,比如 domain 已经没了但 task_state 卡住,可以尝试用重置状态的方式解围。历史上常用的是 nova reset-state --active <server>,新版 python-openstackclient 也可以用:
bash复制# 重置 vm_state 为 active,并清掉 task_state(注意这是带风险的维护操作)
openstack server set --state active <instance-uuid>
不过我要泼一盆冷水:reset-state 只是改数据库,它不会帮你把底层没做完的事补上。如果你重置成 active 但底层虚拟机实际是关的,就会出现"假装活着"的实例,用起来一堆怪问题。所以正确做法是先确保底层 domain 状态符合预期,再考虑重置数据库。
5.2 Start 报 NoValidHost,宿主机明明有资源
这个问题的典型场景前面提过:某计算节点宕机了,上面所有 SHUTOFF 实例在宿主机恢复之前,都无法启动。报错信息类似:
text复制No valid host was found. There are not enough hosts available.
很多人第一反应是"集群里明明有几十台空闲机器,为什么没 host?"。原因在于 SHUTOFF 实例的本地盘和 XML 都绑定在原宿主机上,调度器在 placement 里看到的也是这台实例的资源占用锚定在原节点。原节点不可用,等于这台实例只剩下理论上的定义,没有任何其他节点持有它的数据,自然调度不出结果。
处理方案按优先级排列:如果宿主机只是假死或暂时重启,等它恢复即可;如果宿主机彻底报废,那 SHUTOFF 实例的数据大概率也没了,只能从备份或镜像重新建机。如果你用的是 Cinder Volume 作为根盘,理论上数据还在卷里,可以新建一个实例挂上同一个卷来恢复业务,但这需要手工操作,而且原来实例的网络端口会变成孤儿,要一并清理。
这个案例给运维的教训是:对于"关机保数据"的实例,宿主机健康状态是最大的风险变量。重要实例要么落在健康检查健全的宿主机上,要么用卷存储让数据与宿主机解耦。
5.3 Nova 显示 SHUTOFF,但底层虚拟机还活着
这个故障是最诡异的:openstack server show 显示 status 是 SHUTOFF,task_state 是 None,一切看起来都很正常。但业务方反应 IP 冲突、端口被占,上宿主机一看,virsh list 里那个 domain 还处于 running 状态。
这种"数据与真实世界脱节"的情况,常见原因有三种:一是之前关机过程中超时强杀没杀干净,但数据库已经被更新成了 stopped;二是有人手动在宿主机上用 virsh start 把 domain 拉起来了;三是宿主机曾经重启过,而 domain 配置里带了 autostart,开机又自动起了一遍。
处理方法是先承认现实,以底层为准。你要做的是把底层多余的进程清掉:
bash复制# 在宿主机上确认 domain 存在且不该存在
virsh list --all | grep <instance-uuid>
# 强制销毁 domain(谨慎操作,确认无误后再执行)
virsh destroy <instance-uuid>
清完之后再用 openstack server start 正常启动,让 Nova 重新按自己的流程拉起虚拟机。如果业务需要保留这台机器上的数据,就别急着 destroy,先确认数据是否需要抢救。这种故障最怕的是两边同时操作:一边手工把 domain 杀掉,一边 Nova 又刚好在自动恢复,结果把实例搞得半死不活。
5.4 日志、状态与最后的兜底手段
排障时,日志是你最重要的朋友。常用的几个日志路径:
| 服务 | 日志路径 | 关注点 |
|---|---|---|
| nova-compute | /var/log/nova/nova-compute.log | 实例启停、libvirt 操作、超时强杀 |
| nova-api | /var/log/nova/nova-api.log | API 请求、鉴权和参数校验 |
| nova-conductor | /var/log/nova/nova-conductor.log | RPC 调度、数据库更新异常 |
| libvirt/qemu | /var/log/libvirt/qemu/ |
guest 内部电源管理、QEMU 崩溃信息 |
日志里如果出现 Domain not found,说明 libvirt 侧已经找不到 domain,可能是实例被误删或者宿主机状态异常;如果出现 Operation timeout,多半是 libvirt 驱动等待超时;如果反复出现 Cannot set own manager to 'system' 之类的告警,可以忽略,这类消息大多不影响启停主流程。
最后再说一个兜底思路:如果真的把状态搞乱了,比如某个实例的 task_state 卡死、正常 API 全部拒绝操作,除了 openstack server set --state 之外,还可以直接改数据库。但是!直接改 Nova 数据库(nova 库里的 instances 表)属于最后手段,必须在业务低峰、做好备份、有人复核的前提下做。我曾经只执行过一两次这种操作,每次都是战战兢兢。能不动数据库就不要动,先尝试通过 API 重置状态,再不行就检查底层,最后才考虑数据库兜底。
写在最后的一点体会
从入行到现在,我在 OpenStack 上踩过最深的坑几乎都集中在"以为某个操作很基础"这件事上。Launch 和 Shut Off 看似是两个按钮,背后却是 Nova 状态机、libvirt 驱动、调度器、网络服务、配额系统共同协作的结果。你越早把这条调用链和状态流转模型理解清楚,遇到诡异故障时就越从容。
我个人做运维有一个习惯:每套环境上线前,先把启停、重启、挂起、搁置这几个基础操作在测试机上完整跑一遍,记录每个操作的耗时和状态变化,作为后续故障排查的基线。这个方法费不了多少时间,但能在真正出问题时帮你快速判断"这次的行为偏离基线多远",价值非常大。
如果这篇对你手里的环境有启发,建议你在自己的测试环境里动手操作一遍,重点观察每次 stop 和 start 前后 task_state 和宿主机资源的变化。实践过之后,你会对这两个操作有完全不同的理解。
