vatt'ghern jaskier's ballads

十筆廣告曝光事件,video 與 banner 各存了 5 筆,數筆數兩邊一樣多;把每筆事件自帶的取樣率加起來,卻是 50 比 5。這組數字是我編的,但它濃縮了 2024 年 2 月 15 日那篇 wide events 文章的賭注:事件原樣留著、欄位全留著,三支柱就不必各存一份。

Wide Events 能吞下三支柱嗎

這篇文章是 Ivan Burmistrov 寫的,標題一開頭就把立場亮出來:All you need is Wide Events。它發表在 2024 年 2 月 15 日,距今已兩年多,所以下面不把它當作最新動態,而是當作一份仍可拿來對照的主張。對照的是兩種資料模型。一種是業界熟悉的三支柱,trace、metric、log 各有各的資料形狀和工具;另一種是作者主張的單一模型,所有東西都是一筆有名字的欄位集合。比較沿四條軸走:欄位怎麼對應、計數怎麼算、基數受不受限、問題怎麼探索,再回頭分辨哪些論點是原文寫明的、哪些是作者的立場,最後給一條選擇規則。來源只有這一篇,作者自述曾在 Meta 工作,用的是內部的 Scuba:「When I was working at Meta, I wasn’t aware that I was privileged to be using the best observability system ever.」他拿自己熟悉的系統當範本來論述,後面每一處「作者說」,都請照單一來源、當事人視角的強度來讀。

欄位怎麼對應:一筆事件,三種身分

文章對 Wide Event 的定義很樸素:「Wide Event is just a collection of fields with names and values, pretty much like a json document.」沒有事先宣告的指標名稱,也沒有固定的結構要求,一筆事件就是一份有名字和數值的欄位集合。後面所有論點都從這個定義長出來。如果事件夠寬、欄位夠自由,那麼三支柱也許只是這個模型裡幾種慣用的欄位組合。作者對現況的不滿寫得很直接,他認為混淆從 OpenTelemetry 就開始了:「First, Open Telemetry from the very beginning makes a clear distinctions between traces, metrics and logs:」。換句話說,他把三支柱的分法當成問題的源頭,而不是中立的分類。

他給的對應有三條。第一條是 trace:「These are just wide events having SpanId, TraceId and ParentSpanId fields.」span 不是另一種資料型別,只是帶了三個特定欄位的事件。第二條是 log,他把結構化日誌當作其中一項:「one of them is Structured Log which is pretty much the Wide Event.」。第三條是 metric,下面會單獨談,因為它是三條裡最像類比、最不像等式的一條。下表把三條並排,左邊是三支柱的名字,右邊是它在事件模型裡的樣子。

三支柱在 wide event 模型裡的對應。第三欄是原文引句,第二欄與第四欄是我的整理。
支柱 在 wide events 裡是什麼 原文依據 讀的時候要補問的
trace 的 span 帶 SpanId、TraceId、ParentSpanId 三個欄位的事件 「These are just wide events having SpanId, TraceId and ParentSpanId fields.」 查詢時組呼叫樹的成本由誰付
log 結構化日誌就是 wide event;非結構化日誌先抽模板再取 hash 「one of them is Structured Log which is pretty much the Wide Event.」與「via removing tokens that looks like IDs, and get a hash of this template」 模板化這一步誰維護
metric 每隔一段時間發出一筆帶系統狀態的事件 「We just need to emit a Wide Event once per some interval containing the state of the system」 告警與長期保留怎麼辦

log 那列多一個細節。非結構化的日誌要先把像 ID 的 token 拿掉,再對剩下的模板取 hash,就像上表第三欄第二句寫的;依整理版本的讀法,用途是把相同形狀的日誌歸成一類,好找出頻繁出現的錯誤。非結構化日誌因此也擠得進同一個模型,代價是多一道要自己維護的模板化步驟,大量日誌下它的成本,文章沒有交代。第四欄的三個問題是我補的。

