RFC-0196:FIDL 大型郵件

RFC-0196:FIDL 大型訊息
狀態已接受
區域
  • FIDL
說明

支援透過 FIDL 通訊協定傳送大型訊息。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-06-28
審查日期 (年-月-日)2022-10-27

摘要

目前,FIDL 語言會將管道 (例如管道和通訊端) 中訊息的大小限制在 64 KiB。本文提出高階設計,可處理任意大小的訊息,即使這些訊息會溢出基礎傳輸工具的最大訊息位元組限制也一樣。這項功能的實現方式是將類似於常見現有模式的解決方案 (使用 fuchsia.mem.Data) 提升為第一級,自動推斷 FIDL 語言支援。

提振精神

目前透過線路傳送的所有 FIDL 訊息都會受到大小限制,大小上限為 ZX_CHANNEL_MAX_MSG_BYTES,目前相當於 64 KiB,並衍生自可透過 zircon 管道傳送的訊息最大大小。超過此限制的訊息會無法編碼。

目前解決這項問題的常見方法,是透過 zx.handle:VMOfuchsia.mem.Data 傳送任意大小的資料 Blob,而基礎 VMO 本身則包含要傳送的資料 Blob。這些 Blob 通常包含結構化資料,但使用者無法將這些資料以 FIDL 的形式呈現及編碼/解碼,因此必須手動轉換。目前 fuchsia.git 中大量使用這些包裝函式類型。

由於缺乏大型訊息支援功能,因此會發生一些問題。其中最主要的問題是,由於通訊協定很少需要傳送大型訊息,但在技術上能夠傳送,因此導致大量錯誤。例如,在 WiFi 掃描期間產生的網址或網路清單就非常龐大。每個需要採用 :MAX 大小 vectorstring 的 API 都可能會發生這個問題,其他極端情況也可能會發生這個問題,例如 table 版面配置,因為只有極少情況下會填入所有欄位。一般來說,任何需要以訊息形式接受使用者資料 (且無法證明其大小小於 64KiB) 的項目,都可能受到這種失敗模式的影響。

透過 VMO 傳送未指定類型的資料 Blob 並不符合人體工學,因為這會導致所有類型資訊遺失,必須在接收端手動重建。使用者必須自行對訊息進行編碼,將其套件為另一個 FIDL 訊息,然後在另一端反向重複這個程序,而非利用 FIDL 描述資料的形狀,並抽象化編碼->傳送->解碼->調度管道。舉例來說,ProviderInfo API 有子類型 InspectConfigInspectSource,目前分別以 fuchsia.mem.Bufferzx.handle:VMO 表示,但代表可由 FIDL 描述及處理的結構化資料。

使用 zx.handle:VMOfuchsia.mem.Data 會導致僅限資料 FIDL 類型強制攜帶 resource 修飾符。這會對綁定 API 產生後續影響,導致 Rust 等語言中產生的型別無法擷取 Clone 特徵,即使這些型別理應擷取該特徵也一樣。

由於大型郵件支援功能不足,導致的錯誤和人體工學問題相當普遍。在起草本 RFC 期間進行的調查中,至少有三十個過去和現在的案例,顯示更強大的大型訊息支援功能可協助 FIDL 使用者。

相關人員

導師:hjfreyer@google.com

審查者:abarth@google.com、chcl@google.com、cpu@google.com、mseaborn@google.com、nickcano@google.com、surajmalhotra@google.com

諮詢對象:bprosnitz@google.com、geb@google.com、hjfreyer@google.com、jmatt@google.com、tombergan@google.com、yifeit@google.com

社群化:五個團隊 (元件解析器、DNS 解析器、驅動程式庫開發、WLAN SME 和 WLAN 政策) 已審查使用此設計的原型:

  • 元件解析工具:geb@google.com
  • DNS 解析器:dhobsd@google.com
  • 驅動程式開發:dgilhooley@google.com
  • WLAN 政策:nmcracken@google.com
  • WLAN SME:chcl@google.com

此外,我們也訪談了超過三十位現有 fuchsia.mem.Datafuchsia.mem.Buffer 類型的使用者,瞭解他們對設計的意見回饋和用途適切性。

設計

本文件中的「MUST」、「MUST NOT」、「REQUIRED」、「SHALL」、「SHALL NOT」、「SHOULD」、「SHOULD NOT」、「RECOMMENDED」、「MAY」和「OPTIONAL」等關鍵字,應按照 IETF RFC 2119 的說明進行解讀。

訊息溢位是傳輸層級問題。在 zircon 管道、驅動程式庫程式架構、overnet 等方面,構成大型訊息的條件以及如何最佳處理符合該定義的訊息,差異極大。也就是說,呼叫特定方法要求或回應「large」並非抽象陳述式:該方法擁有的通訊協定必須「一定」定義「large」的確切定義。

以下說明訊息如何宣告「我相對於運送我的傳輸工具來說太大,因此需要特別處理」:此宣告「必須」*.fidl 檔案中定義通訊協定的介面定義時間,以及在執行階段針對任何實際溢出的訊息例項時,都必須可讀。

