RFC-0269:測量記憶體停滯

RFC-0269:測量記憶體停滯
狀態已接受
區域
  • Kernel
說明

偵測記憶體停滯並向使用者空間回報

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2024-10-15
審查日期 (年-月-日)2025-04-10

問題陳述

Zircon 目前不會公開因記憶體壓力造成的暫時性停滯資訊,而 Starnix 需要這項資訊才能實作 PSI 介面 (/proc/pressure/memory)。

這項 RFC 解決了合成相容測量結果的問題,並在超過指定門檻時通知觀察員。

摘要

本文建議採用一種機制,偵測記憶體壓力造成的停滯,並通知使用者空間。這項機制會根據壓力造成的活動所耗費的時間進行評估,這些時間原本可用於完成有用的工作。

提振精神

以 Linux 為基礎的作業系統 (包括 Androidsystemd) 會依賴指定的 userspace 精靈,接收通知並處理記憶體不足造成的延遲 (「停滯」)。這類程式執行的動作通常只是終止整個程序/服務。

在 Starnix 的脈絡中,我們的目標是「如實」實作 Linux API as she is spoke (RFC-0082),因此需要提供相同的停滯偵測 API,在 Linux 上稱為「壓力停滯資訊 (PSI)」

Linux 的 PSI 會計算因記憶體壓力而導致的延遲,方法是測量釋出記憶體所需的額外動作所造成的延遲,而這些統計資料可透過 /proc/pressure/memory 虛擬檔案存取。這項 RFC 建議採用與 Linux 類似的方法,測量壓力造成的延遲。

本文僅著重於 Starnix,以及如何實作 Linux 的 PSI 介面,以達到功能同等性。至於 Starnix 以外的其他 Fuchsia 元件是否也能使用,則超出範圍,留待日後階段處理。

利害關係人

協助人員:lindkvist@google.com

審查者:

  • adanis@google.com
  • eieio@google.com
  • maniscalco@google.com
  • rashaeqbal@google.com

已諮詢:

  • davemoore@google.com
  • lindkvist@google.com
  • wez@google.com

社交:

我們已透過電子郵件、即時通訊和會議,與 Zircon 團隊討論過這項 RFC。

需求條件

由於重點在於 Starnix,因此需求是由要提供給 Linux 程式的介面所驅動。

在 Linux 中,讀取 /proc/pressure/memory 檔案會傳回下列資訊:

  • 自啟動以來有兩個單調計時器,分別標示為 somefull (單位為微秒):根據官方文件,當至少有一個執行緒記憶體停滯時,some 時間會增加;當所有其他可執行的執行緒記憶體停滯時,full 時間會增加。請注意,即使其他執行緒能夠進展,仍可能發生 some 條件,而 full 條件則表示完全沒有任何執行緒進展。因此,some 時間一律大於或等於 full 時間。
  • 上述兩個計時器的成長率平均值 (以單調時鐘的變異量比率表示,因此值介於 0.00 和 1.00 之間),時間範圍為過去 10 秒、60 秒和 300 秒。

除了讀取這些統計資料,Linux 也允許您在達到特定停滯層級時收到通知,方法是開啟該檔案、寫入要觀察的計時器 (somefull)、門檻和觀察視窗 (均以微秒為單位),然後輪詢該檔案描述元。當所選計時器在設定的觀察時間範圍內,增加的量超過指定門檻時,輪詢就會傳回 POLLPRI 事件。

請注意,在 Linux 上,您也可以透過 memory.pressure 檔案在 cgroup 層級啟用相同的測量和通知機制,並使用相同的介面。雖然這項 RFC 提案的設計目前只著重於實作全域 /proc/pressure/memory 檔案,但如果日後有需要,也可以重複使用這項 RFC 中說明的內部評估機制,實作類似功能 (詳情請參閱「缺點、替代方案和未知事項」)。

為盡量減少與 Linux 相比,所測得記憶體停滯量級的差異,我們打算先將相同公式套用至 Zircon,並將進一步調整/偏差留待後續的調整階段。我們將在下一節詳細說明如何將這些公式改編為 Zircon 專用。

設計