把 span 當成事件,就用不著專用的追蹤儲存。一批扁平的事件,只要共用 TraceId,再靠 ParentSpanId 指向上一層的 SpanId,就能組出呼叫樹。下圖用四筆我自己編的示意事件演示這個組裝。欄位名稱取自文章,span 的名字與數量不是文章的內容。

扁平的事件(儲存的樣子) 查詢時組出的呼叫樹 SpanId = a ParentSpanId = 無 TraceId = t1,示意 span 處理請求 SpanId = b ParentSpanId = a TraceId = t1,示意 span 讀取資料 SpanId = c ParentSpanId = a TraceId = t1,示意 span 渲染回應 SpanId = d ParentSpanId = c TraceId = t1,示意 span 序列化 a 處理請求 b 讀取資料 c 渲染回應 d 序列化 ParentSpanId 指向上一層的 SpanId,樹是查詢時才長出來的
四筆事件與 span 名稱都是示意。儲存層裡每一筆只是一份欄位集合,樹狀結構要靠 ParentSpanId 在查詢時重建。

圖左邊四筆事件在儲存層沒有任何「樹」的概念,它們只是四份欄位集合,用同一個 TraceId 撈出來就行。呼叫樹是查詢時才長出來的檢視。追蹤視圖因此只是看資料的一種方式;同一批事件也可以不組樹,直接按別的欄位分組,後面的 widget 就是這樣用的。

metric 的對應最有意思,也最需要拆開讀。作者的做法是:「We just need to emit a Wide Event once per some interval containing the state of the system」,也就是每隔一段時間發一筆帶著系統狀態的事件。他再拿 Prometheus 當對照:「Prometheus, by the way, does exactly that with the scraping approach - takes a snapshot of the system once in a while.」抓取式的指標本來就是定期拍快照,wide events 只是把快照也存成一筆欄位自由的事件。

請求事件流,每個請求一筆,出現時間不固定 狀態快照事件,每隔固定間隔一筆 快照 1 快照 2 快照 3 快照 4 快照 5 固定間隔 固定間隔 固定間隔 固定間隔 請求事件隨流量出現;快照事件隨時鐘出現,欄位是當下的系統狀態。 兩者在模型裡都是一筆有名字的欄位集合,差別只在誰觸發它。
示意圖,圓點與方塊的位置不代表任何實測。快照事件對應文章所說的「emit a Wide Event once per some interval」。

這裡要分清楚哪一句是等式、哪一句是類比。「定期拍快照」這件事,Prometheus 與 wide events 相同,這是文章寫的。但告警規則、長期保留、降採樣這幾件事,我在整理版本裡沒有看到。它們在實務上決定一個指標系統好不好用,所以「metric 是特例」只能讀成資料模型層面的主張,推不到「可以拔掉指標系統」。

把三支柱收進一個模型,換到的是少一套要學的概念,以及欄位在三種用途之間可以共用。同一個服務名稱、同一個部署版本,不必在三套系統裡各標一次,分別用三種寫法。但這個整合也有它沒有說出口的一面:三支柱各自的工具之所以長成那樣,通常是為了配合各自資料的形狀,時間序列適合壓縮、追蹤適合按 ID 取回整條鏈、日誌適合全文搜尋。把它們都當成事件,等於把「怎麼存、怎麼查才快」這個問題全部交給一個儲存引擎去回答。文章的立場是這個引擎做得到,但沒有附上對照的量測。這段取捨是我的推論。

計數怎麼算:SUM(samplingRate) 取代 COUNT(*)