具體來說,在這個設計推出前,寄件者可能會傳送超出基礎傳輸機制最大訊息位元組限制的有效訊息,進而導致難以偵錯的意外 PEER_CLOSED 執行階段錯誤。進行這些變更後,fidlc 編譯器會進行靜態檢查,確認酬載類型是否可能大於傳輸的最大訊息位元組限制,如果是的話,就會產生特殊的「溢位」處理程式碼來處理這項情況。這個模式會為大型訊息啟用次要的執行階段訊息傳遞機制,藉此使用無界限的側邊通道 (在 Zircon 通道的情況下,為 VMO) 來儲存訊息內容。這條新的訊息傳送路徑會完全新增至產生的繫結程式碼的「核心」,藉此維持 API 和 ABI 相容性。FIDL 方法實作者現在可以放心,因為系統不會因為任意位元組大小限制而觸發可配置訊息 PEER_CLOSED

線路格式變更

系統會將稱為 byte_overflow 標記的新位元新增至 FIDL 交易訊息標頭動態標記部分。當這個旗標翻轉時,表示目前保留的訊息只包含訊息的控制層,而訊息的其餘部分則儲存在可能不連續的獨立緩衝區中。

這個獨立緩衝區的位置,以及如何存取緩衝區,取決於傳輸方式。如果 byte_overflow 標記處於啟用狀態,傳輸中控制平面訊息「必須」包含 16 個位元組的交易訊息標頭,接著再加上 16 個位元組的附加訊息,說明大型訊息的大小。也就是說,此訊息「必須」為 32 個位元組:預設的 FIDL 訊息標頭,後面接著所謂的 message_info 結構體,其中包含三個資料片段:用於標記的 uint32、用於排除溢位緩衝區時可能指定附加至訊息的句柄數量的 uint32,以及用於指出 VMO 本身中資料大小的 uint64

type MessageInfo = struct {
  // Flags pertaining to large message transport and decoding, to be used for
  // the future evolution and migration of this feature.
  // As of this RFC, this field must be zeroed out.
  flags uint32;
  // A reserved field, to be potentially used for storing the handle count in
  // the future.
  // As of this RFC, this field must be zeroed out.
  reserved uint32;
  // The size of the encoded FIDL message in the VMO.
  // Must be a multiple of FIDL alignment.
  msg_byte_count uint64;
};

由於需要產生額外的句柄,才能指向溢位緩衝區,因此大型 FIDL 訊息可能只會附加 63 個句柄,而不是通常的 64 個。這種行為對使用者來說不夠優雅,且會讓他們感到意外,因此只會透過執行階段錯誤回報。為了在日後修正尖銳邊緣,我們致力於開發核心改善項目,以彌補這個不幸的特殊情況。

byte_overflow 旗標「必須」占用動態旗標位元陣列中的位元 #6 (即倒數第二個最重大位元)。位元 #5 是保留給未來可能的 handle_overflow 位元,但目前未使用。這個位元「不得」用於其他用途。

執行階段要求

在解碼期間,如果違反多項條件,系統必須產生 FIDL 傳輸錯誤,並立即關閉通訊管道。如果已設定 byte_overflow 標記,控制平面訊息的大小「必須」為 32 個位元組,如上所述,訊息的內容「必須」透過其他媒介傳送。

在 zircon 管道傳輸的情況下,位元組溢位緩衝區的媒介「必須」是 VMO。也就是說,控制層訊息的隨附句柄數量「必須」至少為 1 個。最後一個句柄所指向的核心物件「必須」是 VMO,而接收器從該 VMO 讀取的位元組數量「必須」等於 message_info 結構體的 msg_byte_count 欄位值。如果已知訊息有邊界,這個值「必須」小於或等於問題中酬載的靜態推論最大大小。

訊息傳送者「必須」透過 zx_vmo_create 系統呼叫鑄造新的 VMO,並立即接著使用 zx_vmo_write 為其填入訊息主體。必須確保代表溢位 VMO 的句柄沒有正確的 ZX_RIGHT_WRITE

在接收端,訊息的收件者「必須」使用 zx_vmo_read 讀出所含資料。因此,雖然透過 zircon 管道傳送的一般 FIDL 訊息只需要兩個系統呼叫 (zx_channel_write_etc 用於傳送端,zx_channel_read_etc 用於接收端),但位元組溢位訊息則需要更多系統呼叫 (zx_channel_write_etczx_vmo_createzx_vmo_write 用於傳送端,zx_channel_read_etczx_vmo_readzx_handle_close 用於接收端)。這項處罰相當嚴厲,不過日後的最佳化措施 (例如改善 zx_channel_write_etc API) 可能會讓您收回部分費用。訊息接收器「不得」嘗試寫入收到的溢位 VMO。

程式碼產生作業變更