本 RFC 的目標是精確定義 Zircon 中的停滯概念、如何產生及匯總停滯測量結果、如何向使用者空間公開這項資料,以及如何讓使用者空間在系統停滯超過指定程度時收到通知。

簡而言之,Zircon 會測量記憶體停滯,並匯總這些停滯,然後公開兩個全系統值 (做為新的 zx_object_get_info 主題) 和通知介面 (透過新的系統呼叫),兩者都由新的資源類型控管。這兩個新值 (稱為 somefull 停滯時間) 會以奈秒表示,並以單調遞增的方式持續成長,成長速率是單調時鐘速率的一小部分 (介於零和一之間),取決於目前的停滯程度。

舉例來說,假設系統在 300000 奈秒內沒有發生任何停滯,之後偵測到 100000 奈秒內發生 25% 的停滯,然後在額外 200000 奈秒內發生 50% 的停滯。相應的已回報值為 0% * 300000 + 25% * 100000 + 50% * 200000 = 125000 奈秒。

找出記憶體停滯

原則上,凡是記憶體壓力造成的延遲,導致執行緒無法執行其他有用的工作,都應視為該執行緒的停滯。

實際上,以目前的程式碼集來說,只有等待專屬核心執行緒釋放記憶體時所花費的時間 (換句話說,就是 AnonymousPageRequester::WaitOnRequest 存在於呼叫堆疊時所花費的時間),才會視為記憶體停滯。當系統嘗試分配新記憶體,但可用頁面數量目前低於延遲門檻時 (接近記憶體不足的程度,並在此處計算),就會發生這種情況。

上述作業涉及透過許多不同的排程器狀態追蹤停滯的執行緒。此外,停滯追蹤機制也盡可能通用,方便日後將其他程式碼區段標示為記憶體停滯。為避免新增 ad hoc 狀態,導致目前的排程器更加複雜,本 RFC 建議使用防護物件 (ScopedMemoryStall) 劃分程式碼區段,將其視為停滯,並根據執行緒目前的排程器狀態,以及是否在防護區域中執行,將執行緒分類為 IGNOREDPROGRESSINGSTALLING,以追蹤停滯狀態。

| 排程器狀態 | 如果在防護措施內 | 如果在防護措施外 | |-|-|-| | INITIAL | 不適用 | IGNORED | | READY | STALLING | IGNORED | | RUNNING | STALLING | PROGRESSING | | BLOCKEDTHREAD_BLOCKED_READ_LOCK | STALLING | IGNORED | | SLEEPING | STALLING | IGNORED | | SUSPENDED | STALLING | IGNORED | | DEATH | 不適用 | IGNORED |

為消除核心中內部記憶體記帳活動造成的測量噪音,系統一律會將非使用者模式的執行緒視為 IGNORED

請注意,在上述模型中,如果執行緒在防護措施內執行,或在遭到封鎖或搶占前在防護措施內執行,就會視為 STALLING

somefull 計時器的成長率

停滯計時器不會只是每個執行緒的 STALLING 時間總和。 而是持續評估 STALLINGPROGRESSING 執行緒的總數 (在每個 CPU 上本機執行),然後定期匯總。這會與這份參考資料中記錄的 Linux 模型相符。在內部,系統會將兩個層級的測量資料新增至 Zircon。

第一層是每個 CPU 的本機層,由每個 CPU 的 StallAccumulator 物件追蹤,會測量下列三種非互斥條件中花費的時間 (以單調時鐘做為時間基準):

  • full:與這個 CPU 相關聯的至少一個執行緒處於 STALLING 狀態,且沒有任何執行緒處於 PROGRESSING 狀態。
  • some:與這個 CPU 相關聯的至少一個執行緒處於 STALLING 狀態。
  • active:至少有一個 PROGRESSINGSTALLING 執行緒與這個 CPU 相關聯。

第二層級的測量是由 StallAggregator 單例項實作,會定期查詢所有 StallAccumulator,藉此更新系統層級的統計資料。具體來說,這項作業會執行背景執行緒,定期喚醒並平均計算每個 StallAccumulator 觀察到的 somefull 停滯時間增量,並根據各自的 active 時間加權,然後相應增加全域 somefull 值。

