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工作流程,其框架如下:
    QEMU DirtyLimit特性介绍_第1张图片
    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:
    在这里插入图片描述
    QEMU DirtyLimit特性介绍_第2张图片
  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的脏页速率逐渐下降至设置的值,示意图如下:
    QEMU DirtyLimit特性介绍_第3张图片
  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,我们举例说明理想的限制算法对速率的限制曲线如下:
    QEMU DirtyLimit特性介绍_第4张图片
  • 限制算法的理想行为是:
  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)