FIDL 繫結實作項目「必須」為任何酬載訊息產生溢位處理程序,因為這些訊息的最大位元組數可能會大於其通訊協定運輸的限制。為此,FIDL 訊息可大致分為三類:

  • 受限: 訊息的累積位元組數量上限一律為已知值。這個類別包含大多數 FIDL 訊息。針對這類訊息,繫結產生器必須使用訊息的計算最大位元組數,判斷是否要納入在編碼時設定 byte_overflow 旗標的功能,以及是否要在解碼時檢查該旗標。具體來說,如果累積位元組數量上限超過通訊協定傳輸限制 (在 zircon 管道中為 64KiB),則在產生的程式碼中,必須在編碼時設定 byte_overflow 標記,並進行強制解碼時的標記檢查;否則,則不應如此。
  • 半邊界: 訊息的累積位元組數上限僅會在編碼時才知曉。這個類別包含任何原本會受限,但間接包含 flexible uniontable 定義的訊息。針對這類訊息,繫結產生器「必須」使用訊息的計算最大位元組數,以決定是否要在編碼時加入設定 byte_overflow 旗標的功能,但這個旗標「必須」在解碼時一律會受到檢查。
  • 無限制: 訊息的累積位元組數量,依定義上來說,永遠無法得知。這個類別包含任何訊息,其間接包含遞迴定義或無界限的 vector。針對這類訊息,產生的繫結程式碼「必須」一律包含在編碼時設定 byte_overflow 標記的功能,且「必須」一律在解碼時檢查該標記。
@transport("Channel")
protocol Foo {
  // This request has a well-known maximum size at both encode and decode time
  // that is not larger than 64KiB limit for its containing transport. The
  // generated code MUST NOT have the ability to set the `byte_overflow` on
  // encode, and MUST NOT check it on decode.
  BoundedStandard() -> (struct {
    v vector<string:256>:16; // Max msg size = 16+(256*16) = 4112 bytes
  });
  BoundedStandardWithError() -> (struct {
    v vector<string:256>:16; // Max msg size = 16+16+(256*16) = 4128 bytes
  }) error uint32;

  // This request has a well-known maximum size at both encode and decode time
  // that is greater than the 64KiB limit for its containing transport. The
  // generated code MUST have the ability to set the `byte_overflow` on encode,
  // and MUST check it on decode.
  BoundedLarge() -> (struct {
    v vector<string:256>:256; // Max msg size = 16+(256*256) = 65552 bytes
  });
  BoundedLargeWithError() -> (struct {
    v vector<string:256>:256; // Max msg size = 16+16+(256*256) = 65568 bytes
  }) error uint32;

  // This response's maximum size is only statically knowable at encode time -
  // during decode, it may contain arbitrarily large unknown data. Because it
  // is not larger than 64KiB at encode time, the generated code MUST NOT have
  // the ability to set the `byte_overflow` on encode, but MUST check for it on
  // decode.
  SemiBoundedStandard(struct {}) -> (table {
    v vector<string:256>:16; // Max encode size = 32+(256*16) = 4128 bytes
  });
  SemiBoundedStandardWithError() -> (table {
    v vector<string:256>:16; // Max encode size = 16+32+(256*16) = 4144 bytes
  }) error uint32;

  // This response's maximum size is only statically knowable at encode time -
  // during decode, it may contain arbitrarily large unknown data. Because it
  // is larger than 64KiB at encode time, the generated code MUST have the
  // ability to set the `byte_overflow` on encode, and MUST check for it on
  // decode.
  SemiBoundedLarge(struct {}) -> (table {
    v vector<string:256>:256; // Max encode size = 32+(256*256) = 65568 bytes
  });
  SemiBoundedLargeWithError(struct {}) -> (table {
    v vector<string:256>:256; // Max encode size = 16+32+(256*256) = 65584 bytes
  }) error uint32;

  // This event's maximum size is unbounded. Therefore, the generated code MUST
  // have the ability to set the `byte_overflow` on encode, and MUST check for
  // it on decode.
  -> Unbounded (struct {
    v vector<string:256>;
  });
};

ABI 和 API 相容性

這項設計全面推出後,就會完全與 ABI 和 API 相容。因為任何將先前受限酬載轉換為無限制或半受限的變更 (例如將 struct 變更為 table,或變更 vector 大小邊界) 都會破壞 ABI,因此這項變更一向是安全的。

對於無界限或半界限酬載,無論大小為何,系統一律會在訊息解碼期間檢查 byte_overflow 標記。這表示任何在連線一端編碼的訊息,都可能在另一端解碼,即使進化版本新增不明資料,導致訊息在解碼器的酬載類型檢視中變得異常龐大,也一樣。

在推出期間的中期,如果連線的一方有一個 FIDL 繫結,而該繫結知道大型訊息,而另一方則不瞭解,則大型訊息將無法解碼。這與目前的情況類似,這類訊息會在編碼期間失敗,但現在失敗的情況會稍微遠離來源。

在中期推出期間,解碼失敗的風險被認為較低,因為大多數會傳送大型訊息的 API 已採用通訊協定層級的緩解措施,例如分割。主要風險向量是,如果通訊協定開始透過現有方法傳送現在允許的大型訊息。這類通訊協定「應」改為導入可處理大型訊息的新方法。

