StratoVirt 基於 Rust 的 balloon 功能實踐
StratoVirt 是計算產業中面向雲資料中心的企業級虛擬化 VMM,實現了一套架構統一支援虛擬機器、容器、Serverless 三種場景。StratoVirt 在輕量低噪、軟硬協同、Rust 語言級安全等方面具備關鍵技術競爭優勢。
背景介紹:
通常,在同一臺伺服器上存在著不同的使用者,而多數使用者對記憶體的使用情況是一種間斷性的使用。也就是說使用者對記憶體的使用率並不是很高。在伺服器這種多使用者的場景中,如果很多個使用者對於記憶體的使用率都不高的話,那麼會存在伺服器實際佔用的記憶體並不飽滿這樣一種情況。實際上各個使用者使用記憶體的分佈圖可能如下圖所示(黃色部分表示 used 部分,綠色部分表示 free 的部分)。
解決方案:
為了解決上述伺服器上記憶體使用率低的問題,可以將虛擬機器中暫時不用的記憶體回收回來給其他虛擬機器使用。而當被回收記憶體的虛擬機器需要記憶體時,由 host 再將記憶體歸還回去。有了這樣的記憶體伸縮能力,伺服器便可以有效提高記憶體的使用率。在 StratoVirt 中,我們使用 balloon 裝置來對虛擬機器中的空閒記憶體進行回收和釋放。下面詳細瞭解一下 StratoVirt 中的 balloon 裝置。
balloon 裝置簡介:
由於 StratoVirt 只是負責為虛擬機器分配記憶體,只能感知到每個虛擬機器總的記憶體大小。但是在每個虛擬機器中如何使用記憶體,記憶體剩餘多少。StratoVirt 是無法感知的,也就無法得知該從虛擬機器中回收多少記憶體了。為此,需要在虛擬機器中放置一個“氣球(balloon)”裝置。該裝置通過 virtio 半虛擬化框架來實現前後端通訊。當 Host 端需要回收虛擬機器內部的空閒記憶體時,balloon 裝置“充氣”膨脹,佔用虛擬機器內部記憶體。而將佔用的記憶體交給 Host 使用。如果虛擬機器的空閒記憶體被回收後,虛擬機器內部由於業務要求突然需要記憶體時。位於虛擬機器內部的 balloon 裝置可以選擇“放氣”縮小。釋放出更多的記憶體空間給虛擬機器使用。
balloon 實現:
balloon 的具體程式碼實現位於 StratoVirt 專案的/virtio/src/balloon.rs 檔案中,相關細節可閱讀程式碼理解。程式碼架構如下:
virtio
├── Cargo.toml
└── src
├── balloon.rs
├── block.rs
├── console.rs
├── lib.rs
├── net.rs
├── queue.rs
├── rng.rs
├── vhost
│ ├── kernel
│ │ ├── mod.rs
│ │ ├── net.rs
│ │ └── vsock.rs
│ └── mod.rs
├── virtio_mmio.rs
└── virtio_pci.rs
由於 balloon 是一個 virtio 裝置,所以在前後端通訊時也使用了 virtio 框架提供的 virtio queue。當前 StratoVirt 支援兩個佇列:inflate virtio queue(ivq)和 deflate virtio queue(dvq)。這兩個佇列分別負責 balloon 裝置的“充氣”和“放氣”。
氣球的充放氣時,前後端的資訊是通過一個結構體來傳遞。
struct VirtioBalloonConfig {
/// Number of pages host wants Guest to give up.
pub num_pages: u32,
/// Number of pages we've actually got in balloon.
pub actual: u32,
}
因此後端向前端要記憶體的時候,只需要修改這個結構體中的 num_pages 的數值,然後通知前端。前端讀取配置結構體中的 num_pages 成員。並與本身結構體中的 actual 對比,判斷是進行 inflate 還是 deflate。
-
inflate
如果是 inflate,那麼虛擬機器以 4k 頁為單位去申請虛擬機器記憶體,並將申請到的記憶體地址儲存在佇列中。然後通過 ivq 將儲存了分配好的頁面地址的陣列分批發往後端處理(virtio queue 佇列長度最大 256,也就是一次最多隻能傳輸 1M 記憶體資訊,對於大於 1M 的記憶體只能分批傳輸)。後端通過得到資訊後,找到相應的 MemoryRegion,將對應的 page 標記為”WILLNEED“。然後通知前端,完成配置。
-
deflate
如果是 deflate 則從儲存申請到的記憶體地址佇列中彈出一部分記憶體的地址。通過 dvq 分批次傳輸給後端處理。後端將 page 標記為“DONTNEED"。
下面結合程式碼進行說明:
定義 BalloonIoHandler 結構體作為處理 balloon 事件的主體。
struct BalloonIoHandler {
/// The features of driver.
driver_features: u64,
/// Address space.
mem_space: Arc<AddressSpace>,
/// Inflate queue.
inf_queue: Arc<Mutex<Queue>>,
/// Inflate EventFd.
inf_evt: EventFd,
/// Deflate queue.
def_queue: Arc<Mutex<Queue>>,
/// Deflate EventFd.
def_evt: EventFd,
/* 省略 */
}
其中包含上述的兩個 virtio 佇列inf_queue
和def_queue
,以及對應的觸發事件描述符(EventFd)inf_evt
和def_evt
。兩個佇列均使用了Mutex
鎖,保證了佇列在同一時刻只有一個使用者對該佇列進行操作。保證了多執行緒共享的資料安全。
fn process_balloon_queue(&mut self, req_type: bool) -> Result<()> {
let queue = if req_type {
&mut self.inf_queue
} else {
&mut self.def_queue
}; //獲得對應的佇列
let mut unlocked_queue = queue.lock().unwrap();
while let Ok(elem) = unlocked_queue
.vring
.pop_avail(&self.mem_space, self.driver_features)
{
match Request::parse(&elem) {
Ok(req) => {
if !self.mem_info.has_huge_page() {
// 進行記憶體標記
req.mark_balloon_page(req_type, &self.mem_space, &self.mem_info);
}
/* 省略 */
}
Err(e) => {
/* 省略錯誤處理 */
}
}
}
/* 省略 */
}
當相應的EventFd
被觸發後process_balloon_queue
函式將會被呼叫。通過判斷請求型別確定是“充氣”還是”放氣“,然後再從相應的佇列中取資料進行記憶體標記。其中while let
是 Rust 語言提供的一種迴圈模式匹配機制。藉助該語法可以將佇列中 pop 出來的所有資料遍歷取出到elem
中。
記憶體標記及優化:
標記記憶體在mark_balloon_page
函式中進行實現,起初的實現思路為:將虛擬機器傳送過來的地址逐個進行標記。即,從佇列中取出一個元素,轉化為地址後立即進行標記。後來經過測試發現:balloon 裝置在對頁地址進行一頁一頁標記記憶體時花費時間巨大。而同時也發現通過虛擬機器傳回來的地址中有大段的連續記憶體段。於是通過改變標記方法:由原來的一頁一頁標記改為將這些連續的記憶體統一標記。大大節省了標記時間。下面程式碼為具體實現:
fn mark_balloon_page(
&self,
req_type: bool,
address_space: &Arc<AddressSpace>,
mem: &BlnMemInfo,
) {
let advice = if req_type {
libc::MADV_DONTNEED
} else {
libc::MADV_WILLNEED
};
/* 略 */
for iov in self.iovec.iter() {
let mut offset = 0;
let mut hvaset = Vec::new();
while let Some(pfn) = iov_to_buf::<u32>(address_space, iov, offset) {
offset += std::mem::size_of::<u32>() as u64;
let gpa: GuestAddress = GuestAddress((pfn as u64) << VIRTIO_BALLOON_PFN_SHIFT);
let hva = match mem.get_host_address(gpa) {
Some(addr) => addr,
None => {
/* 略 */
}
};
//將hva地址儲存在hvaset的vec中
hvaset.push(hva);
}
//對hvaset進行從小到大排序。
hvaset.sort_by_key(|&b| Reverse(b));
/* 略 */
//將hvaset中連續的記憶體段進行標記
while let Some(hva) = hvaset.pop() {
if last_addr == 0 {
free_len += 1;
start_addr = hva;
} else if hva == last_addr + BALLOON_PAGE_SIZE {
free_len += 1;
} else {
memory_advise(
start_addr as *const libc::c_void as *mut _,
(free_len * BALLOON_PAGE_SIZE) as usize,
advice,
);
free_len = 1;
start_addr = hva;
}
if count_iov == iov.iov_len {
memory_advise(
start_addr as *const libc::c_void as *mut _,
(free_len * BALLOON_PAGE_SIZE) as usize,
advice,
);
}
count_iov += std::mem::size_of::<u32>() as u64;
last_addr = hva;
}
/* 略 */
}
}
}
首先將 virtio 佇列中的地址全部取出,並儲存在 vec 中,然後將該 vec 進行從小到大的排序。有利於快速找出連續的記憶體段並進行標記。由於 hvaset 中的地址是按照從小到大排列的,因此可以從頭開始遍歷 hvaset,遇到不連續的地址後將前面的連續段進行標記。這樣就完成了由原來逐頁標記到連續記憶體段統一標記的優化。
經過測試,StratoVirt 的 balloon 速度也有了極大的提高。
關注我們
StratoVirt 當前已經在 openEuler 社群開源。後續我們將開展一系列主題分享,如果您對 StratoVirt 的使用和實現感興趣,歡迎您圍觀和加入。
專案地址:https://gitee.com/openeuler/stratovirt
專案 wiki:https://gitee.com/openeuler/stratovirt/wikis
您也可以訂閱郵件列表:https://mailweb.openeuler.org/postorius/lists/virt.openeuler.org/
如有疑問,也歡迎提交 issue:https://gitee.com/openeuler/stratovirt/issues
入群
如果您對虛擬化技術感興趣,可以進入 openEuler StratoVirt 主頁查詢相關資源(點選閱讀原文進入專案主頁,https://www.openeuler.org/zh/other/projects/stratovirt/),包括安裝指導、虛擬機器配置、程式碼倉庫、學習資料等。也歡迎加入Virt SIG 技術交流群,討論 StratoVirt、KVM、QEMU 和 Libvirt 等相關虛擬化技術。感興趣的同學可以新增如下微信小助手,回覆 StratoVirt 入群。

本文分享自微信公眾號 - openEuler(openEulercommunity)。
如有侵權,請聯絡 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。
- 玩轉機密計算從 secGear 開始
- openEuler資源利用率提升之道06:虛擬機器混部OpenStack排程
- openGauss Cluster Manager RTO Test
- JVM 鎖 bug 導致 G1 GC 掛起問題分析和解決【畢昇JDK技術剖析 · 第 2 期】
- 手把手帶你玩轉 openEuler | openEuler 的使用
- 681名學生中選!暑期2021開啟火熱“開源之夏”!
- 手把手帶你玩轉 openEuler | 初識 openEuler
- StratoVirt 中的 PCI 裝置熱插拔實現
- 使用 NMT 和 pmap 解決 JVM 資源洩漏問題
- JNI 中錯誤的訊號處理導致 JVM 崩潰問題分析
- Java Flight Recorder - 事件機制詳解
- 畢昇 JDK 8u292、11.0.11 釋出!
- StratoVirt 中的虛擬網絡卡是如何實現的?
- openEuler結合ebpf提升ServiceMesh服務體驗的探索
- 我的openEuler社群參與之旅
- StratoVirt 的中斷處理是如何實現的?
- 看看畢昇 JDK 團隊是如何解決 JVM 中 CMS 的 Crash
- 使用 perf 解決 JDK8 小版本升級後效能下降的問題【畢昇JDK技術剖析 · 第 1 期】
- 2021年畢昇 JDK 的第一個重要更新來了
- 漏洞盒子 × openEuler | 廣邀白帽共築安全的Linux開放應用生態