對外契約的每一個變更決定,最後都會停在同一種問題上:誰還在用這個東西、用多少、停掉會傷到誰。舊版能不能下線、錯誤一律回 200 的做法能不能改回真實 status、分頁能不能換機制、冪等條款能不能收緊——這些決定各自的判準不難給,而執行判準需要的證據全部來自同一層能力:對消費者的用量觀測。缺這一層時,判準會退化成問了也得不到答案的問題,變更決定只好靠「應該沒人在用吧」推進。

本章寫這層能力該有多細、身分怎麼認、以及它的成本邊界。埋點機制、cardinality 控制與保留階梯的操作面在 4.7 Cardinality 與成本治理

各種契約決策要答的問題不同

觀測要建到什麼粒度,由手上待決的契約問題反推,而不同決定要的證據形狀差很多。

待決的契約決定要答的問題需要的觀測維度
舊版本或 deprecated 欄位下線還有誰在呼叫、量在衰減還是持平消費者身分 × 版本 × 欄位、時間序列
錯誤格式改回真實 status一律回 200 的行為被哪些消費者的解析邏輯依賴消費者身分 × 端點 × 錯誤回應
分頁換定位機制真實流量翻到多深、誰在用頁碼與總數端點 × 分頁參數的分布
冪等條款收緊同 key 衝突率、replay 率、重送集中在哪些消費者消費者身分 × 冪等鍵事件

存在性問題最難也最常缺。版本退場需要的是可以逐一點名的清單、而非「舊版總呼叫量」這個聚合數字——因為退場的執行動作是通知,而通知要有收件人。只有總量時能做的判斷僅止於「還很多」或「幾乎沒有了」,而「幾乎沒有了」正是最危險的一格:長尾的絕對量小、卻可能是那三個佔營收四成的客戶。

形狀問題問的是參數與回應的分布而非呼叫次數,而它有一種特別棘手的形態。「誰依賴一律回 200 的行為」在 server 端觀察不到——server 看得到自己回了什麼,看不到 client 怎麼解讀。實務上能拿到的替代證據是間接的:改版前先在文件與 changelog 預告、觀察誰來反映,或在新端點回真實 status 並比較兩邊的錯誤處理行為。有些問題在服務端沒有直接觀測答案,設計時要先認出它們,才不會把「量不到」誤讀成「沒有人依賴」。

衝突率這一項相對容易,因為冪等事件本來就經過服務端的去重邏輯,計數不必另建管線;而它同時是形狀問題(衝突率的分布),因此抽樣就夠、不佔全量預算。它值得單獨埋的理由是衝突率直接指出消費者的重試行為長什麼樣——衝突集中在少數幾個消費者時,問題通常在那幾家的退避策略而非服務端的條款。

消費者身分是這層觀測的第一個設計決定

「消費者」在觀測上要對應到一個穩定的識別符,而候選欄位各有各的破口。這裡的消費者指的是會被通知、會回報的那一類(API Consumer Shape 的第一種形態)——非人格中介不需要身分維度,因為它們不在通知名單上。

API key 或 OAuth client_id 是最穩的一組:它由服務端發放、可歸屬到一個組織、且輪替時換的是同一個對象這件事是已知的。代價是要在發放時就把它跟組織綁好,事後從一堆裸 key 反推誰是誰,通常補不回來。token 的 subject 適合終端使用者維度,但一個整合方的 token 可能有成千上萬個 subject,拿它當消費者維度會直接把 cardinality 撐爆。User-Agent 是免費的、也是最不可靠的——它由呼叫端自己填,SDK 升級會改、自己寫 client 的人多半留空。IP 只在單一部署的整合方成立,一過雲端 NAT 就失去意義。

務實的做法是分兩層:歸屬層用發放時就綁定組織的識別符(key 或 client_id),承擔「這是誰」;診斷層才用 User-Agent 與版本字串,承擔「他用什麼在打」。兩層分開之後,前者可以要求準確,後者允許髒。

分層要跟著用途走,而用途有三種:通知(退場要找得到人)、舉證(稽核要證明誰讀過什麼)、影響評估(事故當天要算得出波及誰)。後兩者不需要對方是「會回報的人」,因此內部呼叫端與非人格中介也可能需要歸屬層——只看通知這個用途會推導出不必建。另外有一種情境會讓分層反過來:客戶端版本本身就是退場決策的主維度時(韌體不可更新的裝置群),版本字串要升到歸屬層並要求準確,不能留在允許髒的那一層。

