[譯] 可維護的 ETL:使管道更容易支持和擴展的技巧

modularized code example

任何數據科學項目的核心是...噔噔噔...數據!以可靠和可重複的方式準備數據是該過程的基本部分。若是你正在培訓一個模型,計算分析,或者只是未來自多個源的數據組合到另外一個系統中,那麼你將須要構建一個數據處理或 ETL1 管道。html

咱們 Stitch Fix 這裏從事的是全棧數據科學。這意味着咱們以數據科學家的身份負責項目的構思、生產以致維護的整個過程。咱們的受好奇心驅使喜歡快速行動,即便咱們的工做經常是相互聯繫的。咱們所處理的問題具備挑戰性,所以解決方案可能很複雜,但咱們不想在不須要的地方引入複雜性。由於咱們必須支持咱們在生產中的工做,因此咱們的小團隊分擔隨叫隨到的責任,並幫助支持彼此的管道。這讓咱們能夠作一些重要的事情,好比度假。今年夏天,我和妻子要去意大利度蜜月,這是咱們多年前的打算。當我在那裏的時候,我最不想考慮的是個人隊友們是否很難使用或理解我寫的管道。前端

讓咱們也認可數據科學是一個動態的領域,因此同事們會轉向公司以外的新計劃、團隊或機會。雖然一個數據管道可能由一個數據科學家構建,但在其生命週期中,它一般由多個數據科學家支持和修改。像許多數據科學團體同樣,咱們來自不一樣的教育背景,不幸的是,咱們並不是都是「獨角獸」 —— 軟件工程、統計和機器學習方面的專家。python

雖然咱們的算法小組確實有一個龐大的、使人驚歎的數據平臺工程師團隊,它們不會也不想寫 ETL 來支持數據科學家的工做。相反,他們將精力集中在構建易於使用、健壯可靠的工具上,這些工具使數據科學家可以快速構建 ETL、培訓和評分模型,以及建立性能良好的 API,而無需擔憂基礎設施。android

多年來,我發現了一些有助於使個人 ETL 更易於理解,維護和擴展的關鍵作法。本文會帶你們看看如下作法有什麼好處:ios

  1. 創建一系列簡單的任務。
  2. 使用工做流程管理工具。
  3. 儘量利用 SQL。
  4. 實施數據質量檢查。

討論細節以前,我要認可一點:沒有一套構建 ETL 管道的最佳實踐。這篇文章的重點是數據科學環境,其中有兩件事情是正確的:支持人員的組成情況的演變是不斷髮展和多樣化的,開發和探索優先於鐵定的可靠性和性能。git

創建一系列簡單的任務

使 ETL 更容易理解和維護的第一步是遵循基本的軟件工程實踐,將大型和複雜的計算分解爲具備特定目的的離散、易於消化的任務。相似地,咱們應該將一個大型ETL管道劃分爲較小的任務。這有不少好處:github

  1. 更容易理解每一個任務:只有幾行代碼的任務更容易審查,所以更容易吸取處理過程當中的任何細微差異。算法

  2. 更容易理解整個處理鏈:當任務具備明肯定義的目的而且命名正確時,審閱者能夠專一於更高級別的構建塊以及它們如何組合在一塊兒而忽略每一個塊的細節。數據庫

  3. 更容易驗證:若是咱們須要對任務進行更改,咱們只須要驗證該任務的輸出,並確保咱們遵照與此任務的用戶/調用者之間的任何「約定」(例如,結果表的列名稱和數據類型與預修訂格式相匹配)。apache

  4. 提高模塊化程度:若是任務具備必定的靈活性,則能夠在其餘環境中重用它們。這減小了所需的總代碼量,從而減小了須要驗證和維護的代碼量。

  5. 洞察中間結果:若是咱們存儲每一個操做的結果,當出現錯誤時,咱們將更容易調試管道。咱們能夠查看每一個階段,更容易找到錯誤的位置。

  6. 提升管道的可靠性:咱們將很快討論工做流工具,可是將管道分解爲任務的話,發生臨時故障時就能夠更輕鬆地自動從新運行任務。

咱們從一個簡單的示例,就能夠看到將管道拆分爲較小任務的好處。在 Stitch Fix,咱們可能想知道發送給客戶的物品當中,「高價」物品所佔的比例。首先,假設咱們已經定義了一個存儲閾值的表。請記住,閾值將根據客戶羣(例如孩子與女性)和物品種類(例如襪子與褲子)而有所不一樣。

因爲此計算至關簡單,咱們能夠對整個管道使用單個查詢:

