RFC-0207:離線 blob 壓縮

RFC-0207:離線 Blob 壓縮
狀態已接受
區域
  • 軟體推送
說明

下載壓縮的 Blob。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2022-11-02
審查日期 (年-月-日)2023-02-01

摘要

在 Fuchsia 目前的設計中,當套件解析時,系統會透過網路擷取未壓縮的 Blob,然後在寫入磁碟前在裝置上進行壓縮。本 RFC 提出了另一種做法,可直接擷取壓縮 Blob 構件。這麼做可減少頻寬,並消除耗費大量運算資源的裝置端壓縮。

提振精神

pkg-resolver 會從 Blob 伺服器擷取未壓縮的 Blob,並直接將 Blob 寫入 Blobfs。Blobfs 會即時壓縮資料,因此需要大量的 CPU 和記憶體。

使用離線壓縮功能時,Blob 會在擷取前預先壓縮,並以適合直接寫入磁碟的格式傳送至 Blobfs。這麼做可在套件解析和系統更新期間,降低 CPU、記憶體和網路用量。因此,更新作業會更快完成、記憶體不足 (OOM) 的機率會降低,且伺服器頻寬成本也會減少。

由於大型測試二進位檔不需要在裝置上壓縮,因此開發人員的工作效率也會提升。啟動大型二進位檔大小的測試應該會變得更快速。

相關人員

協助人員:

abarth@google.com

審查者:

  • jsankey@google.com (SWD)

  • etryzelaar@google.com (SWD)

  • mlindsay@google.com (本機儲存空間)

  • csuter@google.com (本機儲存空間)

  • bcastell@google.com (本機儲存空間)

  • marvinpaul@google.com (伺服器基礎架構)

  • aaronwood@google.com (產品組裝)

  • amituttam@google.com (PM)

  • dschuyler@google.com (SDK 提交)

  • atyfto@google.com (Infra)

社會化:

在軟體提交團隊的設計討論中,我們討論了離線壓縮和差異更新。相關利害關係人已審查 RFC 的早期草稿。

策略遊戲

改善 OTA 時間和頻寬的方法主要有兩種:

  • 離線壓縮:Blobfs 會動態壓縮磁碟上的 Blob。不過,pkg-resolver 會透過網路擷取未壓縮的 blob。我們可以改為擷取預先壓縮的 Blob,以節省頻寬、CPU 和記憶體用量。
  • 差異修正:不需再次下載未變更的 Blob。這是一種差異壓縮形式,但只有在 Blob 內容保持不變時才有效。如果 Blob 內部有小幅變更,伺服器可以在舊 Blob 和新 Blob 之間產生差異修補程式,並只提供兩者之間的差異。

方法

結合上述策略,我們有以下方法可將 Blob 從 Blob 伺服器傳送至裝置,我們將逐一介紹這些方法,然後展示實驗結果。

未壓縮的 Blob

uncompressed_blobs

這是目前的行為。Blob 會直接以未壓縮的形式下載,並在 Blobfs 將 Blob 寫入永久性儲存空間時即時壓縮。

離線壓縮

offline_compression

套件伺服器支援以適合 Blobfs 攝入的格式,下載預先壓縮的 Blob 構件。不需要裝置端壓縮,而且可以使用線上演算法,透過串流解壓縮器以記憶體效率的方式計算 Merkle 樹狀圖 / 根。

未壓縮 Blob 之間的差異

delta_between_uncompressed_blobs

對於每個新的 Blob,我們會在同一個套件中找到舊 Blob,並在兩個未壓縮 Blob 之間產生修補程式。裝置需要下載修補程式、尋找舊 Blob、解壓縮、套用修補程式,然後壓縮/驗證新的 Blob 內容。

相較於其他方法,這會需要大量額外的 CPU 和記憶體,因為需要額外的解壓縮和壓縮步驟。這麼做也會防止 Blobfs 最終從壓縮程式碼中移除。

壓縮 Blob 之間的差異

delta_between_compressed_blobs

與上述相似,但我們會在兩個離線壓縮 Blob 之間產生修補程式。在這種情況下,我們不需要在裝置上解壓舊 Blob 或壓縮新的 Blob。這種做法的缺點是,使用壓縮功能可能會產生大型差異修正檔。

部分解壓縮 Blob 之間的差異

與上述步驟類似,但在修補步驟中,我們會在前後套用轉換作業,以縮減修補檔案大小。

