那些關於後端和 Data Team 之間的故事


一直以來,Codementor/Arc 總是依賴著 data 來幫我們發現/驗証各種假設。 這陣子我們使用 data 的方式漸漸有了改變。 Application 的變動、各個不同 team 流程的調整、再乘上各種人為犯錯的可能, 即使 data 的量並不多,但各種變動帶來不預期的錯誤卻越來越多。 在這篇文章裡面,我們想把我們切入問題的方式,評估的方式跟選擇的策略跟大家分享, 希望可以對於在 data 這個領域要 “登大人” 的團隊們有所幫助。

從前從前

打從我們的產品上線的第一天開始,我們總是在用各種 data 幫我們發現、驗証各種事情。 這些 data 的來源,主要分成:

  • 我們各個 application DB 放的 data
  • 外部 tracking tool 收下來的 data
  • 有些流程會把 data 留在 3rd party 那邊,像是sales使用的服務等

整體來說,我們的 data 的特性是量不大,但是變化很快。 我們各種 data source 的 data 格式、邏輯每天幾乎都在變化,需求也每天都在跑出來。

Stage 1

在過去很長的一段時間內,我們產出想要的數據 + 圖表的流程是:

PM/Data Analyst 寫 SQL query,搭配 metabase, Redash 等的工具把想看的數據呈現出來。

init

在這個階段,我們其實就有預見潛在的問題是:

1. Queries 維護上的困難

analytical query 是直接依賴 application db。所以當 application db schema 有任何改動(migration)的時候,analytical query 必須立刻跟上。但application這段的改動其實是很頻繁的。

2. Schema 不利於分析 application db 的 schema 是完全依照自己的需求(OLTP)而設計,這樣的schema 在分析的情境下(OLAP)未必適合。

但在這個時間點,因為團隊人數還很少,所有的溝通都很快速,所以這兩個問題都還在掌握之內。

Stage 2 – Analyst 參戰

接著,我們開始有 “對 data 細節比較不熟悉的” 夥伴加入了。同時,application data 和 3rd party 的 data 也變得比過去更複雜且多樣。於是上述的【問題2】 開始變得明顯。於是我們加入了 ETL,可以幫我們整理各種複雜的 query。這時候的 ETL 是以 減少分析端 query 的複雜度 為目標。

這時候的架構演進成這樣:

etl-1

Stage 3 – 到了最近

上述的做法一直持續到了最近,我們的團隊漸漸又長大了。我們發現:

  • “Queries 維護上的困難” 開始更明顯了,application db migration 常常會有溝通漏接的狀況。再加上 query 的量變多,對於維護 query 來說,要一個一個在 UI 上 update 是一件可怕的事情。
  • “Schema 不利於分析” 還是存在:時不時會有因為 application schema 太複雜,導致分析這端的 query 變得很複雜,甚至誤解了 data 的意思。也就是說,stage 2 希望透過 ETL 解決的問題又跑出來了。

另外,加入了新的問題們:

3. ETL 和 Analyst 的 coupling

在 business logic 沒有大改動的狀況下,分析端要加入/改動任何的 query 幾乎都需要 ETL 做相對應的改動。也就是說,分析端並沒有因為 ETL 這層抽象的存在而省去任何的工。反映在流程上,就是分析這端的 iteration 很慢,因為總是要跟其他事情一起競爭工程師的資源。這個對我們來說是一種警訊:如果同一件事還是總需要兩邊來完成,是不是本來在 ETL 和分析之間的職責定義出了什麼問題呢?

4. ETL 和分析 query 的 circular dependency

開始出現了 “分析端修改了 query,卻影響到 ETL” 的狀況。這個像是 circular dependency 的東西是之前沒有預期到的。情境是 ETL 有產出一組資料是 “某類型 user 的 funnel”,裡面記錄著每位 user 的各種關鍵行為發生與否。大部份的來源資料都是從 application 的 DB 裡面得到的,惟獨有一組 “有沒有跟 sales 通過電話” 和 “sale 是否判斷為有價值的客戶” 是從分析端產出的 query 得到。同時這個 funnel 也正在被其他的分析端 query 使用。於是當 Analyst 修改了上述 sales 相關的 query,ETL 產出的 funnel 就因此壞掉,進而影響到其他使用 funnel 的其他分析 query。

在現行的流程下,上述的問題代表:我們可能會在沒有人發現的狀況下,根據錯誤的資訊做出決定。

雖然說各個發生的問題本身都是可以被修復的,但我們問自己: 有沒有什麼辦法可以讓 PM/Analyst 不用再等工程師,並且讓以後類似的變動再發生的時候,相對應的數據不要壞掉呢?

於是我們覺得是時候好好從頭檢視這一切了。

dep1

其中”Query 維護上的困難”、”Schema 不利於分析” 對應到上圖 1、2 兩個黃色的箭頭:過去我們一直都沒有認真用技術的方式去解決。而針對”ETL 和 Analyst 的 coupling”、”ETL和分析 query 的circular dependency” 則是對應到 3 號紅色的箭頭和灰色的虛線:過去我們沒有一個很清楚的定義分析端和 ETL 之間的角色定位 - 誰該負責什麼樣的東西。

尋找解法

  • data 的中間層:我們需要一個在分析端和application db/3rd party data 之間的中間層。它要可以吸收各種變化,並且 expose 給分析端穩定並且直覺的介面。
  • ETL 與分析端的介面:在分析端和 ETL 這邊需要有一個明確的介面,定義誰該依賴誰,還有各自負責的東西是什麼。

針對”data 的中間層”,我們有考慮過一些可能性:

1. 從 application 出發

由 Application 按照 domain model 定義/維護一層抽象。然後 ETL 根據這層抽象去把 data 拿下來。實現的方式之一會是:application 會把這組抽象做成 API 或者是 database 的view,藉此把內部的實作細節包裝起來。

