跳到主要內容

DS 301 · 模組 2

可靠資料產品:驗證、部署、觀測與復原

學會安全的 scheduling 與 incremental run、完整的 logging/metrics/lineage/observability、發布前的人工核准與 rollback,以及測試、失敗復原與 runbook,再於 Independent Lab 交付可靠的 Analytics Workflow。

開始第一節
已完成 0 / 5
同步我的進度加入梯次後,進度可跨裝置保存,講師也能看到

資訊

加入後,你已完成的模組編號會連同「學員代碼」傳送給講師,講師可以看到你的進度。請使用講師提供的代碼,不要輸入姓名、公司機密或其他個資。進度資料與該梯次的其他資料一起保存。

學習目標

  • 設計安全重跑;理解 full refresh 與 incremental processing
  • 記錄 run ID、row counts、freshness、validation、failure、lineage
  • 高影響發布前設 approval,保留前版產物並定義 rollback
  • 使用 unit/data/integration smoke tests;定義 retry、人工介入與 runbook
  • 交付 prototype、architecture canvas、tests、observability、runbook 與 ownership model

Scheduling、Idempotency 與 Incremental Runs

45 分鐘

Scheduling、Idempotency 與 Incremental Runs

學習目標:設計安全重跑;理解 full refresh 與 incremental processing。

排程在凌晨 2 點跑你的月度 pipeline,中途因為網路問題失敗了一半。值班的人手動重新觸發了一次,結果早上你發現輸出表裡同一個 batch_id 被算了兩次,總額整整多了一倍。這是典型的非 idempotent pipeline:重跑同一批輸入,卻沒有把之前寫入的結果覆寫或替換,而是疊加上去。設計安全的重跑,是任何排程化 pipeline 的基本要求,不是進階選配。

從「疊加」到 idempotent 的寫入方式
# 錯誤做法:每次執行都用 append,重跑會造成重複列
result.to_sql("monthly_summary", con=engine, if_exists="append")

# 正確做法:以 run 的處理範圍為 key,先刪除同一範圍的舊結果,再寫入新結果(idempotent)
def publish_idempotent(result: pd.DataFrame, period: str, engine) -> None:
    with engine.begin() as conn:
        conn.execute(
            "DELETE FROM monthly_summary WHERE period = :period",
            {"period": period}
        )
        result.to_sql("monthly_summary", con=conn, if_exists="append", index=False)

# incremental run 需要追蹤已處理到哪個時間點(watermark)
def get_watermark(engine) -> str:
    row = engine.execute("SELECT MAX(period) AS last_period FROM monthly_summary").first()
    return row.last_period if row and row.last_period else "1970-01"
Full refresh 與 incremental processing 的取捨
方式適合情境風險
Full refresh資料量小、來源會回溯修正歷史資料資料量變大後執行時間與成本上升
Incremental資料量大、來源穩定只新增不回溯需要正確追蹤 watermark,並處理 late-arriving data
另一種 idempotent 寫法:以主鍵 upsert

除了「先刪除同一期間再寫入」,另一種常見做法是以 (source_system, period, metric) 當作主鍵做 upsert:如果這個主鍵已存在就更新數值,不存在就新增一列。兩種做法都能讓同一批輸入重跑多次後,輸出表的內容保持一致;選哪一種取決於資料庫是否方便支援 upsert 語法,以及一次重算的範圍是否總是以完整期間為單位。

  1. 確認寫入方式是否 idempotent
    同樣的輸入重跑兩次,輸出表的內容應該完全相同,不應該重複累加。
  2. 選擇 full refresh 或 incremental
    依資料量、來源是否會回溯修正,決定要整批重算還是只算新增部分。
  3. 追蹤 watermark
    incremental pipeline 需要記錄已處理到哪個時間點,作為下次執行的起點。
  4. 處理 late-arriving data
    設計機制讓比預期晚到的資料,仍能被正確涵蓋進對應期間的結果。

注意

「排程有跑、也沒有報錯」不代表結果正確。非 idempotent 的 pipeline 在重跑或排程重疊執行時,最常見的症狀就是數字被悄悄放大或縮小。
自我檢測

