QEMU DirtyLimit特性介绍

编程入门 行业动态 更新时间:2024-10-25 06:30:00

QEMU DirtyLimit<a href=https://www.elefans.com/category/jswz/34/1769892.html style=特性介绍"/>

QEMU DirtyLimit特性介绍

文章目录

  • 背景
  • 基本原理
    • PML
    • Dirty-Ring
    • Dirty-Limit
  • 具体实现
    • 数据结构
      • vcpu_dirty_rate_stat
      • dirtylimit_state
    • 算法实现
      • 接口逻辑
        • qmp_set_vcpu_dirty_limit
        • qmp_cancel_vcpu_dirty_limit
      • 限制算法
        • 算法框架
        • 理想效果
        • 具体实现
  • 测试验证
    • QEMU
    • Libvirt
  • 一个广子

背景

  • 热迁移实现逻辑中,如果虚机内存负载高,源端不断产生新的内存脏页,热迁移由于不断传输源端新产生的脏页,导致剩余脏页量一直无法达到阈值,迟迟无法收敛。因此热迁移实现中,一个重要的逻辑就是实现脏页收敛算法,使得源端脏页能够尽快达到阈值。常用的收敛算法有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特性相关:
  1. Extended-Page-Table Pointer: EPTP字段中的bits[6],控制当物理CPU访问内存页后,硬件是否将对应的accessed and dirty flags 置位,PML需要开启。
  2. 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:

  3. 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的脏页速率逐渐下降至设置的值,示意图如下:
  1. 首先QEMU获取用户设定的DirtyLimit值,将其保存到内存中,DirtyLimit与vCPU为1对1的关系
  2. 之后启动单独线程周期性计算vCPU的脏页速率DirtyRate并保存到内存中,DirtyRate与vCPU也为1对1的关系
  3. 当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

  • 保存DirtyRate的数据结构
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

  • 保存DirtyLimit数据结构
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_limitqmp_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();
}

限制算法

算法框架
  • DirtyLimit算法框架示意图如下:
  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主要逻辑,主要包含以下阶段:
  1. PREPARE (1) - 准备阶段
    这个阶段主要为二阶段- CALCULATE(2)的计算准备输入,主要准备两个值:虚机当前vCPU脏页速率(dirty page rate),用户配置的虚机vCPU脏页速率上限(dirty page rate limit),其脏页速率通过调用现有的脏页速率计算接口得到,参考QEMU脏页速率计算,脏页速率上限通过用户配置得到。
  2. CALCULATE (2) - 计算阶段
    这个阶段主要判断vCPU的脏页速率是否低于用户配置的脏页速率上限,如果不满足,计算在三阶段-SET PENALTY (3)中用于惩罚虚机vCPU的睡眠时间。
  3. 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,我们举例说明理想的限制算法对速率的限制曲线如下:
  • 限制算法的理想行为是:
  1. 如果目标脏页速率与实际脏页速率相差很大,两者相减得到的差值比目标脏页速率一半还多,我们希望惩罚的力度大,让脏页速率在一个速率计算周期内(x_vcpu_dirty_limit_period)线性下降。如图中的第1秒,脏页速率从200MB/s减少到了70MB/s
  2. 如果目标脏页速率与实际脏页速率相差不大,两者相减比max(quota, current)的一半少,我们希望减小惩罚力度,让脏页速率在一个速率计算周期内下降固定值,这样可以避免实际脏页速率在quota值上下大幅的震荡。如图中的第2、3秒,脏页速率在1秒内下降10MB/s。
  3. 如果目标脏页速率与实际脏页速率相差在一个可接受的范围内(默认值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

  1. 源端
  • 通过下面方式启动虚机:
#!/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 \
  1. 目的端
  • 通过下面方式启动虚机,其中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
  1. 迁移操作
  • 通过以下命令行将虚机从源端迁移到目的端:
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

  • TODO

一个广子

  • 本人负责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

更多推荐

QEMU DirtyLimit特性介绍

本文发布于:2023-12-06 10:01:59,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1667230.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:特性   QEMU   DirtyLimit

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!