這樣的好處是因為這些抽象是做在 application 內部的,所以測試起來很快 – 在任何的 db migration 下,application 可以透過內部的 unit test 直接確定這個介面的行為沒有壞掉。但壞處則是 application 會知道一大堆這樣的抽象。這些抽象其實是為了分析而存在的,也就是說,當分析有新的需求的時候,會要同時改動到 application 和 ETL。

2. 由 ETL 出發

Application 這端完全不知道分析端要用的抽象,而是由 ETL 來概括承受。 也就是說,由 ETL 去依賴 application 的 schema,而在每一次 schema 改動的時候,去修改內部的實作來吸收這些改動,維持分析端的一致。這樣的作法好處是當分析有新的需求,application 可以完全不用知道。但壞處則是 ETL 這邊要可以找到自動測試的方法。這在各種 data source 的來源多樣的狀況下可能不一定是件單純的事。

上述的”抽象”舉一個例子來說明的話像是:想像我們 application 內部記錄 payment 的 db schema 很複雜,因為要考慮到各種像是 isolation level 的實作細節。但在這個狀況下,其實分析端在乎的”抽象”可能只是 “某一個 user 在什麼時候付了多少錢”,而根據這個 “抽象”,分析端可以延伸出各種 business 上在乎的資訊:好比說 “付費跟時間的關係”,”付費跟 user 所在位置的關係” 等。

後來我們選擇了第二種方法。主要原因是我們覺得當 application 變動的快的時候,讓 application 維護這樣的抽象溝通成本太高了。而以目前我們 data source 的複雜度,ETL 要進行自動的測試並不是太難的事情。

針對”ETL 與分析端的介面,”

在想過上面的東西之後,其實這部份的答案就呼之欲出了。我們想要的 “ETL跟分析端之間的介面” 其實就是上面的 “抽象”。當定義了適合的抽象作為介面之後,分析端才可以跟據這些簡單的抽象,去排列組合出各種 business insight。這邊的判斷條件是:當 ETL expose 出某個結果給分析端的時候,我們問自己一個問題:ETL 放出的這組 data 代表了 domain model 中的哪個部份呢?如果可以順利回答的話,通常沒什麼問題。

但客觀來說,其實在很多時候這是一個要取平衡點的問題,沒有標準的答案。但可以確定的是,絕大多數的時候不會發生分析端說:”欸幫我用ETL建一個我分析要看的 table,schema 長這樣”。而是雙方應該要討論 “這個要看的東西要怎麼用現有的 domain model 來呈現”,而根據這個來建立 ETL 的 output。

balance

如上圖,如果 ETL 抽象太靠近 Application DB 的話,理論上分析可以做出很多變化。但抽象的意義就失去了:對於分析端來說,schema 還是很複雜。並且分析端承受 application 變化的能力也變弱了。反之,如果太靠近分析端的話,對於分析端理論上 query 可以變得很簡單,也把 application 的 schema 包裝得很完整,但這時候可以用這個抽象來做的變化就少了,同時也代表分析端跟 ETL 會多出很多不必要的溝通。

Layered structure

綜合以上,新的架構變成:

dep1

首先,我們發現常見的 layered architecture 很適合來表達這個概念:每一個 layer 包裝位於自己下方的 layer,並且提供介面給上方的 layer。下方的 layer 不可以依賴上方的 layer。 在這樣的架構下,我們有一個 ETL layer 介於分析跟 application data 的中間,提供具有 domain model 意義的資訊給上方,但本身並不用知道上方是怎麼使用它的。 最上方的分析端,在”某些特殊的時機” (好比說一些還很不確定的實驗等等) 可以跳過中間的ETL這層,直接取用 application/3rd party 的 data schema,但同時也要承受 query 會不預期壞掉的風險。

其實整體來說,實作上的改動並不大,主要是透過這個過程讓我們更清楚了各種類型的 data 扮演的角色和彼此之間的關係:

  • ETL 這層不應該去知道上方分析端的細節,更不該依賴它。
  • ETL 放出的 data 必需要具有 domain model 的意義,而不能只具有分析的意義。

但有了這兩個規範之後,從理解整個工作流程到看懂 code 進而維護它,都因此變得直覺許多。

寫在後面

這一切其實都還是個進行中的過程,我們還是持續中過程/錯誤中去學習。 到目前為止,整理出來我們認為學到的功課有:

  • 在寫程式的時候我們會有 code smell:一些小地方作為警訊,讓我們發現可能潛在的問題。而在不同 team 的溝通上,可能也有類似的 “smell”:當有一些事情好像總是很不順利,也許是背後的東西有什麼誤會。
  • 如同工程的日常,大多數的事情沒有絕對的好壞,而是看我們怎麼做 trade-off。像是 “ETL 該靠 application/分析多近” 就是一個好的例子。但往往難的是 “發現 阿!原來是這個地方要做出 trade-off 阿” 的這個過程。
  • 像 refactor code 一樣,在調整各種流程/架構的時候,”如果某個地方有變動,那狀況會變成怎樣” 是個評估流程/架構的改動好不好的標準。
  • 也像是寫 code 一樣,domain model 會貫穿整個流程:確保所有人,包括 PM/工程師/分析師、甚至是 marketing/sales 團隊的人,對於 domain model 都有共同的認知之後,大部份的事情才有辦法順利運作。
  • 不同領域的概念,有時候可以交替使用:像是一開始的遇到 “ETL 和 Analyst 的 coupling” 其實讓人聯想到 shutgun surgery、layered architecture 等。當這些本來很不同的概念連結起來之後,就有可能可以變成很有趣的 mental model 讓我們在思考上跳關。