第二條軸是計數。大流量系統常常需要取樣,問題是取樣之後,圖上的數字還能不能信。事件模型的答案是讓每一筆事件自己帶著取樣率,指標那一側(一般做法,非文章內容)則在寫入時就把數字加好,維度先定了。文章的做法是把取樣率寫進事件本身:「When a certain event is written to Scuba, it also must write a field called samplingRate」。注意那個 must,這是一條對寫入端的要求,不是可選的欄位。有了這個欄位,Scuba 用它來還原圖上顯示的結果(「Scuba uses this information to properly」),作者的說法是「so there is no need to do this upscaling in the head」,意思是讀圖的人不用在腦中自己乘回去。

文章在「So, the actual query under the hood is」之後列出底層查詢,依我的理解,畫面上的 COUNT(*) 在底層換成了對 samplingRate 的加總,也就是 SUM(samplingRate)。為什麼這個換法重要?如果各事件的取樣率不同,例如重要的事件全部保留,雜訊事件只留十分之一,那麼直接數筆數,會系統性低估被多丟掉的那一類。每筆事件帶著自己的比率,加總時各自乘回去,不同取樣率的事件就能放在同一張圖上比較。文章寫的是原生支援,為什麼重要,是我補的推論。

我在下一節的 widget 裡用十筆編出來的事件示範這個差別。video 的五筆取樣率各是 10,banner 的五筆各是 1。數筆數,兩組都是 5;加總取樣率,是 50 比 5。COUNT(*) 在這組資料上說兩邊一樣多,實際上差了十倍。這組數字是我為了示範編的,不是文章的資料,widget 的圖說會再註明一次。

這個設計也有代價。取樣率大於 1 的事件,還原之後的數字是估計值,不是精確計數;文章沒有談誤差。照統計上的一般推理,取樣率越高,小分組裡剩下的樣本越少,那個分組的估計就越不穩。另一個代價是契約。既然 samplingRate 是寫入端「must」寫的欄位,任何一個漏寫或寫錯的服務,都會讓下游以它為基礎的加總失真,而且從圖上看不出來。

如果你想檢查自己系統的取樣有沒有被正確還原,一個直接的做法是找一段沒有取樣的流量,同時用兩種算法加總:一邊數筆數,一邊加總取樣率,兩邊在沒有取樣時應該相等,開了取樣之後,加總取樣率應該接近未取樣時的筆數。這是我從 SUM(samplingRate) 的定義推出來的檢查方式,文章沒有提過。它的好處是不依賴任何一個工具的內建還原,出了問題時你知道該往寫入端的哪個欄位查。

基數:預先聚合的上限,與原始事件的掃描成本

第三條軸是基數。作者的說法很乾脆:「Scuba works with raw events and doesn’t pre-aggregate anything, and so cardinality is not an issue.」他在別處也寫過「Because it doesn’t matter - any field can be of any cardinality.」。指標那一側的道理屬於一般知識,不出自這篇文章:預先聚合的系統要為每一種標籤組合維護一條時間序列,標籤的值越多,序列越多,基數於是成了上限。原始事件不聚合,分組欄位有多少種值,就只是查詢時掃描的事,不是寫入時要維護的序列數。

兩側因此各背一筆帳。預先聚合的一側在寫入時付出,要事先挑哪些標籤值得留;原始事件的一側把帳挪到查詢時付,每次分組都得回頭掃事件,文章也沒有說這筆查詢成本由誰承擔。用本文的 widget 來看,十筆事件的任何欄位都能分,換來的是每次都從事件重算;預先定義的那一側只留了 CampaignType,寫入便宜,換欄位就沒有資料可算。

探索:不必事先決定要看哪個指標

第四條軸是文章篇幅最重的一塊,用廣告曝光當例子。作者從這句話開始:「the natural desire would be to record Ad Impressions.」第一張圖是曝光數量隨時間的變化。一旦圖上出現不尋常的地方,他的做法是:「So we would group by say impression type, or user country, or the placement, until we would find something suspicious.」接下來的幾步,他依序寫了「Let’s assume it’s CampaignType grouping:」、「For instance, User OS makes sense.」,再來是「Check OS Version maybe?」。