這項轉換作業會撤銷 zstd 中的 tANS 編碼,讓資料位元組對齊並更穩定。因此,差異修補功能會變得更有效率,同時避免在裝置上套用修補程式時,重複執行壓縮演算法中耗時的配對查找部分。

實驗資料

實驗結果顯示,與目前只使用未壓縮的 Blob 的做法相比,上述任何方法都能將 OTA 大小縮減一半以上。未壓縮 blob 之間的差異比離線壓縮小約 30-50%,而壓縮 blob 之間的差異比離線壓縮小約 10-40%。我們目前尚未對部分解壓縮 Blob 之間的差異值進行實驗,但估計結果會介於兩種差異值方法之間。

提案

根據實驗資料,我們建議先實作離線壓縮功能。未來,除了離線壓縮 (如果找不到先前的 blob,則會改用離線壓縮),我們也可能會嘗試在壓縮或部分解壓縮的 blob 方法之間使用差異修正程式。

這個路徑可避免我們在未來使用未壓縮 Blob 之間的差異,因為這會是所有選項中使用最少頻寬的做法,但裝置上額外的 CPU 和記憶體使用量可能會超過潛在的頻寬成本。

差異更新的設計將在日後的 RFC 中推出,其中應包含更新套件格式所需的變更。

設計

提交 Blob

提交資料 blob 是一種格式,適合從伺服器提交至裝置,並以有效的方式寫入。提交的 Blob 會包含含有中繼資料的標頭,以及包含 Blob 資料本身的酬載。

delivery_blob

提交 blob 格式應包含下列欄位,並以位元組對齊和小端序排列:

  • 魔術數字b"\xfc\x1a\xb1\x0b" 4 個位元組的 ID (代表紫紅色 blob),用於信心檢查,不會變更
  • Type (格式):表示 blob 酬載格式 (例如未壓縮、zstd 區塊),4 位元組列舉
  • 標頭長度:標頭的大小,包括魔法和儲存空間專屬中繼資料,4 個位元組
  • 中繼資料:透過 Blobfs 將提交 Blob 寫入裝置儲存空間時所需的儲存空間專屬中繼資料
  • 酬載:Blob 資料,格式由 Type 指定

PayloadMetadata 欄位會隨著時間演進,應視為實作詳細資料,且只能透過儲存空間團隊提供的工具和程式庫與其互動。對於指定的 Type ID,您必須以向後相容的方式變更相關的 MetadataPayload 格式。

產生提交 Blob

正式版

Fuchsia 產品建構程序會建立構件,包括未壓縮的 Blob 和中繼資料,用於指定產品預期收到的支援 傳送 Blob Type。實際運作中的 blob 伺服器應使用儲存空間團隊在 SDK 中提供的工具,壓縮每個 blob,並使用指定的 Type (--type) 產生傳送 blob。如此一來,伺服器就能產生及提供特定 blob 的多種格式,以便支援格式轉換,並處理不同產品中使用的 blob 類型可能出現的差異。

伺服器一律會使用最新發布的 SDK 工具,因此必須先為所有產品的所有管道建立墊腳石,才能停止支援舊版格式。這可確保推送任何新版本時,不必以舊格式產生 Blob。

對於相同的資料、類型和工具版本,工具必須產生確定性的輸出內容,但較新版本的工具可以產生與先前版本位元對位不相符的輸出內容。例如,將基礎壓縮程式庫更新為較新版本,就可能導致此問題。針對特定類型,工具必須保證產生的傳送 Blob 與預期此 Blob 類型的現有 Fuchsia 版本相容。

如果已存在相同雜湊和 Type 的提交資料 blob,伺服器應使用該資料 blob,而非使用最新工具產生新的提交資料 blob。這樣一來,即可確保現有的經過充分測試的 OTA 路徑不會受到日後不相關的版本發布版本影響。

開發人員工作流程

目前,建構作業會在產生 Blobfs 映像檔時壓縮所有 Blob。相反地,系統會將壓縮功能移出圖片產生程序,並使用專用工具在每個 Blob 上分別執行。工具的輸出內容會是指定的提交 blob 格式中的 blob,主機上的 blob 伺服器可直接使用該 blob,以節省建構時間。

產生最終 Blobfs 映像檔時,您可以選擇直接使用傳送 Blob 做為輸入內容 (除了未壓縮的 Blob 外)。

基礎架構工作流程

除了未壓縮的 blob,建構工具也會將產品建構和組合期間產生的傳送 blob 上傳至共用 blob 的 GCS 值區。這些提交資料集會由測試執行工具和部分開發人員工作流程使用,因為這些流程需要從基礎架構下載套件。

