文章目录
- 背景基础
-
- 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_rate
和qmp_query_dirty_rate
,其实现逻辑比较简单,如下图所示,用户通过qmp_calc_dirty_rate
发起脏页速率计算,qemu单独启动一个get_dirtyrate
线程用于计算脏页速率,完成后将结果存放到全局变量DirtyStat
中,然后线程退出。需要查询脏页速率时,通过qmp_query_dirty_rate
查询之前存放到DirtyStat
中的计算结果,返回给用户。
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
中。
速率计算
- 开始测量时,获取全局的脏页计数,保存到本地变量
- 根据用户设置的测量时间,睡眠一段时间
- 再次获取全局的脏页计数,保存到本地变量
- 获取两次脏页计数的增量,得到增加的脏页数,计算增加内存总量,得到脏页速率
- 脏页速率计算在
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;
}
速率计算
- 开始测量时,获取vcpu上脏页计数,保存到本地变量
- 根据用户设置的测量时间,睡眠一段时间
- 再次获取vcpu上脏页计数,保存到本地变量
- 获取两次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