排程因網路問題失敗,值班人員手動重跑了一次同一個 batch,結果輸出表裡的總額變成兩倍。這反映了 pipeline 缺少什麼設計?

排程因網路問題失敗,值班人員手動重跑了一次同一個 batch,結果輸出表裡的總額變成兩倍。這反映了 pipeline 缺少什麼設計?

重點: 能完成步驟不代表結果可信;本節的能力必須能被驗證、重做並清楚說明。

Logging、Metrics、Lineage 與 Observability

45 分鐘

Logging、Metrics、Lineage 與 Observability

學習目標:記錄 run ID、row counts、freshness、validation、failure、lineage。

你的 pipeline 顯示昨晚的排程「成功」,但主管抱怨月報表的數字比平常少了九成。你打開 log,只看到一行「Job completed successfully」,沒有任何列數、沒有任何驗證結果,你完全無從判斷問題出在哪一個 stage。如果只記錄「成功/失敗」這種二元狀態,你等於在沒有儀表板的情況下開車:車子還在動,但你不知道油還剩多少、引擎溫度是否正常。

從「只記成功/失敗」到有意義的 observability
import logging, uuid

logger = logging.getLogger("ds301.pipeline")

def run_pipeline(input_path: str) -> None:
    run_id = str(uuid.uuid4())
    logger.info("run_start", extra={"run_id": run_id})

    raw = load_raw(input_path)
    logger.info("ingest_done", extra={"run_id": run_id, "row_count": len(raw)})

    errors = validate_contract(raw)
    logger.info("validation_done", extra={"run_id": run_id, "error_count": len(errors)})

    transformed = fill_missing_values(filter_current_schema(raw, "v2"))
    summary = summarize_by_metric(transformed)
    logger.info(
        "analyze_done",
        extra={"run_id": run_id, "output_row_count": len(summary), "total_value": float(summary["value"].sum())}
    )

    logger.info("run_end", extra={"run_id": run_id, "status": "succeeded"})
一次執行至少該記錄的內容
項目為什麼重要
run_id把同一次執行的所有 log 串在一起,方便追查
row counts(每個 stage 前後)資料量突然大幅變化通常是問題的第一個訊號
validation 結果記錄違反了哪些 contract 規則,即使只是 warning
freshness這次處理的資料涵蓋到哪個時間點
lineage輸出來自哪個版本的輸入資料與哪個版本的程式碼
  • row count 相較上次執行的變化超過一個合理門檻(例如 ±30%)
  • validation error 數量從 0 變成非 0,即使目前只是 warning 等級
  • 同一個 run_id 的 freshness 落後排程期望的時間超過一定門檻
  • lineage 記錄顯示這次用了預期之外的輸入版本或程式碼版本

注意

「成功/失敗」只回答了「有沒有當機」,回答不了「這次結果對不對」。row counts 突然掉到平常的十分之一,往往比一次明確的錯誤訊息更危險,因為它不會讓 pipeline 失敗。
自我檢測

Pipeline 的 log 只記錄「Job completed successfully」,沒有任何 row counts 或驗證結果。這樣的記錄方式主要缺少了什麼?

Pipeline 的 log 只記錄「Job completed successfully」,沒有任何 row counts 或驗證結果。這樣的記錄方式主要缺少了什麼?

重點: 能完成步驟不代表結果可信;本節的能力必須能被驗證、重做並清楚說明。

Human Approval、Publishing 與 Rollback

45 分鐘

Human Approval、Publishing 與 Rollback

學習目標:高影響發布前設 approval,保留前版產物並定義 rollback。

你的 pipeline 全自動排程、全自動發布到公司內部儀表板,上個月因為上游一個欄位定義悄悄改變,發布出去的月度指標整整少算了一個事業群,而且是全自動流程,沒有任何人在發布前看過一眼。等到主管在會議上發現數字不對,錯誤的報表已經流通了兩週。高影響的資料產品,需要一個人在「發布給使用者」這一步之前確認結果合理,而不是完全信任自動化。

