在元件處於閒置狀態時停止元件

元件通常不會一直運作。大多數元件都以非同步方式編寫,也就是說,它們通常會等待下一個 FIDL 訊息。不過,這些元件會占用記憶體。本指南說明如何調整元件,讓元件在閒置時主動停止並釋出資源。

總覽

並發生以下情況:

  • 您將對元件的程式碼進行一些變更,讓元件可以決定何時停止。元件會在停止前保留狀態和控制代碼。保留這類資料稱為「託管」。

  • 元件的用戶端不會知道元件已停止。以這種方式停止元件,不會中斷與元件的 FIDL 連線。

  • Fuchsia 提供的程式庫可讓您監控 FIDL 連線和傳出目錄連線何時閒置,並在發生這種情況時將這些連線轉回控制代碼。

  • 元件架構提供 API,可讓元件儲存控制代碼和資料,並在下次執行時擷取這些資料,通常是在控制代碼可讀取或收到新的能力要求時。我們會在後續章節詳細說明運作方式。

  • Fuchsia 快照和 Cobalt 資訊主頁會提供實用的生命週期指標。

哪些元件適合遷移?

建議您查看具有下列特性的元件:

  • 流量不穩定。元件可以啟動並處理這些流量,然後在完成後返回停止狀態。啟動和更新路徑中的許多元件只有在這些時候才需要,但其他時候則會閒置並浪費 RAM,例如 core/system-update/system-updater。

  • 不會過度依賴狀態。您可以在元件停止前保留狀態。在極限情況下,我們可以編寫程式碼來保留所有重要狀態。在實務上,我們會在節省記憶體和保存必要狀態的複雜度之間做出取捨。

  • 記憶體用量偏高。使用 ffx profile memory 查看元件的記憶體用量。舉例來說,在一般系統上,console-launcher.cm 使用 732 KiB 的私人記憶體。私人記憶體是只有該元件參照的記憶體,因此我們保證在停止該元件時,至少會釋出該數量的記憶體。請參閱「評估記憶體用量」。

    Process name:         console-launcher.cm
    Process koid:         2222
    Private:              732 KiB
    PSS:                  1.26 MiB (Proportional Set Size)
    Total:                3.07 MiB (Private + Shared unscaled)
    

http-client.cm 是一個範例元件,不會在 HTTP 載入器連線中保留狀態,且僅用於上傳指標和當機資料。因此,我們已調整這項功能,只要設定完成,就會在閒置時停止。

已知限制

  • Hanging-get:如果元件是 Hanging-get FIDL 方法的伺服器或用戶端,則很難保留該連線,因為 FIDL 繫結無法儲存及還原進行中呼叫的相關資訊。您可以將該 FIDL 方法轉換為事件和單向確認。

  • 目錄:如果元件提供目錄通訊協定,則很難保留該連線,因為目錄通常由 VFS 程式庫提供。VFS 程式庫目前不公開取回基礎管道和相關聯狀態 (例如搜尋指標) 的方法。

只要有充分理由,這些都能獲得支援。您可以與元件架構團隊聯絡,說明您的用途。

偵測閒置狀態

如要停止閒置元件,第一步是強化該元件的程式碼,以便瞭解元件何時進入閒置狀態,也就是:

  • FIDL 連線處於閒置狀態:元件通常會宣告多個 FIDL 通訊協定能力,用戶端會在需要時連線至這些通訊協定。這些連線不應有需要元件處理的待處理訊息。

  • 傳出目錄閒置:元件會提供傳出目錄,發布傳出功能。不應有代表此元件能力要求的待處理訊息,且除了 component_manager 建立的連線外,不應有其他連線連至傳出目錄。

  • 其他背景商業邏輯:舉例來說,如果元件在背景發出網路要求以回應 FIDL 方法,除非該網路要求已完成,否則我們可能不會將該元件視為閒置。如果該元件在要求處理期間停止,可能會有安全疑慮。

我們有 Rust 程式庫,可偵測各種情況下的閒置狀態。https://fxbug.dev/332342122 追蹤 C++ 元件的相同功能。

偵測閒置的 FIDL 連線

