QEMU脏页速率计算原理

文章目录

  • 背景基础
    • 数据结构
    • 逻辑框架
  • dirty-bitmap
    • 计算原理
    • 数据结构
    • 实现流程
      • 脏页数统计
      • 速率计算
  • dirty-ring
    • 计算原理
    • 数据结构
    • 实现流程
      • 脏页数统计
      • 速率计算

背景基础

  • 脏页速率在QEMU中定义为虚机单位时间内产生脏内存页的速率,用于描述虚机内存变化的快慢,脏页速率越大,虚机内存变化越快,迁移时花费的时间就越多。脏页速率计算公式为:
dirtyrate = increased_memory / meaurement_time
  • 脏页速率计算的关键是如何获取单位时间内虚拟机新增的脏页数,不同的获取方式有不同的速率计算实现,QEMU已经实现了一种计算方式,即抽样,抽样方式假设虚机读写内存的区间是均匀随机的,但实际情况并非如此,因此存在一定误差。除抽样方式外,还有两种新的脏页速率计算实现(预计在6.1版本合入QEMU主线),分别是dirty-bitmap和dirty-ring。dirty-bitmap是一种新的脏页速率实现,通过查询dirty-bitmap,获取一段时间内新增的脏页数,计算脏页速率。dirty-ring同样是一种新的脏页速率实现,通过查询vcpu上脏页数,获取一段时间内vcpu上新增的脏页数,计算vcpu的脏页速率,从而得到整个虚机的脏页速率。

数据结构

  • 脏页速率统计信息由数据结构struct DirtyRateStat表示,因速率计算方式不同,需要的统计信息也不同,对于抽样的方式,描述统计信息的结构体是SampleVMStat,对于dirty-ring的方式,描述统计信息的结构体是VcpuStat,而dirty-bitmap方式的统计信息比较简单,就是一个全局变量total_dirty_pages,它用于记录脏页数量。数据结构定义在migration/dirtyrate.h中。
/* 抽样方式的统计信息 */
typedef struct SampleVMStat {
    uint64_t total_dirty_samples; /* total dirty sampled page */
    uint64_t total_sample_count; /* total sampled pages */
    uint64_t total_block_mem_MB; /* size of total sampled pages in MB */
} SampleVMStat;


 struct DirtyRateStat {
 	/* 保存虚拟机的脏页速率 */
    int64_t dirty_rate; /* dirty rate in MB/s */
    int64_t start_time; /* calculation start time in units of second */
    int64_t calc_time; /* time duration of two sampling in units of second */
    uint64_t sample_pages; /* sample pages per GB */
    union {
    	/* 保存抽样方式的统计信息 */
        SampleVMStat page_sampling;
        /* 保存dirty-ring方式的统计信息 */
        VcpuStat dirty_ring;
    };
};
/* 记录测量前后内存的脏页数,两者的差值为新增的脏页数 */
typedef struct DirtyPageRecord {
    uint64_t start_pages;
    uint64_t end_pages;
} DirtyPageRecord;

逻辑框架

  • 脏页速率计算对外提供了两个命令,用于计算和查询,分别是qmp_calc_dirty_rateqmp_query_dirty_rate,其实现逻辑比较简单,如下图所示,用户通过qmp_calc_dirty_rate发起脏页速率计算,qemu单独启动一个get_dirtyrate线程用于计算脏页速率,完成后将结果存放到全局变量DirtyStat中,然后线程退出。需要查询脏页速率时,通过qmp_query_dirty_rate查询之前存放到DirtyStat中的计算结果,返回给用户。
    QEMU脏页速率计算原理_第1张图片

dirty-bitmap

计算原理

  • dirty-bitmap计算脏页速率,顾名思义,是通过查询bitmap来获取新增的虚机脏页数。我们知道虚机在迁移时会在每个迭代周期以slot为单位,通过kvm提供的ioctl命令字从内核空间获取虚拟机的新增脏页,从而得到本轮迭代需要发送的内存页。该ioctl命令字返回bitmap数据结构,dirty-bitmap脏页速率的计算就基于此接口。简单讲,其原理是通过调用ioctl命令获取虚机脏页位图,根据位图统计出脏页数量,再除以时间,得到脏页速率。

