搞OpenStack运维的朋友肯定都有过这种体验:实例起不来、重启卡住、快照删不掉,翻日志翻到怀疑人生。其实这几个问题背后往往都指向同一个根源——你对Nova实例在生命周期里的状态流转不够熟悉。这个OpenStack系列写到第31篇,这一次我把实例生命周期里最基础也最容易出岔子的三个操作一次性讲透:Start Instance、Nova reboot,以及经常被忽略的lock/unlock。
这三个操作单独看都不难,一条CLI命令就能触发,但它们背后涉及Nova的状态机设计、API层的权限校验、Conductor到Compute的RPC链路,再到Libvirt驱动层和QEMU进程的交互。任何一个环节没对上,表现就是实例创建成功却拉不起来、reboot请求发出去了但虚拟机纹丝不动、或者干脆报一个让你摸不着头脑的Could not acquire lock(s)错误。这篇文章我不会只讲命令怎么敲,而是会把每一步的原理、参数含义、日志定位思路都拆开给你看,适合刚入门想系统理解Nova的人,也适合在生产环境被实例故障折磨已久的运维老手。
1. 先把底子铺明白:Nova实例状态机与生命周期基础
1.1 三个状态字段,搞混了必然踩坑
Nova在设计上把一个实例的状态拆成了三个不同的字段,很多人一开始没搞明白这三个字段的区别,后面排查问题就容易被表象误导。这三个字段分别是vm_state、power_state、task_state。
vm_state是Nova自己维护的虚拟机的生命周期状态,它描述的是这个实例"在Nova的认知里处于什么阶段",比如ACTIVE、BUILDING、STOPPED、SHELVED、ERROR。这个状态是写进数据库的,Nova API在做很多操作校验时看的就是它。power_state则是对底层hypervisor上报的电源状态的描述,RUNNING、SHUTDOWN、PAUSED这些值都是从Libvirt那边同步过来的。task_state是最容易忽略的一个字段,它表示"当前是否有一个异步任务正在运行",比如spawning、rebooting、powering-off、deleting。
可以这么理解:vm_state是业务层面的状态,power_state是物理层面的状态,task_state是操作过程中间的临时标记。举个典型例子,一个实例被nova stop之后,数据库里vm_state是STOPPED,power_state是SHUTDOWN,这两个状态是匹配的。但如果直接去底层用virsh把虚拟机destroy了,Nova的vm_state还是ACTIVE,power_state在Libvirt上报之后才会变成SHUTDOWN,这时候如果不做同步,Nova会认为实例仍然在运行中。所有生命周期操作的核心逻辑,本质上就是在正确的时间把这三个状态修改到正确的值。
1.2 生命周期操作之间的依赖关系
Start、Reboot、Lock在Nova的状态机里踩的是完全不同的路径,而且它们对实例当前状态的要求也各不相同。Start这个操作要求实例必须处于STOPPED或者SHELVED_OFFLOADED状态,如果实例还在ACTIVE状态下执行Start,Nova API会直接抛Conflict (409)。而Reboot要求实例必须处于ACTIVE状态,如果你对一个STOPPED状态的实例执行reboot,同样会被拒绝。Lock则是特殊情况,它不直接修改vm_state,而是在实例记录上打一个locked标记,用来在API层挡住那些可能对实例产生破坏性影响的操作。
从实际操作顺序来看,这三个操作在Keepalive上经常被放在一起,比如运维人员习惯性地把stop -> start -> reboot当作一个"重启三连",这在Nova的视角里其实是非常重的操作组合。stop会走一遍powering-off的task_state,要求guest OS通过ACPI关机,然后开始start,又走一遍spawning或powering-on。相比之下,一个reboot操作只会在rebooting这个task_state上跑一圈,开销小得多。理解这层差异之后,你会逐渐意识到,在Nova里操作实例并不是想怎么组合就怎么组合的,状态的合法性校验是第一道关卡。
2. Start Instance 操作详解:从OpenStack命令行到底层虚拟化驱动
2.1 一条Start命令背后的完整调用链路
先看日常最常用的命令形式:
openstack server start <instance_id_or_name>如果你习惯用老版的Nova client,等价命令是nova start <server>。加上--wait参数可以让CLI一直等到实例真正进入ACTIVE状态才返回,这在脚本里特别实用,不至于sleep几秒钟然后拿着不准确的状态去做下一步判断。
Start操作从API到虚拟化驱动的完整链路大概是这样的:
- nova-api接收
POST /v2.1/{project_id}/servers/{server_id}/action请求,请求体是{"os-start": null}。 - nova-api从数据库取实例记录,校验
vm_state是STOPPED或SHELVED_OFFLOADED。 - nova-api通过RPC调用nova-conductor,conductor再把任务转发给该实例所在计算节点上的nova-compute。
- nova-compute的
start_instance方法开始执行,它要做的事情包含:调用network_api重新为实例配置网络、调用driver.power_on启动虚拟机、等待Libvirt上报运行状态。 - 最后把数据库里实例的
task_state清掉,vm_state迁移到ACTIVE,power_state更新为RUNNING。
其中第4步是真正的重头戏。driver.power_on在Libvirt驱动里实际上会执行domain.create(),然后在创建之前会重新把网络接口(VIF)和云硬盘(Volume)都接回去。你可以从nova-compute服务的日志里看到这段过程,从Starting instance...一直到Instance is running,中间夹杂着各种关于network、volume的操作记录。
2.2 为什么实例启动这么慢:网络和存储是主要耗时点
生产环境里,一个实例从Start到进入可用状态,耗时长短跟底层规模密切相关。一个只挂了系统盘、没有额外云硬盘的实例,如果在同一个分布式网络中虚拟机的tap设备都已经提前创建好,启动过程可能只需要几秒到十几秒。但如果你挂载了几块大容量Cinder云硬盘,并且用的还是后端为LVM或者iSCSI的存储方案,那么Start操作在driver.power_on之前还要执行attach_volume,这会等待存储后端把块设备映射到计算节点,然后由Libvirt的qemu进程以设备方式挂载进去。这个流程一旦网络延迟升高或者存储节点负载暴涨,几十秒甚至几分钟就出去了。
这里就涉及到一个非常常见的排障点:当执行openstack server start后,实例长时间停留在ACTIVE但power_state已经不是RUNNING,或者卡在BUILDING或者spawning这种状态,不要只盯着nova-compute看,先检查网络节点和存储节点是不是有异常。我曾经遇到过一例实例启动超时的案例,最后定位到是计算节点上bridge的MAC地址表出了问题,虚拟机domain都已经被Libvirt创建了,但tap设备没有正确接入bridge,导致虚拟机压根无法与外网通信,而Nova认为资源已经分配完毕,状态自然也就没有继续推进。
2.3 启动实例时Scheduler真的会介入吗
一个很关键但容易被误解的问题是:openstack server start的时候,nova-scheduler到底还参不参与决策?答案是不参与。Scheduler只在实例创建、迁移、热迁移等需要选节点的场景下才会被调用。Start操作的目标节点是已经确定的,因为实例在数据库中本来就记录了host字段,对应的计算节点是固定的。也就是说,实例在哪个计算节点启动,几乎完全依赖于该节点上nova-compute的状态。
所以如果实例启动失败,最先应该去查的就是目标计算节点上的服务状况。比如计算节点的内存超分策略配置有问题、memory_ratio设置得过大导致系统可用内存不足、CPU pinning规则和实际CPU拓扑不匹配、或者Libvirt连接不稳定,所有这些异常都会导致Start操作无法完成。另一个常见情况是计算节点在维护期间被nova-compute搞挂了,实例数据库里记录的host还是那个节点,但该节点已经无法提供服务,这时候Start就会一直卡住,日志里反复出现Failed to connect to libvirt之类的错误。
2.4 从日志到数据库:Start失败的排查三板斧
排查Start失败,我一般的操作顺序是:
- 先看
openstack server show <server>输出的fault字段,这个字段包含最近一次API层能感知到的错误摘要,很多时候问题就写在这一行。 - 然后看目标计算节点上的nova-compute日志,路径一般在
/var/log/nova/nova-compute.log,搜索实例ID,从Starting instance...开始往下翻,直到看到Exception或ERROR关键字。 - 如果Nova日志里没有明显异常,再进底层看Libvirt状态,比如
virsh list --all确认domain是否已经被创建,virsh dumpxml <domain>检查XML配置是否正确,尤其是CPU型号、启动磁盘、VIF类型这几个关键项。
结合我自己的实战经验,第2步其实能解决大部分问题。很多Start失败不是Nova的逻辑问题,而是底层QEMU创建进程时报错,比如XML配置里引用了不存在的镜像路径、磁盘格式不正确、或者/dev/kvm不可用导致无法开启硬件虚拟化。这些错误信息会原样封装进Nova-compute的日志里,找到最底层那行libvirtError通常就是根因。
3. Nova reboot:软重启与硬重启的细节与坑
3.1 soft reboot不是简单地下个重启命令
openstack server reboot <server>默认执行的是软重启,也就是soft reboot。在Libvirt驱动里,软重启的实现方式是调用domain.reboot(),这个接口会向QEMU发送ACPI重启信号,要求guest OS内的init系统友好地关闭所有服务,然后重新引导。你可以在虚拟机里执行reboot命令之后观察宿主机的磁盘信息,如果是通过Libvirt发出的重启,guest OS里的日志一般会记录到一次正常的系统关机和启动过程。
但这里有一个非常不为人知的细节:domain.reboot()在Libvirt内部的实现,对于一些没有ACPI支持的guest OS,可能只会做一次优雅关机,然后就停在那里。如果虚拟机配置里没有开启ACPI(比如某些精简Linux发行版镜像为了缩减体积关闭了ACPI),guest OS根本收不到系统总线上的重启信号,虚拟机就会停留在一种"系统关了一部分但进程没死"的诡异状态,实例一直处于rebooting状态,半天不动。所以如果你发现一个实例软重启后长时间卡住,virsh domstate又显示running,除了怀疑guest OS有问题之外,第一个要查的就是镜像里ACPI有没有启用,以及Libvirt domain XML里<features><acpi/></features>这个标签是否存在。
3.2 hard reboot的风控:什么时候才该用
硬重启对应的是openstack server reboot --hard <server>,在底层实现上相当于对虚拟机先执行强制断电,再重新上电启动。Libvirt驱动的实现逻辑是domain.destroy()然后domain.create(),destroy()对于QEMU进程来说就是无条件终止,guest OS内的所有未落盘数据都会丢失,所以不到万不得已不要在生产环境对数据库这类服务使用hard reboot。
那么什么时候该用?我的判断标准很简单:如果guest OS已经完全无响应,比如ping不通、SSH连不上、控制台也输出不了内容,这种情况软重启大概率也没有任何效果,因为ACPI信号发进去guest根本不会处理。此时再等下去只会白白延长故障时间,果断硬重启。另一种情况是软重启已经失败,Nova本身在reboot_instance的代码里也会有软重启超时后自动fallback到硬重启的逻辑,但这取决于具体版本和配置,不要完全依赖它,关键时刻自己判断更靠谱。
hard reboot还有一个值得留意的点:它本质上相当于把QEMU进程杀掉再重新拉起来,所以网络设备、磁盘设备会全部重新初始化。如果实例挂载了云硬盘,并且存储后端在detach/attach过程中有状态一致性要求,比如某些老版本的LVM后端在非正常卸载后需要做fsck,那么硬重启之后实例的启动速度会明显变慢,甚至可能出现一个小概率的启动失败。这些现象在文档里不会写,但运维中见得多,心里有数就行。
3.3 reboot过程中的状态流转与并发保护
reboot操作在Nova里的task_state是rebooting,从API发出到执行完成,这中间如果运维手滑又发了一个其他操作,比如同时发了resize或snapshot,那么Nova的并发保护机制会拒绝后者,这在业务上是合理的:你不能在一台正在重启的机器上做热迁移或者生成快照,底层资源的状态是混乱的。
理解状态流转还有一个实际价值:当你用脚本批量对多台实例执行reboot时,每台实例的reboot请求会进入nova-compute的工作队列,而task_state=rebooting是防止同一实例被重复发起重启的关键。我在写自动化工具时,习惯在每个reboot请求发出后立刻轮询openstack server show的输出,重点不是看status,而是看task_state是否又变回了空值。只有task_state为空并且vm_state=ACTIVE,才说明这一轮reboot已经彻底结束。这个小技巧能帮你避免很多脚本层面的误判。
举一个具体的踩坑例子。某次生产变更,我写了一个遍历所有实例执行reboot的脚本,一开始只判断了status字段,结果发现status在reboot过程中变成了ACTIVE但task_state仍然是一大串很长的字符串,脚本误以为reboot完成,继续往下执行后续操作,导致一部分实例收到了重复的reboot请求。从那次之后,我再也没有用单一状态字段做过并发判断,状态机这种东西必须组合着看。
4. Nova lock/unlock:很多人忽略的防误删利器
4.1 lock实例后究竟锁住了什么
nova lock这个操作在OpenStack社区里的定位非常明确:它是Nova API层的一个轻量级保护机制,防止实例被意外删除或执行一些破坏性操作。老方法用nova lock <server>,新版本的OpenStackCLI也支持openstack server lock <server>。实例被锁定后,数据库里的locked字段会被置为True,此后所有跟删除、迁移、快照、resize、rebuild等相关的API请求在走到nova-api校验层时,都会检查这个锁定标记并直接拒绝。
这就有意思了,因为很多人以为lock了实例之后啥都不能干,其实不是。Start、Stop、Reboot这些生命周期的操作是不受lock限制的,也就是说你锁定的实例照样可以正常开关机和重启。这个设计的逻辑是:锁定的目的是防止误操作删除或改动配置,而不是防止对实例做基本的启停管理。如果你想要的是那种"禁止一切操作"的锁,Nova的lock是做不到的,得靠其他的资源管理策略。
4.2 locked状态与重建、删除、快照的冲突
实际操作里最常见的case是:实例被锁定,然后你执行openstack server delete <server>,API直接返回403 Forbidden,这其实就是在提醒你实例处于保护状态。如果你确定需要删除,必须先把锁解开,命令是nova unlock <server>,管理员还可以用nova unlock --force <server>强制解锁,想了解和管理这个状态的运维同学一定要记清楚这个细节。
顺带说一个我在多租户环境中遇到的场景:经常有用户创建完实例之后手动执行nova lock,结果过了几个月要删除实例的时候怎么都删不掉,然后开ticket找管理员。这个问题的本质不是安全问题,而是用户对lock语义的理解有偏差。处理这种问题的时候,管理员可以先openstack server show <server>看一眼locked字段,确认之后用nova unlock --force <server>解锁,然后再让用户去删除。这种情况在生产环境一个月能碰到好几起,所以我把lock和unlock单独拎出来讲,也是想提醒大家,lock不是银弹,用之前想清楚用途,用之后也要记得公布规范的解锁流程。
4.3 结合Start和Reboot一起用:锁定状态下的运维套路
运维上一套比较稳妥的组合拳是:对核心数据库实例执行nova lock,同时在变更窗口期内照常使用openstack server reboot --hard <server>重启实例,再配合openstack server start启动实例。这样既保证了实例不会被某个手滑的操作误删,又不影响正常的重启和维护。
但有一点要特别强调:locked状态对Libvirt层是不生效的。也就是说,锁只是Nova API层面的行为约束,如果你在计算节点上直接用virsh destroy去关掉对应的QEMU进程,Lock是完全不知道的,Nova只会通过后续的周期任务发现实例失联。所以在做底层的强制操作时,一定要先评估实例的锁状态和业务影响,不要觉得有了lock就万事大吉。同样的道理也适用于热迁移和跨节点迁移,lock不会阻止你在底层直接移动或者破坏实例的磁盘文件。
另外一个值得记录的经验是:lock和reboot联动的并发问题。在Nova的高版本代码里,API层对locked实例的校验顺序很多时候是放在状态校验之前的,也就是先检查锁定再判断状态。这意味着即使实例处于ACTIVE状态,如果它被锁了,某些操作也会直接被拒。所以如果脚本里连续执行了lock、reboot、unlock,要注意操作顺序对执行结果的影响,我建议lock之后等实例状态稳定再执行其他操作,不要一口气串起来。
5. 常见问题排查技巧:从热词场景到实战故障速查
5.1 实例起不来,日志又显示"could not acquire lock(s)"怎么办
这个报错是OpenStack运维圈一个非常经典的热词。你在nova-compute.log或者libvirtd.log里看到could not acquire lock(s)时,多数情况是Libvirt在尝试对磁盘镜像或者云硬盘加锁时失败。为什么会失败?常见的原因有两类:一是同一个磁盘设备被两个QEMU进程同时打开了,这在共享存储环境中特别容易出现,尤其是当你的实例被意外调度到两台计算节点上同时启动的时候;二是Lock Manager的锁文件目录权限问题,Libvirt默认把锁文件放在/var/lib/libvirt/lockd/files/下,如果这个目录被误改权限或者磁盘空间满了,也会导致锁获取失败。
既然Lock报错了,最直接的做法是先对实例做一次openstack server stop,然后去计算节点上确认没有残留的QEMU进程在访问同一块磁盘,用ps aux | grep qemu检查一下,再用virsh list --all确认domain是否已经不存在。清理干净之后重新openstack server start,绝大多数情况下就能恢复。如果你用的是Kolla部署的容器化环境,还要记得lockd目录是在宿主机上挂载挂入容器的,不要把宿主机路径和容器路径搞混。
顺带说一个重要经验:could not acquire lock(s)不一定意味着你的实例坏了,很多时候它只是上一个没被清理干净的进程留下的锁文件残留。如果你确认没有其他进程在用同一块磁盘,直接把锁文件删掉(当然前提是确认实例已经停止)就能解决。但这属于"脏修复",用之前要反复确认实例处于真正停止的状态,如果实例还在运行,你强行删锁会导致两个进程同时操作磁盘,数据损坏的风险非常高。
5.2 多架构虚拟机和QEMU启动失败的特殊场景
近两年ARM架构的实例需求越来越大,通过QEMU部署多架构虚拟机的场景也越来越多。在OpenStack里跑ARM实例,通常需要底层计算节点支持x86模拟ARM,也就是用qemu-system-aarch64这类二进制文件配合固件启动。如果你在Start一个ARM实例时报错,不要先急着怀疑Nova的代码,先检查两样东西:一是计算节点上有没有对应的qemu-system-aarch64可执行文件,二是Libvirt的domain XML里<os>标签下的loader和firmware路径是否正确。
大多数多架构实例起不来的原因,无非是镜像不够纯净、内核和initrd不配套、或者虚拟机的CPU类型和架构不匹配。排查的时候,直接看QEMU进程的error输出往往是最快的,然后进virsh edit检查一下XML里的CPU型号和machine type,比如virt-4.0这种。如果对QEMU命令行不太熟,建议先在宿主机上手动执行一次QEMU命令,确认命令行本身没问题,再用它反推Libvirt的XML配置怎么改。这个思路在目前的多架构部署场景里,基本能解决90%以上的启动类问题。
5.3 重启后网络不通:别忘了检查端口绑定和DHCP
实例reboot之后网络不通也是一个高频问题,很多人第一时间想到的是安全组或者路由规则,但在Nova的场景里,还要加一步检查:实例对应的Neutron端口与本机的绑定关系。在openstack server reboot --hard之后,实例的VIF底层会重新plug一遍,但极端情况下,比如计算节点neutron-agent和nova-compute的顺序发生变化,或者openvswitch的流表被刷掉,端口可能没有重新绑到正确的bridge上。
快速定位方法是这样:在计算节点上执行ip link看tap设备是否存在,然后virsh dumpxml里找到对应的<interface type='bridge'>配置,确认target dev和本机实际网卡对得上。如果对不上,先从neutron侧删除端口再重建试试,或者重启neutron-openvswitch-agent服务。这种问题跟运维平台的联动关系比较大,多踩几次坑自然就有感觉了。
综合来说,Start、Reboot、Lock这三个操作虽然基础,但每一个都在Nova状态机的设计里占据着独特的位置。把这几个操作吃透,你在处理日常运维故障时的排查思路就会清晰很多。我自己在跑生产环境的过程中,最大的体会是要学会用"状态机视角"去看待实例,而不是停留在命令层面。每一条指令的本质都是状态迁移的触发条件,顺着状态变化的脉络去排查问题,往往比淹死在日志里高效得多。下一篇我们可以接着聊Live Migration和Resize这类更高级的操作,那些操作对状态机的依赖更深,理解了本篇的基础,下篇会更轻松。