RFC-0069:ELF 執行器中的標準 I/O

RFC-0069:ELF Runner 中的標準 I/O
狀態已接受
區域
  • 元件架構
說明

機制可讓 ELF 元件將 stdout 和 stderr 串流轉送至 LogSink 服務。

Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2021-02-02
審查日期 (年-月-日)2021-02-17

摘要

導入兩個新的旗標 forward_stdout_toforward_stderr_to,針對在啟動時想要接收 stdout 和/或 stderr 串流的元件,控制選擇加入的行為。啟用後,系統會使用 LogSink 服務備份串流。

提振精神

啟動元件管理工具時,其 stdout 和 stderr 串流會繫結至 debuglog,因為啟動程序的這個階段沒有其他選項。舉例來說,Archivist 是提供 LogSink 服務的元件,本身是由元件管理員啟動。

最近之前,元件管理員會將所有元件的 stdout 和 stderr 串流重新導向至核心的 debuglog。這項功能是透過複製元件管理員的 stdout 和 stderr 句柄,並將其繫結至調試記錄而實現的。不過,這會造成一些問題。首先,使用者模式元件不應寫入偵錯記錄,因為這項功能是保留給核心使用和高度特權的使用者模式元件。相反地,大多數使用者模式元件應寫入 LogSink 服務。第二,提供兩個外送串流,但未明確選擇加入,違反了最低權限原則

目前元件管理工具完全缺少這項功能,我們希望能將這項功能帶回,同時解決先前存在的兩個缺點。我們建議您在 ELF 執行器中新增旗標,以便明確選擇加入,而不是對所有元件隱含授予 stdout 和/或 stderr。

元件架構團隊正在進行長期的遷移作業,從 appmgr (元件 v1) 遷移至元件架構 (Components v2)。這項計畫的主要專案之一,就是遷移網路堆疊團隊擁有的所有元件。stdout/stderr 支援是遷移所有這些元件的必要條件。這項規定的原因是網路堆疊元件是以 Go 編寫。Go 程式與以 C++ 或 Rust 編寫的程式不同,會在執行階段環境中執行,在開發人員編寫的程式開始執行前,會在設定期間將錯誤傳送至 stderr。由於錯誤會在程式進入點之前記錄,Go 元件作者無法將 stdout 和 stderr 句柄繫結至記錄服務。為瞭解決這個問題,我們可以分支並修改 Go 的執行階段,在開始執行使用者編寫的 Go 程式之前,先加入必要的記錄初始化作業。這當然會為我們維護 Go 分支的技術負擔帶來重大影響。或者,元件管理器可以在元件 (和 Go 執行階段) 啟動前,將 stdout 和 stderr 句柄繫結至記錄服務,讓 Fuchsia 的記錄服務擷取錯誤訊息。

更廣泛來說,我們希望降低上述 v1 至 v2 遷移作業的技術負擔。目前,appmgr 會為所有 v1 元件提供 stdout 和 stderr 句柄,並將這些句柄導向至 debuglog。因此,我們有理由認為 Fuchsia 中的許多開發人員都會依賴這項功能。隨著我們在 2021 年開始遷移越來越多的元件,我們應允許開發人員維護所需的 stdout 和 stderr 支援。

需求條件

備援記錄服務必須是 LogSink,而非 debuglog。我們必須改用 LogSink 的原因有很多。首先,如上所述,debuglog 是用於核心使用。其次,debuglog 會為所有程序使用小型 (128kb) 共用環緩衝區,並依 FIFO 原則輪替訊息。Archivist 會定期排空這些訊息,並轉送至 LogSink。不過,如果在 Archivist 將這些記錄讀入 LogSink 之前,就從調試記錄緩衝區推出,這些記錄就可能會「遺失」。使用 LogSink 的 stdout 和 stderr 記錄服務,不僅可避免訊息遭到捨棄,還能降低使用 debuglog 的其他元件和程序遭到捨棄的機率。此外,當緩衝區的旋轉速度比排空速度快時,系統並未提供任何機制來追蹤遺失的資料量。第三,debuglog 不支援嚴重性層級 (例如 DEBUG、INFO 等)。這是必要的條件,因為我們需要區分 stdout 訊息和 stderr 訊息。就記錄而言,我們只能將每個輸出串流對應至特定嚴重性層級。

設計

這項提案是針對 ELF 元件 forward_stdout_toforward_stderr_to,在 program 節中引入兩個新的列舉值。對於 ELF 元件,這個列舉是選用的,預設為 none。如果沒有,ELF 執行元件就不會將 stdoutstderr 句柄繫結至 LogSink 服務 (目前的行為),且這些串流將繼續遭到忽略。當這些值設為 log 時,ELF 執行元件會建立一個 Socket,用於擷取 stdoutstderr 串流的輸出內容。然後將讀取的位元組轉送至 LogSink 服務。對於 stdout,它會傳送 INFO 訊息;對於 stderr,它會傳送 WARN 訊息。訊息會以換行符號分隔,每行都會分割為 LogSink 服務的獨立訊息。LogSink 服務的訊息大小上限為 32 KB,因此我們會將位元組串流緩衝區設為 30 KB (以便為訊息中繼資料保留一些空間)。超出該界線的位元組會遭到捨棄,只有部分訊息會傳送至 LogSink 服務。這會產生一個有趣的極端案例,部分程式碼點可在 30 KB 邊界處分割。在這種情況下,系統不會執行任何特殊處理,並將碼點無效的前半部解碼。所有輸入位元組都會使用 String::from_utf8_lossy 解碼為 UTF-8。根據這個函式的 API,所有無效的 UTF-8 序列都會替換為 U+FFFD REPLACEMENT CHARACTER