由於 GCS 儲存桶會在所有版本之間共用,因此基礎架構應在上傳前驗證解壓縮的傳送 Blob 的雜湊是否相符。在 fuchsia.git 變更中,絕對不應略過這項驗證,以免任何惡意變更在未經驗證的情況下,執行這些 Blob 的上傳前作業。

Blob 伺服器通訊協定

目前狀態

目前,我們下載 blob 所需的唯一中繼資料就是雜湊,我們可以使用雜湊在 blob 伺服器上尋找未壓縮的 blob 網址,例如 https://blob.server.example/781205489a95d5915de51cf80861b7d773c879b87c4e0280b36ea42be8e98365

Blobfs 目前需要 Blob 的大小才能開始寫入程序,而且可以從 Content-Length HTTP 標頭取得。

離線壓縮

針對離線壓縮,指定 blob 的雜湊值仍會相同 (也就是說,它會與未壓縮 blob 的 Merkle 根節點相符)。因此,pkg-resolver 可以使用雜湊,根據網址在 blob 伺服器上尋找傳送 blob,並包含 blob 格式/類型。例如,類型 1 的 Blob 網址會是 https://blob.server.example/1/781205489a95d5915de51cf80861b7d773c879b87c4e0280b36ea42be8e98365

您可以從 Content-Length HTTP 標頭取得提交 Blob 的大小。提交 Blob 格式包含標頭長度,可在需要時有效擷取酬載。

如果要求類型中沒有 blob,伺服器會傳回 404。在這種情況下,我們會改為使用現有的寫入路徑,下載並寫入未經壓縮的 Blob。這項備用機制會在正式版中停用,並在離線壓縮功能全面推出後移除。

與其他選項相比,檢測 Blob 類型是否可用的好處在於,它不需要變更格式。不過,探測的主要缺點是,安裝套件時,HTTP 要求的數量可能會增加一倍。我們可以避免這種情況,方法是確保 blob 一律可用,並在實際工作環境中停用備用方案。

Blobfs

目前狀態

目前寫入 Blob 的流程如下:

  1. Create 模式中,將檔案句柄 Open 傳送至 /blob/781...
  2. Truncate 檔案的長度與未壓縮 Blob 的長度完全相同。
  3. Write 未壓縮的 Blob 酬載。

離線壓縮

封裝系統會在寫入 blob 時傳送提交 blob 格式類型,藉此將 blob 寫入 Blobfs。雖然兩者之間的通訊協定是內部通訊,但我們建議您擴充目前的系統,以便透過以下方式實作:

  1. 為寫入傳送 blob,pkg-cache 會開啟 /blob/v1-781... 進行寫入,並原樣寫入傳送 blob。由於 傳送 blob 標頭包含 Blobfs 寫入 blob 所需的所有資訊,因此不再需要截斷。

  2. 如果套件解析工具改為下載原始未壓縮格式,pkg-cache 會改為開啟 /blob/781...,並遵循現有的寫入路徑

無論 Blob 是如何寫入裝置,從 Blobfs 讀取 Blob 的結果都會相同,使用 Merkle 根目錄做為路徑 (例如 /blob/781...)。

轉換至離線壓縮功能後,blobfs 和 pkg-cache 可能會移除舊版未壓縮 blob 格式的寫入支援功能,並移除 v1- 前置字串。

為了在寫入期間減少記憶體用量,Blobfs 會將 Blob 資料串流至儲存空間,並使用串流解壓縮工具產生 Merkle 樹狀圖。由於 Blob 應視為系統的不信任輸入內容,因此所有解壓縮作業都必須在沙箱化元件中執行。詳情請參閱「安全性考量」。

離線壓縮功能推出後,我們可能會利用 zx stream 支援功能,進一步改善寫入路徑的效能。

大小檢查

在這個 RFC 之後,blobfs 中特定 blob 的壓縮大小可能會出現更明顯的差異,因為不同的軟體版本可能會為同一個 blob 使用不同的壓縮演算法。如果 Blob 是使用舊版壓縮演算法在 blobfs 中快取,磁碟上的大小可能會與伺服器上相同 Blob 的新版壓縮方式不同。產品在決定版本大小時,應考量這一點,並為空間受限的裝置設定版本大小上限。

實作