此外,當產生的成長率超過註冊監控者設定的門檻時,StallAggregator 會通知這些監控者 (詳情請參閱「新的 zx_system_watch_memory_stall 系統呼叫」)。

Stall 資源

系統會導入名為「ZX_RSRC_SYSTEM_STALL_BASE」的新型核心資源。

由於這項 RFC 只會測量全球暫停層級 (相當於 /proc/pressure/memory),因此只要使用單一資源,就能控管全球暫停測量資料的存取權。

啟動時會建立這個資源的控制代碼,並由元件管理員透過 fuchsia.kernel.StallResource 通訊協定 (做為內建功能) 提供服務,這只會傳回重複的控制代碼。控管這項能力的路徑,就能控管誰有權存取停滯測量結果。

ZX_INFO_MEMORY_STALL主題

系統會新增 ZX_INFO_MEMORY_STALL 主題 (位於攤位資源),並公開下列兩個欄位:

typedef struct zx_info_memory_stall_t {
    // Total monotonic time spent with at least one memory-stalled thread.
    zx_duration_mono_t stalled_time_some;

    // Total monotonic time spent with all threads memory-stalled.
    zx_duration_mono_t stalled_time_full;
} zx_info_memory_stall_t;

Starnix 會查詢這個主題,合成 /proc/pressure/memory 檔案。此外,Starnix 也會定期取樣,在內部計算過去 10、60 和 300 秒的移動平均值。

新的 zx_system_watch_memory_stall 系統呼叫

與現有的 zx_system_get_event 系統呼叫類似,新的 zx_system_watch_memory_stall 系統呼叫也會傳回事件控制代碼,核心會根據所選停滯計時器的目前成長率,在該控制代碼上判斷/取消判斷 ZX_EVENT_SIGNALED

完整的系統呼叫原型如下:

zx_status_t zx_system_watch_memory_stall(zx_handle_t stall_resource,
                                         zx_system_memory_stall_type_t kind,
                                         zx_duration_mono_t threshold,
                                         zx_duration_mono_t window,
                                         zx_handle_t* event);

引數:

  • stall_resource:攤位資源的控制代碼。
  • kindZX_SYSTEM_MEMORY_STALL_SOMEZX_SYSTEM_MEMORY_STALL_FULL,選取要觀察的計時器。
  • threshold:觸發信號的最短暫停時間 (以奈秒為單位)。
  • window:觀察期長度 (奈秒)。
  • event:由核心填入,這是事件的控制代碼,如果上次觀察 window 期間,kind 選取的停滯計時器至少增加 threshold,就會判斷為事件 (ZX_EVENT_SIGNALED)。

只要觸發條件適用,傳回的事件就會保持判斷結果,而當條件不再適用時,核心就會取消判斷結果。

可能發生的錯誤:

  • ZX_ERR_BAD_HANDLEstall_resource不是有效的控制代碼。
  • ZX_ERR_WRONG_TYPEstall_resource的種類不是 ZX_RSRC_KIND_SYSTEM
  • ZX_ERR_OUT_OF_RANGEstall_resource 不是攤位資源。
  • ZX_ERR_INVALID_ARGSkindthresholdwindow 無效 (請參閱下方附註)。

呼叫者的工作政策必須允許 ZX_POL_NEW_EVENT

請注意,即使 API 以奈秒為單位定義 thresholdwindow,Zircon 仍可不使用所要求值的完整精確度。此外,0 < threshold <= window 必須成立,且為了限制必要的記帳記憶體量,我們也規定 window 不得超過 10 秒 (Linux 也有相同限制)。

實作

Zircon 變更會分成多個 CL,大致依下列順序進行:

  • 新增基礎架構,以偵測每個 CPU 層級的停滯 (StallAccumulator)。
  • AnonymousPageRequest 中遭封鎖的時間計為記憶體停滯。
  • 定期將所有 CPU 值匯總為全域指標 (StallAggregator),並以低優先順序核心執行緒執行。
  • 將新的暫停資源做為元件管理員內建能力 (fuchsia.kernel.StallResource) 提供。
  • 實作新的 ZX_INFO_MEMORY_STALL 主題。
  • 新增基礎架構,偵測何時超過停滯門檻。
  • 透過 zx_system_watch_memory_stall 系統呼叫公開。