該不該設 human approval gate?
情境判斷
內部開發用的中繼資料表,只有工程師自己看通常不需要,自動發布即可
對外或給高階主管決策用的月度報表應該設 approval gate,發布前由人確認
金額、法遵或會計相關的資料產品幾乎一定需要 approval,並保留審核紀錄
在 publish 前加入 approval gate 與版本化輸出
def publish_with_approval(result: pd.DataFrame, period: str, approved_by: str | None) -> None:
    if approved_by is None:
        # 高影響輸出:沒有人核准就不能發布,先寫入待審核區
        write_to_pending_review(result, period)
        raise RuntimeError(f"period={period} 尚未核准,已寫入待審核區,等待人工確認")

    # 版本化輸出:保留可識別的前一版,才有辦法 rollback
    archive_previous_version(period)
    publish_idempotent(result, period, engine)
    log_publish_event(period, approved_by)

def rollback(period: str) -> None:
    restore_previous_version(period)
    log_rollback_event(period)
  • 發布後發現上游資料在發布前已經悄悄變質,但沒有被 contract 檢查攔下
  • 核准者事後反悔,認為這次的結果不應該對外發布
  • 下游使用者回報數字與他們自己的紀錄明顯不一致
  • 發現這次發布使用了錯誤的程式碼版本或設定

注意

Rollback 的前提,是你必須先保留可識別的前一版輸出與切換回去的具體方法。如果每次發布都直接覆寫掉唯一的輸出,出錯時根本沒有東西可以回退。

資訊

Human, AI, Human:AI 或自動化可以產出候選結果,但高影響的發布仍需要一個人確認「這個結果可以對外負責」,這正是本課程強調的人工審核點。
自我檢測

一份給高階主管決策用的月度報表,目前是全自動排程、全自動發布,沒有任何人在發布前檢視。依本課程原則,最適當的改善是什麼?

一份給高階主管決策用的月度報表,目前是全自動排程、全自動發布,沒有任何人在發布前檢視。依本課程原則,最適當的改善是什麼?

重點: 能完成步驟不代表結果可信;本節的能力必須能被驗證、重做並清楚說明。

Testing、Failure Recovery 與 Runbook

45 分鐘

Testing、Failure Recovery 與 Runbook

學習目標:使用 unit/data/integration smoke tests;定義 retry、人工介入與 runbook。

凌晨三點,pipeline 因為上游 API 暫時逾時而失敗。排程設定了「失敗就無限 retry」,結果整晚不斷重試、不斷失敗,隔天早上你發現系統資源被耗盡,而真正的問題(上游 API 早就恢復了,但你的程式碼有另一個永久性的 bug)從來沒有被解決。可靠的工作流需要三件事同時存在:測試能在問題發生前攔下已知錯誤、有分寸的 retry 策略能區分暫時性與永久性失敗、以及一份 runbook 讓值班的人知道遇到失敗該怎麼辦。

區分暫時性錯誤與永久性錯誤的 retry 策略
import time

class TransientError(Exception): ...
class PermanentError(Exception): ...

def call_upstream_with_retry(fetch_fn, max_attempts: int = 3) -> dict:
    for attempt in range(1, max_attempts + 1):
        try:
            return fetch_fn()
        except TransientError as e:
            if attempt == max_attempts:
                raise
            time.sleep(2 ** attempt)  # 指數退避,避免持續打爆上游
        except PermanentError:
            # 永久性錯誤(例如資料格式壞掉):retry 不會成功,直接記錄並轉交人工
            log_for_manual_intervention(fetch_fn.__name__)
            raise
三層測試各自驗證什麼
測試類型驗證的內容
Unit test單一 function 的邏輯是否正確(例如彙總公式)
Data test實際流入的資料是否符合 contract 與品質預期
Integration smoke test整個 pipeline 串接起來是否能跑完,輸出是否落在合理範圍
一則 runbook 條目範例

