vatt'ghern jaskier's ballads
本文 2 個互動圖表在手機上以重點摘要呈現,互動版請以桌面瀏覽器開啟。

依 Cloudflare 自己公布的數字,Workers KV 的 Instant 模式把讀取 P99 從 287 毫秒壓到 1.62 毫秒,代價是每個 namespace 最多一萬對 key-value、總量 1 MB,而且每秒只能寫一次。

KV Instant 與 Classic 怎麼選

Workers KV 在 2018 年的 Birthday Week 推出,是 Cloudflare 提供給開發者的全球鍵值儲存。Cloudflare 內部另有一套 2020 年介紹的鍵值儲存 Quicksilver,設計目標是快速的全球複製與低延遲存取。這兩條線現在接在一起了:KV Instant 模式「provides the same API as Workers KV, but powers it using Quicksilver」,目前在 private beta,需要先登記才能使用。

所以這篇要比的是同一個產品的兩種模式,Classic 與 Instant。API 長得一樣,取捨完全不同。Instant 在讀取延遲和全球可見速度上大幅領先,但資料量、寫入頻率、metadata 與 list 行為都被收緊。哪個比較好沒有意義,要問的是你的資料屬於哪一邊。

先交代這篇的證據邊界。所有數字都來自 Cloudflare 發表的這一篇公告,屬於廠商自行公布,我讀到的內容沒有說明測量方法,也沒有第三方重現。公告附了一張延遲與複製表、一張價格表,兩種模式都有數字,所以下表大部分格子是公告原值。公告沒有列到的 Classic 容量、key 長度、metadata 與 list 行為,仍標「出處未載明」。寫入複製那幾列還有一個定義上的陷阱,下一節會說明。

Classic 與 Instant 的並排對照(來源為 Cloudflare 公告,private beta,廠商自行公布)
項目 Classic Instant
讀取 P99(快取命中) 160 ms N/A
讀取 P99(全部讀取) 287 ms 1.62 ms
寫入複製中位數 小於 1 s 107 ms
寫入複製 P95 小於 1 s 181 ms
寫入複製 P99 4.38 s 256 ms
寫入頻率 每個 key 每秒 1 次 每個 namespace 每秒 1 次
容量 出處未載明 10,000 對、整個 namespace 1 MB
key 長度 出處未載明 300 bytes 以內
metadata 出處未載明 不支援,getWithMetadata 回傳 null
list 出處未載明 一次回傳所有符合的 key,不分頁
讀取價格(Class B) $0.50 每百萬次 $0.20 每百萬次
寫入、刪除、list 價格(Class A) $5.00 每百萬次 $0.10 每次
儲存價格 $0.50 每 GB 每月 $100 每 MB 每月
赭色格子是 Instant 較弱或受限的項目,綠色是較強的項目。寫入複製兩列的定義不同,Instant 量的是複製到所有邊緣節點,Classic 量的是複製到所有必要的儲存後端,兩者不能直接逐格相比。

讀取延遲:287 毫秒與 1.62 毫秒差在哪個尾巴

公告的主打數字是 P99 讀取,Instant 為 1.62 毫秒,Classic 為 287 毫秒。公告自己的說法是「KV Instant offers read latency that is over 100 times faster than classic mode, with reads resolving in under two milliseconds even at the 99th percentile of response time (p99)」。照這兩個數字相除約 177 倍,超過一百倍的說法站得住,不過這個倍數是用廠商數字做的算術,不是另一次量測。

Instant 的賣點寫成「pushes your changes globally for instant availability without cold read penalties」,也就是把變更主動推到全球,讀取時不再有冷讀取的懲罰。但公告的表把 Classic 的讀取拆成兩欄:快取命中的 P99 是 160 毫秒,全部讀取的 P99 是 287 毫秒。用表中兩欄相減,冷讀取最多只解釋了差距的一部分,約 127 毫秒(這是我的推算,公告沒有這樣拆);即使命中快取,Classic 的 P99 仍有 160 毫秒,離 1.62 毫秒還差得很遠。Instant 在快取命中那一欄是 N/A。

