文章目录
- 背景
- 基本原理
-
- PML
- Dirty-Ring
- Dirty-Limit
- 具体实现
-
- 数据结构
-
- vcpu_dirty_rate_stat
- dirtylimit_state
- 算法实现
-
- 接口逻辑
-
- qmp_set_vcpu_dirty_limit
- qmp_cancel_vcpu_dirty_limit
- 限制算法
-
- 测试验证
-
- 一个广子
背景
- 热迁移实现逻辑中,如果虚机内存负载高,源端不断产生新的内存脏页,热迁移由于不断传输源端新产生的脏页,导致剩余脏页量一直无法达到阈值,迟迟无法收敛。因此热迁移实现中,一个重要的逻辑就是实现脏页收敛算法,使得源端脏页能够尽快达到阈值。常用的收敛算法有auto-converge、xbzrle、compression、multifd等,这些算法各有其优缺点。
- 其中auto-converge是最有效的收敛算法,其核心思想是降低脏页产生的速率。通过减少虚拟机vCPU运行时间来降低虚机脏页产生速率,使其小于迁移拷贝速率,以满足迁移收敛条件,参考cpu throttle原理浅析。该算法优点是任何虚拟化场景都适用且有效。缺点是在限制虚机脏页产生的同时,也限制了虚机vCPU运行时间,虚机的计算性能在迁移过程中也随之下降。
- 内核引入dirty-ring后,提供了一种基于dirty-ring实现虚机内存脏页统计的方式,参考Dirty Ring脏页统计。dirty-ring的机制让我们很可以计算每个vCPU的脏页速率,如果再给每个vCPU设置一个速率上限,当vCPU超过上限时通过throttle的方式让其睡眠以达到降低脏页速率的目的。最后周期性计算vCPU脏页速率并对比设置的速率上限,当某个vCPU超过该上限时,就通过睡眠“
惩罚
”它。这样可以实现将所有vCPU都控制在一个设置的速率上限内。这就是DirtyLimit的核心思想。如果将其应用在热迁移中,可以达到和auto-converge相同的收敛效果,并且热迁过程中读性能还不会下降。DirtyLimit的介绍可以参考天翼云公众号文章: 迁移速度与计算性能兼得!天翼云DirtyLimit技术大显身手
基本原理
PML
- Dirty-Ring基于Intel PML(page-modification log buffer)实现,我们首先介绍PML工作流程,其框架如下:
Intel VT-x提供的VMCS(virtual machine control structure)中,有三个地方与PML特性相关:
- Extended-Page-Table Pointer: EPTP字段中的bits[6],控制当物理CPU访问内存页后,硬件是否将对应的
accessed and dirty flags
置位,PML需要开启。
- VM-execution control fields: 控制区域中的
Secondary Processor-Based VM-Execution Controls
子字段的bits[17]控制位用于控制是否开启PML,PML需要开启。Control Field for Page-Modification Logging
存放一段4K物理内存的地址,这段内存是就是PML的buffer,其内容是vCPU访问的物理内存页地址(GPA), 每条地址64bit,一共512条,因此其大小为512 * 64bit = 4K:
- Guest State area: Guest状态字段中的
PML index
子字段,用于指示物理CPU将下一次访问的内存地址记录到PML buffer的哪一条,一旦设置好,CPU填写了一条PML entry后,硬件会自动将PML index加1。
- 使能PML的整个流程是,开启EPTP的页表标脏功能,开启
VM-execution control fields
的PML开关,为每个CPU的PML buffer分配4K内存,将内存地址填入VM-execution control fields
的地址字段,最后设置将CPU填写PML buffer的起始位置写入PML index,执行VMLAUNCH
指令进入guest模式。
Dirty-Ring
- 基于PML buffer,QEMU引入了可以对每个vCPU脏页跟踪的Dirty-Ring机制,基于该机制实现了每个vCPU的脏页速率计算。参考QEMU脏页速率计算原理中的Dirty-Ring一节。
Dirty-Limit
- 实现脏页速率计算后,我们可以设置DirtyLimit并周期性地计算每个vCPU的脏页速率,对比两者的值,如果脏页速率大于DirtyLimit,便通过让对应vCPU睡眠来惩罚它,让vCPU的脏页速率逐渐下降至设置的值,示意图如下:
- 首先QEMU获取用户设定的DirtyLimit值,将其保存到内存中,DirtyLimit与vCPU为1对1的关系
- 之后启动单独线程周期性计算vCPU的脏页速率DirtyRate并保存到内存中,DirtyRate与vCPU也为1对1的关系
- 当KVM将某个vCPU的Dirty Ring的条目填满之后,对应vCPU便会抛出异常,vCPU线程从内核态退出到用户态,此时QEMU在该路径上将其拦截,对比该vCPU的DirtyRate和DirtyLimit的,如果发现DirtyRate大于DirtyLimit,便计算用于
“惩罚”
vCPU的睡眠时间,然后让对应vCPU线程睡眠。
具体实现
- dirty-limit的实现在单独的一个文件中:softmmu/dirtylimit.c
数据结构
- dirty-limit的核心数据结构就是两个全局变量:DirtyRate和DirtyLimit
vcpu_dirty_rate_stat
typedef struct VcpuStat {
/* 保存rates数组中的vCPU个数 */
int nvcpu; /* number of vcpu */
/* dirtyrate数组,记录每个vCPU对应的脏页速率 */
DirtyRateVcpu *rates; /* array of dirty rate for each vcpu */
} VcpuStat;
struct {
VcpuStat stat; /* 保存虚机脏页速率 */
bool running; /* 标记当前是否正在周期性计算脏页速率 */
QemuThread thread; /* 周期性计算脏页速率的线程 */
} *vcpu_dirty_rate_stat
dirtylimit_state
typedef struct VcpuDirtyLimitState {
int cpu_index; /* vCPU ID*/
bool enabled; /* 标记该vCPU是否使能DirtyLimit*/
/*
* Quota dirty page rate, unit is MB/s
* zero if not enabled.
*/
uint64_t quota; /* vCPU使能DirtyLimit时用户设置的上限值 */
} VcpuDirtyLimitState;
struct {
VcpuDirtyLimitState *states; /* 保存虚机的DirtyLimit */
/* Max cpus number configured by user */
int max_cpus; /* 虚机配置的最大vCPU数 */
/* Number of vcpu under dirtylimit */
int limited_nvcpu; /* 虚机使能DirtyLimit的vCPU数 */
} *dirtylimit_state;
/* protect dirtylimit_state */
static QemuMutex dirtylimit_mutex;
/* dirtylimit thread quit if dirtylimit_quit is true */
static bool dirtylimit_quit;
算法实现
接口逻辑
- DirtyLimit算法的核心逻辑主要在
qmp_set_vcpu_dirty_limit
和qmp_cancel_vcpu_dirty_limit
中实现,我们简单分析其逻辑
qmp_set_vcpu_dirty_limit
void qmp_set_vcpu_dirty_limit(bool has_cpu_index, /* qmp是否传入*/
int64_t cpu_index,
uint64_t dirty_rate,
Error **errp)
{
/* dirty-limit依赖dirty-ring,首先检查是否配置 */
if (!kvm_enabled() || !kvm_dirty_ring_enabled()) {
error_setg(errp, "dirty page limit feature requires KVM with"
" accelerator property 'dirty-ring-size' set'");
return;
}
/* dirty-limit可以不指定vCPU index,这是QEMU会限制所有vCPU,如果指定,检查vCPU index是否超出范围 */
if (has_cpu_index && !dirtylimit_vcpu_index_valid(cpu_index)) {
error_setg(errp, "incorrect cpu index specified");
return;
}
/* 如果当前热迁移正在使用dirtylimit算法,不希望受用户设置的影响,因此热迁移期间不允许使能和取消dirtylimit */
if (!dirtylimit_is_allowed()) {
error_setg(errp, "can't set dirty page rate limit while"
" migration is running");
return;
}
/* 如果dirty_rate传入0,表示取消dirty-limit */
if (!dirty_rate) {
qmp_cancel_vcpu_dirty_limit(has_cpu_index, cpu_index, errp);
return;
}
/* 为保证qmp_set_vcpu_dirty_limit接口线程安全,保护dirtylimit_state全局变量,加锁 */
dirtylimit_state_lock();
/* 如果是第一次使能dirty-limit,完成初始化工作
* 初始化中主要是初始化全局变量并启动脏页速率计算线程
* 周期性计算其vCPU速率并更新到vcpu_dirty_rate_stat
* 最后,使能DirtyLimit限制,周期性地为每个超过dirty-limit的vCPU计算睡眠时间 */
if (!dirtylimit_in_service()) {
dirtylimit_init();
}
/* 设置用户配置的dirty-limit,核心工作就是更新全局变量dirtylimit_state */
if (has_cpu_index) {
dirtylimit_set_vcpu(cpu_index, dirty_rate, true);
} else {
dirtylimit_set_all(dirty_rate, true);
}
dirtylimit_state_unlock();
}
qmp_cancel_vcpu_dirty_limit
void qmp_cancel_vcpu_dirty_limit(bool has_cpu_index,
int64_t cpu_index,
Error **errp)
{
/* 检查依赖是否满足 */
if (!kvm_enabled() || !kvm_dirty_ring_enabled()) {
return;
}
/* 检查index是否在范围之内 */
if (has_cpu_index && !dirtylimit_vcpu_index_valid(cpu_index)) {
error_setg(errp, "incorrect cpu index specified");
return;
}
/* 检查是否与热迁移冲突 */
if (!dirtylimit_is_allowed()) {
error_setg(errp, "can't cancel dirty page rate limit while"
" migration is running");
return;
}
/* 如果当前dirty-limit已经停止,直接返回 */
if (!dirtylimit_in_service()) {
return;
}
/* 更新dirtylimit_state全局变量前加锁 */
dirtylimit_state_lock();
/* 更新dirtylimit_state */
if (has_cpu_index) {
dirtylimit_set_vcpu(cpu_index, 0, false);
} else {
dirtylimit_set_all(0, false);
}
/* 如果最后一个使能dirty-limit的vCPU被取消,停止脏页速率计算线程,停止DirtyLimit限制逻辑 */
if (!dirtylimit_state->limited_nvcpu) {
dirtylimit_cleanup();
}
dirtylimit_state_unlock();
}
限制算法
算法框架
main --------------> throttle thread ------------> PREPARE(1) <--------
thread \ | |
\ | |
\ V |
-\ CALCULATE(2) |
\ | |
\ | |
\ V |
\ SET PENALTY(3) -----
-\ |
\ |
\ V
-> virtual CPU thread -------> ACCEPT PENALTY(4)
- 当qmp_set_vcpu_dirty_limit命令被调用时,QEMU主线程启动了throttle线程,其作用是实现DirtyLimit主要逻辑,主要包含以下阶段:
- PREPARE (1) - 准备阶段
这个阶段主要为二阶段- CALCULATE(2)的计算准备输入,主要准备两个值:虚机当前vCPU脏页速率(dirty page rate),用户配置的虚机vCPU脏页速率上限(dirty page rate limit),其脏页速率通过调用现有的脏页速率计算接口得到,参考QEMU脏页速率计算,脏页速率上限通过用户配置得到。
- CALCULATE (2) - 计算阶段
这个阶段主要判断vCPU的脏页速率是否低于用户配置的脏页速率上限,如果不满足,计算在三阶段-SET PENALTY (3)中用于惩罚虚机vCPU的睡眠时间。
- SET PENALTY (3) - 惩罚阶段
这个阶段是具体让vCPU睡眠的阶段,其主要逻辑是在vCPU因为KVM_EXIT_DIRTY_RING_FULL而异常退出的代码路径上让vCPU睡眠。
- 通过上面三个阶段的操作。QEMU期望让将vCPU的脏页速率限制在用户配置的阈值范围内。
理想效果
- 接口实现中我们提到了使能DirtyLimit限制,其核心逻辑就是计算超过DirtyLimit的vCPU的睡眠时间,假设vCPU当前脏页速率
current=200MB/s
,目标速率quota=40MB/s
,速率计算和睡眠时间更新的周期(x_vcpu_dirty_limit_period
)为1s,我们举例说明理想的限制算法对速率的限制曲线如下:
- 限制算法的理想行为是:
- 如果目标脏页速率与实际脏页速率相差很大,两者相减得到的差值比目标脏页速率一半还多,我们希望惩罚的力度大,让脏页速率在一个速率计算周期内(
x_vcpu_dirty_limit_period
)线性下降。如图中的第1秒,脏页速率从200MB/s减少到了70MB/s
- 如果目标脏页速率与实际脏页速率相差不大,两者相减比
max(quota, current)
的一半少,我们希望减小惩罚力度,让脏页速率在一个速率计算周期内下降固定值,这样可以避免实际脏页速率在quota
值上下大幅的震荡。如图中的第2、3秒,脏页速率在1秒内下降10MB/s。
- 如果目标脏页速率与实际脏页速率相差在一个可接受的范围内(默认值25MB/s),保持惩罚力度,让vCPU的睡眠时间不变。如图中的3~9秒。
具体实现
- 上面是限制算法的理想效果,具体实现时没法完全达到预期,其核心函数是
dirtylimit_adjust_throttle
,我们进一步分析:
static void dirtylimit_set_throttle(CPUState *cpu,
uint64_t quota,
uint64_t current)
{
int64_t ring_full_time_us = 0;
uint64_t sleep_pct = 0;
uint64_t throttle_us = 0;
/* 如果当前vCPU的速率已经是0,取消惩罚,直接返回 */
if (current == 0) {
cpu->throttle_us_per_full = 0;
return;
}
/* 获取当前虚机vCPU的脏页速率和dirty-ring的size,
* 计算理想情况下,vCPU以当前的脏页速率填满一个空的dirty-ring表需要多长时间 */
ring_full_time_us = dirtylimit_dirty_ring_full_time(current);
/* 如果目标脏页速率和当前脏页速率相差过大,让vCPU速率线性下降 */
if (dirtylimit_need_linear_adjustment(quota, current)) {
if (quota < current) {
/* 根据目标脏页速率和当前脏页速率,计算一个睡眠时间占填满整个dirty-ring表时间的百分比
* 根据百分比计算vCPU的睡眠时间,期望达到的效果是:
* 当vCPU速率很大时,睡眠的时间会相对较短,反之,睡眠时间会相对长
*/
sleep_pct = (current - quota) * 100 / current;
throttle_us =
ring_full_time_us * sleep_pct / (double)(100 - sleep_pct);
cpu->throttle_us_per_full += throttle_us;
} else {
sleep_pct = (quota - current) * 100 / quota;
throttle_us =
ring_full_time_us * sleep_pct / (double)(100 - sleep_pct);
cpu->throttle_us_per_full -= throttle_us;
}
trace_dirtylimit_throttle_pct(cpu->cpu_index,
sleep_pct,
throttle_us);
} else {
/*取一个测试效果最好的经验值作为固定的睡眠时间 */
if (quota < current) {
cpu->throttle_us_per_full += ring_full_time_us / 10;
} else {
cpu->throttle_us_per_full -= ring_full_time_us / 10;
}
}
/* 为保证vCPU睡眠时间过长导致Guest内核线程softlockup,设置vCPU睡眠时间的上限 */
cpu->throttle_us_per_full = MIN(cpu->throttle_us_per_full,
ring_full_time_us * DIRTYLIMIT_THROTTLE_PCT_MAX);
cpu->throttle_us_per_full = MAX(cpu->throttle_us_per_full, 0);
}
- 上面分析了睡眠时间计算,最终的数值被保存到了
CPUState
结构体的throttle_us_per_full
字段中,限制算法的最后阶段就是让vCPU睡眠throttle_us_per_full
指定的时间,这个逻辑在QEMU处理vCPU线程退出时完成,如下:
kvm_cpu_exec
/* 执行vCPU线程,陷入内核 */
run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
/* 从内核exit到用户态空间 */
switch (run->exit_reason) {
/* 根据exit_reason做对应处理 */
case KVM_EXIT_IO:
......
/* 如果退出是由于Dirty-Ring满了,做对应的处理
* 这里QEMU的主要工作就是清空Dirty-Ring,让内核可以继续填写
*/
case KVM_EXIT_DIRTY_RING_FULL:
/*
* We shouldn't continue if the dirty ring of this vcpu is
* still full. Got kicked by KVM_RESET_DIRTY_RINGS.
*/
trace_kvm_dirty_ring_full(cpu->cpu_index);
qemu_mutex_lock_iothread();
/*
* We throttle vCPU by making it sleep once it exit from kernel
* due to dirty ring full. In the dirtylimit scenario, reaping
* all vCPUs after a single vCPU dirty ring get full result in
* the miss of sleep, so just reap the ring-fulled vCPU.
*/
if (dirtylimit_in_service()) {
/* 当dirty-limit开启时,仅清空对应vCPU的Dirty-Ring
* 这样可以保证每个vCPU满了之后都会走到该路径,保证其接受惩罚
* 如果某个vCPU满了,但是这里我们把所有vCPU的Dirty-Ring都清空的话
* 就会导致有些脏页速率较大的vCPU永远接收不到惩罚 */
kvm_dirty_ring_reap(kvm_state, cpu);
} else {
kvm_dirty_ring_reap(kvm_state, NULL);
}
qemu_mutex_unlock_iothread();
/* 调用睡眠函数,实施最终的惩罚 */
dirtylimit_vcpu_execute(cpu);
ret = 0;
break;
......
测试验证
QEMU
- 源端
#!/bin/bash
/usr/bin/qemu-system-x86_64 \
-display none -vga none \
-name guest=migrate_src,debug-threads=on \
-monitor stdio \
-accel kvm,dirty-ring-size=65536 -cpu host \
-kernel /home/work/fast_qemu/vmlinuz-6.1.19-7.0.0.17.oe2303.x86_64 \
-initrd /home/work/fast_qemu/initrd-stress.img \
-append "noapic edd=off printk.time=1 noreplace-smp cgroup_disable=memory pci=noearly console=ttyS0 debug ramsize=4" \
-chardev file,id=charserial0,path=/var/log/mig_dst_console.log\
-serial chardev:charserial0 \
-D /var/log/mig_dst.log \
-m 4096 -smp 2 \
- 目的端
- 通过下面方式启动虚机,其中initrd-stress.img是QEMU内存压测工具简介中介绍的引导镜像。其中192.168.31.155是目的端IP,9000是目的端QEMU监听迁入内存的端口:
#!/bin/bash
/usr/bin/qemu-system-x86_64 \
-display none -vga none \
-name guest=migrate_src,debug-threads=on \
-monitor stdio \
-accel kvm,dirty-ring-size=65536 -cpu host \
-kernel /home/work/fast_qemu/vmlinuz-6.1.19-7.0.0.17.oe2303.x86_64 \
-initrd /home/work/fast_qemu/initrd-stress.img \
-append "noapic edd=off printk.time=1 noreplace-smp cgroup_disable=memory pci=noearly console=ttyS0 debug ramsize=4" \
-chardev file,id=charserial0,path=/var/log/mig_dst_console.log\
-serial chardev:charserial0 \
-D /var/log/mig_dst.log \
-m 4096 -smp 2 \
-incoming tcp:192.168.31.155:9000
- 迁移操作
QEMU 8.1.50 monitor - type 'help' for more information
(qemu) migrate_set_capability
auto-converge background-snapshot block
compress dirty-bitmaps dirty-limit
events late-block-activate multifd
pause-before-switchover postcopy-blocktime postcopy-preempt
postcopy-ram rdma-pin-all release-ram
return-path switchover-ack validate-uuid
x-colo x-ignore-shared xbzrle
zero-blocks zero-copy-send
(qemu) migrate_set_capability dirty-limit on
(qemu) migrate -d tcp:192.168.31.155:9000
/* 查看迁移使用的capability,可以看到dirty-imit被开启 */
(qemu) info migrate_capabilities
xbzrle: off
rdma-pin-all: off
auto-converge: off
zero-blocks: off
compress: off
events: off
postcopy-ram: off
x-colo: off
release-ram: off
block: off
return-path: off
pause-before-switchover: off
multifd: off
dirty-bitmaps: off
postcopy-blocktime: off
late-block-activate: off
x-ignore-shared: off
validate-uuid: off
background-snapshot: off
zero-copy-send: off
postcopy-preempt: off
switchover-ack: off
dirty-limit: on
/* 查看迁移过程中的实时数据,dirty-limit会在迁移迭代的第三轮开启 */
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
clear-bitmap-shift: 18
Migration status: active
total time: 3433 ms
expected downtime: 300 ms
setup: 33 ms
transferred ram: 23936 kbytes
throughput: 26.16 mbps
remaining ram: 1438576 kbytes
total ram: 4195080 kbytes
duplicate: 684655 pages
skipped: 0 pages
normal: 4471 pages
normal bytes: 17884 kbytes
dirty sync count: 1
page size: 4 kbytes
multifd bytes: 0 kbytes
pages-per-second: 363313
precopy ram: 23936 kbytes
/* 再次查看迁移信息,dirty-limit有相关信息输出 */
(qemu) info migrate
globals:
store-global-state: on
only-migratable: off
send-configuration: on
send-section-footer: on
decompress-error-check: on
clear-bitmap-shift: 18
Migration status: active
total time: 372685 ms
expected downtime: 63130 ms
setup: 19 ms
transferred ram: 3598269 kbytes
throughput: 99.79 mbps
remaining ram: 386204 kbytes
total ram: 4195080 kbytes
duplicate: 822808 pages
skipped: 0 pages
normal: 895998 pages
normal bytes: 3583992 kbytes
dirty sync count: 5
page size: 4 kbytes
multifd bytes: 0 kbytes
pages-per-second: 3039
dirty pages rate: 2015 pages
precopy ram: 3598269 kbytes
dirty-limit throttle time: 23272722 us
dirty-limit ring full time: 235078 us
Libvirt
一个广子
- 本人负责QEMU社区Dirty Limit和Dirty page rate模块的维护,欢迎虚拟化领域的同学提交patch,为社区贡献力量,模块包含以下文件:
Migration dirty limit and dirty page rate
F: system/dirtylimit.c
F: include/sysemu/dirtylimit.h
F: migration/dirtyrate.c
F: migration/dirtyrate.h
F: include/sysemu/dirtyrate.h