情境:pipeline 在 validate stage 失敗,錯誤訊息顯示「出現未知的 metric 值」。判斷:先確認這是否為上游新增的指標,還是單純的拼字錯誤。處置:若是拼字錯誤,通知上游修正並重跑該批次;若是新指標,先與資料團隊確認是否要納入 ALLOWED_METRICS,再決定是否更新 contract。升級對象:資料平台負責人。

  1. 區分錯誤類型
    把錯誤分成暫時性(可以 retry)與永久性(retry 沒有意義)。
  2. 設定 retry 上限
    避免無限 retry 消耗資源或卡住整個排程。
  3. 定義人工介入時機
    永久性錯誤或超過 retry 上限時,應該通知人而不是繼續嘗試。
  4. 撰寫 runbook
    寫下常見失敗情境、判斷方式、處置步驟與升級對象。

注意

無限 retry 不是「更保險」,而是把一個明確的失敗,變成一個不斷消耗資源、卻永遠沒有結論的隱形問題。
自我檢測

Pipeline 因為資料格式永久性錯誤而失敗(重試也不會成功),但排程設定為失敗就無限 retry。這個設計的問題是什麼?

Pipeline 因為資料格式永久性錯誤而失敗(重試也不會成功),但排程設定為失敗就無限 retry。這個設計的問題是什麼?

重點: 能完成步驟不代表結果可信;本節的能力必須能被驗證、重做並清楚說明。

Independent Lab:可靠 Analytics Workflow

60 分鐘

Independent Lab:可靠 Analytics Workflow

學習目標:獨立交付 prototype、architecture canvas、tests、observability、runbook 與 ownership model。

資訊

這是 DS 301 的收斂實作:不再有逐步引導,你需要獨立把整門課學到的能力整合成一份可靠的 analytics workflow,並交付 Analytics Workflow Canvas + Executable Workflow Prototype + Runbook。
  1. Input Contract
    寫清楚這個 workflow 的輸入資料契約與允許的假設。
  2. Executable Prototype
    交付一份可執行的 pipeline,涵蓋 ingest→validate→transform→analyze→publish 五個 stage。
  3. Tests
    至少包含一個 unit test 與一個 data validation 檢查。
  4. Observability
    記錄 run ID、row counts、validation 結果與 lineage。
  5. Human Approval & Rollback
    說明發布前的人工核准點,以及如何回退到前一版輸出。
  6. Runbook
    寫下至少兩種常見失敗情境的判斷方式、處置步驟與升級對象。
  7. Ownership Model
    明確寫出誰是這個 data product 的 owner,以及交接時需要知道的事。
Independent Lab 應包含的最少內容(請填入你自己的輸出結果)
項目要回報的內容
Executable Prototype你實際交付的五個 stage 與其 checkpoint
Observability你記錄的 run ID、row counts 與 validation 結果範例
Runbook至少兩種失敗情境的判斷與處置步驟
仍待人工判斷的項目至少一項需要業務端或治理層確認的假設或決策
  • 只做了 happy path 測試,沒有測試資料違反 contract 時 pipeline 是否真的會失敗
  • observability 只記錄成功/失敗,沒有記錄 row count 或 validation 結果
  • runbook 寫得很籠統(例如「聯繫工程師」),沒有具體的判斷步驟與升級對象
  • ownership model 只寫了名字,沒有說明交接時需要知道哪些背景知識

注意

「pipeline 能跑完」不等於「這是一個可靠的 data product」。缺少測試、觀測或 runbook 的 pipeline,只是換了一種形式的一次性 notebook。

完成

完成後請自問:如果這個 pipeline 半夜失敗,值班的人能不能只靠你留下的 runbook 與 log,判斷該怎麼處理,而不需要叫醒你?
自我檢測

你在 Independent Lab 交付了一個可以順利執行、產出正確結果的 pipeline,但沒有撰寫任何 runbook,也沒有記錄 run ID 或 row counts。這樣算完成本課程的最終能力嗎?

你在 Independent Lab 交付了一個可以順利執行、產出正確結果的 pipeline,但沒有撰寫任何 runbook,也沒有記錄 run ID 或 row counts。這樣算完成本課程的最終能力嗎?

重點: 能完成步驟不代表結果可信;本節的能力必須能被驗證、重做並清楚說明。

接下來做什麼?