尾端延遲在什麼情況下會痛,要看讀取放在哪裡。公告舉的例子是在每個請求上讀一個 launch flag,決定首頁要顯示什麼內容。這種讀取在請求路徑上、每次都發生,P99 的 287 毫秒會直接變成你頁面回應時間的尾巴;換成 1.62 毫秒,這次讀取就小到可以忽略。反過來說,如果你的讀取發生在背景工作或快取預熱,沒有人在等它,這個 287 減 1.62、約 285 毫秒的差距對使用者沒有意義。

下面的圖把三個 P99 並排,預設用對數座標,這樣 1.62 毫秒才看得見。切成線性座標,你會看到 Instant 的長條縮成一條細線。兩種畫法用的是同樣的數字,線性畫法更接近使用者感覺到的差距,對數畫法更適合看量級。

切換座標刻度,比較三個 P99 · 3 個數值

1 10 100 1000 讀取 P99 延遲(ms,對數刻度) Instant 全部 Classic 快取命中 Classic 全部 1.62 ms 160 ms 287 ms
資料來源為 Cloudflare 公告的延遲表,廠商自行公布。對數座標下長條的長度比不等於數值比,切到線性座標可以看到實際的倍數。

讀這類廠商數字時,有幾件事要先問。這個 P99 是在哪裡量的,從 Worker 內部、從邊緣節點、還是從使用者端。量的是熱資料還是包含冷資料。樣本有多少個請求,持續了多久。公告沒有交代這些條件,所以 1.62 毫秒更像是一個方向性的證據,不是你能直接寫進 SLO 的數字。287 毫秒也一樣,它是 Cloudflare 對自己舊產品的量測。

這一軸的結論很單純:如果資料能放進 Instant,讀取延遲沒有懸念。真正的問題在後面三軸,它們決定資料放不放得進去。

寫入複製:幾百毫秒內到全球,但每秒只准寫一次

Instant 的寫入有兩個數字要一起看。第一個是速度:公告給的寫入複製延遲,中位數 107 毫秒,P95 為 181 毫秒,P99 為 256 毫秒,範圍涵蓋 300 多個邊緣節點。第二個是頻率:同一個 namespace 每秒最多一次寫入。速度快、頻率低,這個組合描繪得很清楚,它是給「很少改、改了要馬上全球生效」的資料用的。

Classic 的對應數字在同一張表裡:中位數與 P95 都是小於 1 秒,P99 是 4.38 秒。但公告用腳註說明兩者量的不是同一件事。Instant 是「Time to replicate to all edge locations (over 300 as of publication)」,Classic 是「Time to replicate across all required storage backends」,而且 Cloudflare 承認「We do not have sub-second fidelity for replication lag in classic mode」,所以 Classic 的兩個「小於 1 秒」只是下界不明的上限。4.38 秒除以 256 毫秒約 17 倍,只能當量級看,不能當精確倍數。下面這條軸只畫 Instant 的三個百分位點,可以拖動,看某個時間點之後哪些點已被越過;Classic 的 4.38 秒遠在軸的右側之外。

拖動把手或用方向鍵移動時間點 · 0 到 300 ms · 3 個百分位點

0 300 ms 中位 107 P95 181 P99 256 181 ms
已越過 P95(181 ms):依百分位的一般意義,約 95% 的複製在這個時間內完成,P99 的 256 ms 還沒到。
公告只給三個百分位點,沒有給連續的分布曲線,所以這條軸只在三個點上做判斷,不在點與點之間內插。Classic 的 P99 為 4.38 秒,且量測定義不同,未畫在這條 0 到 300 ms 的軸上。

這組數字有一個直接的設計含意。寫入後 256 毫秒才是 P99,代表在這段時間內,某些邊緣節點讀到的仍可能是舊值。對 feature flag 這類資料,幾百毫秒的不一致通常可以接受;對「寫完立刻要在另一個地區讀到」的流程,這是一個要自己吸收的條件。這是從複製延遲推出來的結論,公告沒有討論讀到舊值的情境。

