前两天碰到一个基于QNX
的虚拟化平台上的项目问题,同事反馈系统很卡,点击页面明显有延迟,卡顿严重。用top
看了下Android
系统的负载,还有20%
左右的空闲,其他的如用户态、内核态以及中断的占用都比较正常,唯独有一个%host
的占用特别高,最高能占到60%
以上。这个host
的占用是什么意思了?这篇文章,我们就基于这个问题,来详细阐述分析下虚拟化平台中host
占用高的问题以及在虚拟化平台KVM
是如何计算host
占用的。
1 |
|
问题定位与排查
通过adb shell
进到设备里top
看下系统整体状态,可以发现系统内存还有不少空间,CPU的空闲只有不到30%
,占大头的就是%host
这一部分。
先尝试用DeepSeek
问了下,top
指令中的host
占用到底是什么意思? DeepSeek
很快给出了答案:
在Linux的top命令中,”host的CPU占用”(通常对应%host或st字段)是虚拟化环境特有的性能指标,用于反映虚拟机被宿主机(Hypervisor)抢占的CPU时间百分比
就是说,host
过高,可能是由于虚拟化平台中宿主机有异常,比如处于高负载,CPU配置不合理(给guest系统的资源太少),导致了客户机Android
系统一直无法抢占到CPU,处于挂起等待的状态。那么,究竟如何排查这类问题了?接着问下DeepSeek
: 如何排查steal CPU占用过高的问题,DeepSeek
给出了一些可能的解释:
顺着这个思路,进入设备继续查看,通过mpstat -P ALL
查看整体的负载,观察到类似的情况, %steal
这一栏显示,Android
相当一部分负载都来自于等待宿主机上;这里多个核的%steal
占比加起来就对应top
中的%host
:
基于这些数据,我们推测很可能宿主机QNX
侧有问题(之前的版本没有异常),拉着开发人员对齐了下,确认了虚拟机上的CPU
分配没有太大问题(系统只有一个客户机Android
,可以访问所有的物理CPU),那么基本可以排除资源不足引起的问题;是不是最近有什么修改引入了这个问题?
开发反馈只有两个修改点: 一个是更新了车控相关的信号,一个是在QNX
上新增了一个PPS
节点。更新车控信号矩阵不应该对系统负载有太大的影响,而且从已有的数据看,Android
的各个进程的负载并不高,况且在台架上也没有太多的车控信号需要传输,因此可以排除。那么,很可能是新增PPS
节点导致了QNX
侧的负载过高,从而引起Android
侧无法拿到CPU。最后,开发排查了相关的进程,发现是某个PPS
节点的配置异常,有一个进程在高频的写数据导致QNX
侧的负载太高(QNX
的idle
已经接近0),因而客户机Android
无法拿到足够的CPU资源。
PPS(Persistent Publish-Subscribe)是QNX用于跨进程通讯的一种协议
问题到这里也算告一段落。但是,为了对这类问题有更多的了解,后续碰到相似问题时时能够快速的定位分析,还是决定要深入代码层面来了解下虚拟化平台中steal time
究竟是怎么来计算的。
虚拟化中的CPU steal-time
既然是‘偷来的时间’(steal-time
),那么就说明不是客户机自己执行指令导致的CPU占用,而是等待宿主机分配资源所消耗的时间,此时宿主机可能是在处理其他客户机的请求,也有可能是在忙着处理内部的事务。
Steal time is the percentage of time a virtual CPU waits for a real CPU while the hypervisor is servicing another virtual processor (or host itself).
无论是top
指令中展示的%host
,还是mpstat
中的%steal
都是通过内核的/proc/stat
获取到的CPU占用数据。查看Android
的源码external/toybox/toys/posix/ps.c
,CPU的steal-time
就是/proc/stat
的最后一列数据。我们继续查看下内核的代码。
1 |
|
内核中对应/proc/stat
的状态显示在fs/proc/stat.c
中实现的,可以看到steal-time
的计算是通过一个per-cpu
结构体变量kernel_cpustat
中的cpustat
数组对应的CPUTIME_STEAL
索引获取到:
1 |
|
内核中统计各个维度的CPU占用数据在kernel/sched/cputime.c
统一实现了相关的接口,只有开启了内核配置CONFIG_PARAVIRT
的虚拟化平台中才会计算steam-time
时间,其他的则直接返回0
:通过函数paravirt_steal_clock
获取对应CPU的客户机系统的steal
时间。
1 |
|
函数paravirt_steal_clock
在arch/arm64/include/asm/paravirt.h
中定义,最终实际是通过一个结构体pv_time_ops
中的函数steal_clock
调用获取客户机的steal
时间。
1 |
|
对于虚拟化平台来说,系统在初始化的时候,会通过pv_time_init
函数对pv_ops
进行初始化设置(arch/arm64/kernel/paravirt.c
):
pv_time_init_stolen_time
: 初始化存放steal
时间相关的变量内存区域(用于宿主机hypervisor
与客户机进行数据共享)pv_ops.time.steal_clock
: 对提供给外部获取steal-time
的接口进行赋值
1 |
|
函数pv_time_init_stolen_time
注册一个CPU
热插拔的回调,等CPU
状态变为online
收到回调后,调用stolen_time_cpu_online
函数,初始化steal-time
相关的配置:
1 |
|
可以看到,stolen_time_cpu_online
主要用于客户机与hypervisor
协商一块固定的内存区域用于交换steal-time
的时间,主要有几个步骤:
- 首先通过一个
HVC
异常陷入指令(用于切换不同的Exception Level-EL),让客户机从EL1(内核态)进入到EL2(虚拟机态),获取到宿主机用于保存steal-time
的内存地址,并通过arm_smccc_res
返回给客户机 - 内核将拿到的地址映射到一块内存,这样在内核中就可以通过函数访问到这块内存区域,从而读取到
steam-time
1 |
|
这里以Linux的虚拟化方案KVM(Kernel-based Virtual Machine)
为例来说明,在虚拟机层面如何处理steal-time
的计算,并给到客户机系统的。在KVM
接收到HVC
的指令ARM_SMCCC_HV_PV_TIME_ST
后,最终会调用对应的异常处理函数kvm_hvc_call_handler
(arch/arm64/kvm/hypercalls.c
):
1 |
|
可以看到,在这里虚拟机hypervisor
会调用kvm_init_stolen_time
来初始化steam-time
相关的配置:
- 通过客户机对应的
vcpu
结构体kvm_vcpu
获取到steal
变量的基地址base
(对应客户机的物理地址) - 通过
kvm_write_guest
将一个pvclock_vcpu_stolen_time
初始化值写入到base
地址,正是在这个结构体中保存了客户机的steal-time
1 |
|
最后,KVM
运行的时候,监听一个进程上下文切换的事件,在vcpu
进程切换时主动调用kvm_update_stolen_time
更新steal-time
,从这里我们也可以看到,客户机的steal-time
实际读取的是调度器的统计数据run_delay
,就是vcpu
调度的延迟-在调度队列里等待的时间。
1 |
|
总结
这篇文章,我们基于一个虚拟化项目的CPU占用高的问题,分析了排查的手段与思路;以KVM
为例,深入分析了虚拟化平台CPU steal-time
的计算与更新流程,这样在后续碰到类似的问题时,可以更加得心应手了。在平常的项目实践中,遇到了一些疑难问题,如果有知识盲点,花点时间深入研究下背后的原理与机制,学习的效果比单纯的理论研究要好很多。