StratoVirt 基於 Rust 的 balloon 功能實踐

語言: CN / TW / HK

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_queuedef_queue,以及對應的觸發事件描述符(EventFd)inf_evtdef_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源創計劃”,歡迎正在閱讀的你也加入,一起分享。