轉售與嵌入通路會讓這個模型出現一個必須明講的斷點:服務端發的 key 給了轉售商,而真正在用的是它底下的終端客戶。這時候「誰在用」有兩個答案,而版本退場要通知的是前者、事故影響評估要算的是後者。契約上能做的是要求轉售方在請求裡帶一個下游識別符,通常以自訂 header 承載。它的準確性由對方保證,因此性質跟其餘維度不同:其餘維度量的是服務端自己看到的事實,這一格量的是對方願意申報的內容。設計時要把這個差別標在資料上,事故當天才不會拿它當成已知事實去算影響範圍。

欄位級用量問不問得出來,取決於介面有沒有宣告機制

「還有誰在用這個欄位」這個問題的可答性,跟 API 風格綁得很死,而這件事在選風格時多半沒有被算進去。

消費者顯式宣告要哪些欄位的介面(GraphQL 的 query、支援 sparse fieldsets 的 REST)天然帶著這一層:每次請求就是一份宣告,欄位級用量是解析請求就得到的副產品。這是 versionless(不做版本號、只加不改)路線的前提之一——只加不改的紀律要能執行,得先查得出「這個 deprecated 欄位上週有誰呼叫」,而宣告式介面讓答案在請求裡就有;剩下的是埋點與 cardinality 成本,固定形狀的 REST 則連原始資料都沒有(該路線的完整代價見 版本策略流派之爭)。

固定回應形狀的 REST 端點沒有這一層。server 回了整個物件,消費者用了哪幾格完全在對方的程式碼裡。能做的替代是三種,強度遞減:在端點層做決定(把欄位退場升級成端點版本退場,粒度變粗但可觀測)、要求消費者宣告(引入 sparse fieldsets,這本身是一次介面變更)、或接受用不可觀測的方式退場(預告加 brownout——退場前刻意短時間讓舊行為失效,用一次真實故障觸達沒讀公告的人)。第三種在 11.5 的 deprecation 工具箱 有完整展開,這裡的重點是:它之所以必要,正是因為欄位級觀測在這個風格下拿不到。

固定形狀 REST 上查不出「上週有誰呼叫這個欄位」是結構性的,不是團隊沒做好。這件事的實際意義是那個檢查問法在這種風格上不該當成閘門,而該當成風格選型的輸入——宣告式介面免費給答案,正是 versionless 路線只在那類介面上成立的原因。

風格選型時因此多一條判準:這個 API 的欄位預期會不會退場。答案是會、且消費者無法一次改完時,宣告式介面在退場成本上有結構性優勢,而它屬於風格選型時的演進成本這條判準(見 11.2 風格選型)。

存在性問題要全量、形狀問題可以抽樣

per-consumer × per-field 是乘積,而乘積在 metrics 實作下會爆。兩百個消費者乘上三百個欄位是六萬個組合——這個數字本身對現代 TSDB 不算大,而且實際上是稀疏的(每個消費者只碰得到一小部分欄位);真正會爆的是再乘上端點與版本維度、且每個組合都做成時間序列的時候。乘積算出來之後要跟它服務的決定比:一次退場決定弄錯的代價,可能就高於整年的儲存費,那種情況下貴的不是觀測。

分界有兩層,而第一層是實作形態——它常被跳過,於是後面的成本討論建立在一個沒說出口的前提上。

存在性問題問「還有沒有人」,它不需要時間序列。一張 (消費者, 欄位, 最後看到的時間) 的表、每次請求 upsert 一次就答完了:列數是活躍組合數而非時間點數、原地更新、沒有保留階梯,成本比同一份資訊做成 metrics 低一到兩個量級,而且可以永遠開著。形狀問題問「分布長什麼樣」,它才需要時間序列與直方圖,而它也是唯一抽樣無損的一類——抽樣保留比例、毀掉個體,而形狀問題只要比例。

把兩者都塞進 metrics 是成本問題的來源。抽樣會讓長尾消失,而長尾正是退場風險所在,所以存在性問題一旦用 metrics 實作就變成「要嘛全量很貴、要嘛抽樣沒有意義」的兩難——而那個兩難是實作選擇造成的,不是問題本身的性質。

同一個維度在不同時候會落在不同一側,這是這條分界最實用的性質。分頁參數分布平常是形狀問題(翻頁深度的直方圖,抽樣就夠),要決定某個舊參數能不能移除時變成存在性問題(還有沒有人送它)。欄位級用量同理。

