前言

在我離開 Airbnb 的時候,Airbnb 的語意層 Minerva 已經滲透到公司的每一個角落。我們有大約 200 位資料生產者,每週送出 100 多個 PR,將近 10,000 份設定檔產生出 20,000 多個 DAG 和 10,000 多張 Iceberg 資料表。Minerva 經常佔用 Airbnb 一半以上的回填運算量,以 Airflow DAG 數量來看是公司最大的資料框架。在使用端,它支撐了 60,000 多張 Superset 圖表和 Tableau 儀表板,服務超過 4,000 位使用者。

這些在一開始幾乎都不在我們的視野裡。這些年來我們做了不少工程決策,有好有壞,一路把平台推到今天的樣子。這篇文章想分享其中一些決策,以及我們從中學到了什麼。

開發者體驗

每天有這麼多資料生產者在用我們的工具,工作流程的設計就變得非常重要。使用者最在意的永遠是迭代速度。大家希望能建立、測試並反覆調整自己的變更,而且回饋循環要快。這成了我們的北極星,也決定了我們怎麼看待設定、驗證、審查和試跑這幾件事。

設定即程式碼

Minerva 是一個以設定驅動的框架。使用者用 YAML 宣告語意定義,而 Minerva 把這些定義當成程式碼看待:存在 git 裡、納入版本控制、合併前要經過審查。把資料當成程式碼管理現在很流行,一部分要歸功於 dbt 這類工具,而 Minerva 是在 Airbnb 的規模上做這件事。

把語意存在 git 裡,會帶來一個很實際的問題:東西變得不好找。沒有一套慣例的話,根本無從瀏覽這些檔案;而當定義被數百個團隊共用時,找不到既有定義的人最後就會自己重寫一份。Minerva 的第一個版本裡,我們和 Airbnb 的資料目錄 Dataportal 整合,讓大家在搜尋指標和維度時可以直接看到對應的設定。第二個版本走得更遠,做了一個專屬的介面 Minerva Studio,除了同樣的資訊之外,還提供更豐富的中繼資料。

設定檔本身好不好讀,又是另一個問題。第一個版本沒有像 sqlglot 這種工具可以解析 SQL,所以我們靠自訂欄位和一套 DSL 來定義指標,每次有人需要重用某個定義,我們就再加一點欄位、再加一點 DSL。久而久之這變得很難收拾。要看懂一個定義,你得先學會這套 DSL,再從設定檔的好幾個地方把它拼起來。第二個版本把指標定義改成純 SQL,用 sqlglot 來解析和驗證,情況因此單純很多,因為定義就攤在那裡,可以直接讀完。

驗證

不是每份設定都是對的,所以框架需要驗證機制,在合併之前把問題攔下來。我們建了一套驗證流程,在開發流程的兩個時間點執行:使用者在本機修改設定的當下,以及開 PR 之後的 CI。

Minerva 的第一個版本裡,各種驗證是靠一連串脆弱的 shell 指令碼串起來的。同一批指令碼同時服務使用者和 CI,所以我們常常得依環境加上分支邏輯。它們的職責也不單一,會去呼叫其他做特定驗證的指令碼,而且沒有簡單的方法知道哪一段是誰負責的。再加上是 shell 指令碼,幾乎不可能寫單元測試。

開發第二版 Minerva 時,我的同事 Philip 和 Krist 接下了重寫驗證流程這個吃力的任務。他們用 Cerberus(一套 Python 的資料驗證框架)把基本的 YAML 驗證標準化。在 Cerberus 裡你會定義一份驗證 schema,也就是一組「schema 鍵」對「schema 值」的對應,值則是預先定義好的規則,用來限制每個鍵可以放什麼。舉例來說:

schema = {'name': {'type': 'string', 'maxlength': 10}}

這表示 name 這個欄位必須是字串,長度不能超過 10。這類標準化的驗證,Cerberus 處理掉了很大一部分,我們只在必要時才加自訂規則,驗證的維護成本因此壓得很低。

Krist 帶進來的另一個改進,是他稱為 Spell 的框架,一個建構在 Typer 之上的 CLI 工具。不同的驗證工作都可以實作成一個 Spell,而每個 Spell 都能在我們的程式碼庫裡寫單元測試。每個 Spell 就是一個帶有自己參數的 CLI 指令,使用者可以在本機執行:

minerva validate

使用者在本機跑的那些 Spell,在 CI 裡跑的是同一批。其中特別好用的一組是自動修正的 Spell,它們會偵測問題並直接幫使用者修好。有些操作比較慢,所以我們開始追蹤 CI 工作的執行時間,確保沒有造成效能退步。CI 上每多一秒,都會反映在某個人的迭代循環裡。

我們也花了不少功夫記錄每個欄位的意義,並且補上清楚的錯誤訊息,附帶指引告訴使用者該怎麼修正設定。這讓大家跑來 oncall 頻道發問的次數少了很多。

擁有權、審查與 PR 核准

