RFC-0269:測量記憶體停滯

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

偵測記憶體停滯情形並回報至使用者空間

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

問題陳述

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

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

摘要

本文提出一種機制,可偵測記憶體壓力造成的停滯情形,並通知使用者空間。這項機制會根據壓力導致的活動所花費的時間進行評估,這些活動原本可用於執行有用的作業。

提振精神

以 Linux 為基礎的作業系統 (包括 Androidsystemd) 都會依賴指定的使用者空間守護程序,以便在記憶體壓力導致延遲 (「停頓」) 時收到通知並採取行動。這類程式執行的動作通常只是回應性地終止整個程序/服務。

在 Starnix 的情況下,其目標是實作 Linux API,就像她所說的一樣 (RFC-0082),我們需要提供相同的停滯偵測 API,在 Linux 上稱為 Pressure Stall Information (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 時間。
  • 過去 10、60 和 300 秒內,上述兩個計時器的成長率平均值 (以單調計時器的變化率做為比率,因此會提供 0.00 到 1.00 之間的值範圍)。

除了讀取這些統計資料,Linux 還可在達到特定停滯等級時,透過開啟該檔案、寫入要觀察的計時器 (somefull)、閾值和觀察視窗 (兩者皆以微秒為單位),然後輪詢該檔案描述元,接著通知您。當所選計時器在設定的觀察時間內,增加超過指定的門檻值時,Poll 就會傳回 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 時所花費的時間) 才會視為記憶體停滯。當系統嘗試分配新記憶體,但可用頁面數量目前低於延遲門檻 (接近記憶體不足等級,且計算方式如此處所述) 時,就會發生這種情況。

上述作業涉及在許多不同的排程器狀態中追蹤停滯的執行緒。此外,停滯追蹤機制旨在提供足夠的通用性,以便在日後輕鬆將其他程式碼區段標示為記憶體停滯。為避免新增臨時狀態而使現有排程器更複雜,此 RFC 建議將程式碼區塊劃分為以下兩種狀態:使用監控物件 (ScopedMemoryStall) 的停滯狀態,以及用於追蹤停滯狀態的 IGNOREDPROGRESSINGSTALLING,這取決於目前的排程器狀態,以及是否在受監控區域執行。

| 調度器狀態 | 如果內部守護者 | 如果外部守護者 | |-|-|-| | INITIAL | n/a | IGNORED | | READY | STALLING | IGNORED | | RUNNING | STALLING | PROGRESSING | | BLOCKEDTHREAD_BLOCKED_READ_LOCK | STALLING | IGNORED | | SLEEPING | STALLING | IGNORED | | SUSPENDED | STALLING | IGNORED | | DEATH | n/a | 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 系統呼叫」)。

停滯資源

我們將推出名為 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_ARGS:無效的 kindthresholdwindow (請參閱下方附註)。

呼叫端的工作政策必須允許 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 值。
  • 系統會每秒 (約) 輪詢 ZX_INFO_MEMORY_STALL,在 Starnix 中計算平均值,並在循環佇列中保留最近 300 個樣本,並根據這些樣本計算移動平均值。
  • PSI 通知將在 zx_system_watch_memory_stall 之上實作,並新增速率限制器 (在 Starnix 中實作),以符合 Linux 的 PSI 行為,即每個視窗最多只能傳送一則通知。

成效

如前幾節所述,建議的實作方式會在每個 CPU 資料結構中維持停滯測量,並定期從專用核心執行緒匯總這些測量。效能測試並未發現因停滯時間記錄而導致的任何顯著回歸。

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

人體工學

除了 Linux 的通知速率限制之外,建議採用的 Zircon API 幾乎可讓您在 Starnix 中輕鬆實作 PSI。理論上,我們也可以在 Zircon 核心中實作速率限制,並最終採用與 RFC-0237 所述的頻閃信號傳送模式。不過,為了讓 zx_system_watch_memory_stall 的行為更容易描述,並與現有的 zx_system_get_event 信號保持一致,我們選擇只公開簡單的層級觸發信號,並將頻率限制通知的複雜性委派給使用者空間。

回溯相容性

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

安全性考量

將使用者空間的存取權授予新效能評估,可能會導致建立不必要的側邊管道。不過,在這種情況下,由於存取新測量值的能力只會根據 Starnix 容器的安全性模型,路由至信任的 Fuchsia 元件或 Starnix 中的信任 Linux 程序,因此風險已降低。

為了消除無限的核心記憶體配置風險,我們讓核心可以自由地從要求的閾值和視窗值降低精確度 (啟用統計資料的內部量化),並限制可接受的值範圍。

隱私權注意事項

這項變更不會影響隱私權。

測試

我們會新增兩種 Zircon 測試:

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

說明文件

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

系統會將視為停滯的條件清單視為實作細節,因此不會納入說明文件,以便在不會中斷 API 的情況下,進一步擴充及調整這類條件。

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

Syscall 途徑

本 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 事件中,我們無法保證使用者空間在回應時採取的動作,最終會釋放足夠的記憶體來返回「Normal」狀態。

結語

在 Android 映像檔上進行的初步測試顯示,根據本 RFC 所述,測量停頓時間所產生的通知事件,與 Linux 上對應工作負載所產生的通知事件相似。不過,由於 Zircon 和 Linux 是兩個具有截然不同的 VMM 子系統的不同核心,因此這項功能無法適用於所有極端情況,日後可能需要進行精進,例如擴充/修改 Zircon 認為為停滯狀態的定義。

既有技術與參考資料