把這幾句連起來看,是同一個動作重複三次:換一個欄位分組,看哪一組的線和別人不同,再在那一組裡換下一個欄位。從 CampaignType 到 OS 再到 OS Version,每一步都把範圍縮小一層。作者把這種做法稱為處理未知問題的基礎:「This approach is laying the groundwork for dealing with unknown unknowns」。他還補了一個條件:「Meaning that there is an easy way to see what kind of values we can pick.」下鑽能走下去,不只是因為資料裡有這些欄位,還因為使用者看得到有哪些欄位、哪些值可以選。

這三步的順序不是固定的,下一步分哪個欄位,取決於上一步看到了什麼。儀表板要在事故發生之前就決定好每一張圖,下鑽的路徑卻要事故發生之後才知道,兩者很難對上。作者用「until we would find something suspicious」描述分組過程,讀起來沒有預設答案,停在哪裡由圖上看到的東西決定;這是我對語氣的解讀。

與預先定義的指標比,差別在維度要不要事先決定。如果你當初只建了「依 CampaignType 的曝光數」這個指標,那麼 OS 與 OS Version 那兩步就走不下去,除非先改程式、等新資料累積。下面的 widget 把這個差別放到你手上:十筆示意事件,三個可以分組的欄位,兩種模式。先看預設畫面,再切到「預先定義的指標」模式,點 OS,看會發生什麼。

按按鈕換分組欄位與模式,看 COUNT(*) 與 SUM(samplingRate) 的差別 · 3 個欄位 × 2 種模式

模式
分組欄位
十筆示意事件(我編的) CampaignType OS OS Version samplingRate 1videoiOS17.210 2videoiOS17.210 3videoiOS17.110 4videoAndroid1410 5videoAndroid1310 6banneriOS17.21 7banneriOS17.11 8bannerAndroid141 9bannerAndroid131 10banneriOS17.21 COUNT(*),存了幾筆 SUM(samplingRate),推算幾次 video 5 50 banner 5 5 依 CampaignType 分組,video 存了 5 筆、banner 存了 5 筆。 加總 samplingRate 依序是 50、5,合計 55。
十筆事件與取樣率都是我為了示範編的,不是文章的資料。欄位名稱 CampaignType、OS、OS Version 取自文章的例子。預設畫面是依 CampaignType 分組。

預設畫面是依 CampaignType 分組。COUNT(*) 那條綠色短柱,兩組一樣長,各是 5 筆;SUM(samplingRate) 那條赤陶色柱,video 是 50,banner 是 5。切到 OS,iOS 存了 6 筆、Android 存了 4 筆,加總取樣率是 33 與 22;切到 OS Version,四個版本的加總是 22、11、11、11。不管怎麼切,合計都是 55,因為這是同一批事件,只是換了切法。再切到「預先定義的指標」,CampaignType 還是能分,OS 與 OS Version 就只剩一句話:沒有被存下來。

這個對比刻意簡化了。真實的指標系統可以預先建很多維度,只是每多一個維度,就多一個要事先決定、也要承擔基數的東西。事件模型也有難處:事件越寬、欄位越多,就越需要欄位文件與命名紀律,否則「看得到有哪些欄位」會變成「一長串沒人認得的欄位」,這是文章沒談的部分。預先定義的指標則有穩定的另一面,告警條件和儀表板都綁在一個不會因為誰多加一個欄位就變形的名字上。以上是我的工程判斷。

論點的強度:原文寫明的、作者的立場、沒有答案的

四條軸比完,剩下的問題是每條軸上的論點站得多穩。有的可以對著原文核對,有的是作者的立場,有的文章根本沒有碰。我把它們分成三堆。第一堆是文章直接寫的事:Wide Event 的定義、samplingRate 欄位由寫入端提供、Scuba 處理原始事件而不預先聚合、span 是帶三個欄位的事件。第二堆是作者的看法:三支柱是 wide event 的特例、OpenTelemetry 從一開始就把三者分開造成混淆、他用過的系統是最好的。第三堆是文章沒有回答、需要你自己補的問題。