每秒一次寫入的上限更難繞。公告說「KV Instant also restricts write frequency to one write per namespace per second」,並指出這跟 Classic 的限制相似,Classic 是每個 key 每秒一次。差別在粒度:Classic 的限制跟著 key 走,Instant 的限制套在整個 namespace 上,同一個 namespace 裡不同 key 的寫入要共用這一秒一次的額度。公告沒有說超過上限時是回錯誤、排隊還是丟棄,所以如果有多個寫入者共用同一個 namespace,得自己在上游序列化,或者把設定拆到不同 namespace。計數器、session、使用者活動紀錄這類寫入頻繁的資料,直接排除在 Instant 的適用範圍之外。

還有一個容易忽略的事:Class A 操作包含 put、delete 與 list,也就是說 list 在計價上跟寫入同一級。後面談價格時會再回到這點。

名字叫 Instant,數字說的是另一回事。寫入要一百多到兩百多毫秒才到全球,中位 107 毫秒,P99 256 毫秒,這是「幾百毫秒內全球生效」的即時,不是「寫完下一行程式就讀得到」的即時。差別在於你的程式能不能容忍這一小段空窗。公告沒有討論一致性模型,這一點得自己向 Cloudflare 確認。

資料形狀:一萬對、1 MB、沒有 metadata

讀取和寫入的數字都漂亮,限制清單才是這個模式的真正輪廓。Quicksilver 是 Cloudflare 自己用的內部儲存,公告說它是為「incredibly fast global replication and low-latency access」設計的。限制收得這麼窄,合理的推測是跟這個設計目標有關,公告本身沒有說明限制的成因。

Instant 的限制清單,公告寫得很直接:每個 namespace 最多 10,000 個 key-value pair,整個 namespace 總大小 1 MB,key 長度上限 300 bytes。再加上兩項行為差異:不支援 metadata,呼叫 getWithMetadata 會回傳 null;list 會一次回傳所有符合的 key,不做分頁。

把前兩個上限相乘相除,可以得到一個有用的直覺。1 MB 除以 10,000 對,平均每對只有約 100 bytes,這是粗略的算術,公告沒有說 1 MB 是否包含 key 本身。意思是兩個上限同時用滿的話,每一對只放得下一個短字串。一個 feature flag 的布林值或一小段 JSON 綽綽有餘;每個 key 放幾 KB 的設定物件,一萬對的上限就不可能達到,1 MB 會先到頂。

key 長度的 300 bytes 上限,對一般的命名慣例綽綽有餘。把環境、旗標名稱、版本串成一個 key,離上限還很遠;真正需要檢查的是用整段 URL 或長雜湊字串當 key 的做法。這一項在公告的限制清單裡最不容易成為阻礙,檢查器仍然把它列出來,因為它是唯一一項會在寫入時才發現問題、而不是在設計時就能估算的限制。最後這半句是我的判斷,公告沒有說明超長 key 的錯誤行為。

metadata 那項要小心的是失敗的方式。公告寫的是「getWithMetadata calls always return null」,沒有說會丟錯誤。原本依賴 metadata 的程式碼搬到 Instant 之後,不會直接當掉,而是拿到 null 後走進沒測過的分支。如果你的 Classic namespace 把版本號或過期時間放在 metadata,搬移前要先檢查這些讀取路徑。

list 不分頁的影響剛好反過來。公告只說會回傳所有符合的 key,沒有說單次回應有多大。namespace 本身被鎖在一萬對以內,這個回應在最壞情況下也有天花板,對小 namespace 比分頁迭代更省事。麻煩在計價:list 屬於 Class A,在請求路徑上對它呼叫,每次都會一次取回整批、同時按寫入等級計費。如果你的程式在每個請求上都 list,應該改成對已知的 key 逐一 get。