{
    program: {
        "runner": "elf",
        "forward_stdout_to": "log",
        "forward_stderr_to": "log",
    }
}

實作

由於這項功能將限制於 ELF 執行元件,因此實作時所需的變更相當有限。我們預估實施這項變更時,最多只需要 2 個 CL。

成效

由於這類行會直接傳送至 LogSink,因此效能成本極低。這項功能會引入最顯著的開銷,包括剖析位元組串流和分割換行符號。不過,由於記錄是一種不規則的作業,而位元組串流本身往往較短,因此這項額外負擔的成本相對較低。更重要的是,這只會影響明確選擇採用此行為的元件。因此,如果確實發生效能問題,只要在這些元件中將 forward_stdout_toforward_stderr_to 設為 none,即可快速解決問題,暫時回到原點,直到我們解決了潛在的效能問題為止。

安全性考量

Archivist 已設有歸因和流程控制機制,因此不良元件無法透過垃圾郵件形式的 stdout 拒絕服務。因此您不需要採取其他行動。不過,請注意,這項功能提供一種機制,可將任意位元組放入另一個程序的位址空間 (從元件到 Archivist)。如果緩衝區太大,可能會造成問題,但我們在上述內容中已提到緩衝區的緩解措施。此外,由於這項實作只會以最少的程度處理輸入串流,因此可降低權限提升的風險。如果這是複雜的剖析器,發生錯誤和安全漏洞的可能性就會增加。

隱私權注意事項

LogSink 後端和所有 LogSink 用戶端都符合隱私權規定,因為記錄會歸因於來源,且已採用足夠的 PII 清除機制。因此不需要額外進行設定。

測試

除了單元測試之外,我們也會加入整合測試,確保記錄會寫入 Archivist。這些整合測試將使用所有支援的語言編寫:C/C++、Rust 和 Go。由於 ELF 執行元件不會處理 Dart 元件執行作業,因此不會測試 Dart。

說明文件

這個標記會在「Components v2 說明文件的 ELF 執行程式部分中說明。

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

編號控點

我們已嘗試從程序架構將編號句柄提升至元件架構,以便支援這項功能。雖然我們決定不採用,但資訊清單檔案的粗略設計如下:

{
    program: {
        "runner": "elf",
        "handles": ["STDOUT", "STDERR"]
    }
}

ELF 執行元件會讀取常數字串,並在內部將其對應至適當的編號句柄。我們最終決定不採用這個做法,因為我們不清楚其他編號帳號代碼的立即用途。此外,如果我們確實找到應在 ELF 執行元件的 program 節中設定的另一個編號句柄,我們可以輕鬆更新資訊清單檔案語法和 ELF 執行元件實作。

元件管理服務

我們也嘗試為編號句柄引入新的架構層級能力。資訊清單檔案的粗略設計如下:

{
    use: [
        {
            "handle": "stdout-to-log",
            "from": "framework",
            "number": "STDOUT",
        },
        {
            "handle": "stderr-to-log",
            "from": "framework",
            "number": "STDERR",
        }
    ]
}

基於上述關於編號句柄的原因,以及在元件管理員層級引入此做法會導致與元件管理員的 POSIX 相容性相關的新問題,因此我們認為這種做法不理想。舉例來說,是否所有執行緒都必須實作這項功能?對於不使用 stdout/stderr 的執行元件 (例如「web」執行元件項),這會是什麼樣子?因此,我們決定將 POSIX 相容性問題視為單獨的工作流程,不屬於本 RFC 的範圍。

推出新元件

我們也探討了如何在由元件架構團隊擁有及管理的新元件中,實作轉譯層,也就是解析 stdout/stderr 位元組串流並將其轉送至 LogSink 服務的部分。不過,經過多次討論後,我們決定這種做法不切實際,因為我們必須設計一種方法,在記錄訊息時保留記錄歸因。Archivist 會使用事件能力取得元件來源資訊 (例如路徑名稱),如果使用中介層元件,所有資訊都會遺失。

FDIO

我們也嘗試使用 Fuchsia 的 POSIX 相容性程式庫 fdio 來實作這項功能。也就是說,建立新類型來辨識 stdout/stderr 檔案描述符,並在內部 (在 fdio 中) 將輸出內容重新導向至 LogSink。不過,經過多次討論後,我們決定放棄修改 fdio,因為這樣做會使導入作業更加困難。我們發現,在 POSIX 相容性方面,有些極端情況無法透過 fdio 中的 LogSink 轉送器實作。此外,以 fdio 為基礎的實作方式會產生更多不確定性,並造成重複作業。或者,如果我們使用上述的通訊端,就會「預設」符合 POSIX 標準。