設計原則

這項設計遵循幾項重要原則。

用多少付多少

FIDL 語言的主要設計原則是「用多少付多少」。本文所述的大型訊息功能,就是為了實現這項理想。

使用受限酬載的方法,在這個 RFC 存在的情況下,其效能不會降低。使用半受限或無限負載的做法,但不會傳送比通訊協定傳輸 Byes 計數上限更大的訊息,只需在接收端支付單位元旗標檢查的成本。只有實際使用大型訊息溢位緩衝區的訊息,才會受到效能影響。

不需大型訊息支援功能 (也就是大多數可能以 FIDL 表示的方法/通訊協定) 的使用者,無須支付任何費用,無論是執行階段效能成本,還是編寫 FIDL API 時產生的心理負擔。

不遷移

大型訊息現在可在任何可能使用這些酬載的酬載中啟用,且不會遷移現有的 FIDL API 或其用戶端/伺服器實作。先前會導致 PEER_CLOSED 執行階段錯誤的情況現在「正常運作」。

客製化運輸

這項設計可彈性因應現有和推測的不同傳輸需求。舉例來說,只要 byte_overflow 位元翻轉,且傳輸程序知道如何排序包含封包的溢位,就可以使用透過網路傳送的多封包訊息等慣例。

實作

這項功能會在實驗性 fidlc 標記推出後才會推出。接著,系統會修改每個繫結後端,以便根據此 RFC 的規定,針對明確指定實驗標記的輸入內容處理大型訊息。一旦功能穩定,我們就會移除標記,讓一般使用者也能使用。

這項屬性不應需要額外的 fidlc 支援,因為它只會將執行溢位檢查所需的資訊傳遞至後端選項,而後端會學習如何支援大型訊息。

在本 RFC 之前,綁定會將編碼/解碼緩衝區普遍放在堆疊上。日後,建議您針對未翻轉 byte_overflow 標記的訊息,讓繫結繼續執行這項行為。對於有此需求的訊息,繫結「應」改為在堆積上進行分配。

成效

在稍微自訂的情況下,您可以使用核心微型基準測試來估算建議的傳送方式對效能造成的影響,方法是將下列 B 值 (16KiB、32KiB、64KiB、128KiB、256KiB、512KiB、1024KiB、2048KiB 和 4096KiB) 的總和與以下兩種情況進行比較:傳送單一管道訊息 (大小為 B) 與傳送 16 個位元組管道訊息和傳送大小為 B - 16 的 VMO 的總和。

清單 1:表格顯示以 16 個位元組的管道訊息和大小為 B - 16 的 VMO 傳送 B 個位元組資料,而非以大小為 B 的管道訊息傳送時,預估的1 傳送時間效能「稅金」。

訊息大小 / 策略 僅限管道 管道 + VMO VMO 使用費
16KiB 2.5μs 5.9 微秒 136%
32KiB 4.5 微秒 7.7 微秒 71%
64KiB 7.9 微秒 13μs 65%
128KiB 16.5 微秒 23.3 毫秒 41%
256KiB 35.8 微秒 54.4 微秒 52%
512KiB 71.3 毫秒 107.4 微秒 51%
1024KiB 157.0 毫秒 223.4 微秒 42%
2048KiB 536.2μs 631.8 微秒 18%
4096KiB 1328.2 微秒 1461.8 微秒 10%

清單 2:圖表顯示以 16 個位元組的管道訊息和大小為 B-16 的 VMO 傳送 B 個位元組的資料,而非以大小為 B 的管道訊息傳送時,預估的傳送時間效能「稅金」。

僅電視頻道 vs. 管道 + VMO 比較圖

清單 3:以 16 個位元組的管道訊息傳送 B 個位元組的資料,與以 B - 16 的 VMO 傳送,而非以 B 大小的管道訊息傳送,比較傳送時間成效的線性比較。

不同酬載大小的 VMO 使用量懲罰圖表

這項資料提供了幾項有趣的觀察結果。我們可以看到,資料大小與放送時間之間的關係大致呈線性。兩種方法之間的成效差異相當明顯,但有趣的是,隨著訊息大小增加,差距似乎會縮小。

結合這些結果,我們可以使用此設計中指定的方法,模擬傳送 FIDL 大型訊息的預期效能。我們預期,在特定大小下,使用同樣大小的舊版管道訊息 (如果允許) 時,所謂的「VMO 稅」會導致端對端傳送時間增加約 20% 至 60%。有趣的是,隨著傳送訊息的大小增加,百分比差距會略微減少,這表示 VMO 稅率會以略低於線性的方式,隨著酬載大小而增加。

清單 4:這份表格顯示本文件所述設計的模擬提交時間效能。

訊息大小 / 策略 僅限管道 訊息 + VMO
16KiB 2.5μs --
32KiB 4.5 微秒 --
64KiB 7.9 微秒 13μs
128KiB -- 23.3 毫秒
256KiB -- 54.4 微秒
512KiB -- 107.4 微秒
11024KiB -- 223.4 微秒
2048KiB -- 631.8 微秒
4096KiB -- 1461.8 微秒