WITH added_threshold as (
  SELECT
    items.item_price,
    thresh.high_price_threshold
  FROM shipped_items as items
  LEFT JOIN thresholds as thresh
    ON items.client_segment = thresh.client_segment
      AND items.item_category = thresh.item_category
), flagged_hp_items as (
  SELECT
    CASE
      WHEN item_price >= high_price_threshold THEN 1
      ELSE 0
    END as high_price_flag
  FROM added_threshold
) SELECT
    SUM(high_price_flag) as total_high_price_items,
    AVG(high_price_flag) as proportion_high_priced
  FROM flagged_hp_items
複製代碼

這第一次嘗試實際上至關不錯。它已經經過使用公共表表達式(CTE)或 WITH 塊進行了模塊化。每一個塊都用於特定目的,它們簡短且易於吸取,而且別名(例如 added_threshold)提供足夠的上下文,以便審閱者能夠記住塊中所完成的操做。

另外一個積極方面是閾值存儲在單獨的表中。咱們可使用很是大的 CASE 語句對查詢中的每一個閾值進行硬編碼,但這對於審閱者來講很快就會變得難以理解。它也很難維護,由於咱們只要想更新閾值,就必須更改此查詢以及使用相同邏輯的任何其餘查詢。

雖然這個查詢是一個良好的開端,但咱們能夠改進實現的方式。最大的不足是咱們沒法輕鬆訪問任何中間結果:整個計算只需一次操做便可完成。你可能想知道,爲何我要查看中間結果?中間結果容許你進行即時調試,得到實施數據質量檢查的機會,而且能夠證實在其餘查詢中可重用。

例如,假設企業添加了一個新的物品類別 —— 例如,帽子。咱們開始銷售帽子,但咱們忘記更新閾值表。在這種狀況下,咱們的聚合指標就會漏掉高價的帽子。因爲咱們使用了 LEFT JOIN,由於鏈接不會刪除行,可是 high_price_threshold 的值將爲 NULL。到了下一個階段,全部和帽子有關的行,其 high_price_flag 的值都會是零,而這個數值會帶到咱們最終進行計算的 total_high_price_itemsproportion_high_priced

若是咱們將這個大的單個查詢分解爲多個查詢並分別編寫每一個階段的結果,咱們就可使這個管道更易於維護。若是咱們將初始階段的輸出存儲到單獨的表中,咱們能夠輕鬆檢查咱們是否沒有丟失任何閾值。咱們須要作的就是查詢此表並選擇 high_price_threshold 值爲 NULL 的行。若是什麼都沒有返回,就表明咱們遺漏了一個或多個閾值。咱們將在帖子後面介紹這種類型的數據運行時驗證。

這種模塊化的實現也更容易修改。假設咱們不是要考慮全部曾寄出的物品,而是決定只想計算過去 3 個月發送的高價物品。要是用原來的查詢方式,咱們就會對第一階段進行更改,而後查看最終得出的總數,指望獲得正確的數值。經過單獨保存第一階段,咱們能夠添加一個具備發貨日期的新列。而後,咱們能夠修改查詢並驗證結果表中的發貨日期是否都在咱們預期的日期範圍內。咱們還能夠將咱們的新版本保存到另外一個位置並執行「數據差別」以確保咱們正在刪除正確的行。

最後一個示例將此查詢拆分爲單獨的階段帶來了最大的好處之一:咱們能夠重用咱們的查詢和數據來支持不一樣的用例。假設一個團隊想要過去 3 個月的高價項目指標,但另外一個團隊僅在最後一週須要它。咱們能夠修改第一階段的查詢以支持這些並將每一個版本的輸出寫入單獨的表。若是咱們爲後期查詢動態指定源表 2,相同的查詢將支持兩種用例。此模式也能夠擴展到其餘用例:具備不一樣閾值的團隊,按客戶端細分和項目類別細分的最終指標與彙總。

咱們經過建立分階段管道進行了一些權衡。其中最大的一個是運行時性能,尤爲是當咱們處理大型數據集時。從磁盤讀取和寫入數據會形成很大的開銷,而且在每一個處理階段,咱們讀取前一階段的輸出並寫出結果。和舊的 MapReduce 範例相比,Spark 的一大優點是臨時結果能夠緩存在工做節點(執行程序)的內存中。Spark 的 Catalyst 引擎還優化了 SQL 查詢和 DataFrame 轉換的執行計劃,但它優化時沒法跨越讀/寫邊界。這些分階段管道的第二個主要限制是它們使建立自動化集成測試變得更加困難,這涉及測試多個計算階段的結果。

有了 Spark,就能夠解決這些不足之處。若是我必須執行幾個小的轉換而且我想要保存中間步驟的選項,我就會建立一個管理程序腳本,這個腳本只有在設置了命令行標誌時才執行轉換,以及輸出中間表 3。當我正在開發和調試更改時,我可使用該標誌來生成驗證新計算是否正確所需的數據。一旦我對個人更改有信心,我能夠關閉標記以跳過編寫中間數據。