還有一條容易漏看:Instant 模式必須在建立 namespace 時指定。公告沒有描述把既有 Classic namespace 轉成 Instant 的方式。如果遷移是新增一個 namespace 再複製資料,寫入速度會受每秒一次的限制約束:一萬對資料要寫完,光是寫入就得花超過 2 小時 45 分鐘,這是用上限數字做的算術,實際遷移流程要看 Cloudflare 之後的說明。

下面的檢查器把這些限制做成可以輸入自己資料的表單。拉動滑桿,輸入你的資料量、最長的 key、每秒寫入次數,並勾選是否需要 metadata,它會逐項告訴你哪一項過關、哪一項被擋下。限制數字全部取自公告。公告沒有列 Classic 的容量與 key 長度限制,所以檢查器只檢查 Instant。

拉動滑桿輸入自己的資料形狀 · 5 項限制

2,000
200 KB
64
0.1
限制數字取自 Cloudflare 公告,private beta 期間可能調整。1 MB 以 1,024 KB 計,公告未說明換算方式,也未說明 key 是否計入總大小。

限制數字取自 Cloudflare 公告,private beta 期間可能調整

Instant 須同時滿足 10,000 對內、總量 1 MB、key 300 bytes 內、每秒一寫、不需 metadata,任一超標就留 Classic。

把滑桿往右拉,可以親手驗證前面的算術:只要每對平均超過 100 bytes 上下,先到頂的是 1 MB,不是一萬對。一萬對對 feature flag 清單是很寬的上限,1 MB 對帶有一點內容體積的資料則很窄。

價格:讀取便宜,儲存按 MB 計高價

公告的價格表兩種模式並列。Classic 的讀取(Class B)是每百萬次 $0.50,Class A(put、delete、list)是每百萬次 $5.00,儲存是每 GB 每月 $0.50。Instant 的讀取是每百萬次 $0.20,Class A 是每次 $0.10,儲存是每 MB 每月 $100。文字說明寫得很直接:Class B 操作「are charged at $0.20 per million keys requested, 60% cheaper than Workers KV default mode」。

讀取是 Instant 唯一比較便宜的一項。其餘兩項,公告自己的說法是「storage and Class A operations are significantly more expensive than Workers KV」。把單價換成同一個單位算,Class A 從每次 $0.000005 變成每次 $0.10,約兩萬倍。儲存從每 MB 約 $0.0005 變成 $100,約二十萬倍。這兩個倍數是我用公告的單價做的算術。

Instant 的 namespace 最大只有 1 MB,儲存用滿一整個 namespace 一個月是 $100。這個單價加上容量上限,把 Instant 定位在小而熱的資料。Class A 的 $0.10 每次也要照字面讀:寫入頻率開到上限,每秒一次、一天 86,400 次,一天的寫入費是 $8,640。Cloudflare 把這種價格放在「很少更新」的資料旁,跟它對目標資料的描述一致。

下面的試算器讓你輸入每月讀取量、儲存大小與每天的寫入次數,把 Instant 與 Classic 的三項費用並排畫成長條。Classic 在這裡只算同樣大小的資料,公告沒有列 Classic 的容量上限,試算器也不替它設上限。

拉動三個滑桿比較兩種模式的月費 · 3 項費用 × 2 種模式

100
0.10 MB
10
讀取 儲存 寫入 Instant Classic
單價取自 Cloudflare 公告的價格表,廠商自行公布,private beta 期間可能調整。長條長度以六項中最大者為 100%,各項的絕對金額標在長條旁。儲存以 1 MB 等於 1/1,024 GB 換算,一個月以 30 天計。

單價取自 Cloudflare 公告的價格表,廠商自行公布,private beta 期間可能調整

Instant 的讀取較便宜,但寫入單價約為 Classic 的兩萬倍、儲存約二十萬倍,只在小資料與低寫入頻率時才划算。