完成 Zircon 變更後,系統會修改 Starnix,在這些變更的基礎上實作 /proc/pressure/memory,如下所示:

  • 系統會直接從 ZX_INFO_MEMORY_STALL 讀取 somefull 的總值。
  • Starnix 會每秒輪詢 (大約) 一次,在循環佇列中保留最後 300 個樣本,並從中計算移動平均值。ZX_INFO_MEMORY_STALL
  • PSI 通知將在 zx_system_watch_memory_stall 頂端實作,並新增速率限制器 (在 Starnix 本身實作),以符合 Linux 的 PSI 行為,即每個視窗最多只會傳送一則通知。

效能

如前幾節所述,建議的實作方式會在每個 CPU 的資料結構中維護停滯測量值,並定期從專屬核心執行緒匯總這些值。效能測試結果顯示,由於停滯時間的記帳作業,並未出現任何明顯的迴歸現象。

如果訂閱的觀察者過多,Zircon 核心在超過停滯門檻時必須維護及通知這些觀察者,可能會導致效能風險。zx_system_watch_memory_stall如有需要,您可以透過專屬使用者空間元件代理存取權,藉此減輕影響,該元件會透過單一 Zircon 訂閱項目彙整及服務多個用戶端。與 Zircon 類似 (請參閱上方的「新的 zx_system_watch_memory_stall 系統呼叫」),這類 Proxy 可從要求的值中捨棄精確度,盡可能提高匯總機會。

人體工學

提議的 Zircon API 幾乎完全簡化了 Starnix 中的 PSI 實作程序,但 Linux 的通知速率限制除外。理論上,我們也可以在 Zircon 核心中實作速率限制,並採用類似 RFC-0237 所述的選通訊號機制。不過,為了方便說明 zx_system_watch_memory_stall 的行為,並與現有的 zx_system_get_event 信號保持一致,我們選擇只公開簡單的層級觸發信號,並將通知的速率限制複雜性委派給使用者空間。

回溯相容性

本 RFC 只會導入新介面 (新資源類型、ZX_INFO_MEMORY_STALL 主題和 zx_system_watch_memory_stall 系統呼叫),不會造成任何破壞性 API/ABI 變更。

安全性考量

如果使用者有權存取新的成效指標,可能會建立不必要的旁路管道。不過,在這種情況下,由於只有受信任的 Fuchsia 元件或 Starnix 內受信任的 Linux 程序 (根據 Starnix 容器的安全模式) 會收到存取新指標的能力,因此風險得以降低。

為避免無限制的核心記憶體配置風險,我們允許核心從要求的門檻和視窗值中捨棄精確度 (啟用統計資料的內部量化),並限制允許的值範圍。

隱私權注意事項

這項異動不會影響隱私權。

測試

將新增兩種 Zircon 測試:

  • 使用者空間測試 (core-tests),可監控記憶體停滯事件、產生記憶體壓力、查詢資訊主題,並確認事件是否實際觸發。
  • 核心內單元測試,可模擬防護措施的效果,並驗證測得的時間是否符合預期。ScopedMemoryStall

說明文件

說明文件會新增至相關的系統呼叫介面 (新的 ZX_INFO_MEMORY_STALL 主題和新的 zx_system_watch_memory_stall 系統呼叫),以及 fuchsia.kernel.StallResource FIDL 通訊協定。

我們會將視為停滯的條件清單視為實作細節,不會納入說明文件,以便進一步擴充及調整這些條件,同時不中斷 API。

缺點、替代方案和未知事項

系統呼叫介面

本 RFC 中的設計與在 Starnix 中實作全域 PSI 密切相關,而系統呼叫 API 則是根據其需求建立模型。

我們考慮過透過新的「停滯量表」核心物件類型 (而非新的資源類型) 公開新功能,以利日後實作 cgroup 的 memory.pressure 階層式統計資料。不過,由於目前不需要這類擴充功能,因此我們決定簡化 API 變更,避免過度規劃。

