在 Cloudflare 的流量樣本裡,佔六成多 bytes 的是圖片與影片——多半早就壓過,再壓一次沒有意義。真正沒壓過、值得再壓一次的,是佔了將近七成請求、卻只佔兩成多 bytes 的 HTML、JSON、CSS 與 JavaScript。
快取裡的東西,值得再壓一次嗎
Cloudflare 最近在 Pingora 裡加了一段轉碼邏輯:物件已經進了快取、已經落地,還要再花一次 CPU,把它從完全沒壓縮的原始格式重新編碼成 zstd——已經帶著 gzip 或 Brotli 的物件不在轉碼範圍內。這不是在壓縮新內容,是對「已經存在磁碟上」的東西動手——拿一次性的 CPU 成本,換長期的儲存與跨資料中心頻寬。這是作者在 Cloudflare 1.1.1.1 Intern Program 實習期間做出來的原型,文章通篇用「prototype」稱呼它,不是已經全量上線的正式功能,門檻與參數都還在調。
這跟「請 origin 自己先壓縮好內容」是兩件不同的事。很多網站的 origin 本來就不見得會設定 gzip 或 Brotli——可能是舊系統沒配置、可能是動態產生的內容嫌壓縮麻煩沒開——CDN 端能拿到的就是沒壓縮的原始 bytes。轉碼做的是「CDN 自己把已經進來、已經存在磁碟上的東西,在 CDN 這一側補一次壓縮」,不需要等 origin 端配合,也不改變 origin 看到的請求跟回應。
轉碼發生在寫盤那一刻
轉碼的插入點很精確。Cache miss 時,Pingora-based proxy「把 body 用 zstd 編碼之後才寫盤」;但同一次 miss 裡,回給這次請求的 client 之前,body 會先「解回原本的 identity representation」——也就是說轉碼只動磁碟上存的東西,這一次請求收到的內容完全不受影響。後續的 cache hit 反過來:「儲存的 zstd 物件被從磁碟讀出、解碼」,這才是 decode 成本真正累積的地方。一個物件填一次 cache,卻可能被讀取成千上萬次;encode 只需要付一次,decode 卻要付很多次,這個不對稱是整套設計划算與否的關鍵。
Tiered Cache 架構下,這條路徑又多疊了一層:壓縮過的內容「以壓縮形式從上層搬到下層,只有在最後 client-facing 那一跳才解碼」。換句話說,省下來的不只是磁碟——上下層之間、甚至跨資料中心搬運的那段頻寬,也一路維持壓縮狀態,直到真的要交給瀏覽器才解開。為了不讓同一個物件在不同層之間被重複編碼,儲存層會標記編碼狀態:「收到別層物件的快取層,能看出它已經是用 zstd 存的,並且維持這個形式」,不會白白再編碼一次。
這個標記機制解決的是一個具體的浪費:如果沒有它,一個物件在多層快取之間搬運時,每一層都可能重新對它做一次 zstd 編碼——同樣的 bytes 被壓縮好幾次,CPU 白白多付好幾份成本,卻沒有多省任何儲存空間。標記讓下游快取層一看就知道「這份東西已經是 zstd 了」,直接原樣保留、不用再動手。這個細節不起眼,但如果拿掉,前面提到的「幾個百分點 CPU」很可能不再成立——多層架構下重複編碼的成本會隨著層數疊加。把轉碼放在 cache miss 的寫入路徑上,而不是另外排一個背景工作定期掃描、事後轉碼,是這裡另一個沒有明講但看得出來的設計選擇。合理的推測是:背景轉碼要多維護一套排程與掃描邏輯,還要處理「轉碼跑到一半、物件被逐出快取」這種競態;寫在 miss 路徑上,編碼跟寫盤是同一個原子操作的一部分,物件從落地那一刻起就是 zstd,不會有一段「尚未轉碼」的中間狀態需要另外追蹤。這個選擇的代價是每一次 cache miss 都要多付一次 encode 成本,即使這個物件之後只被讀一次——換來的是實作上不用維護第二套背景系統。這一整套機制先把「轉碼要在哪個時間點動手」這件事定下來;至於「哪些物件夠格被動手」,是下一層決定,跟寫入路徑的時機是兩個獨立的判斷。
資格判斷:什麼進得去,什麼被擋在外
轉碼不是來者不拒。這個 prototype 的判斷只認三個條件同時成立:「Content-Encoding 未設定、Content-Type 是可壓縮文字、而且已知的 Content-Length 至少 4 KiB 的 200 OK 回應才會被轉碼」。三個條件缺一個都不轉——已經帶壓縮標頭的、非文字內容、或大小未知的物件,全部原樣通過。排除清單另外列了六種情況:「slice subrequest、已經由上游主動壓縮的回應、range request、已經預先壓縮好的回應、長度未知的 body,以及二進位內容,全部維持原樣不轉碼」。這幾類有一個共通點——合理的推測是,它們的語意本來就跟「整份物件、確定大小、可以整包重新編碼」這個假設衝突:range request 只要一段 bytes、slice subrequest 本身就是切好的片段。文章本身沒有針對每一類逐一說明理由,只列出了這份清單。「已知的 Content-Length」這個條件也順帶排除了用 chunked transfer encoding 送出、沒有事先宣告總長度的回應——這類回應在傳輸完成之前,代理層根本不知道最終大小,也就沒辦法判斷有沒有超過 4 KiB 這條線,自然只能跳過。「已經由上游主動壓縮的回應」被排除也是類似邏輯:如果 origin 已經用某種編碼(例如 Brotli)跟上游協商過,代理層再把它轉成 zstd 存起來,等於是把 origin 原本的決定蓋掉——這牽涉到 origin 端可能有自己的理由選特定編碼(相容性、既有的 CDN 設定),轉碼機制目前選擇不去動這塊,只處理「完全沒壓縮」的乾淨案例。
點擊或聚焦底線詞彙看實際標頭範例 · 3 個條件
三個條件同時成立才會被轉碼:
Content-Encoding 未設定回應標頭裡沒有 Content-Encoding 這一行——代表 origin 沒有主動壓縮過,例如只有 Content-Type: text/html 而沒有 Content-Encoding: gzip。、
Content-Type 是可壓縮文字涵蓋 HTML、JSON、CSS、JavaScript 這類文字格式;圖片、影片、字型這類二進位內容不算在內。、
以及Content-Length 至少 4 KiB大小必須是已知且達到門檻的,例如 Content-Length: 8192;長度未知(chunked 且無總長度)的回應不算。——三個條件缺一個,物件就原樣通過,不會被轉碼。
這裡有一個文章沒有講清楚的地方:zstd 作為 HTTP 的 Content-Encoding 早就有標準依據——IANA 在 HTTP Content Coding Registry 裡登記了 zstd 這個名稱,對應的 RFC 8878 由 zstd 的原作者 Yann Collet 跟 Murray Kucherawy 在 2021 年 2 月發布。也就是說,client 端理論上可以用 Accept-Encoding: zstd 直接跟 origin 或 CDN 協商拿到 zstd 編碼的內容,不需要經過這篇文章描述的「存 zstd、還原成原格式再回應」這一步。但文章通篇沒有提到這條路——沒有講 client 支不支援 zstd 怎麼判斷、有沒有走 Accept-Encoding 協商、Vary 標頭怎麼處理。合理的猜測是,這個 prototype 現階段刻意選擇「轉碼只影響磁碟上存的東西,回應內容維持不變」這條保守路線,協商的部分留給以後——但這只是推測,文章沒有明講為什麼。
門檻定在 4 KiB 不是隨便挑的:「4 KiB 門檻卡掉了大量小物件的請求,卻只放棄了原本符合資格裡大約 1% 的 bytes」——用犧牲請求數換來幾乎不損失的 byte 覆蓋率,是這個 prototype 目前唯一直接量化過的門檻決策。換算成比例來看,這是拿捨棄大量小物件請求去換來幾乎完整的 byte 覆蓋率,對於用「省了多少儲存空間」來衡量成效的系統來說,是個容易解釋、也容易驗證的取捨。把這條門檻換成一個可以拖曳的數字更直觀:調整一個假想物件的大小,看它落在門檻的哪一邊,以及過線之後,用測試量到的平均壓縮比可以省下多少。
拖曳滑桿改變假想物件大小 · 門檻 4 KiB
流量的形狀決定了轉碼值不值得
決定要不要轉碼的關鍵不是演算法本身,是流量長什麼樣子。Cloudflare 抓的流量樣本裡,圖片、影片、字型這類 media 內容佔了 21.4% 的請求,卻是 63.3% 的 bytes——這塊媒體流量「多半早就壓縮過了」,再轉一次幾乎沒有邊際收益,反而白付一次 CPU。真正的目標在另一頭:HTML、JSON、CSS、JavaScript 這類 compressible text 只佔 22.3% 的 bytes,卻佔了 67.3% 的請求;而且在這塊裡面,「約 71% 是完全沒帶壓縮標頭、原封不動送過來的裸文字」。
這組數字反過來讀才有意思:只看請求數,會把力氣放在 media 上,因為那是多數請求碰到的類型;但看 bytes——也就是實際佔用磁碟跟頻寬的量——media 才是已經被壓過、榨不出更多的區域,compressible text 才是真正還有油水的地方。轉碼挑的是後者。這個對照也解釋了為什麼「依請求數」這個直覺角度容易讓人做錯決定。如果照請求數排優先序,工程師會先處理佔多數請求的 media,但那正是已經被壓過、榨不出更多的部分;真正值得花 CPU 的 compressible text,用請求數看反而不顯眼——它佔的位元組本來就少。這篇文章挑的門檻設計(4 KiB、只認未壓縮文字)等於是把「bytes 視角」直接寫進資格判斷的邏輯裡,而不是留給下游工程師自己去猜。這些百分比都來自 Cloudflare 自己抓的「流量樣本」,文章沒有交代樣本涵蓋多長的時間窗、是不是涵蓋所有地區跟客戶——這類流量組成數字在不同 CDN 客戶、不同時段之間本來就會有差異,21.4% 跟 67.3% 這組數字比較像是「這次觀察到的樣子」,不是放諸四海皆準的常數。
切換整體流量/text 內部拆解兩種檢視 · 2 個開關
zstd level 3 的算術:2.8 倍儲存換幾個百分點 CPU
選 zstd 不是沒有理由的。Zstandard「由 Yann Collet 在 Facebook 開發、2016 年開源」;IETF 那邊也有正式的 RFC 8878 定義它、把 zstd 註冊進 HTTP 的 Content Coding Registry,不是 Cloudflare 自創的私有格式。Cloudflare 早期的瀏覽器壓縮測試裡,zstd「比 Brotli 快 42%,產生的檔案大小卻幾乎一樣」,「跟 gzip 相比,在速度相近的情況下檔案小 11.3%」——壓縮率跟 Brotli 打平、比 gzip 更小,速度比 Brotli 快、跟 gzip 相近,這是選它當 cache transcoding 演算法的背景。這組比較數字有個限定要留意:明講是「早期的瀏覽器壓縮測試」得出的,不是這次 cache transcoding 專案重新測的——延續舊測試的結論,而不是針對這個新場景重新驗證一次。zstd 本身的標準地位倒是很扎實:定義它的 RFC 8878 由 Yann Collet 本人跟 Murray Kucherawy 聯名寫成、2021 年 2 月發布,不是哪個廠商自己關起門定義的私有格式。
| 比較對象 | 速度 | 檔案大小 |
|---|---|---|
| zstd vs Brotli | 快 42% | 幾乎相同 |
| zstd vs gzip | 速度相近 | 小 11.3% |
Prototype 用的是 zstd level 3,理由很直白:「這給了我們大部分的壓縮效益,同時沒有把 cache fill 變成 CPU 瓶頸」。在一次超過百萬請求規模、橫跨 10 台 cache server 的測試裡——用兩個實際物件(195 KiB 跟 272 KiB)反覆測——結果是「兩個物件都壓縮了大約 2.8 倍」,表格裡給的精確數字是 2.834x。下面這個小工具把這個倍率直接畫成大小對照:同一份東西,轉碼前是一塊,轉碼後縮成大約三分之一。
點擊按鈕看轉碼前後的相對大小 · 2.834 倍壓縮比
CPU 端的帳怎麼算?「在 zstd level 3 下,模型估計額外的 CPU 成本,在我們測試的流量與重用假設下只有幾個百分點」——這句話把估算範圍講清楚了:是模型推算,不是量產環境的實測,而且明確綁定在特定的流量與重用假設上。細拆下去,encode 一個 byte 要 4.31 奈秒(約每秒 232 MB),decode 一個 byte 只要 1.56 奈秒(約每秒 641 MB)——decode 比 encode 快將近三倍,但 encode 只在填 cache 那一刻付一次,decode 卻是每次 cache hit 都要重付。一個物件填進 cache 一次,可能被讀取成千上萬次,這筆帳是「一次貴一點的成本,換很多次便宜的收益」,不是單純比誰的 ns/byte 數字比較小。Cloudflare 自己把這筆帳的規模講成:「CPU 只增加一點點,換來的是 petabytes 等級的等效快取容量,還減少了資料中心之間要搬的資料量」——沒有給出精確的 PB 數字,是定性的規模描述。拿數字驗證這個不對稱:decode 的每秒吞吐量是 641 MB,encode 是 232 MB,兩者相差大約 2.76 倍。這個倍數跟 2.834x 的壓縮比在數值上接近,但兩者量的是不同的東西——一個是編解碼的速度差,一個是資料的體積比,文章沒有把它們連在一起,這裡也不該。工程上的意義是:如果一個物件的「讀取次數:填入次數」比值遠大於 1(也就是熱門內容),把 CPU 預算優先花在維持這個比例不對稱上是划算的;如果一個物件填進去之後幾乎沒人讀,encode 的那筆成本就很難靠後續的 decode 節省攤平回來。「petabytes 等級的等效快取容量」這句話值得放回前面的流量數字一起看:如果 compressible text 真的佔了六成多請求、而其中七成本來沒壓縮,那麼在 Cloudflare 這種規模的 CDN 上,光是這一塊流量乘上 2.834 倍的壓縮比,省下來的磁碟空間確實有機會累加到 petabytes 這個量級——但這仍然是把兩個各自獨立的數字放在一起做的定性推算,文章本身沒有把這兩組數字直接相乘給出一個精確的 PB 總數。
門檻與版本都只是參數,不是永久限制
這整套設計目前明確是一個 prototype,不是已經全量上線的功能——作者是 Cloudflare 1.1.1.1 Intern Program 的實習生,在實習期間把這個原型建出來的。文章對下一步講得很直接:「接下來我們打算評估更高的 zstd level、測試更廣泛的內容類型與物件大小、調整不同的參數」,而「未來的工作也可以研究 range request、預先壓縮過的 origin 回應」這兩塊目前完全被排除在轉碼之外的類別。4 KiB 這條線跟 level 3 這個選擇,都不是寫死的常數——是可以隨著測試結果調整的旋鈕,文章沒有承諾這些數字會長期不變,只是把「目前這樣設」講清楚。測試涵蓋的物件大小目前只到一般網頁資源的範圍,超大型 JSON API 回應、或是動態產生、幾乎不會被重複讀取的內容,是不是還適用同一套「多壓縮換多省」的算術,文章沒有給答案——這正是它自己列出的下一步要測的東西。對想在自己系統上做類似設計的工程師,這篇文章留下的空白(協商細節、每種內容類型的差異化門檻、更完整的生產環境量測)本身也是一種資訊——代表這條路線目前還在驗證階段,還沒到「照抄就能用」的成熟度。
這套設計換來的:在不改變快取語意的前提下,把已經落地的物件重新編碼成更省空間的格式,讓「符合資格才轉碼」跟「門檻本身是可調參數」這兩件事同時成立——資格判斷放在寫入路徑而不是讀取路徑、用真實流量的 bytes 分布決定值不值得轉碼,這些設計原則能直接搬到別的快取層上,門檻設在哪裡、level 選幾,都是等流量長什麼樣子再決定的工程判斷,不是先驗答案。