如要支援離線壓縮功能,請按照下列步驟操作:

  • Blobfs 會更新離線壓縮的實驗導入作業,以便在路徑中加入格式類型,並預設啟用這項功能。

  • pkg-cache 支援將提交 blob 寫入 Blobfs。

  • pkg-resolver 支援下載指定類型的提交 blob

  • Blob 伺服器會產生各種類型的提交 Blob,並提供這些 Blob。

  • 建構系統會將遞送 Blob 發布至 devhost TUF 存放區。

  • ffx repository 支援直接提供提交 Blob

成效

離線壓縮功能可大幅降低下載 blob 時的頻寬、CPU 和記憶體用量。

清除建構時間可能會變慢,這取決於建構作業包含多少宇宙套件。這是因為這些套件目前並未在建構期間壓縮,而是在解析套件時,在每部裝置上壓縮。

由於重建 blobfs 映像檔可能不再需要重新壓縮所有基本 blob,因此漸進式建構時間可能會大幅縮短。

人體工學

由於我們在多個位置都有提供傳送 Blob 格式,因此提供可與其互動的主機工具,有助於進行偵錯。

我們會在 ffx 中提供主機工具,可執行以下操作:

  • 在傳送 Blob 和未壓縮 Blob 之間轉換。
  • 顯示提交 Blob 格式資訊:版本、未壓縮大小、裝置空間大小、壓縮比率、區塊數量、區塊大小等。

回溯相容性

從未壓縮遷移至離線壓縮,以及從一種提交 Blob 格式類型遷移至另一種,這兩種情況非常相似,因此本節將同時說明這兩種情況。

指定格式類型的提交 blob 必須與接受/支援該類型的任何現有裝置相容。對特定 blob 格式進行的任何重大變更「必須」使用不同的類型 ID。

實際工作環境和開發人員工作流程會採用不同的策略進行轉換,因為兩者權衡的因素不同。在實際工作環境中,一個伺服器會為現場的所有裝置提供服務,但對於開發人員而言,通常只有一個裝置。

正式版

在轉換期間,伺服器必須為同一個版本產生多種 blob 類型。這項操作是必要的,因為您必須先更新所有可能收到該版本更新的裝置,才能安裝中繼版本。

這種做法可讓我們更快開始推出新類型,而且只需要一個踏腳石就能結束轉換作業。您也不需要在實際工作環境中使用用戶端備用邏輯。

開發人員工作流程

開發主機一次只能發布一個 Blob 格式。因此,主機端伺服器不保證具有舊的 Blob 格式,以便回溯相容性。

為了讓 fx ota 繼續運作,我們會使用裝置端的備用方案。在轉換期間,執行較新版本的裝置會先嘗試取得較新的 blob 類型。如果主機上的套件伺服器沒有這個 Blob 格式,則裝置應要求舊型 Blob 做為備用方案。當大多數裝置都已更新後,我們可以切換主機端來產生新類型,並移除裝置端對這個備用方法的支援。

安全性考量

離線壓縮會擴大解壓縮程式的攻擊面,因為除了儲存在磁碟上的資料外,解壓縮程式現在也必須剖析透過網路傳送的資料。除了使用 TLS 之外,以傳送格式下載的 Blob 不含簽署,只能透過將不受信任的資料餵入解壓縮器來驗證。

因此,我們必須使用沙箱解壓縮器進行離線壓縮,以便進行深層防護。如果攻擊者在解壓縮器中發現漏洞,除非連結其他攻擊來逃離沙箱,否則系統不會遭到入侵。任何解壓縮資料都會使用 Merkle 根雜湊值進行驗證,以免遭到入侵的解壓縮程式產生惡意輸出內容。

系統會使用安全性核准的剖析程式庫,以提交格式剖析中繼資料。

另一種可能的攻擊是惡意 blob 上傳至基礎架構 GCS 值區,這部分請參閱「基礎架構工作流程」一節。

隱私權注意事項

裝置傳送至伺服器的額外資訊只有提交的 Blob 格式類型。

測試

測試團隊會透過 pkg-resolver 整合測試、E2E OTA 測試和手動 OTA 測試,測試這項功能。

說明文件

我們會更新 fuchsia.dev 的OTA 更新說明文件,說明在套件解析期間會下載傳送 Blob,而 Blob 的 Merkle 根雜湊仍為未壓縮的資料。

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

缺點

離線壓縮會將壓縮作業從裝置端移至伺服器端,因此套件解析作業會加快,但建構作業可能會變慢,且將建構作業上傳至伺服器可能會使用更多資源,並需要更長的時間。

替代方案

未壓縮 Blob 之間的差異

