🔗 https://github.com/QuarkContainer/Quark/blob/main/doc/Hibernate.pdf
英文標題:Hibernate Container: A Deflated Container Mode for Fast Startup and High-density Deployment in Serverless Computing
0 摘要
無伺服器計算是一種流行的雲計算範式,要求低響應延遲以處理按需使用者請求。有兩種顯著的技術用於減少響應延遲:保持完全初始化的容器處於活動狀態(熱容器1)或減少新容器的啟動(冷啟動)延遲。本文提出了第三種容器啟動模式:休眠容器2,啟動速度比冷啟動容器模式更快,記憶體消耗比熱容器模式更少。休眠容器本質上是一個「瘦身」的熱容器。其應用記憶體被交換到磁碟,釋放的記憶體被回收,基於檔案的mmap記憶體被清理。休眠容器的瘦身記憶體在響應使用者請求時被膨脹。由於休眠容器的應用程式已完全初始化,其響應延遲低於冷啟動模式;而且由於應用記憶體被瘦身,其記憶體消耗
低於熱容器模式。此外,當休眠容器被「喚醒」以處理請求時,喚醒的容器具有與熱容器相似的響應延
遲,但記憶體消耗更少,因為並非所有的瘦身記憶體都需要被膨脹。我們將休眠技術作為開源的Quark安全容器運行時專案的一部分進行實現,我們的測試演示表明,休眠容器的記憶體消耗約為熱容器的7%到25%。所有這些都導致了更高的部署密度、更低的延遲以及整體系統效能的顯著改善。
1熱容器是指作為熱啟動過程的一部分創建的完全初始化的容器
2休眠容器是指休眠容器模式
1 引言
無伺服器計算,包括函式即服務(FaaS)和無伺服器容器,正變得越來越成為一種流行的雲計算範式。無伺服器計算環境通常在共享環境中運行多租戶工作負載,以處理按需使用者請求。它得到了主要雲服務提供商的支持,如 AWS Lambda [1]/Fargate [2]、Google Function [3]/Cloud Run [4] 和 Azure Function[5]/Container Instance [6]。
為了在共享環境中託管多租戶使用者應用程式工作負載,主要雲服務提供商通常使用基於虛擬機器的安全容器運行時,而不是像 runC/LXC 這樣的基於行程的容器運行時。基於虛擬機器的安全容器運行時提供的安全隔離就像傳統的虛擬機器一樣。例如,AWS使用 Firecracker[7],GCP 使用 gVisor[8],而阿里雲和華為雲使用 Kata 容器[9] 進行無伺服器計算。但這些基於虛擬機器的安全容器運行時消耗更多記憶體,並且響應延遲高於基於行程的容器運行時。
使用者請求的響應延遲對於無伺服器計算環境至關重要。使用者請求的響應延遲包括三個部分:容器運行時啟動、使用者應用程式初始化和使用者請求處理。容器運行時的啟動時間通常需要大約100毫秒,而應用程式初始化時間範圍從10毫秒到10秒不等,而使用者請求處理時間通常較短,僅需幾毫秒到10秒。與請求處理時間相比,容器運行時啟動和應用程式初始化延遲的貢獻相當顯著。減少容器運行時啟動和應用程式初始化延遲是無伺服器計算設計中的關鍵挑戰之一。通常有兩種最佳化方法用於減少延遲:
- 熱啟動最佳化:一種常見的技術是保持執行運行時處於活動狀態,即熱容器,持續一段短時間,以便於這樣未來調用相同請求時,可以重新使用熱容器。熱啟動確實減少了冷啟動的開銷,但保持容器處於活動狀態會消耗大量計算資源,尤其是記憶體資源。這反過來又導致更高的系統資源需求。目前有多種正在進行的努力,以提高熱啟動效率,例如減少容器運行時開銷[10][8]和最佳化熱容器保持活動排程策略[11]。
- 冷啟動延遲最佳化:當然,由於系統資源限制,我們無法保持所有無伺服器容器處於活動狀態。另一個研究領域是減少冷啟動延遲。這一努力包括減少容器運行時啟動時間[10][8]和應用初始化延遲[12][13][14]。
本文提出了一種基於傳統記憶體交換技術的第三種啟動機制,該技術非常適合緩解無伺服器計算的啓
動時間。以下是與此相關的關鍵考慮因素:
- 快速交換存儲:隨着高效能二級存儲(如SSD、NVM)在公共雲中的商業可用性[15]的出現,記憶體交換效能獲得了巨大的提升。
- 輕量級無伺服器工作負載:無伺服器計算環境的快速啟動要求需要輕量級和低記憶體佔用的工作負載。例如,在AWS [16]中,47%的函式設定為使用預設的最小記憶體設置128 MB。總體而言,只有14%的AWS Lambda函式的記憶體分配超過512 MB。而在Azure[17]中,90%的應用程式從未消耗超過400MB,50%的無伺服器應用程式工作負載最多分配170MB。由於無伺服器計算工作負載的記憶體佔用較小,因此產生的交換記憶體成本相對較低。
總之,在無伺服器計算環境中採用記憶體交換技術以實現低延遲啟動,並結合保持活動容器的低記憶體
消耗,存在很大的機會。以記憶體交換技術為關鍵推動力,我們的論文提出並實現了休眠容器,這是一
種瘦身的保持活動的熱容器。休眠容器利用以下關鍵最佳化來實現低延遲啟動和低記憶體消耗:
- 記憶體: 休眠容器的記憶體消耗遠低於熱容器,因為它將應用程式記憶體交換到磁碟,回收並將容器應用程式的空閒記憶體返回給主機作業系統核心,最後清理檔案支援的mmap記憶體並返回給主機作業系統。
- CPU: 它完全不消耗系統CPU周期因為它將使用者應用程式置於完全暫停狀態。
休眠容器的使用者請求響應延遲遠低於冷啟動。這主要是因為休眠容器的使用者應用程式已完全初始化,並且容器運行時正在使用以下保持活動的資源:
- 主機作業系統物件: 休眠容器保持其主機作業系統物件處於活動狀態,例如容器運行時作業系統行程、控制組、容器網路、容器檔案系統、行程。作業系統物件消耗很少的系統記憶體,但保持它們的存活可以節省大量的重新初始化成本。
- 阻塞的容器運行時執行緒:容器運行時的主機執行緒被阻塞以等待使用者請求。它不消耗CPU周期,但系統會立即響應,類似於熱容器。
重要的是要強調,喚醒容器對後續請求的響應延遲幾乎與熱容器相似,但它消耗的記憶體少於熱容器。這主要是因為喚醒容器在處理使用者請求時並不需要所有的膨脹記憶體。
總體而言,這一切導致了更高的部署密度和更好的系統整體效能。
以下是我們主要貢獻的總結:
- 我們提出並實現了休眠容器模式,作為開源Quark容器運行時的一部分 [18]。休眠容器模式消耗的記憶體少於熱容器,並且啟動速度比冷啟動快。此外,從休眠容器派生的喚醒容器消耗的記憶體比熱容器少,並且請求響應延遲幾乎相似。
- 我們發現主要的記憶體換入延遲是由於SSD磁碟的隨機讀取造成的。受到REAP[16](一種記錄和預取機制)的啓發,我們將批量記憶體預取換入機製作為休眠容器膨脹過程的一部分進行實現。我們比較了基於頁面錯誤的換入和REAP換入在不同基準測試中的表現。
- 我們實現了一種新的回收導向記憶體管理演算法,以高效地將空閒記憶體頁面返回給主機作業系統核心。這避免了複雜的氣球化技術的需求。
Ballooning技術形象地在客體機佔用的記憶體中引入氣球(Balloon)的概念,氣球中的記憶體是可以供主機機使用的(但不能被客體機存取或使用),所以,當主機機記憶體使用緊張,空餘記憶體不多時,可以請求客體機回收利用已分配給客體機的部分記憶體,客體機就會釋放其空閒的記憶體,此時若客體機空閒記憶體不足,可能還會回收部分使用中的記憶體,可能會換出部分記憶體到客體機的交換分區(swap)中,從而使得記憶體氣球充氣膨脹,從而讓主機機回收氣球中的記憶體可用於其他行程(或其他客體機)。反之,當客體機中記憶體不足時,也可以讓客體機的記憶體氣球壓縮,釋放出記憶體氣球中的部分記憶體,讓客體機使用更多的記憶體。
2 背景與動機
休眠容器是一個在Quark容器運行時中實現的瘦身熱容器。它的瘦身過程包括回收使用者應用程式的空閒記憶體和將使用者記憶體交換到二級存儲。本節討論Quark安全容器運行時設計的背景、現有的客體作業系統空閒記憶體回收和交換機制。最後,本節討論了我們論文的動機,並列舉了最佳化當代最先進的無伺服器計算環境的各種機會。
2.1 安全容器與Quark運行時
如前所述,我們將休眠容器模式作為開源Quark容器運行時的一部分進行了實現[18]。在這一小節中,我們簡要描述了各種最先進的安全容器運行時技術的現狀。我們特別深入探討了Quark運行時架構的細節。
無伺服器計算在共享環境中託管多租戶工作負載。傳統的容器運行時,如RunC/LXC,不適合無伺服器計算環境。這主要是因為這些容器運行時無法提供多租戶級別的安全隔離。相反,主要雲服務提供商在各自的無伺服器計算環境中使用虛擬機器(VM)級別的安全容器運行時。此類虛擬機器級容器運
行時的例子包括Kata[9]/Firecracker[19]和gVisor[8]。