使用工做流程管理工具

使用可靠的工做流管理和調度引擎,能夠實現巨大的生產力提高。一些常見的例子包括 AirflowOozieLuigiPinball。這項建議須要時間和專業知識來創建;這不是個別數據科學家可能負責管理的事情。在 Stitch Fix,咱們開發了本身的專有工具,由咱們的平臺團隊維護,數據科學家用它就能夠建立、運行和監控咱們本身的工做流程。

工做流工具能夠輕鬆定義計算的有向非循環圖(DAG),其中每一個子任務都依賴於任何父任務的成功完成。這些工具一般能讓使用者得以指定運行工做流的計劃,在工做流啓動前等待外部數據依賴,重試失敗的任務,在失敗時恢復執行,在發生故障時建立警報,以及運行不相互依賴的任務在平行下。這些功能相結合,使用戶可以構建可靠,高性能且易於維護的複雜處理鏈。

儘量利用SQL

這多是我提出的最具爭議性的建議。即便在 Stitch Fix 中,也有許多數據科學家反對 SQL,而是提倡使用通用編程語言。不久以前我仍是這個陣營的一員。在實踐方面,SQL 很難測試 — 特別是經過自動化測試。若是你來自軟件工程背景,那麼測試的挑戰可能會讓你以爲有足夠的理由來避免使用 SQL 。我在過去也陷入過關於 SQL 的情感陷阱:「SQL 技術性較差,專業性較差;真正的數據科學家應該編碼。」

SQL 的主要優勢是全部數據專業人員都能理解:數據科學家、數據工程師、分析工程師、數據分析師、數據庫管理員和許多業務分析師。這是一個龐大的用戶羣,能夠幫助構建,審查,調試和維護 SQL 數據管道。雖然 Stitch Fix 沒有不少這些數據角色,但 SQL 是咱們這些不一樣數據科學家的共同語言。所以,利用 SQL 能夠減小對團隊中專業角色的需求,這些團隊具備強大的 CS 背景,爲整個團隊建立管道,沒法公平地分擔支持職責。

經過將轉換操做編寫爲 SQL 查詢,咱們還能夠實現可伸縮性和某種級別的可移植性。使用適當的 SQL 引擎,能夠用相同的查詢語句來處理一百行數據,而後針對太字節數量級的數據運行。若是咱們使用內存處理軟件包(如 Pandas)編寫相同的轉換操做,那麼隨着業務或項目的擴展,咱們將面臨超出處理能力的風險。全部東西運行起來都不會有問題,但一到了數據集過大、內存沒法容納時,就會出錯。若是這項工做正在進行中,這可能致使急於重寫事情以使其恢復運行。

不一樣 SQL 語言變體有不少共通之處,咱們從一個 SQL 引擎到另外一個 SQL 引擎具備必定程度的可移植性。在 Stitch Fix 中,咱們使用 Presto 進行 adhoc 查詢,使用 Spark 進行生產管道。當我構建一個新的 ETL 時,我一般使用 Presto 來理解數據的結構,並構建部分轉換。一旦這些部件到位,我幾乎老是用 Spark 4 運行相同的查詢語句,不做任何修改。若是我要切換到 Spark 的 DataFrame API,我須要徹底重寫個人查詢。反過來一樣能夠體現這種可移植性的好處。若是生產做業存在問題,我能夠從新運行相同的查詢並添加過濾器和限制以將數據的子集拉回以進行目視檢查。

固然,不是全部操做都能用 SQL 完成。你將不會使用它來訓練機器學習模型,並且還有許多其餘狀況下,SQL 實現即便可行,也會過於複雜。對於這些任務,你絕對應該使用通用編程語言。若是你遵循關鍵的建議,把你的工做分紅小塊,那麼這些複雜的任務將在範圍內受到限制,而且更容易理解。在可能的狀況下,我嘗試在一系列簡單準備階段的末尾隔離複雜的邏輯,例如:鏈接不一樣的數據源、過濾和建立標誌列。這使得驗證進入最後一個複雜階段的數據變得容易,甚至能夠簡化一些邏輯。通常來講,我在本篇文章的其他部分已經再也不強調自動化測試,但處理有複雜邏輯的任務時,着力實現測試覆蓋就頗有意義了。

實施數據質量檢查

要驗證複雜的邏輯時,自動單元測試很是有用,但對於做爲分階段管道的一部分的相對簡單的轉換,咱們一般能夠手動驗證每一個階段。就 ETL 管道而言,自動化測試提供了混合的好處,由於它們不會覆蓋最大的錯誤來源之一:咱們的管道上游的故障致使咱們的初始依賴關係中出現舊的或不正確的數據。

