| RFC-0069:ELF Runner 中的標準 I/O | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | ELF 元件將 stdout 和 stderr 串流轉送至 LogSink 服務的機制。 |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2021-02-02 |
| 審查日期 (年-月-日) | 2021-02-17 |
摘要
導入兩個新旗標 forward_stdout_to 和 forward_stderr_to,可控制元件的選擇加入行為,以便在啟動時接收 stdout 和/或 stderr 串流。啟用後,系統會使用 LogSink 服務備份串流。
提振精神
啟動元件管理員時,由於啟動程序的這個階段沒有替代方案,因此 stdout 和 stderr 串流會繫結至 debuglog。舉例來說,提供 LogSink 服務的元件 Archivist 本身是由元件管理員啟動。
最近之前,元件管理工具會將所有元件的 stdout 和 stderr 串流重新導向至核心的 debuglog。這是透過複製元件管理員本身的 stdout 和 stderr 控制代碼達成,這些控制代碼本身會繫結至 debuglog,如上所述。不過,這會造成一些問題。首先,使用者模式元件不應寫入 debuglog,因為這是保留給核心使用和高權限使用者模式元件的,大多數使用者模式元件應改為寫入 LogSink 服務。其次,未經明確同意就提供兩個輸出串流,違反了最小權限原則。
目前元件管理工具完全沒有這項功能,我們希望重新推出這項功能,同時解決先前存在的兩項缺點。我們建議在 ELF 執行器中新增旗標,以明確選擇加入的方式,取代隱含授予所有元件 stdout 和/或 stderr 的做法。
Component Framework 團隊目前正進行長期遷移作業,從 appmgr (Components v1) 遷移至元件架構 (Components v2)。這項工作的主要專案之一,是遷移網路堆疊團隊擁有的所有元件。stdout/stderr 支援是遷移所有這些元件的前提。這項要求的原因是網路堆疊元件是以 Go 編寫。與以 C++ 或 Rust 編寫的程式不同,Go 程式會在執行階段環境中執行,並在開發人員編寫的程式開始執行前,於設定期間將錯誤傳送至 stderr。由於錯誤是在程式的進入點之前記錄,因此 Go 元件的作者無法將 stdout 和 stderr 控制代碼繫結至記錄服務。如要解決這個問題,我們可以分叉並修改 Go 的執行階段,在開始執行使用者編寫的 Go 程式之前,加入必要的記錄初始化作業。當然,維護 Go 分支會為我們帶來重大的技術負擔。或者,元件管理員可以在啟動元件 (和 Go 執行階段) 之前,將 stdout 和 stderr 控制代碼繫結至記錄服務,這樣一來,Fuchsia 的記錄服務就能擷取錯誤訊息。
更廣泛來說,我們希望減輕上述 v1 -> v2 遷移作業的技術負擔。目前,appmgr 會將 stdout 和 stderr 控制代碼提供給所有 v1 元件,並將這些控制代碼路由至 debuglog。因此,我們有理由相信 Fuchsia 內許多開發人員都依賴這項功能。隨著我們進入 2021 年,並開始遷移越來越多的元件,我們應允許開發人員維護他們所依賴的 stdout 和 stderr 支援。
需求條件
支援的記錄服務必須是 LogSink,而非 debuglog。我們必須改用 LogSink 的原因有幾個。首先,如上所述,debuglog 適用於核心用途。其次,debuglog 會為所有程序使用小型 (128 KB) 共用環形緩衝區,並以先進先出 (FIFO) 為基礎輪替訊息。Archivist 會定期排空這些訊息,並轉送至 LogSink。不過,如果 Archivist 將這些記錄讀取到 LogSink 前,這些記錄就從 debuglog 緩衝區推出,就可能會「遺失」。使用 LogSink 進行 stdout 和 stderr 記錄服務,不僅可避免訊息遭到捨棄,還能降低其他使用 debuglog 的元件和程序捨棄訊息的機率。此外,如果緩衝區的輪替速度快於排空速度,系統不會追蹤損失的資料量。第三,debuglog 不支援嚴重程度層級 (例如 DEBUG、INFO 等),這是重要需求,因為我們需要區分 stdout 訊息和 stderr 訊息。就記錄檔而言,我們只能將每個輸出串流對應至特定嚴重程度。
設計
提案是為 ELF 元件的 program 節引入兩個新的列舉值,即 forward_stdout_to 和 forward_stderr_to。對於 ELF 元件,列舉為選用項目,預設值為 none。如果沒有,ELF 執行元件不會將 stdout 和 stderr 控制代碼繫結至 LogSink 服務 (目前行為),且系統會繼續忽略這些串流。如果將這些值設為 log,ELF 執行元件就會建立插座,擷取 stdout 和 stderr 串流的輸出內容。然後將讀取的位元組轉送至 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_to 和 forward_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,例如「網頁」執行元件器,會是什麼樣子?因此,我們決定將 POSIX 相容性問題做為獨立的工作流程,不納入本 RFC 的範圍。
全新元件登場
我們也探討了實作翻譯層,也就是剖析 stdout/stderr 位元組串流並轉送至 LogSink 服務的部分,這項作業由元件架構團隊擁有及管理的新元件負責。不過,經過多次討論後,我們認為這種做法不可行,因為在記錄訊息時,我們必須設計一種方式來保留記錄歸因。封存工具會使用事件能力取得元件來源資訊 (例如路徑名稱),如果使用中介層元件,所有這類資訊都會遺失。
FDIO
我們也探討了如何使用 fdio (Fuchsia 的 POSIX 相容性程式庫) 實作這項功能。也就是說,建立可辨識 stdout/stderr 檔案描述元的全新型別,並在內部 (fdio 內) 將輸出內容重新導向至 LogSink。不過,經過多次討論後,我們決定放棄修改 fdio,因為這樣會增加實作難度。我們發現 fdio 中 LogSink 轉送程式無法實作 POSIX 相容性的極端情況。此外,以 fdio 為基礎的實作方式會產生更多不確定性,並導致重複作業。或者,如果我們使用上述建議的通訊端,它會「開箱即用」,符合 POSIX 標準。