您可以使用 detect_stall::until_stalled,將 Rust FIDL 要求串流轉換為在連線閒置超過指定逾時時間時,自動解除繫結 FIDL 端點的串流。您必須將元件新增至 src/lib/detect-stall/BUILD.gn 的瀏覽權限清單。詳情請參閱 API 文件和測試。http-client.cm 的使用方式如下:

async fn loader_server(
    stream: net_http::LoaderRequestStream,
    idle_timeout: fasync::Duration,
) -> Result<(), anyhow::Error> {
    // Transforms `stream` into another stream yielding the same messages,
    // but may complete prematurely when idle.
    let (stream, unbind_if_stalled) = detect_stall::until_stalled(stream, idle_timeout);

    // Handle the `stream` as per normal.
    stream.for_each_concurrent(None, |message| {
        // Match on `message`...
    }).await?;

    // The `unbind_if_stalled` future will resolve if the stream was idle
    // for `idle_timeout` or if the stream finished. If the stream was idle,
    // it will resolve with the unbound server endpoint.
    //
    // If the connection did not close or receive new messages within the
    // timeout, send it over to component manager to wait for it on our behalf.
    if let Ok(Some(server_end)) = unbind_if_stalled.await {
        // Escrow the `server_end`...
    }
}

偵測閒置的傳出目錄

您可以使用 fuchsia_component::server::ServiceFs::until_stalled 方法, 將 ServiceFs 轉換為在檔案系統中沒有工作時,自動解除繫結傳出目錄伺服器 端點的 。詳情請參閱 API 文件和測試。http-client.cm 的使用方式如下:

#[fuchsia::main]
pub async fn main() -> Result<(), anyhow::Error> {
    // Initialize a `ServiceFs` and add services as per normal.
    let mut fs = ServiceFs::new();
    let _: &mut ServiceFsDir<'_, _> = fs
        .take_and_serve_directory_handle()?
        .dir("svc")
        .add_fidl_service(HttpServices::Loader);

    // Chain `.until_stalled()` before calling `.for_each_concurrent()`.
    // This wraps each item in the `ServiceFs` stream into an enum of either
    // a capability request, or an `Item::Stalled` message containing the
    // outgoing directory server endpoint if the filesystem became idle.
    fs.until_stalled(idle_timeout)
        .for_each_concurrent(None, |item| async {
            match item {
                Item::Request(services, _active_guard) => {
                    let HttpServices::Loader(stream) = services;
                    loader_server(stream, idle_timeout).await;
                }
                Item::Stalled(outgoing_directory) => {
                    // Escrow the `outgoing_directory`...
                }
            }
        })
        .await;
}

等待其他背景商業邏輯

ServiceFs 產生 Item::Stalled 訊息後,就不會再產生更多能力要求。如果背景工作導致元件無法停止,但 ServiceFs 同時進入閒置狀態,並提早解除繫結傳出目錄端點,就可能發生問題。如要處理這類情況,您可以防止 ServiceFs 進入閒置狀態。ServiceFs 產生的 Item::Request 包含 ActiveGuard。只要範圍內有有效的防護機制,ServiceFs 就不會進入閒置狀態,並會持續產生收到的能力要求。

同樣地,您可以建立 ExecutionScope,產生與處理 FIDL 連線相關的所有背景工作,並呼叫 ExecutionScope::wait() 等待工作完成。舉例來說,http-client.cm 中的 loader_server 函式在背景工作完成前不會傳回,這會使 Item::Request 中的 active_guard 保持在範圍內,防止 ServiceFs 停止。

將控點和狀態託管給架構

連線閒置且程式庫提供未繫結的伺服器端點後,下一步是託管這些控制代碼,也就是將控制代碼傳送至元件架構妥善保管。

無狀態通訊協定

注意:通訊協定必須標示為 @discoverable,才能支援託管。這樣一來,元件從閒置狀態喚醒後,就能重新連線至管道。