Kata和Firecracker使用基於Linux核心的虛擬機器進行安全隔離。由於它們都使用現有的通用Linux核心,因此它們在無伺服器計算環境中的啟動延遲和資源開銷相當高。
Quark[18]和gVisor[8]是另外兩個專門為無伺服器計算設計的顯著安全容器運行時。它們由使用者空間
作業系統核心和輕量級虛擬機器器監視器(VMM)組成。它們旨在提供與Linux相容的系統調用介面,並支持CRI/OCI介面,以便現有的Linux容器鏡像可以無須任何更改地運行。如前所述,這兩個安全容器運行時針對無伺服器工作負載進行了高度最佳化。因此,它們的啟動延遲和資源開銷低於Kata/Firecracker運行時。然而,與Kata/Firecracker安全容器運行時明確基於Linux核心不同,Quark和gVisor安全容器運行時的Linux相容性並不好。
圖2展示了Quark的架構。Quark的架構與傳統的Linux虛擬機器相似。它們都運行在Linux主機核心之上,使用KVM虛擬機器器監視器。Quark運行時行程在一個標準的Linux容器內運行,使用Cgroup和網路/檔案系統命名空間進行隔離。Quark包括一個新的使用者空間作業系統核心(QKernel)和虛擬機器器監視器(QVisor),這些都是為無伺服器計算進行了大量最佳化的。Quark提供對虛擬化系統調用介面的支持,該介面模擬Linux系統調用。Quark實現了大多數Linux核心功能,如記憶體管理、行程管理和輸入輸出管理等。
Quark專門為無伺服器計算環境設計,併集成了無伺服器計算特定功能,如支持休眠容器模式等。
2.2 客體作業系統釋放的記憶體回收
如前所述,休眠容器模式的一個關鍵價值主張是將使用者應用程式釋放的記憶體返回給主機作業系統。釋
放的記憶體回收過程對於像Linux這樣的通用客體作業系統來說並不是一項簡單的任務。通常,當Linux客體作業系統的應用程式釋放記憶體給Linux客體作業系統核心時,理想情況下,Linux客體作業系統核心應該將應用程式釋放的記憶體返回給主機Linux核心,以便釋放的記憶體可以重新分配給其他主機作業系統行程。不幸的是,Linux 客體作業系統將釋放的記憶體保留在其記憶體池中,而不將其返回給主機 Linux 作業系統。這是因為 Linux 針對裸金屬機器環境進行了最佳化,在這種環境中沒有記憶體回收的要求。簡而言之,這意味着客體作業系統釋放的記憶體不會在傳統虛擬化設置中被主機作業系統釋放和回收。
目前,有兩種不同的方法來解決 Linux 客體作業系統釋放記憶體回收的問題:
- 氣球化[20]:氣球化方法依賴於駐留在客體作業系統中的特殊氣球化驅動程式,並與虛擬機器器監視器合作,動態調整虛擬機器的記憶體大小。從本質上講,氣球化是一種計算機記憶體回收技術,虛擬機器器監視器使用它來允許實體主機系統從某些客體虛擬機器中檢索未使用的記憶體並與其他虛擬機器共享。
- 記憶體外掛[21]:記憶體外掛機制依賴於作業系統核心對熱添加/熱移除實體記憶體的支持。記憶體熱移除使記憶體區域對使用者不可用。它需要進行頁面遷移,將已使用的頁面移動到另一個區域。然而,頁面遷移會導致效能損失。記憶體外掛已在虛擬機器記憶體收集 [22] 中使用。
基於虛擬機器的容器運行時,如Kata/Firecracker,也基於Linux客體作業系統,因此它們也面臨釋放記憶體回收問題。根據我們的了解,它們都沒有採用氣球化或記憶體外掛的方法。這主要是因為氣球化和記憶體外掛方法在無伺服器計算環境中過於複雜,難以採用。
作為我們休眠容器工作的一個部分,我們在Quark安全容器運行時中實現了一個專用記憶體管理系統,以提高無伺服器計算環境中的回收效率。
2.3 客體應用記憶體交換
如前所述,休眠容器模式的一個關鍵價值主張是將使用者應用程式的記憶體換出。
換出是一種記憶體管理方案,它暫時將非活動記憶體頁換出到二級存儲,並在頁面表中將該頁面的條目標記為不存在。當系統需要存取被換出的頁面時,作業系統的虛擬記憶體管理系統會生成一個頁面錯誤,以便換入該頁面。
在一個常見的虛擬化環境中,當前主機作業系統的交換機制效率不高,因為目前的交換方式是以不
合作的方式進行的。VSWAPPER[23] 的研究調查了虛擬化環境中不合作交換的低效問題。問題包括靜默交換寫入、過時交換讀取和錯誤交換讀取等。VSWAPPER 實現了一個與客體機無關的記憶體交換器,以解決所有這些問題。VSWAPPER 在解決常見虛擬化環境中的不合作交換問題方面表現相當不錯,但並不特別適用於無伺服器計算環境。
對於無伺服器計算,我們有許多機會來實現更好的交換效能,因為我們希望交換出一個空閒容器的整個記憶體:
- 有應用程式記憶體頁面的批量換出:對於常見的交換場景,作業系統核心選擇交換出不活躍的頁面。在無伺服器計算環境中,我們交換出一個空閒容器的所有使用者應用程式記憶體集。這反過來導致了與記憶體管理過程相關的成本節省。
- 暫停應用程式的無競爭條件換出:在無伺服器計算環境中,我們可以在交換出記憶體頁面時暫停空閒的使用者應用程式行程。這避免了常見交換過程中的複雜競爭條件處理。
- 頁面換入的批量順序磁碟讀取:常見的作業系統記憶體頁面換入是由頁面錯誤觸發的,二級交換存儲以隨機讀取的方式存取。對於常見的二級存儲,無論是 HDD 還是 SSD,批量順序讀取的效能總是優於隨機讀取。REAP[14] 揭示了函式在同一函式的不同調用中存取相同的穩定工作集頁面。在識別出頁面集後,可以使用批量順序讀取預取這些頁面。與基於頁面錯誤的換入方法相比,批量換入方法不僅可以節省與磁碟隨機頁面載入成本相關的開銷,還可以節省與頁面錯誤處理和客機之間切換相關的成本。
我們的動機是解決上述所有提到的最佳化機會,並為無伺服器計算環境開發一個更高效的交換機制。
3 設計與實現
以下小節深入探討了啓用休眠容器功能作為 Quark安全容器運行時的一部分的架構和設計細節。
3.1 休眠容器的狀態機
本節介紹休眠容器如何響應傳入的使用者請求。
圖3顯示了為服務傳入使用者請求而進行的各種容器狀態轉換。