清單 5:線性比例圖表,顯示本文件所述設計的模擬提交時間成效。請注意,在 64 KiB 從一般訊息切換為大型訊息時,會出現中斷現象。

模擬成效的線性圖表

人體工學

這項變更大幅改善了人因工程,因為現在可以使用第一類 FIDL 概念來描述 zx.handle:VMOfuchsia.mem.Bufferfuchsia.mem.Data 的所有目前用途。後端繫結程式碼也會受惠,因為先前必須透過無型別的線路傳送的資料,現在可以使用一般 FIDL 路徑處理。簡而言之,針對大型訊息產生的 FIDL API 與非大型訊息產生的 API 相同。

回溯相容性

這些變更將完全回溯相容。現有 API 的語意略有變動 (從每則訊息 64KiB 的限制,變成無限制),但由於這項變更是放寬先前的限制,因此不會影響現有 API。

安全性考量

這些變更對安全性影響極小。使用 fuchsia.mem.Data 結構,即可將模式提升至「第一級」狀態,且不會對安全性造成任何負面影響。不過,請務必確保在所有情況下,實作方式都安全無虞。

這項設計也擴大了與 FIDL 通訊協定相關的阻斷服務風險。先前,傳送 VMO 的攻擊向量會分配大量記憶體,導致接收器當機,但這類攻擊向量只會影響明確傳送 fuchsia.mem.Data/fuchsia.mem.Buffer/zx.handle:VMO 包含類型的通訊協定。如今,所有通訊協定都會面臨這項風險,只要其中至少包含一個方法,且該方法的酬載為無界限或半界限。由於 zircon 中存在許多拒絕服務向量,因此目前認為這項問題是可以接受的。我們會在這個設計範圍之外,尋求更全面的解決方案來解決這個問題。

這個設計不要求在接收端檢查 ZX_INFO_VMO,因此會引入額外的拒絕服務向量。這會讓使用分頁器的 VMO 永遠不會提供承諾提供的網頁,進而導致伺服器當機。實際上,發生這種意外的風險很低,因為只有少數程式會使用以分頁器為後端的 VMO 機制。與上述推論相似,我們會容許這種拒絕服務向量,直到日後設計中可導入更全面的解決方案為止。

隱私權注意事項

重要的隱私權考量是,訊息傳送者「必須」確保為每則 VMO 訊息使用新建立的 VMO。絕對不應在訊息之間重複使用 VMOs,否則可能會發生資料外洩的情形。您必須使用繫結才能強制執行這些限制。

測試

針對 fidlc 的單元測試,以及針對下游和繫結輸出的黃金檔案的標準 FIDL 測試策略,將擴展以支援大型訊息用途。

說明文件

您需要更新 FIDL 線路格式規格,以便說明這份文件所引入的線路格式變更。

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

缺點

這種設計有許多缺點。雖然這些問題被判定為次要,尤其是相較於不採取任何行動或導入所考慮的替代方案的成本,但仍值得指出。

效能懸崖

效能探索中所述,此 RFC 中所述的策略會在 ZX_CHANNEL_MAX_MSG_BYTES 截斷點產生效能「斷崖」,使用者會開始支付「稅金」來傳送較大的訊息。具體來說,如果郵件大小比 64 KiB 大 1 個位元組,則接收郵件所需的時間會比郵件大小剛好為 64 KiB 的郵件多出約 60%。雖然這種落差並不理想,但相對來說幅度不大,日後透過核心變更也有可能改善。

阻斷攻擊

任何至少包含一個採用無邊界或半邊界酬載的方法的通訊協定,現在都可能遭受記憶體拒絕服務攻擊:惡意攻擊者可以傳送溢位訊息,在 message_info 結構體的 msg_byte_count 欄位中附加非常大的值,接收端隨後會被迫分配足夠的記憶體來處理此酬載,如果惡意酬載過大,就會導致程式無可避免地當機。

如上述安全性考量所述,這是非常實際的風險,而這個設計會在日後找到更全面的解決方案前,先擱置解決。

處理溢位邊緣情況

這項設計並未完全排除在執行階段因訊息過大而發生意外 PEER_CLOSED 的可能性:如果一般訊息的句柄超過 64 個,或大型訊息的句柄超過 63 個,仍會觸發錯誤狀態。作者知道在實際情況中並未使用這類酬載,因此目前認為這項做法可行。這個邊緣情況可視需要處理。在 message_info 結構體中加入 reserved 欄位,可確保日後設計的句柄溢位支援功能具有彈性。

依據情境而異的訊息屬性

byte_overflow 和標記將是第一個可為不同傳輸機器提供不同意義的標頭標記 (除了靜止標記,其他標記則視情況而定,取決於我們是否將 靜止 FIDL 視為「傳輸機器」)。這會導致一些模糊之處:如果只查看以線路編碼的 FIDL 交易訊息,而不知道傳送該訊息的傳輸方式,可能就無法充分處理訊息。目前需要進行「預先處理」步驟,根據郵件標頭旗標和郵件傳送的傳輸方式,我們會執行特殊程序來組合完整的郵件內容。舉例來說,在 zircon 管道傳輸中溢出的非句柄訊息現在會在其句柄陣列中取得句柄,而溢出的 fdf 訊息則可能不會。