部分 FIDL 連線不會傳輸狀態。無論是透過相同連線或不同連線傳送,每項要求的功能都相同。如要建立這類連結,請按照下列步驟操作:

  • 如果尚未在元件資訊清單中宣告功能,請先完成這項操作。如果這個通訊協定連線衍生自其他連線,且通常不會從傳出目錄提供,您可能需要宣告這項能力。

  • 宣告能力時,請新增 delivery: "on_readable"。接著,架構會監控新連線要求在伺服器端點上的可讀取信號,並在有待處理訊息時,將伺服器端點連線至供應器元件。範例:

    capabilities: [
        {
            protocol: "fuchsia.net.http.Loader",
            delivery: "on_readable",
        },
    ],
    
  • 從 self 為能力新增使用宣告,讓程式可以從傳入命名空間連線到該功能。您可以在 /escrow 目錄中安裝能力,以便與元件使用的其他功能區分。範例:

    {
        protocol: "fuchsia.net.http.Loader",
        from: "self",
        path: "/escrow/fuchsia.net.http.Loader",
    },
    
  • 從傳入命名空間連線至能力,並傳遞 detect_stalled::until_stalled 的未繫結伺服器端點。

    if let Ok(Some(server_end)) = unbind_if_stalled.await {
        // This will open `/escrow/fuchsia.net.http.Loader` and pass the server
        // endpoint obtained from the idle FIDL connection.
        fuchsia_component::client::connect_channel_to_protocol_at::<net_http::LoaderMarker>(
            server_end.into(),
            "/escrow",
        )?;
    }
    

整體而言,這表示元件架構會監控閒置連線,直到連線再次可讀取,然後在發生這種情況時將該能力傳回元件。如果元件已停止,這項操作會啟動元件。

傳出目錄

我們必須使用不同的 API 來託管主要傳出目錄連線 (即 Item::Stalled 中 ServiceFs 傳回的連線),因為該伺服器端點是所有其他連線連至元件的進入點。如果是 ELF 元件,您可以透過 fuchsia.process.lifecycle/Lifecycle.OnEscrow FIDL 事件將傳出目錄傳送至架構:

  • 將 lifecycle: { stop_event: "notify" } 新增至元件 .cml:

    program: {
        runner: "elf",
        binary: "bin/http_client",
        lifecycle: { stop_event: "notify" },
    },
    
  • 取得生命週期編號控制代碼,將其轉換為 FIDL 要求串流,然後使用 send_on_escrow 傳送事件:

          let lifecycle =
        fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::Lifecycle, 0)).unwrap();
    let lifecycle: zx::Channel = lifecycle.into();
    let lifecycle: ServerEnd<flifecycle::LifecycleMarker> = lifecycle.into();
    let (lifecycle_request_stream, lifecycle_control_handle) =
        lifecycle.into_stream_and_control_handle();
    
    let outgoing_dir = None;
    // Later, when `ServiceFs` has stalled and we have an `outgoing_dir`.
    lifecycle_control_handle
        .send_on_escrow(flifecycle::LifecycleOnEscrowRequest { outgoing_dir, ..Default::default() })
        .unwrap();
    

    元件傳送 OnEscrow 事件後,就無法監控更多能力要求。因此,它應該在之後立即退出。下次執行時,元件會在啟動資訊中取回與上次執行時傳送的相同 outgoing_dir 控制代碼。

    如要瞭解如何整合這些項目,請參閱 http-client。

檢查資料

元件停止後,系統就不會再提供其 Inspect VMO。不過,您可以使用架構託管凍結的 Inspect 資料寫入時複製版本。這樣一來,即使元件停止運作,診斷工具仍可讀取元件檢查樹狀結構的最後狀態。

inspect_runtime::publish 函式會傳回 PublishedInspectController。在元件停止前,您可以對這個控制器呼叫 escrow_frozen 方法。這會為架構提供元件 Inspect VMO 的凍結副本。

以下是使用範例:

// When initializing your component...
let inspector = finspect::Inspector::default();
let inspect_controller =
    inspect_runtime::publish(&inspector, inspect_runtime::PublishOptions::default());

// ...

// When your component is about to stop...
if let Some(controller) = inspect_controller {
    let frozen_inspect_token = controller
        .escrow_frozen(inspect_runtime::EscrowOptions::default())
        .await;
    // The token can be stored in the escrowed dictionary if needed,
    // though often just escrowing is sufficient.
}

有狀態通訊協定和其他重要狀態