隨著愈來愈多商業定義被寫進 Minerva,我們意識到這個平台不只是存放唯一真實來源的地方,它同時也是推動大規模變更管理與資料治理的那部機器。使用者必須看得到什麼被改了、是誰改的,也需要一個管道在有疑問時找到負責人。我們從第一天就把擁有權當成一等公民,而這對審查、核准和生命週期管理都有實際的影響。

Minerva 裡有一個叫做 team 的概念。每個 team 帶有維護者名單、PagerDuty 信箱、Slack 頻道這類資訊。team 可以被掛在特定資料集上成為擁有者,而擁有一份資料集是要負實際責任的。

只要有別人改動了自己的資料集,負責人就會被標記,並且可以在變更合併前核准或要求修改。PR 審查流程於是變成不同團隊辯論、對齊商業語意該怎麼定義的場合。這確實替開發流程帶來了摩擦,要集滿所有負責人的核准有時得等上一陣子。我們後來引入了不同層級的審查者,讓比較輕量的變更走比較輕量的流程,也和 analytics engineering 合作建立了 Minerva 審查者制度,由這些審查者行使否決或核准。

負責人同時也是管線失敗和延遲的第一線。任何因失敗而觸發的警報都會送到擁有該資料集的團隊,由他們去查為什麼管線延遲或失敗,只有在他們認為是更大範圍的基礎設施問題時,才升級給 Minerva 團隊。這對有專職 oncall 工程師的團隊運作得很好。沒有 oncall 輪值的團隊則常常被警報信件轟到疲乏,乾脆略過不看,直接找上我們的支援頻道,這對我們來說變成很重的維運負擔。

負責人也是最能回答「這個商業定義到底是什麼意思」的人。他們對脈絡的掌握比 Minerva 平台團隊深,所以我們把他們呈現在 Dataportal 上每一個 Minerva 資產頁面。我們在自家的 SQL 編輯器 SQL Lab 裡也做了同樣的事,讓查詢指標或維度時遇到問題的人可以直接找到負責人。在 AI 的時代,不難想像這些負責人會成為各自領域知識的策展人與守護者。我想這會是大規模脈絡工程(context engineering)的關鍵之一。

試跑

Minerva 早期沒有任何機制讓使用者試跑自己的變更。所有變更都是直接在正式環境測試,這造成了很實際的問題。迭代週期很慢,使用者光是要測一下自己的變更,就得先走完 PR 審查流程、拿到核准。這既花錢,對資料正確性來說也有風險:我們浪費運算資源去回填可能是錯的資料,而且有可能在使用端毫不知情的狀況下就把壞資料發布出去。

這個缺口大到讓我們決定引入一套新的流程,也就是試跑。想法是使用者應該在把變更推上正式環境之前先測過。當使用者開一個 PR,我們會把 YAML 設定檔的內容序列化,和正式環境裡的版本做比對。從這份 diff 我們算出哪些資料集有變動,原理和 git diff 類似,並且呈現出像是變更樹狀圖、這次變更預估的回填成本這類有用的資訊。Philip 在他的這場演講裡有更詳細的示範,有興趣的人可以看看。

試跑不會觸發完整的回填。它只跑一段指定的日期範圍,並把資料寫進一個和正式環境隔離的暫存命名空間。試跑完成後,使用者要執行一個指令產生一張收據,作為「確實測過」的憑證;沒有這張收據,驗證就會標記這個變更尚未經過測試。試跑上線之後很受歡迎,大家不必再為了測一件小事而走完一整套繁複的審查流程。他們可以在暫存命名空間裡把玩資料、繼續修改,迭代週期因此快了非常多。

在我離開 Airbnb 之前,團隊正在投入下一代的試跑體驗。最關鍵的改變是使用者連 PR 都不用開就能試跑,而且暫存資料不只會出現在資料倉儲裡,也會出現在查詢層。這代表使用者可以直接在我們的 BI 工具裡查詢這些測試資料!這套流程大部分是以 CLI 驅動的,也就是說 AI 也能執行它。我們的目標是讓 AI 最終能在幾乎不需要人介入的情況下走完整個循環,把迭代速度再往上推一級。我認為這會是開發者體驗的下一次跳躍式改變。

總結

在打造資料框架的時候,開發者體驗很重要。使用者只有幾個人時,這件事沒那麼要緊;但到了某個規模,體驗上的小改善會產生不成比例的巨大影響。

打造 Minerva 讓我們明白,使用者非常在意迭代速度,而這件事在 AI 時代並沒有改變,如果有什麼不同,是標準變得更高了。把語意當成程式碼放在 git 裡,讓我們白白得到了版本控制和審查機制,代價則是我們得自己扛起「讓定義好找、好讀」這個責任。我們在驗證上花了很多時間,盡可能早地把錯誤攔下來,該自動修的就在背景直接修掉。試跑流程也投入了大量資源,因為它讓大家能把變更從頭到尾測過一遍。最後,我們投資在擁有權上:把它落實在審查和 oncall 制度裡,再把負責人放到資料目錄最顯眼的位置,這才讓擁有權能夠以分散的方式擴展開來。

下一篇我會談 Minerva 的第二個大元件——運算,以及我們如何打造一套系統,讓資料倉儲裡的資料跟上最新的商業定義。