遭拒的替代方案

在設計本 RFC 時,我們考慮了許多替代方案。以下列出最有趣的提案。

提高 zircon 訊息大小限制

zx.channel 傳輸服務目前的郵件大小上限為 64 KiB,因此最需要處理大型郵件的需求。一個明顯的解決方法就是提高這個限制。

這並非理想的做法,原因如下:第一個問題是,這只是將問題延後處理。由於這類 ABI 違規的核心限制遷移作業並非易事,因此需要謹慎管理,確保在限制提高後編譯的二進位檔不會意外傳送比限制提高前編譯的二進位檔更多的資料。

許多 FIDL 實作也會根據限制做出實用的假設。部分繫結 (例如 Rust 的繫結) 會針對收到的訊息採用「猜測和檢查」的配置策略。它們會配置小型緩衝區,然後嘗試 zx_channel_read_etc。如果該系統呼叫在 ZX_ERR_BUFFER_TOO_SMALL 中失敗,則會傳回實際的訊息大小。這樣一來,繫結就能分配適當大小的緩衝區並重試。

其他繫結 (例如 C++ 的繫結) 則不必這麼小心謹慎,並且一律為傳入訊息分配 64 KiB,以免發生多個系統呼叫的情況,但代價是需要較大的配置。後者策略無法縮放至任意大型訊息。

最後,使用 VMOs 是經過充分測試的解決方案:多年以來,透過 fuchsia.mem.Datafuchsia.mem.Buffer 類型傳輸大量訊息,一直是首選做法。提高核心限制並非成熟的解決方案,可能會產生更多未知的潛在問題。

zx_vmar_map 取代 zx_vmo_read

對目前設計進行最佳化後,FIDL 編碼器會直接使用 zx_vmar_map 從 VMO 緩衝區讀取資料。這種做法有兩個問題。

主要問題是這個方法不安全,必須修改核心原始碼才能解決這個問題。問題是,對應記憶體會導致訊息傳送者在收件者閱讀訊息時修改訊息內容,進而產生 TOCTTOU 風險。讀取器可以嘗試直接從已對應且可能可變動的 VMO 讀取資料,而無須先進行複製作業,但即使執行防禦性複製作業,也很難安全地執行此操作。透過 zx_vmo_create_child 呼叫強制執行 VMO 的不可變性,即可降低這些安全風險,但代價是額外的系統呼叫和最壞情況的複製額外負擔。

記憶體對應效能相關的其他問題,以及線路 C++ 繫結的複雜性 (例如決定何時釋放記憶體),都讓這個選項不太合適。

分包

這裡的想法是將非常大的訊息分割成多個訊息,每個區塊大小不得超過 64 KiB,然後在另一端組合。交易訊息標頭會包含某種接續標記,指出是否預期後續會有「更多資料」。這項做法的好處是內建流程控制,且對使用標準程式庫中含有串流原始類型的程式語言的程式設計師來說,也相當熟悉。

這種做法的缺點是,如果訊息不容易分割,這種做法就沒有明顯幫助。這項作業也更加複雜:當多個執行緒傳送時,令人困惑的部分訊息片段陣列會堵塞傳輸,需要在另一端重新組合。

最令人擔心的是,這項策略會帶來拒絕服務風險,而這項風險無法單靠新的核心原始碼或日後新增的受限通訊協定來修正:惡意或有錯誤的用戶端可能會傳送非常長的訊息封包串流,但無法傳送「關閉」封包。接收器會在等待最後一個封包時,強制保留記憶體中的所有剩餘封包,讓用戶端可在伺服器上「預訂」可能無限的持續性記憶體配置。當然,我們有其他方法可以解決這個問題,例如逾時和政策限制,但這很快就會演變成透過 FIDL 重新實作 TCP。

明確溢位

本文提出的設計會抽象化大型訊息從使用者端傳送的方式。使用者只要定義酬載,綁定作業就會在幕後執行。

另一種設計可讓使用者在每個酬載或每個酬載成員層級,以宣告方式指定 VMO 的具體使用時機。這項作業基本上只需要修改 FIDL 語言,為 fuchsia.mem.Data 提供更清楚的拼寫方式。

現有設計會在效能提升和 API 相容性之間取得平衡,犧牲了較少的決定論和精細控制,但這項權衡被視為值得的交換。

只允許值類型

這項設計的早期版本建議只為值類型啟用 overflowing。原因很簡單:現有的用途都不是資源類型,而且單一 FIDL 訊息不太可能需要一次傳送超過 64 個句柄,因此我們判定這項功能的優先順序較低。

在為 fuchsia.component.resolution library 製作此解決方案的原型時,我們發現了一個問題。某些方法已使用資料表來傳送酬載,因此建議擴充資料表,逐步淘汰 fuchsia.mem.Data 的用法,而非全面取代該方法。具體來說:

// Instead of adding a new method to support large messages, the preferred
// solution is to extend the existing table and keep the current method.
protocol Resolver {
  Resolve(struct {
    component_url string:MAX_COMPONENT_URL_LENGTH;
  }) -> (resource struct {
    component Component;
  }) error ResolverError;
};

type Component = resource table {
  // Existing fields - note the two uses of `fuchsia.mem.Data`.
  1: url string:MAX_COMPONENT_URL_LENGTH;
  2: decl fuchsia.mem.Data;
  3: package Package;
  4: config_values fuchsia.mem.Data;
  5: resolution_context Context;

  // Proposed additions for large message support.
  6: decl_new fuchsia.component.decl.Component;
  7: config_values_new fuchsia.component.config.ValuesData;
};

這些方法會產生一個有趣的問題:雖然酬載量較大和酬載量含有句柄的情況在實際上是互斥的,但 fidlc 編譯器並不知道這一點。在其角度來看,這些只是資源類型。雖然可以使用某些補救措施,讓編譯器瞭解這個特定情況,但在開發出更合適的核心原始碼前,我們認為只允許大型訊息使用 63 個句柄會比較簡單。

overflowing 修飾符

這項設計的早期迭代版本可讓使用者在定義通訊協定方法的 FIDL 中設定 overflowing 桶,如下所示:

// Enumerates buckets for maximum zx.channel message sizes, in bytes.
type msg_size = flexible enum : uint64 {
  @default
  KB64 = 65536;    // 2^16
  KB256 = 262144;  // 2^18
  MB1 = 1048576;   // 2^20
  MB4 = 4194304;   // 2^22
  MB16 = 16777216; // 2^24
};

@transport("Channel")
protocol Foo {
  // Requests up to 1MiB are allowed, responses must still be less than or equal
  // to 64KiB.
  Bar overflowing (BarRequest):zx.msg_size.MB1
      -> (BarResponse) error uint32;
};

最終我們認為這項功能過於複雜且難以理解,因為它允許使用者選擇多個選項,但沒有明確的說明,讓使用者難以決定要選哪一個。歸根究柢,大多數使用者可能只想回答簡單的「是/否」問題 (例如「我需要大型訊息支援嗎?」),而不會擔心特定限制對效能造成的微小影響。

我們也考慮使用單一 overflowing 關鍵字 (不含分層) 做為替代方案,讓使用者清楚知道自己接受的 API 效能可能較低。最終決定成效差距並未達到足以在語言本身中使用這類呼籲的程度,且在任何情況下都能縮小差距。

未來可能的工作

我們也進行了許多相關工作,雖然這些工作並非啟用大型訊息功能的必要條件,但卻是這項工作的必要補充和最佳化措施。

核心變更

雖然並非實作這項功能的關鍵路徑,但有許多可能的核心變更,無疑有助於減少系統呼叫衝突並提升效能。大型訊息的使用者 API 會在實作這些額外最佳化功能時「不應」變更。大多數現有的 fuchsia.mem.Data 使用者對延遲不太敏感 (否則他們不會使用 fuchsia.mem.Data),因此修改核心的主要用途,就是針對在 FIDL 中啟用大量訊息後彈出的緊急用途,改善效能。

一流的串流

每當大型訊息用途出現時,就會有人問:「在 FIDL 中實作第一類串流,無法解決這個問題嗎?」為了針對任何特定情況回答這個問題,我們建議您考慮兩個實用的屬性,以便將大量資料分類:可分割性可附加性

可分割性是指資料是否可分割成有用的子部分,而更重要的是,資料接收端是否只針對子集執行有用的作業。從本質上來說,這項差異在於 T 類型的資料,與 vector<T>array<T> 類型的資料,在清單的部分檢視畫面仍可執行操作的情況。分頁清單可分割,但傳送用於排序的項目清單則不行。同樣地,樹狀結構也無法分割:一般來說,使用樹狀結構的 (任意) 部分,無法做太多事情。

附加功能會考量資料在傳送後是否可修改。可附加的 API 的經典範例是 Unix 管道:當資料從讀取端讀取時,可能會從寫入端新增更多資料,甚至預期會新增更多資料。可附加的資料是指在傳送後可新增資料的資料。即使是傳送時無法變更的資料 (即使是清單形式),也不算是。

清單 6:針對所有可能的區塊化和附加功能組合,提供偏好的大量資料處理策略矩陣。

大型資料處理策略矩陣

這兩種區別很實用,因為在矩陣中結合這兩種區別,可提供良好的指導方針,說明哪些大型訊息或串流較為合適。

對於靜態 blob (例如資料傾印、B 樹狀結構或大型 JSON 字串),使用者不想串流傳輸:對他們來說,這只是單一訊息,而且這類資料對於 FIDL 來說太大 (或至少可能太大),這點意外的複雜性通常與他們的疑慮無關。在這種情況下,他們希望能夠以某種方式告訴系統「盡可能取得所有訊息,但不包括最不合理的大小」。資料已在裝置的記憶體中實體化後,就沒有必要在程序之間逐一移動。