在傳入使用者請求時,無伺服器平臺執行1️⃣冷啟動。這反過來會生成一個新的熱容器,使用者請求被轉
發到新生成的熱容器。當熱容器接收到使用者請求時,它2️⃣轉變為運行狀態以處理請求,當請求處理完成後,它3️⃣返回到熱狀態。
為了減少使用者請求的響應延遲,無伺服器平臺可能會將熱容器保持活躍一段短時間。如果在短時間內有其他傳入的使用者請求(相繼發生),這些請求可能會由熱容器處理,以實現低響應延遲。然而,當熱容器處於空閒狀態時,它仍然會消耗特定於應用程式的分配記憶體佔用。在這種情況下,無伺服器平臺可能會在記憶體壓力下驅逐熱容器,以釋放記憶體供其他無伺服器功能容器使用。在熱容器被驅逐後,下一個使用者請求會經歷更高的冷啟動延遲。簡而言之,系統中熱容器的數量越多,使用者請求的響應延遲就越好。
除了上述傳統無伺服器計算環境中的容器狀態外,我們提出了以下三種新的容器狀態:
休眠:休眠容器是一個膨脹的熱容器,其記憶體佔用低於熱容器。無伺服器平臺可以選擇將熱容器「膨脹」到休眠容器,而不是完全驅逐熱容器,以釋放記憶體。
無伺服器平臺可以通過向熱容器發送 SIGSTOP 信號,將熱容器4️⃣的狀態轉變為休眠容器,從而啟動熱容器的通貨緊縮。
休眠運行:休眠容器在接收到使用者請求以處理使用者請求時,可能會像運行容器一樣7️⃣過渡到休眠運行狀態。
喚醒:休眠運行容器8️⃣在使用者請求處理完成後返回到喚醒狀態。喚醒容器6️⃣在下一個使用者請求時再次進入休眠運行狀態。喚醒容器9️⃣在接收到SIGSTOP信號時也可能返回到休眠狀態。喚醒容器的使用者請求響應延遲幾乎與熱容器相似,但消耗更少的記憶體。當無伺服器平臺預測到有即將到來的使用者請求時,它也可能通過向休眠容器發送SIGCONT信號5️⃣,將其「喚醒」為喚醒容器,以減少響應延遲。
3.2 脫氣過程概述(Deflation Process Overview)
休眠容器本質上是一個被壓縮的熱容器。休眠容器是通過以下四個步驟從熱容器派生而來的:
- 暫停熱容器使用者應用程式行程,並阻塞容器運行時主機作業系統執行緒,以等待「喚醒」觸發。
- 回收並將釋放的應用程式記憶體頁返回給主機Linux核心。
- 將應用程式提交的記憶體頁換出到本地磁碟。
- 通過使用MADV DONTNEED作為「建議」參數的madvise()清理應用程式的檔案支援的mmap記憶體,並將其返回給Linux核心。
在步驟#1中,休眠容器不消耗任何系統CPU周期。在步驟#2、#3、#4中,休眠容器的使用者應用程式分配的記憶體被返回給主機Linux核心,因此它消耗的記憶體遠低於熱容器。我們在子節3.3、3.4、3.5中分別描述步驟#2、#3、#4的詳細資訊。
休眠容器可以轉變為熱容器,轉變過程包括記憶體膨脹和使用者應用程式行程的恢復。休眠容器可以通過兩種觸發器被喚醒:
- 使用者請求:當無伺服器平臺收到使用者請求時,無伺服器平臺可以直接將請求轉發給休眠容器,而不首先喚醒它,以減少整體系統延遲。休眠容器通過將容器運行時執行緒置於阻塞狀態來實現這一點,以等待使用者請求,例如 Posix 套接字 sys accept 或 sys read。當有 Posix 套接字客戶端連接請求或套接字資料可用時,容器運行時執行緒會被 Linux 主機核心解鎖,並開始剩餘的喚醒處理,即記憶體換入步驟,使用者應用程式行程恢復。
- 無伺服器系統控制平面:無伺服器平臺可以在預期使用者請求即將到來時顯式喚醒容器。由於在使用者請求到來之前部分完成了記憶體膨脹,因此使用者請求響應延遲相較於使用者請求觸發步驟更低。
3.3 記憶體回收方向記憶體管理
休眠容器回收客戶使用者應用程式釋放的記憶體頁,並將其返回給主機Linux核心,就像虛擬機器氣球化技術一樣。
QKernel在KVM虛擬機器中運行,其客體實體記憶體是主機Linux作業系統的虛擬記憶體。QKernel的客體實體記憶體頁(即主機虛擬記憶體頁)在被存取之前不會被主機Linux作業系統核心提交。Quark可以通過系統調用sys madvise()將已提交的記憶體頁返回給主機Linux核心,使用MADV DONTNEED作為「建議」參數[24]。在成功的madvise()操作之後,後續對該範圍內的頁的存取將成功,但這會導致匿名私有映射的零填充按需頁。理想情況下,在從客體作業系統核心記憶體管理系統中識別出空閒記憶體區域後,可以通過調用madvise()進行回收。
不幸的是,原始的 Quark 運行時並不容易回收已釋放的記憶體。Quark 容器運行時目前使用二進制夥
伴分配器 [25]。它不適合記憶體回收,因為它將空閒記憶體塊維護在一個空閒列表中,該列表是一個線性鍊表,其「下一個」指標保存在空閒記憶體塊中。對於裸金屬最佳化的作業系統核心,空閒列表工作良好。但對於客體作業系統核心,如果我們使用 madvise()來回收空閒記憶體頁塊,由於對記憶體塊的後續存取是零填充的,因此「下一個」指標被清除,從而導致空閒列表資料結構損壞。總之,現有的夥伴分配器不適合記憶體頁的回收。相反,我們實現了位圖頁面分配器(bitmap page allocator)來解決這個特定問題。我們將在後續段落中介紹位圖頁面分配器的設計。
原始Quark容器運行時的記憶體分配分為以下兩個領域:
- 客戶使用者應用程式記憶體地址空間分配器:客戶使用者應用程式通過系統調用(如sys brk,sys mmap)從客體作業系統核心分配記憶體。這些系統調用僅分配記憶體地址空間,記憶體頁在通過記憶體頁面錯誤處理程式存取之前不會被提交。
- Quark運行時全局堆分配器:Quark運行時是使用Rust編程語言開發的。Rust運行時支持自定義堆分配器。Quark使用基於二進制夥伴分配器的自定義堆分配器。QKernel的核心資料結構,如核心堆疊,是從堆中分配的。原始Quark還在頁面錯誤處理程式中為使用者應用程式從全局堆中分配記憶體頁,這對記憶體回收不友好。因此,我們決定為休眠容器開發第三種記憶體頁分配器:位圖頁面分配器(bitmap page allocator)。