用預設值看:每月 1 億次讀取,Instant 是 $20,Classic 是 $50;0.1 MB 的儲存,Instant 是 $10,Classic 不到一美分;每天 10 次寫入,Instant 約 $30,Classic 約 $0.0015。合計 Instant 是 $60,Classic 約 $50,Instant 反而貴。讀取每百萬次省 $0.30,0.1 MB 的儲存費要每月讀取超過約 3,300 萬次才抵得掉,1 MB 則要超過約 3.3 億次,這還沒算寫入。這是用公告單價做的算術。

所以 Instant 的價格結構有一個清楚的方向:讀取量大、資料很小、寫入極少,帳單才會比 Classic 好看。資料一多或寫入一頻繁,儲存與 Class A 的單價很快就超過讀取折扣省下的錢。

這四軸算下來,兩種模式在讀取延遲、價格、寫入頻率的粒度上都有公告給的數字,在容量、key 長度、metadata 與 list 上,只有 Instant 的限制是明確的。下面的選擇規則因此有一半建立在兩邊的數字上,另一半建立在 Instant 的硬限制上。

依 workload 選:五個條件全過才用 Instant

把前面四軸整理成一條規則:Instant 的優勢是讀取延遲和全球可見速度,條件是資料小、很少改、不需要 metadata、願意接受 beta 狀態。五項條件任何一項不成立,就留在 Classic。這條規則的前三項來自公告的硬限制,後兩項是我加的判斷,價格那一項在上面的試算器裡可以自己驗算。

屬於 Instant 的是公告自己點名的那類資料:「infrequently updated application configuration data」,也就是很少更新的應用程式設定,例如 feature flag 與全域設定,需要全球可用、讀取延遲要壓到最低。公告的範例是每個請求都讀一個 launch flag 來決定首頁內容。這類資料量小、寫入以手動或部署為單位、讀取在請求路徑上,每一項都對上 Instant 的強項。

留在 Classic 的有三類。資料量超過一萬對或 1 MB 的,例如內容快取、設定檔很大的多租戶資料。寫入頻率超過每個 namespace 每秒一次的,例如計數器、session。依賴 metadata 或需要分頁 list 的。前兩類由 Instant 的限制數字直接排除,第三類由功能差異排除。Classic 的容量與 key 長度限制公告沒有列,搬去 Classic 後是否一切順利,仍要對照 Workers KV 的官方文件。

拿三個假設的情境對照。首頁每個請求都要讀的 launch flag,這是公告自己舉的例子,資料小、改動由人手觸發,五項條件看起來都過。一份由後台頻繁更新的清單,寫入頻率與資料量都可能越線,要先用上面的檢查器量過自己的數字。每個使用者一個 key 的瀏覽紀錄或計數器,寫入頻率與 key 數量很容易同時超標,通常沒有討論空間。

兩種模式在 namespace 層級指定,所以同一個專案可以同時使用兩種。最自然的組合是把請求路徑上的少量旗標放進 Instant namespace,大量或常改的資料留在 Classic namespace。這個混用做法公告沒有明講,但它符合「模式在建立 namespace 時決定」這個事實。

採用前有三件事要自己確認。第一,用自己的 Workers 在實際流量下量讀取 P99,廠商公布的 1.62 毫秒是否在你的路徑上成立,公告沒有給出測量條件。第二,問清楚超過每秒一次寫入時的行為。第三,問 private beta 到正式上線的時程與價格是否會變,公告只說「launching today in private beta」,沒有給後續時間表。

最後說一個比較保守的判斷。這份公告是廠商發表,數字沒有第三方驗證,而 Instant 又是 beta。就算限制全部對得上,也建議先在一個非關鍵的旗標上試跑,觀察一段時間後再決定要不要把請求路徑上的重要開關搬過去。讀取延遲的收益來自公告的數字,搬移成本與寫入超限的行為,是現階段最可能讓決定翻盤的兩件事。

How to choose:資料能放進一萬對與 1 MB、很少寫、不用 metadata、而且讀取在請求路徑上,就把這批旗標放進 Instant;其餘一律留在 Classic,並在寫入超限行為與 beta 時程確認前,先別把關鍵路徑押上去。