fuchsia.process.lifecycle/Lifecycle.OnEscrow 事件會採用另一個引數,即 escrowed_dictionary_handle zx.Handle:EVENTPAIR,這是 Dictionary 物件的參照。字典是鍵/值對應,可保存資料或功能。

  • 您可以使用架構中的 fuchsia.component.runtime.Capabilities 建立 Dictionary (例如透過 Rust 中的 fuchsia_component::runtime::Dictionary::new):

    use: [
        {
            protocol: "fuchsia.component.runtime.Capabilities",
            from: "framework",
        }
    ]
    
          let dictionary = Dictionary::new().await;
    
  • 您可以對 Dictionary 呼叫 insert,將部分資料 (例如位元組向量) 新增至 Dictionary。如需其他方法,請參閱 fuchsia.component.runtime FIDL 程式庫說明文件:

          let bytes = vec![1, 2, 3];
    let data = Data::new(DataValue::Bytes(bytes)).await;
    dictionary.insert("my_data", data).await;
    
  • 在結束前,請在 send_on_escrow 中傳送 Dictionary 控制代碼:

          lifecycle_control_handle.send_on_escrow(flifecycle::LifecycleOnEscrowRequest {
        outgoing_dir,
        escrowed_dictionary_handle: Some(dictionary.handle),
        ..Default::default()
    })?;
    
  • 下次啟動時,您可以從啟動控制代碼取得這個字典:

          let Some(dictionary) =
        fuchsia_runtime::take_startup_handle(HandleInfo::new(HandleType::EscrowedDictionary, 0))
    else {
        return Err(anyhow!("Couldn't find startup handle"));
    };
    
    let dictionary = Dictionary::from(zx::EventPair::from(dictionary));
    let Some(Capability::Data(data)) = dictionary.remove("my_data").await else {
        return Err(anyhow!("Bad capability type from dictionary"));
    };
    let DataValue::Bytes(bytes) = data.get_value().await else {
        return Err(anyhow!("Bad data value from dictionary"));
    };
    // Do something with the data...
    

Dictionary 物件支援多種項目資料類型。如果元件的狀態小於 fuchsia.component.runtime/MAX_DATA_LENGTH,建議您儲存 fuchsia.component.runtime/Data 項目,該項目可保留位元組向量。

我想等待管道可供讀取

在停止之前,如果您想安排元件架構等待管道可讀取,然後將管道傳回元件,可以使用相同的 delivery: "on_readable" 技術。這項原則也適用於元件未公開的 FIDL 通訊協定,例如服務成員。甚至支援不使用 FIDL 通訊協定的管道。舉例來說,假設元件含有 Zircon 例外狀況管道,且需要指示架構等待該管道可供讀取,然後啟動元件,則可宣告下列 .cml:

capabilities: [
    {
        protocol: "exception_channel",
        delivery: "on_readable",
        path: "/escrow/exception_channel",
    },
],
use: [
    {
        protocol: "exception_channel",
        from: "self",
        path: "/escrow/exception_channel",
    }
]

請注意,exception_channel 能力不會公開。這項能力由元件本身使用。元件可能會從傳入的命名空間開啟 /escrow/exception_channel,並指定要等待的管道。當該管道可讀取時,架構會在傳出目錄中開啟 /escrow/exception_channel,並視需要啟動元件。總之,您可以宣告功能,並從 self 使用這些功能,將控制代碼託管至 component_manager。

如果您需要其他類型的觸發條件,例如等待自訂信號或等待計時器,請與元件架構團隊聯絡。

測試

建議您強化現有的整合測試,一併測試元件是否能自行停止並重新啟動,而不會中斷 FIDL 連線。如果您已有整合測試,可啟動元件並傳送 FIDL 要求,則可使用元件事件比對器,驗證元件在沒有訊息時是否會停止。如需操作範例,請參閱 http-client 測試。

到達網頁和指標

如要針對特定產品最佳化這個元件,可以在元件中加入結構化設定,控管是否要設定閒置逾時,以及逾時時間長度。

元件架構會記錄元件在執行期間啟動和停止的時間,並上傳至 Cobalt。您可以在這個資訊主頁查看這些指標,微調閒置逾時設定。

擷取意見回饋快照時 (例如在欄位中遇到錯誤時),初始和最新元件執行的時間戳記會分別顯示在選取器 <component_manager>:root/lifecycle/early 和 <component_manager>:root/lifecycle/late。您可以將這些事件與其他錯誤記錄建立關聯,協助調查錯誤是否因元件不當停止而造成。