除了讓 zx_system_watch_memory_stall 系統呼叫傳回事件例項,我們也考慮過傳回新的專用「停滯監控」物件型別,並提供專用信號 (例如 ZX_STALL_MONITOR_ABOVE_THRESHOLD)。雖然這種做法在意圖和對應內部核心實作方面更明確,但除了這個原因之外,這種做法還需要新的核心物件型別。現有事件物件類型的 API 已足夠表達,因此遭到拒絕。

最後,許多新 API 名稱都改用「停滯」而非「記憶體停滯」,以便日後擴充至其他類型的停滯測量 (例如 CPU 和 I/O 停滯)。

延遲測量和匯總

在建議的測量機制中,我們討論過是否要將等待其他停滯執行緒所持資源的執行緒,視為停滯執行緒本身 (「停滯繼承」),但由於額外的複雜性,以及缺乏評估其優點的基準資料,因此至少在初始實作中,我們拒絕了這項選項。

最初的計畫是將解壓縮 ZRAM 所花的時間也視為記憶體停滯,因為這是資源耗盡的跡象 (嘗試重新存取先前壓縮的匿名頁面)。不過,由於 1) Zircon 會主動壓縮背景中閒置的頁面,即使有大量可用記憶體也是如此,以及 2) 只有在再次需要頁面時才會解壓縮,這類事件最多只會發出延遲的指標,最糟的情況則是發出虛假的通知。此外,實驗結果顯示,ZRAM 解壓縮造成的停滯時間貢獻目前比 AnonymousPageRequester::WaitOnRequest 低好幾個數量級。基於上述原因,我們決定暫時不將 ZRAM 解壓縮視為記憶體停滯,並將其排除在初始實作項目之外。

我們也考慮過另一種匯總機制,讓核心透過無鎖定資料結構中的共用記憶體,公開每個執行緒的原始停滯時間計數器。然後由使用者空間定期執行聚合,並採用產品專屬邏輯,將聚合政策與核心 API 分離。這種做法的缺點是精確度較差 (因為是取樣),而且設計更複雜 (依賴共用記憶體,容易出錯),還需要更頻繁地啟動使用者空間。

我們也考慮過更簡單的停滯測量方式:只要測量執行驅逐器所花費的時間即可。不過,這個方法無法區分 somefull 的停滯時間,也無法找出受到停滯影響的程序。

與現有記憶體壓力事件的關係

Zircon 已透過五個互斥事件 (Normal、Warning、Critical、Imminent Out Of Memory 和 Out Of Memory),公開目前記憶體壓力層級的資訊,這些事件是由核心判斷,且其控制代碼可透過 zx_system_get_event 系統呼叫取得。雖然 Zircon 的 API 合約並未明確說明觸發條件,但實際上,Zircon 實作的觸發條件是將目前的可用記憶體量與五個對應範圍之一進行比對。

除了本 RFC 建議的停滯時間長度計時方式,我們也評估了從現有壓力事件合成虛假延遲的選項。不過,由於 Linux 的 PSI 訊號預期動態變化差異極大,因此遭到拒絕:Zircon 現有壓力事件之間的轉換速度緩慢 (在某些情況下,為避免變更過於頻繁,還會受到人為延遲影響),但 Linux 的 PSI 計時器預期會近乎即時更新。PSI 計時器的成長率尤其需要快速變化,才能在超過相應門檻時立即產生脈衝,觸發已註冊的監看程式,並在回落至門檻以下時立即停止觸發。

此外,使用 Linux 的 PSI 計時器時,閒置系統的成長率應為零。但目前 Zircon 事件無法保證使用者空間採取的回應動作,最終能釋出足夠的記憶體,讓系統回到「正常」狀態。

結論

Android 映像檔的初步測試顯示,如本 RFC 所述測量停滯時,產生的通知事件與 Linux 上對應工作負載產生的通知事件相當。不過,由於 Zircon 和 Linux 是兩個不同的核心,VMM 子系統也大相逕庭,因此並非所有極端情況都能獲得保證,未來可能需要進行修正,例如擴充/修改 Zircon 視為停滯狀態的定義。

既有技術和參考資料