数据结构

  • dirty-bitmap脏页速率计算的数据结构非常简单,只有一个用于统计脏页数的全局变量,定义在migration/dirtyrate.c中:
uint64_t total_dirty_pages;

实现流程

脏页数统计

  • 统计脏页数在memory_global_dirty_log_sync的执行路径中完成,memory_global_dirty_log_sync在从内核获取到位图之后,会将其保存到ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION] dirty bits中,在这个过程中,我们可以统计脏页数,流程如下:
memory_global_dirty_log_sync
	memory_region_sync_dirty_bitmap
		log_sync -> kvm_log_sync
			kvm_physical_sync_dirty_bitmap
				/* 从内核获取到slot的脏页位图,缓存到slot->dirty_bmap中 */
				kvm_slot_get_dirty_log
				/* 将缓存的slot->dirty_bmap保存到DIRTY_MEMORY_MIGRATION dirty bits中 */
				kvm_slot_sync_dirty_pages
					cpu_physical_memory_set_dirty_lebitmap
  • cpu_physical_memory_set_dirty_lebitmap负责将slot中的bitmap设置到dirty bits中,新增修改用于统计脏页数量,如下:
@@ -373,6 +375,10 @@ static inline void cpu_physical_memory_set_dirty_lebitmap(unsigned long *bitmap,
                         qatomic_or(
                                 &blocks[DIRTY_MEMORY_MIGRATION][idx][offset],
                                 temp);
+                        if (unlikely(
+ 						  	/* 如果因为脏页速率计算开启了脏页日志跟踪 */
+                            global_dirty_tracking & GLOBAL_DIRTY_DIRTY_RATE)) {
+ 							/* 位图中1的个数代表脏页数量
+                            * 统计1的个数将其加入到全局变量total_dirty_pages中 */
+                            total_dirty_pages += ctpopl(temp);
+                        }
                     }

                     if (tcg_enabled()) {
@@ -403,6 +409,9 @@ static inline void cpu_physical_memory_set_dirty_lebitmap(unsigned long *bitmap,
         for (i = 0; i < len; i++) {
             if (bitmap[i] != 0) {
             	/* 逐一按long型长度取出位图,统计其中1的位数 */
                 c = leul_to_cpu(bitmap[i]);
+                if (unlikely(global_dirty_tracking & GLOBAL_DIRTY_DIRTY_RATE)) {
+                    total_dirty_pages += ctpopl(c);
+                }
                 do {
                     j = ctzl(c);
                     c &= ~(1ul << j);
  • 脏页数量统计的实现在cpu_physical_memory_set_dirty_lebitmap函数中,根据逐一取出long型位图,统计其为1的bit数,得到脏页数。存放到全局变量total_dirty_pages 中。

速率计算

  • 速率计算比较简单,步骤如下:
  1. 开始测量时,获取全局的脏页计数,保存到本地变量
  2. 根据用户设置的测量时间,睡眠一段时间
  3. 再次获取全局的脏页计数,保存到本地变量
  4. 获取两次脏页计数的增量,得到增加的脏页数,计算增加内存总量,得到脏页速率
  • 脏页速率计算在calculate_dirtyrate_dirty_bitmap中实现,分析如下:
static void calculate_dirtyrate_dirty_bitmap(struct DirtyRateConfig config)
{
    int64_t msec = 0;
    int64_t start_time;
    /* 脏页计数本地变量,保存测量前后的脏页数量,用于保存测量时间内的脏页增加数量 */
    DirtyPageRecord dirty_pages;
	/* 获取BQL锁 */
    qemu_mutex_lock_iothread();
    /* 开启脏页记录,将global_dirty_tracking变量的GLOBAL_DIRTY_DIRTY_RATE位置位 */
    memory_global_dirty_log_start(GLOBAL_DIRTY_DIRTY_RATE);
	/* 
	 * 高版本的内核如果使能了KVM_DIRTY_LOG_INITIALLY_SET特性
	 * 第一次查询脏页位图时内核会返回全1,这样新增的脏页数就是
	 * 虚机所有可读写内存页,这个页数量无法用于统计
	 * 因此默认跳过第一次,脏页查询增长的脏页数量
	 * /
    /*
     * 1'round of log sync may return all 1 bits with
     * KVM_DIRTY_LOG_INITIALLY_SET enable
     * skip it unconditionally and start dirty tracking
     * from 2'round of log sync
     */
    memory_global_dirty_log_sync();
	/* 
	 * 高版本内核可能会使能KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE特性 
	 * 该特性在脏页位图查询后不会将kvm的页表的脏页位清零,而是等到用户态qemu
	 * 发送内存页之后再清零,这个时间窗口内虚机新增的脏页不会被kvm记录
	 * 这里,因为位图不用于迁移,所以需要在脏页查询后调用kvm提供的ioctl命令字
	 * 将kvm的内存页的脏页位清零,这样kvm才能记录脏页
	 * */
    /*
     * reset page protect manually and unconditionally.
     * this make sure kvm dirty log be cleared if
     * KVM_DIRTY_LOG_MANUAL_PROTECT_ENABLE cap is enabled.
     */
    dirtyrate_manual_reset_protect();
    qemu_mutex_unlock_iothread();
	/* 
	 * 至此开始测量脏页速率
	 * 首先保存全局的脏页计数total_dirty_pages到本地变量dirty_pages中
	 * 下一次脏页同步获取到的位图就是新增的内存脏页数
	 * */
    record_dirtypages_bitmap(&dirty_pages, true);
	/* 获取当前的测量时间 */
    start_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
    DirtyStat.start_time = start_time / 1000;
	
    msec = config.sample_period_seconds * 1000;
    /* 睡眠config中指定的测量时间 */
    msec = set_sample_page_period(msec, start_time);
    DirtyStat.calc_time = msec / 1000;
	/* 睡眠醒来后触发脏页同步,然后停止脏页记录 */
    /*
     * dirtyrate_global_dirty_log_stop do two things.
     * 1. fetch dirty bitmap from kvm
     * 2. stop dirty tracking
     */
    dirtyrate_global_dirty_log_stop();
    /* 
     * 脏页同步的过程中会累加新增的脏页数到全局脏页计数total_dirty_pages
     * 这里将全局的脏页技术total_dirty_pages保存到dirty_pages中
     */
    record_dirtypages_bitmap(&dirty_pages, false);
    /* 
     * 根据新增的脏页数量计算脏页速率,保存到全局变量DirtyStat中
     * */
    do_calculate_dirtyrate_bitmap(dirty_pages);
}
  • record_dirtypages_bitmap将全局的脏页计数变量保存到本地变量dirty_pages,根据start标志将其保存到对应域
static inline void record_dirtypages_bitmap(DirtyPageRecord *dirty_pages,
                                            bool start)
{
    if (start) {
        dirty_pages->start_pages = total_dirty_pages;
    } else {
        dirty_pages->end_pages = total_dirty_pages;
    }
}
  • do_calculate_dirtyrate_bitmap根据传入的DirtyPageRecord结构体变量计算脏页速率,将其保存到脏页统计全局变量DirtyStat
static void do_calculate_dirtyrate_bitmap(DirtyPageRecord dirty_pages)
{
    uint64_t memory_size_MB;
    int64_t time_s;
    uint64_t increased_dirty_pages =
        dirty_pages.end_pages - dirty_pages.start_pages;

    memory_size_MB = (increased_dirty_pages * TARGET_PAGE_SIZE) >> 20;
    time_s = DirtyStat.calc_time;

   DirtyStat.dirty_rate = memory_size_MB / time_s;
}

dirty-ring

计算原理

  • dirty-ring计算脏页速率是基于dirty-ring的脏页统计原理,它统计每个vcpu上新增的脏页数量,计算每个vcpu的脏页速率,累加所有vcpu的脏页速率,得到整个虚机的脏页速率

数据结构

  • QEMU CPUState数据结构中新增dirty_pages域用于统计每个vcpu上的脏页数量。
/* dirty-ring方式计算脏页速率时用于保存vcpu脏页个数 */
struct CPUState {
	/* 用户态通过dirty-ring得到脏页时,默认将脏页数量累加到dirty_pages域 */
    uint64_t dirty_pages;
    ......
}
  • dirty-ring方式计算脏页速率是基于vcpu粒度,因此其结果是一个vcpu速率的链表。每个vcpu都对应一个脏页速率。
struct DirtyRateVcpu {
    int64_t id;			/* vcpu idx */
    int64_t dirty_rate;	/* vcpu脏页速率 */
};
/* dirty-ring方式的统计信息 */
typedef struct VcpuStat {
    int nvcpu; /* number of vcpu */
    /* 保存每个vcpu的脏页速率 */
    DirtyRateVcpu *rates; /* array of dirty rate for each vcpu */
} VcpuStat;

实现流程

脏页数统计

  • 如果使能了dirty-ring特性,qemu在启动虚机时会创建kvm-reaper线程,kvm-reaper会周期性地检查每个vcpu上的diryt-ring,确认是否有新增的脏页,如果有,根据脏页地址找它在slot位图(KVMSlot->dirty_bmap)中的位,将其置位。这个脏页查询的过程可以用于脏页数统计。其流程如下:
kvm_dirty_ring_reaper_thread
	kvm_dirty_ring_reap
		kvm_dirty_ring_reap_locked
			/* 遍历虚机的每个vcpu,检查其上的dirty-ring是否有新增的脏页*/
			CPU_FOREACH(cpu) {
        		total += kvm_dirty_ring_reap_one(s, cpu);
    		}
  • kvm_dirty_ring_reap_one检查vcpu上的dirty-ring是否有新增脏页,如果有,将其所在slot上位图的对应位置位,新增的修改用于统计每个vcpu上的脏页数:
--- a/accel/kvm/kvm-all.c
+++ b/accel/kvm/kvm-all.c
@@ -469,6 +469,7 @@ int kvm_init_vcpu(CPUState *cpu, Error **errp)
     cpu->kvm_fd = ret;
     cpu->kvm_state = s;
     cpu->vcpu_dirty = true;
     /* vcpu初始化时将脏页计数初始化为0,之后在vcpu运行过程中一直递增 */
+    cpu->dirty_pages = 0;
@@ -743,6 +744,7 @@ static uint32_t kvm_dirty_ring_reap_one(KVMState *s, CPUState *cpu)
         count++;
     }
     cpu->kvm_fetch_index = fetch;
     /* 在统计vcpu上dirty-ring的新增脏页时,如果有新增脏页
      * 在标记完位图之后,将新增的脏页数累加到dirty_pages中
      * */
+    cpu->dirty_pages += count;
 
     return count;
 }
  • 分析kvm_dirty_ring_reap_one函数的流程:
static uint32_t kvm_dirty_ring_reap_one(KVMState *s, CPUState *cpu)
{
	/* 取出vcpu上的dirty-ring */
    struct kvm_dirty_gfn *dirty_gfns = cpu->kvm_dirty_gfns, *cur;
    /* 取出ring的大小 */
    uint32_t ring_size = s->kvm_dirty_ring_size;
    /* 取出本次dirty-ring检查脏页的位置 */
    uint32_t count = 0, fetch = cpu->kvm_fetch_index;

    while (true) {
    	/* 由于脏页位置是递增的,需要对ring的大小取模,获得其在环上的位置 */
        cur = &dirty_gfns[fetch % ring_size];
        /* 查看对应位置的页是否为脏,如果不脏表示没有新增的脏页,中断循环,如果有继续 */
        if (!dirty_gfn_is_dirtied(cur)) {
            break;
        }
        /* 将脏页在slot位图对应位置1 */
        kvm_dirty_ring_mark_page(s, cur->slot >> 16, cur->slot & 0xffff,
                                 cur->offset);
        dirty_gfn_set_collected(cur);
        trace_kvm_dirty_ring_page(cpu->cpu_index, fetch, cur->offset);
        fetch++;
        /* 统计新增的脏页数 */
        count++;
    }
    cpu->kvm_fetch_index = fetch;
    /* 将新增的脏页数累加到dirty_pages中 */
    cpu->dirty_pages += count;
    return count;
}

速率计算

  • dirty-ring速率计算步骤如下:
  1. 开始测量时,获取vcpu上脏页计数,保存到本地变量
  2. 根据用户设置的测量时间,睡眠一段时间
  3. 再次获取vcpu上脏页计数,保存到本地变量
  4. 获取两次vcpu脏页计数的增量,得到增加的脏页数,计算增加内存总量,得到vcpu的脏页速率,累加后得到虚机脏页速率
  • 速率计算在calculate_dirtyrate_dirty_ring函数中实现,流程如下:
static void calculate_dirtyrate_dirty_ring(struct DirtyRateConfig config)
{
    CPUState *cpu;
    int64_t msec = 0;
    int64_t start_time;
    uint64_t dirtyrate = 0;
    uint64_t dirtyrate_sum = 0;
    /* 声明用于保存脏页计数的本地变量数组,数组大小是vcpu的个数 */
    DirtyPageRecord *dirty_pages;
    int nvcpu = 0;
    int i = 0;
	/* 获取虚机vcpu个数*/
    CPU_FOREACH(cpu) {
        nvcpu++;
    }
	/* 为存放脏页计数的本地变量数组分给空间 */
    dirty_pages = malloc(sizeof(*dirty_pages) * nvcpu);
	/* 设置脏页统计信息,因为脏页速率粒度是vcpu,因此保存的速率有nvcpu个,为其分配空间 */
    DirtyStat.dirty_ring.nvcpu = nvcpu;
    DirtyStat.dirty_ring.rates = malloc(sizeof(DirtyRateVcpu) * nvcpu);
	/* 开启脏页日志记录,开始脏页速率测量 */
    dirtyrate_global_dirty_log_start();
	/* 遍历虚机vcpu上的统计计数,将其保存到本地变量数组dirty_pages中 */
    CPU_FOREACH(cpu) {
        record_dirtypages(dirty_pages, cpu, true);
    }
	/* 获取当前的测量时间 */
    start_time = qemu_clock_get_ms(QEMU_CLOCK_REALTIME);
    DirtyStat.start_time = start_time / 1000;
	/* 睡眠config中指定的测量时间 */
    msec = config.sample_period_seconds * 1000;
    msec = set_sample_page_period(msec, start_time);
    DirtyStat.calc_time = msec / 1000;
	/* 睡眠醒来后触发脏页同步,然后停止脏页记录 */
    dirtyrate_global_dirty_log_stop();
	/* 遍历虚机vcpu上的统计计数,将其保存到本地变量数组dirty_pages中 */
    CPU_FOREACH(cpu) {
        record_dirtypages(dirty_pages, cpu, false);
    }
	/* 
	 * 针对每个vcpu,逐一计算其脏页速率,将其存放到全局统计变量DirtyStat中
	 * 同时累加vcpu的脏页速率,得到整个虚机的脏页速率
	 * */
    for (i = 0; i < DirtyStat.dirty_ring.nvcpu; i++) {
        dirtyrate = do_calculate_dirtyrate_vcpu(dirty_pages[i]);
        trace_dirtyrate_do_calculate_vcpu(i, dirtyrate);

        DirtyStat.dirty_ring.rates[i].id = i;
        DirtyStat.dirty_ring.rates[i].dirty_rate = dirtyrate;
        dirtyrate_sum += dirtyrate;
    }
	/* 将虚机的脏页速率保存到全局统计变量DirtyStat中 */
    DirtyStat.dirty_rate = dirtyrate_sum;
    free(dirty_pages);
}
  • 最后打个广告,欢迎大家向以下QEMU子模块提交patch:
Migration dirty limit and dirty page rate
M: Hyman Huang 
S: Maintained
F: softmmu/dirtylimit.c
F: include/sysemu/dirtylimit.h
F: migration/dirtyrate.c
F: migration/dirtyrate.h
F: include/sysemu/dirtyrate.h

你可能感兴趣的:(虚拟化,QEMU)