對於可分割且具動態資料結構的資料 (例如網路封包串流),串流是明顯的選擇 (名稱就很清楚!)。使用者已建立自訂疊代器來處理這種情況,並編寫可在傳送端設定串流,並在接收端清楚顯示串流的程式庫,因此這似乎是第一類處理方式的理想候選項目。這也是非常自然的模式,在 FIDL 綁定的大部分語言 (C++、Dart、Rust) 中,都提供完善的支援和程式設計人員熟悉的模式。

那麼,如果訊息可分割,但大多為靜態,例如列出與裝置連線的周邊裝置的快照,該怎麼辦?將這些資訊分割並公開為串流非常容易,但這是否有益並不明顯:有幾個 API 會公開這類資訊,但作者認為分頁功能只是為了滿足 FIDL 而加入的臨時解決方案,並非核心功能。在這種情況下,要選擇串流還是大型訊息,似乎要視情況而定。

總而言之,大型訊息只是取得大量資料的潛在方法工具箱中的其中一種工具。FIDL 很可能會在未來提供一流的串流實作項目,這項項目將補足大型訊息提供的功能,而非取代這些功能。

受限通訊協定和彈性信封大小限制

這種設計存在非常實際的服務拒絕風險,因此某些通訊協定 (尤其是在許多獨立用戶端之間共用的通訊協定) 可能會想避免這種情況。為此,您可以考慮在通訊協定中加入 bounded 修飾符,提供編譯時的強制執行,確保所有方法只使用受限類型:

// Please note that this syntax is very speculative!
bounded protocol SafeFromMemoryDoS {
  // The payload is bounded, so this method compiles.
  MySafeMethod(resource struct {
    a bool;
    b uint64;
    c array<float32, 4>;
    d string:32;
    e vector<zx.handle, 64>;
  });
};

這項設計的一個後果是,FIDL 通訊協定作者必須面對「分支」的選擇:新增 bounded 可讓通訊協定免於因無限大的消息而遭到拒絕服務,但會防止該方法的酬載透過 tableflexible union 類型傳遞。這是一個不幸的權衡,因為可進化性和 ABI 相容性是 FIDL 語言的主要目標。強制使用者採用 ABI 穩定型別,可大幅限制他們日後改進酬載的能力。

一種可能的折衷做法是,為 flexible 封裝版面配置引入明確的大小限制。這可提供 ABI 相容性,因為彈性定義會隨著時間而變更,但仍會對類型的最大大小強制實施 ABI 破壞限制:

// Please note that this syntax is very speculative!
@available(added=1)
type SizeLimitedTable = resource table {
  1: orig vector<zx.handle>:100;
  // Version 2 still compiles, as it contains <=4096 bytes AND <=1024 handles.
  @available(added=2)
  2: still_ok string:3000;
  // Version 3 fails to compile, as its maximum size is greater than 4096 bytes.
  @available(added=2)
  3: causes_compile_error string:1000;
}:<4096, 1024>; // Table MUST contain <=4096 bytes AND <=1024 handles.

這類大小限制可提供某種「軟性」彈性:酬載仍可隨時間變更,但在首次定義酬載時,會對這類成長範圍設下硬性 (也就是 ABI 中斷) 限制。

既有技術與參考資料

這項提案的先例是 fuchsia.mem.Data,而在此之前,fuchsia.mem.Bufferzx.handle:VMO 在 fuchsia.git 程式碼庫中廣泛使用且受到支援。這個決定基本上是這個經過充分測試的模式的「第一級」演進。

先前棄用的 RFC 描述了與這份 RFC 類似的內容,也使用 VMOs 做為大型訊息的基礎傳輸機制。

附錄 A:fuchsia.git 中 FIDL 酬載的界限

下表顯示截至 2022 年 8 月初,fuchsia.git 程式碼庫中溢位 (大於 64KiB) 和標準酬載之間的邊界分布情形。這些資料是透過建構 fuchsia.git 的「everything」版本收集而來。接著,我們透過一系列 jq 查詢來分析產生的 JSON IR。

清單 7:表格顯示 fuchsia.git 存放區中各類酬載邊界和大小的測量頻率。

邊界 / 訊息類型 標準 溢出 總計
已綁定 3851 (76%) 45% 3896 (77%)
半邊界 530 (10%) 70 (1%) 600 (11%)
無界線 0 (0%) 602 (12%) 602 (12%)
總計 4381 (86%) 717 (14%) 5098 (100%)

  1. 您可能會想要比較潛在的分封包解決方案 (例如 4 MiB 訊息) 的成效,方法是將 64 KiB 訊息的傳送時間乘以 64,但這並非正確做法。在進行效能比較時,處理器快取對機器的影響可確保 <=1MiB 的傳輸速度,相較於依序傳輸超過 1MiB 的傳輸速度,會較為快速;這可在清單 2 中顯示的圖表中,於 1MiB 處看到「凹口」。這個基準測試方法唯一可用的比較方式,就是直接比較相同大小的訊息。