文章直接寫的

  • Wide Event 是有名字與數值的欄位集合
  • 寫入端必須提供 samplingRate 欄位
  • Scuba 處理原始事件,不預先聚合
  • span 是帶 SpanId、TraceId、ParentSpanId 的事件
  • metric 是定期發出的狀態事件

作者的看法

  • trace、metric、log 都是 wide event 的特例
  • OpenTelemetry 一開始就把三者分開,造成混淆
  • 他在 Meta 用的是最好的觀測性系統
  • 廠商應該表態反對這種混淆

要自己補問的

  • 取樣還原後的估計誤差有多大
  • 不預先聚合,查詢成本由誰付
  • 告警、長期保留、降採樣怎麼辦
  • 欄位治理在多團隊下怎麼維持
  • 文章之後的兩年,工具有沒有變
三欄的前兩欄對應原文引句,第三欄是我整理的問題,文章沒有回答,也不代表文章答錯。

第二堆裡有兩句措辭值得單獨看。作者寫:「Which is really upsetting, because Traces, Metrics and Logs are all just special cases of Wide Events really:」,字眼帶著情緒,是倡議文的口氣。他談到廠商時說:「I just wish that observability vendors took a stand against confusion」,同樣是期望,而非觀察。這兩句不減損前面的技術論點,但提醒你這篇文章有立場,證據來自單一作者,例子取自他熟悉的系統,並列比較其他系統成本的段落,我在整理版本裡沒有看到。

時間也是一個變數。文章寫於 2024 年 2 月 15 日,我沒有查這之後工具生態的變化,所以不評估現況。如果你要拿它做決定,先確認它說的限制在你正在看的產品上是否還存在。文章描述的是作者在 Meta 用過的 Scuba,所以任何「能不能在你的環境做到」的問題,文章本身都不能代答。

怎麼選:先看問題的維度是否事先已知

對照到這裡,兩種模型的差別可以收斂成一個問題:你要回答的問題,維度是不是事先已知。如果是,例如「這個服務的錯誤率有沒有越線」,預先定義的指標是直接的做法,告警條件好寫,儀表板穩定;這是我的判斷,文章沒有這樣主張。如果不是,例如事故中「曝光掉了,但不知道是哪一群使用者」,就需要能隨時換欄位分組的事件層級資料,文章的廣告曝光例子講的正是這種情況。

如果你考慮往 wide events 靠,文章的內容至少能轉成三個先查的問題。寫入端能不能一致地寫入 samplingRate,文章用的是 must,漏寫的服務會讓加總失真。欄位有沒有地方可以被發現,也就是「easy way to see what kind of values we can pick」這一句在你的工具上成不成立。最後是查詢原始事件的成本由誰承擔,文章沒有給數字,得自己量。三個問題的問法是我整理的。

兩種做法也不必二選一。保留現有的指標處理告警與長期趨勢,另外把最想任意切分的那部分事件,用帶 samplingRate 的寬事件存起來,專用在事故中的下鑽。這樣只為真正需要探索的資料付出原始事件的儲存與查詢成本,也不必一次拆掉運作中的告警。這是我的建議;文章要的是單一模型,比這激進。

這篇文章拿來當尺最合用。它給了清楚的資料模型主張,也留下實作上的空白,從取樣誤差、欄位治理到告警都是。把主張拆成可以驗證的問題,再拿你自己的資料去驗,比整套採納可靠。三支柱該不該被取代,要看你自己的流量、你自己的問題,以及你願意為哪一種限制付代價。

怎麼選:問題的維度事先已知,就用預先定義的指標;維度事先未知、需要在事故中任意換欄位分組,就需要 wide events 與 samplingRate 這套事件層級的做法。