文章目錄 背景基礎 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_syncmemory_region_sync_dirty_bitmaplog_sync -> kvm_log_synckvm_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_pagescpu_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_threadkvm_dirty_ring_reapkvm_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脏页速率计算原理 的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網站內容還不錯,歡迎將生活随笔 推薦給好友。