位圖頁面分配器旨在為客戶使用者應用程式的記憶體頁面分配管理。它僅用於頁面錯誤處理程式中使用者應用程式的固定大小 4KB 記憶體頁面分配。
如圖 4 所示,位圖頁面分配器使用 4MB 記憶體塊進行 4KB 記憶體頁面分配。4MB 記憶體塊的起始地址也是 4MB 對齊,4MB 記憶體塊的第一個 4KB 記憶體頁面被保留為控制頁面。控制頁面包含 3 個字段:
- 「下一個」指標:在位圖頁面分配器中,所有具有空閒頁面的 4MB 記憶體塊在一個空閒列表中鏈接,該空閒列表是一個線性鍊表。線性鍊表的「下一個」指標保存在控制頁面中。
- 空閒頁面位圖:一個 4MB 塊包含總共 1024 個4KB 記憶體頁面。在此基礎上,第一頁用作控制頁面。剩餘的1023頁可以分配。位圖頁面分配器保持一個L2(第2級)位圖,包含一個16 x 64位整數陣列(總共1024位),以指示頁面是否已分配(即一個頁面對應一個位)。為了加速空閒頁面查找,位圖頁面分配器保持另一個64位整數作為L1(第#1級)位圖,以指示L2位圖中的64位整數是否為零。因此,對於每個頁面分配,位圖頁面分配器需要存取兩個64位整數:一個是L1位圖,另一個來自L2位圖。例如,要查找一個空閒頁面,位圖頁面分配器首先查找L1位圖64位整數的第一個非零位。
假設第一個非零位是第4位,這意味着第4個L2位圖64位整數指示有空閒頁面。位圖頁面分配器其次查找第4個L2位圖64位整數的第一個非零位。通過這種方式,位圖頁面分配器的空閒頁面查找複雜度為 O(2)。
- 記憶體頁引用計數陣列:作業系統核心的記憶體頁可能會被多個頁面表引用,當進行行程克隆時。位圖頁面分配器還在控制頁面中保持頁面引用計數,使用一個16位原子整數陣列[26]以最大化記憶體使用效率並提高效能。
記憶體頁分配和引用計數的增加和減少如下。
- 記憶體頁分配:在頁面分配請求時,位圖頁面分配器從空閒列表的第一個4MB記憶體塊中分配一個頁面,並更新控制頁面的位圖。如果第一個4MB記憶體塊中沒有更多的空閒頁面,它將從空閒列表中移除。如果空閒列表中沒有更多的 4MB記憶體塊,位圖頁面分配器將從全局堆中分配另一個 4MB 記憶體塊,即全局二進制夥伴分配器。記憶體分配需要獲取全局鎖以避免競爭條件。
- 頁面引用計數增加和減少:每當有客體行程克隆/終止或頁面寫時複製 (COW),客體頁面引用計數就會增加和減少。位圖頁面分配器還在控制頁面中存儲頁面引用計數以提高效能。由於 4MB 記憶體塊是 4MB 對齊的,任何客體頁面可以通過清除其地址的最低 22位來找到其控制頁面(即 4MB 塊的第一頁)。因此,從任何使用者記憶體頁面地址查找其控制頁面不需要查找表。在找到頁面的控制頁面後,引用計數的增加和減少通過 Rust 的原子操作完成:原子取加和原子取減 [26],這是無鎖操作。當引用計數減少到零時,頁面被釋放並通過更新空閒頁面位圖返回給頁面分配器。如果4MB記憶體塊的空閒頁面計數在有新的空閒頁面時為零,則該記憶體塊被放回空閒列表。當4MB塊的空閒頁面計數達到最大空閒頁面計數,即1023時,該4MB記憶體塊可以返回給全局堆。
當Quark運行時開始處理休眠容器時,需要將釋放的頁面返回給主機Linux核心。由於位圖頁面分配器中的釋放資料頁面由控制頁面中的位圖指示,因此記憶體頁中沒有存儲諸如二進制夥伴分配器的空閒列表中的「下一個」指標等資料。在進行休眠時,Quark運行時通過調用主機系統調用 madvise() 將空閒頁面返回給主機作業系統。通過這種方式,Quark的休眠過程比虛擬機器氣球化技術簡單得多。
此外,每當休眠容器被喚醒並且回收的記憶體頁被重新分配給客體應用程式時,記憶體頁由主機Linux核心通過主機作業系統頁面錯誤進行提交。
該過程由主機Linux處理,對客體作業系統Quark是透明的。因此,Quark不需要顯式地重新分配頁面。這反過來減少了喚醒延遲和整體系統複雜性。
3.4 休眠容器記憶體交換

休眠容器將客體應用程式記憶體換出到二級存儲,換出的記憶體頁可能在休眠容器被喚醒時被換入。
有兩種類型的換入機制:
- 基於頁面錯誤的換入:就像常見的作業系統換入一樣,當客體應用程式存取一個換出的頁面時,會觸發頁面錯誤,頁面錯誤處理程式載入記憶體頁。
- 批量 REAP 換入:一個休眠容器可以預取所有由 REAP 過程記錄的頁面。
如圖5所示,對於每個容器沙箱,有一個用於基於頁面錯誤的換入的交換檔,以及另一個用於批量
REAP換入的REAP檔。交換檔專用於一個沙箱,不會在沙箱之間共享,以減輕潛在的安全漏洞,這些檔案在沙箱終止時被刪除。
基於頁面錯誤的換入與基於 REAP 的換出過程不同,具體細節如下。
3.4.1 基於頁面錯誤的換入和換出
本節描述基於頁面錯誤的換入及其對應的換出。
無伺服器平臺可以通過向熱容器發送信號 SIGSTOP 來觸發空閒熱容器的休眠過程。之後,圖 5 中的Quark 容器運行時的交換管理器執行以下步驟以換出記憶體。
- 暫停客體應用程式:交換管理器暫停所有客體應用程式作為一個通用SIGSTOP 處理程式。之後,客戶使用者應用程式執行緒被阻塞,不會存取任何記憶體頁,因此交換管理器不需要處理任何複雜的競爭條件,從而簡化了整體過程;
- 遍歷並修改客體應用程式頁面表:交換管理器遍歷所有客體應用程式的頁面表以識別匿名頁面並執行以下工作:
- 將每個匿名頁面的頁面表項標記為未存在,以便後續存取該頁面時觸發頁面錯誤;
- 設置頁面表項的旗標位#9,這是一個自定義位,以指示頁面錯誤是由於頁面換出;
- 將頁面的客體實體地址放入雜湊表中,以便在多個頁面表中存在對同一客體實體頁面地址的多個引用時進行頁面去重。
- 將記憶體頁寫入交換檔:休眠容器為每個Quark沙箱都有一個交換檔。交換管理器枚舉雜湊表並將記憶體頁寫入交換檔,然後將記憶體頁的檔案偏移量保存回雜湊表。
- 將記憶體頁返回給主機Linux作業系統:交換管理器通過調用系統調用madvise()將換出的頁面返回給主機Linux作業系統。
當休眠容器被喚醒時,它會恢復暫停的使用者應用程式的執行。當使用者應用程式執行存取一個換出的記憶體頁時,它會觸發一個頁面錯誤以換入該頁。
頁面錯誤處理過程如下。
- 確認頁面錯誤是否來自換出頁面:Quark頁面處理程式檢查頁面表項的自定義旗標位#9。如果該位被設置,則它是一個換出頁面,換入過程將作為步驟#2開始。
- 從交換檔載入記憶體:頁面錯誤處理程式的虛擬CPU(vCPU)從客體模式退出到主機模式,並從交換檔讀取記憶體頁。
- 更新頁表項:頁表項的旗標位#9被清除,並且該項也被標記為存在,以便不再因該頁觸發頁面錯誤。
基於頁面錯誤的換入成本很高。其成本包括以下幾個部分。
- 頁面錯誤處理:在頁面錯誤處理過程中,客戶vCPU從客戶使用者空間轉移到客體核心空間,所有通用寄存器都存儲在主記憶體中。
- 在客體模式和主機模式之間切換:客體模式和主機模式的切換成本很高。它不僅需要在主記憶體中存儲通用寄存器,還需要存儲浮點上下文。我們在測試環境中觀察到這種客戶/主機切換的延遲約為15微秒。
- 從SSD隨機頁面記憶體讀取:休眠容器在SSD磁碟上進行了測試。儘管SSD的隨機4KB讀取吞吐量遠高於HDD,但仍然低於順序批量讀取。根據我們的測試環境,4K頁面隨機讀取吞吐量約為100MB/秒,而順序批量讀取吞吐量超過1GB/秒。
根據我們的觀察,基於頁面錯誤的換入僅載入30%到90%的換出頁面來處理使用者請求。例如,在我們對Node.js Hello World休眠測試的實驗中,總共約10MB的記憶體被換出,而使用者請求處理僅換入約4MB的記憶體。這是因為換出頁面包含了用於客體應用初始化和請求處理的記憶體頁面。而當膨脹休眠容器時,應用初始化已經完成,因此相關的記憶體頁面不會被存取和換入。基於這一觀察,並受到REAP[14]的啓發,休眠容器實現了一種批量REAP預取換入機制。
3.4.2 REAP換出和批量換入記錄與預取
(REAP)的主要概念是在處理使用者請求時記錄客體實體記憶體工作集,並在下次喚醒時批量預取它們。這是基於頁面錯誤的換入機制的一種最佳化。
與基於頁面錯誤的換出相比,REAP在第一次休眠和喚醒後增加了一個記錄過程。在容器第一次進入休眠狀態後,記錄過程執行以下步驟:
- 示例使用者請求:無伺服器平臺發送一個示例使用者請求以觸發其切換到休眠運行狀態;
- 實體記憶體工作集記錄:在休眠運行狀態下,請求行程的客體實體記憶體工作集通過頁面錯誤換入從交換檔載入,而未觸及的頁面仍保留在交換檔中;3.REAP 休眠:在樣本使用者請求過程完成後,容器返回到喚醒狀態。無伺服器平臺發送 SIGSTOP 信號以觸發喚醒容器的休眠。REAP 換出過程被觸發,其過程如下。
- 暫停所有的使用者行程。
- 遍歷所有頁面表以獲取所有活動的匿名記憶體頁。
- 將客體實體記憶體頁地址記錄在雜湊 IO 向量中,並根據 IO 向量使用批量寫入 pwritev() 系統調用將記憶體頁保存到圖 5 中的 REAP交換檔。
- 通過調用系統調用 madvise() 釋放客體實體記憶體頁。
我們可以看到 REAP 換出與基於頁面錯誤的換出是不同的。
- REAP 換出不會更改頁面表項,因此不會觸發頁面錯誤;
- REAP 換出將寫入專用的 REAP 交換檔,這可以通過磁碟批量順序讀取加速換入過程。
REAP 換入過程比基於頁面錯誤的換入更簡單。
其步驟如下。
- 根據在 REAP 換出過程中創建的雜湊 IO 向量,使用批量順序讀取 preadv() 從 REAP 交換檔中預取所有記憶體頁。
- 恢復客體應用程式行程。REAP 交換過程的效能優於基於頁面錯誤的換入,原因如下:
- 無頁面錯誤開銷:REAP 換出不會更改頁面表項,因此在換入時不會觸發頁面錯誤,從而避免了頁面錯誤處理和客戶/主機切換的開銷。
- 批量檔案讀取:REAP 換入以批量讀取的方式預取所有記憶體頁,這比隨機讀取具有更高的吞吐量。
3.5 檔案支援的記憶體共享和安全問題
休眠容器還使用 madvise() 清理檔案支援的 mmap 記憶體,以將其返回給主機 Linux。Quark 可能會在不同的容器之間共享檔案支援的 mmap 記憶體,採用寫時複製(Copy On Write)技術。當記憶體被共享時,休眠容器的壓縮過程不需要清理這些記憶體,因為它們可能被其他容器使用。記憶體共享不僅可以減少容器啟動延遲,還可以降低整體系統記憶體佔用。這對於無伺服器計算來說是一種相當有吸引力的技術。
不幸的是,在多租戶的無伺服器計算環境中,當檔案支援的 mmap 記憶體在不同租戶之間共享時,會導致安全風險,例如快取側信道攻擊 [27]。因此,在多租戶生產環境中不推薦使用記憶體共享 [7]。
對於在安全容器中運行的應用程式,有兩種主要類型的檔案支援記憶體可能會在容器之間共享:
- 使用者應用程式語言運行時二進位檔:使用者應用程式是基於不同程式語言運行時二進位檔開發的,例如 Node.js 運行時和 Python 運行時。這類檔案的類型被記憶體映射到使用者記憶體空間並可以被使用者應用程式直接存取。在不同租戶之間共享它們是有風險的。
- 安全容器運行時二進位檔:這是安全容器運行時的可執行二進位檔和函式庫檔,例如Kata運行時的Linux客體核心二進位檔。這種類型的檔案不會被映射到使用者記憶體空間,使用者應用程式無法直接存取它們,因此安全風險低於使用者應用程式語言運行時二進位檔。RunD [10] 在生產環境中共享Linux客體核心,以加速冷啟動並減少記憶體佔用。
休眠容器選擇啓用Quark運行時二進位檔的共享,但禁用程式語言運行時二進位檔的共享。
運行時二進位檔共享導致請求響應延遲顯著減少。例如,我們在測試環境中評估了Node.js運行時二進位檔共享。當我們啓用Node.js二進制記憶體共享時,休眠的Node.js hello-world容器請求響應延遲從25毫秒減少到11毫秒。存在一些緩解措施,如[28] [29],以解決程式語言運行時共享的安全風險。在生產部署中採用了一些緩解措施。例如,Cloudflare Worker [29] 基於 V8 引擎的隔離妥協了多租戶隔離的安全風險。當問題解決後,休眠容器可能通過採用這些緩解措施實現更好的效能,啓用更多基於檔案的記憶體共享。
3.6 休眠容器實現
休眠容器的程式碼庫是用 Rust 編程語言編寫的,並作為 Quark 安全容器運行時環境的一部分運行。Quark運行時是一個虛擬使用者空間作業系統,擁有超過 20萬行的 Rust 程式碼。休眠容器的功能改變了 Quark 運行時的關鍵程式碼路徑,例如虛擬記憶體管理、IO 管理和虛擬機器器監視器 (VMM)。我們從頭開始實現了交換管理器和位圖分配器。交換管理器有780行程式碼,位圖分配器有484行程式碼。我們通過修改Quark的檔案備份記憶體管理和頁面表管理模組開發了記憶體回收管理器,程式碼大約有500行。在Quark信號處理模組和IO管理中還有大約300行程式碼的更改,用於觸發休眠過程。
4 評估
在本節中,我們通過實驗展示了休眠容器的效能。我們的實驗在一臺設定為1×12核的Intel(R) Core(TM)i7-8700K CPU @ 3.70GHz、64GB RAM、PM981 NVMe三星512GB SSD的機器上進行,運行Ubuntu 20.04.4 Linux,核心版本為5.15.0-46-generic。
我們使用兩組微基準測試來評估容器休眠對請求響應延遲和記憶體佔用的影響。
- Python基準測試:我們從Function Bench[30]中選擇了一組微基準測試,以覆蓋不同的行程類型、記憶體使用和行程延遲。
- 浮點運算:此浮點算術運算工作負載具有較小的記憶體使用和處理延遲;
- 影片處理:影片處理工作負載將OpenCV庫的灰度效果應用於影片輸入。它的記憶體佔用超過200 MB,處理延遲超過1000毫秒。
- 影像處理:該程式使用Python Pillow庫執行影像變換任務。我們選擇了2種不同大小的影像檔以評估資料大小對記憶體使用和相同程式處理延遲的影響。
- 程式語言運行時hello-world:我們測試Python、Node.js、Golang、Java的hello-world程式,以評估不同程式語言運行時的行為。
4.1 使用者請求響應延遲
該實驗表明,休眠容器的使用者請求響應延遲低於冷啟動,而喚醒容器的響應延遲與熱容器相似。我們還比較了頁面錯誤和基於REAP的換入的延遲。
由於休眠容器已經啟動並處於保持活動狀態,因此我們測量的是使用者應用程式請求/響應的端到端延遲,而不是應用程式啟動延遲。除了冷啟動測試外,我們在HTTP服務中運行微基準測試,並通過HTTP請求觸發應用程式處理,然後測量HTTP響應延遲。

在實驗中,我們收集了冷啟動、熱容器、帶有頁面錯誤/REAP換入的休眠容器和喚醒容器的延遲,如下所示:
- 冷啟動:在不使用HTTP請求觸發的情況下,容器啟動和請求處理的過程延遲;
- 熱:容器完全初始化後的請求響應延遲;
- 休眠:容器休眠後第一次請求的請求響應延遲。我們還收集了頁面錯誤和REAP換入機制的延遲。
- 喚醒:喚醒容器的請求響應延遲。
圖6顯示了測試結果。由此,我們可以得出以下結論:
- Hibernate容器的請求處理延遲低於冷啟動延遲:例如,Hibernate REAP請求響應延遲佔冷啟動過程延遲的3%(Python/Golang Hello-world)到67%(影像處理,檔案大小為2.6 MB)。Hibernate REAP可以節省從296毫秒(Golang Hello-world)到2407毫秒(影片處理)的冷啟動過程延遲。
- 喚醒容器的請求處理延遲與熱容器幾乎相似。
- 帶有頁面錯誤交換的Hibernate容器請求延遲高於REAP:在大多數基準測試中,REAP的表現優於基於頁面錯誤交換的交換機制。唯一的例外是影像處理,檔案大小為2.6 MB,但差異可以忽略不計。
測試結果證明,休眠容器的響應延遲低於冷啟動,而喚醒容器的處理延遲與熱容器相似。此外,休眠容器可以使程式語言運行時受益,例如 Python、Node.js、Golang 和 Java。
4.2 容器記憶體消耗
該實驗表明,休眠容器及其派生的喚醒容器消耗的記憶體少於熱容器。我們通過 Linux 工具「pmap」收集基準記憶體消耗的比例集大小(PSS),以便於以下狀態:
- 熱:容器處理少量使用者請求。
- 休眠:容器從熱狀態過渡到休眠狀態。
- 喚醒:休眠容器通過使用者請求被喚醒。
如第 3.4 節所述,休眠容器共享 Quark 運行時二進位檔。因此,當運行更多實例時,記憶體 PSS 更少。在我們的環境中,我們收集了10個正在運行的基準應用實例的PSS資料。

我們可以從圖7中得出以下結論:
- 「休眠」狀態的記憶體消耗遠低於「熱」狀態:例如,「休眠」狀態的記憶體消耗約為「熱」狀態的7%(影片處理)到25%(Golang Hello-world)。總記憶體節省範圍從12 MB(總計16 MB,Golang Hello-world)到252 MB(總計281 MB,影像處理,檔案大小為2.6 MB)。
- 」喚醒」狀態的記憶體消耗低於」熱」狀態:例如,」喚醒」狀態佔用」熱」狀態記憶體的28%(Node.js Hello-world)到90%(影像處理,檔案大小為2.6MB)。總記憶體節省範圍從7MB(總共16 MB的Golang Hello-world)到151 MB(總共226 MB的影片處理)。
測試結果證明,休眠容器及其派生的喚醒容器比熱容器消耗更少的記憶體。這種記憶體節省適用於不同的編程語言運行時,包括Python、Node.js、Golang和Java。
從響應延遲和記憶體測試來看,我們可以得出結論:
- 休眠容器和喚醒容器的共同部署可以實現比熱容器更高的部署密度。
- 喚醒容器的記憶體消耗更少,但請求延遲與熱容器相似。在可能的情況下,將熱容器轉換為通過休眠容器作為保持活動容器的喚醒容器是有益的。
5 相關工作
已經有幾項相關工作正在進行,特別是與冷啟動最佳化相關的工作。冷啟動通常由兩個不同的活動組成:安全容器運行時啟動和使用者應用程式啟動。
以下小節描述了這兩個與冷啟動相關的活動的各種正在進行的最佳化工作。
5.1 基於虛擬機器的安全容器運行時最佳化
基於虛擬機器的安全容器運行時啟動包括Linux容器環境(例如Cgroup、容器網路、容器檔案系統)設置和虛擬機器作業系統核心啟動任務。RunD [10] 努力通過預創建Cgroup、最佳化容器rootfs映射來加速容器環境的設置。RunD使用Kata模板來減少每個微虛擬機器的記憶體開銷以及啟動延遲的減少。
Firecracker[19] 引入了一種輕量級虛擬機器器監視器來替代常見的虛擬機器器監視器,如QEMU或CloudHypervisor。Firecracker針對無伺服器計算進行了最佳化,因為它僅支持少量設備:支持virtio網路設備和單個塊設備類型。這反過來導致了與安全容器記憶體使用以及沙箱啟動延遲相關的最佳化收益。
Quark[18]和gVisor[8]則引入了一個新的使用者空間作業系統核心和一個專門針對無伺服器工作負載的新虛擬機器器監視器,從而減少了記憶體佔用和啟動延遲。
5.2 應用啟動最佳化
上述冷啟動最佳化的關鍵思想是從一種狀態啟動容器,該狀態與熱容器相對「更接近」。[13][31] 使用安全容器運行時 gVisor 或 JVM 提供的檢查點恢復技術。[32] 重用一個待回收的熱容器來託管另一個容器鏡像。
基於檢查點恢復(即 C/R),催化劑 [13] 實現了無初始化啟動。在 C/R 之上還有更多最佳化,例如 REAP 使用批量預取來加速 VMM 鏡像載入。Sock [12]擴展了 Zygote,通過分叉一個帶有預導入軟體包的輔助容器來啟動一個新容器,以節省軟體包導入成本。催化劑引入了一個沙箱分叉(sfork),它從一個現有的熱容器中分叉,帶有完整的應用狀態,並在父/子容器之間共享記憶體。
結論
低延遲容器啟動時間對於無伺服器計算環境的整體使用者體驗至關重要。除了冷啟動和熱啟動作為當代無伺服器計算環境的一部分,本文提出了第三種啟動模式:休眠容器。休眠容器本質上是一個瘦身的熱容器。
我們的實驗表明,休眠容器的記憶體消耗遠低於熱容器,並且其請求響應延遲低於冷啟動。在休眠容器被喚醒時,喚醒容器的記憶體消耗也低於熱容器,並且在同一時間具有相同的請求響應延遲。所有這些都導致了更高的部署密度、更低的請求響應延遲,以及整體系統效能的顯著提升。