第二層分界是前瞻還是回溯,而它決定了什麼時候開觀測來得及。前瞻的問題(「接下來這三個月還有沒有人用」)可以等到要決定時再開。回溯的問題(「過去十二個月誰讀過這個含個資的欄位」「事故發生那週誰在拿這批資料」)只有常設觀測查得出來——稽核、法遵與事故調查問的幾乎都是回溯型,而它們共同的性質是問題出現的時候,能回答它的資料要嘛已經在那裡、要嘛永遠不在了。判斷哪些維度要常設,看的是這一層而非重要性。

last-seen 表建不起來、而問題又是前瞻型時,退路是取樣窗口:決定要退場某個欄位或參數時開一段全量觀測,窗口結束後關掉。它的適用前提要講清楚——這個維度本來就在發射,只是平常抽樣。固定形狀的 REST 端點在欄位級上連原始資料都沒有,窗口對它無效,那種情境的替代手段在下一節。

窗口長度由這個端點已知的最長呼叫週期決定,而不是一個固定天數,且要留餘裕:兩週只涵蓋兩個週批次,一次連假就讀成「沒有人在用」,實務上取最長週期的三倍或最長週期加一個週期。呼叫週期本身也是一個存在性問題,答案就在 last-seen 表裡(相鄰呼叫的最大間隔)。週期查不出來或長到窗口撐不住時,正解是換手段而非拉長觀測——預告加反映、或 brownout。

窗口量到的是「還有沒有人在呼叫」,而它推不出「他們多久改得完」——後者由消費者側的變更節奏決定(對方多久發一次版、更新覆蓋率爬到多少、簽核走幾天),是另一個數,且不由服務端控制。兩個數的用途不同:呼叫週期決定窗口要開多長,變更節奏決定通知要提前多久發(該數怎麼進排程見 11.13 既有 API 的改造路徑 的第零層)。

窗口這條路還有一個容易被忽略的成本反轉:每次退場決定都要新的埋點、review 與部署,前置兩到四週。一年要退十幾個欄位時,窗口序列跑不完、並行跑就等於全量常開,此時 last-seen 表反而便宜。

長期維持全量的只有一組維度:消費者身分 × 版本 × 端點的呼叫計數。它是存在性問題裡唯一需要連續數月觀察衰減曲線的,其餘都可以用窗口換空間。這組維度要做成常設而非每次臨時撈 log——衰減曲線的價值在於它涵蓋了決定要看它之前的那幾個月,而臨時撈只回答得了「現在」。「常設」指的是資料持續累積這件事,跟有沒有一套觀測平台無關:消費者數量少到列得出名單時,從既有的 access log 每天聚合一次寫進一張表就滿足這個條件,而那份表就是這一層。規模決定的是實作形態,不是要不要做。

cardinality 的控制手段、sampling 策略與保留階梯的完整操作面在 4.7 Cardinality 與成本治理;本章只負責界定「哪些維度值得付這筆錢」。

判讀訊號

訊號判讀
退場決策的討論停在「應該沒什麼人在用了吧」沒有消費者身分維度,決定建立在感覺上
有總量、但列不出呼叫方名單觀測建在聚合層,通知這個動作沒有收件人
每次要查「誰在用 X」都要臨時撈 log這層能力還是專案而非設施,下次決定又要重來一次
deprecated 欄位標了很久、沒有一個真的移除缺欄位級證據,標注因此只能累積
事故當下算不出影響哪些客戶歸屬層識別符沒跟組織綁定,或只有 User-Agent 這類自填欄位
觀測成本被質疑、而指不出它服務哪些決定維度是照著「能埋就埋」長出來的,沒有從待決的契約問題反推

多數訊號往回追都指向同一個根因:沒有消費者身分這個維度。缺了它,退場決策只能建立在感覺上、通知找不到收件人、deprecation 標注只能累積——三種症狀、一個原因。

另外兩種訊號指向的是這層能力的成熟度而非有無:每次都要臨時撈 log,表示它還是專案而不是常設的觀測設施;事故當天算不出影響哪些客戶,表示歸屬層的識別符從發放時就沒跟組織綁定,而那一步補不回來。

成本被質疑卻指不出它服務哪些決定,則是建置順序被顛倒的直接後果。先列出未來一年要做的契約決定、再從那份清單反推維度,成本討論會很短。

下一步路由

本章的維度分界、身分分層與取樣窗口做法為機制推導,未見公開規範明文處理。