這麼做也可以大幅減少 OTA 大小,但會使用更多裝置的 CPU 和記憶體。

使用壓縮雜湊的內容位址

除了未壓縮的雜湊值外,我們也可以在參照套件中的 Blob 時納入壓縮雜湊值,對於不會變更的 Blob,如果壓縮雜湊值有所變更,我們仍需再次擷取 Blob。

優點:

  • 無論採用哪種升級路徑,執行相同版本的裝置都會擁有完全相同的 Blob。
  • Blobfs 不需要支援非常舊的 blob 格式。
  • 空間用量計算結果更加準確。

缺點:

  • 需要大幅變更套件格式,但因元資料/內容格式變更而受阻。
  • 要求將已綁定套件網址格式變更為包含壓縮雜湊值。
  • Blobfs 必須做更多工作,才能追蹤未壓縮和已壓縮的雜湊值。
  • 如果壓縮方式有所變更,裝置下載相同 blob 時會使用更多頻寬。
  • 變更 Blob 格式的更新會需要更新每個 Blob,導致裝置儲存空間接近上限。

在套件解析期間驗證提交 Blob 雜湊值

與「含有壓縮雜湊的內容位址」類似,我們可以在 Package Resolver 下載 blob 時,驗證傳送 blob 雜湊,以獲得部分相同的安全性優點。儲存在 Blob 儲存庫中的提交 Blob 可以變更為在開頭加入雜湊:

  • 傳送 Blob Merkle:在傳送 Blob 中計算剩餘位元組的 Merkle。

接著,您可以變更套件格式、TUF 中繼資料、Omaha 回應和已固定的套件網址,以便同時納入未壓縮 blob 和提交 blob 的 Merkle。

下載 blob 時:

  • 套件解析器會開始下載前 32 個位元組,並從提交資料 Blob 中擷取提交資料 Blob Merkle。
  • 套件解析器會開啟 /blobs/,並將其截斷至遞送 blob 的長度減去 32 個位元組。
  • 套件解析器會在計算傳送 blob 的實際 Merkle 時,開始將前 32 個位元組之後的每個區塊串流傳送至 blobfs。
  • 收到最後一小段後,請先驗證傳送 Blob 是否有正確的 Merkle,再將該小段寫入 Blobfs。如果是,請將最後一個區塊寫入 blobfs;否則請提早關閉檔案。
  • Blobfs 在收到檔案關閉通知後,會在沙箱中解壓縮傳送 blob,並驗證其 Merkle 是否與未壓縮的 blob Merkle 相符。

優點:

  • 由於提交 Blob 雜湊會儲存在已簽署的中繼資料中,因此裝置會拒絕惡意 Blob,因為攻擊者擁有 Blob 儲存區的寫入權限,因此會透過解壓縮攻擊寫入 Blob。

缺點:

  • 您需要變更套件格式,可以使用新的 meta/contents 格式,也可以在 meta/fuchsia.pkg 中使用新的檔案。
  • 無法防範本機攻擊者,因為攻擊者可以直接使用解壓縮攻擊來覆寫本機 blob 至 blobfs。
  • 我們不會覆寫本機的任何 blob,因此無法保證所有裝置都會擁有完全相同的 blob。
  • 必須變更已固定的套件網址格式,才能納入壓縮的雜湊值。

簽署提交 Blob

在將酬載傳遞給解壓縮器之前,可以將壓縮資料的簽章納入中繼資料,以便進行驗證。這可解決解壓縮不受信任資料的安全性疑慮。

不過,為了讓這項功能可供串流,我們必須在傳送 blob 中納入壓縮資料的完整 Merkle 樹狀結構,並驗證每個區塊的雜湊。我們也需要考量金鑰管理、金鑰輪替等。

這會導致串流流程和 Blob 產生程序變得非常複雜,而且無法確定如何與第三方 Blob 伺服器搭配運作。

我們認為,使用安全性核准的剖析程式庫和解壓縮沙箱,就足以降低這項風險。

既有技術與參考資料

Chrome OS

Crostini 映像檔是使用 gzip 壓縮的 squashfs,puffin 會將解壓縮串流的 Huffman 編碼解碼為自訂格式,藉此在兩個壓縮映像檔之間產生有效的差異修正檔案。

Android

Android 中的非 A/B 更新會使用 imgdiff 修補 APK,並在 APK 中未壓縮的檔案上執行 bsdiff。當 Android 改用 A/B 更新時,imgdiff 會由 puffin 取代。