一個常見的錯誤來源是在啓動管道以前未能確保咱們的源數據已更新。例如,假設咱們依賴於天天更新一次的數據源,而且咱們的管道在數據源更新以前就開始運行。這意味着咱們要麼用的是(前一天計算的) 舊數據,要麼使用舊數據和當前數據的混合數據。這種類型的錯誤可能難以識別和解決,由於上游數據源可能在咱們獲取舊版本的數據後不久就完成更新。

上游故障還可能致使源數據中出現錯誤數據:字段計算錯誤,模式更改和/或缺失值頻率更高。在動態且互聯的環境中,利用另外一個團隊建立的數據源進行實驗的作法並很多見,而這些源也經常會出現意外更改;咱們在 Stitch Fix 運做時所處的環境很大程度上就是如此單元測試一般不會標記這些故障,但能夠經過運行時驗證(有時稱爲數據質量檢查)來發現它們。咱們能夠編寫單獨的 ETL 任務,若是咱們的數據不符合咱們指望的標準,它們將自動執行檢查並引起錯誤。上面提到了一個簡單的例子,其中缺乏高價的帽子門檻。咱們能夠查詢組合出貨物品和高價閾值表,並查找缺乏閾值的行。若是咱們找到任何行,咱們能夠提醒維護者。這個想法能夠推廣到更復雜的檢查:計算零分數、平均值、標準差、最大值或最小值。

在特定列的缺失值高於預期的狀況下,咱們首先須要定義預期的內容,這能夠經過查看上個月天天丟失的比例來完成。而後咱們能夠定義觸發警報的閾值。這個想法能夠推廣到其餘數據質量檢查(例如,平均值落在一個範圍內),咱們能夠調整這些閾值,使咱們對警報的敏感度進行增減。

正在進行的工做

在這篇文章中,咱們已經完成了幾個實際步驟,可使你的ETL更易於維護,擴展和生產支持。這些好處能夠擴展到你的隊友以及你將來的自我。雖然咱們能夠爲構建良好的流水線而感到自豪,但編寫ETL並非咱們進入數據科學的緣由。相反,這些是工做的基本部分,使咱們可以實現更大的目標:構建新模型,爲業務提供新看法,或經過咱們的API提供新功能。建造不良的管道不只須要時間遠離團隊,還會給創新帶來障礙。

我在上一份工做中嚐到的苦果,讓我明白到管道若是難以使用,就會讓項目難以維護和擴展。我當時在某個創新實驗室工做,該實驗室率先使用大數據工具來解決組織中的各類問題。個人第一個項目是創建一條管道來識別信用卡號被盜的商家。我構建了一個使用 Spark 的解決方案,由此產生的系統在識別新的欺詐活動方面很是成功。然而,一旦我把它傳遞到信用卡部門支持和擴展,問題就開始了。我在編寫管道時打破了我列出的全部最佳實踐:它包含一個執行許多複雜任務的做業,它是用 Spark 編寫的,當時對公司來講是新的,它依賴於 cron 進行調度而且沒有'發生故障時發送警報,它沒有任何數據質量檢查,以確保源數據是最新的和正確的。因爲這些缺陷,管道沒有運行的時間延長。儘管有一個普遍的路線圖來增長改進,但因爲代碼很難理解和擴展,所以不多可以實現這些改進。最終,整個管道以一種更容易維護的方式重寫

就像你的 ETL 正在進行的數據科學項目同樣,你的管道永遠不會真正完整,應該被視爲永遠不斷變化。經過每次更改,每次更改都是實現小幅改進的契機:提升可讀性,刪除未使用的數據源和邏輯,或簡化或分解複雜的任務。這些建議並非什麼重大突破,但若是要始終如一地踐行,就須要自律。就像獅子馴服同樣,當管道很小時,它們相對容易控制。然而,它們長得越大,就越難管控,也越容易表現出突發且意外的錯亂行爲。到了那種地步,你只得從新開始、採起更好的作法,否則就可能會冒着失敗的風險 [5][#f5]。


註釋

[1]↩ 提取、轉換和加載的縮寫。

[2]↩ 最簡單的方法是使用簡單的字符串替換或字符串插值,可是你能夠經過模板處理庫(如 jinja2)實現更大的靈活性。

[3]↩ 對於 Python,像標準庫中的 ClickFire,甚至 argparse 這樣的庫能夠輕鬆定義這些命令行標誌。

[4]↩ 操做日期和從 JSON 中提取字段等操做須要修改查詢,但這些更改很微小。

[5]↩ 在撰寫博客時,沒有獅子或數據科學家受到傷害。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索