iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Kubernetes

從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台系列 第 4

[Day 4] 容器化實踐 (二):Python FastAPI 與 Delta Lake 的大數據容器配置 —— 在 Python 容器中處理大數據存儲與 ACID 特性。

  • 分享至 

  • xImage
  •  

Day 4: 容器化實踐 (二):Python FastAPI 與 Delta Lake 的大數據容器配置

在 Python 容器中處理大數據存儲與 ACID 特性。

1. 為什麼選擇 FastAPI + Delta Lake?

在工業大數據場景下,每天可能會產出數千萬筆的晶圓測試資料(像 Thickness、Resistance 這些參數)。傳統關聯式資料庫(如 PostgreSQL)面對這種高維度、又要頻繁聚合運算的場景,真的會跑到懷疑人生。

所以 Wafer BI 的後端計算引擎選了 Python + FastAPI,搭配 Delta Lake 做大數據存儲。Delta Lake 不只有 Parquet 格式帶來的高效能壓縮,還提供 ACID 事務特性,資料寫入時的不一致問題直接根治。

2. FastAPI 核心架構解析

直接看專案中 services/wafer-bi/main.py 的核心配置。為了處理龐大的資料量並確保讀取效能,我們直接用 deltalake 庫讀取掛載的 Delta Table:

from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from deltalake import DeltaTable
import pandas as pd

app = FastAPI(title="Wafer BI API")

# 允許跨域請求
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

DELTA_PATH = "/app/wafer_delta_table"

def get_df():
    try:
        # 直接讀取 Delta Table 並轉為 Pandas DataFrame
        dt = DeltaTable(DELTA_PATH)
        return dt.to_pandas()
    except Exception as e:
        raise HTTPException(status_code=500, detail=f"Failed to read Delta table: {str(e)}")

FastAPI 自帶的 Swagger UI (/docs) 是開發時最好的朋友,所有統計 API 一目了然,前端同事再也不會來問你 API 長怎樣(是說前端也是我 哈哈):

https://ithelp.ithome.com.tw/upload/images/20260806/20182549CuijZCWwca.png

▲ FastAPI Swagger UI:wafer-map、cdf、stats、yield 等統計 API

3. 把文件寫進程式碼裡:一個端點的完整範例

上面那張圖只列出了 API 有哪些,但「這個參數要傳什麼」「回傳格式長怎樣」這些細節,光看清單看不出來。FastAPI 的作法是把文件寫在程式碼本身——用型別標註跟 docstring,而不是額外維護一份 API 文件。拿 /stats/{lot_id} 這支算盒鬚圖統計的端點來看:

@app.get(
    "/stats/{lot_id}",
    tags=["Statistics"],
    summary="取得批次的統計盒鬚圖數據",
    response_description="每片晶圓的 min/q1/median/q3/max 五數概括,以及各晶圓平均值的趨勢序列",
)
async def get_lot_stats(
    lot_id: str = Path(..., description="批次編號", examples=["Lot1"]),
    parameter: str = Query(
        "Thickness",
        description="要統計的測試參數名稱,對應資料表裡的 parameter 欄位",
        examples=["Thickness", "Resistance"],
    ),
):
    """
    依批次 (Lot) 與參數 (Parameter) 計算每片晶圓的五數概括統計 (Five-Number Summary),
    是箱型圖 (Box Plot) 與趨勢圖的資料來源。

    計算方式:先篩選出屬於這個批次、這個參數的所有量測值,
    再依 wafer_id 分組,對每片晶圓的數值算出 min / Q1(25百分位) / median / Q3(75百分位) / max,
    並取平均值當作跨晶圓比較的趨勢指標。

    找不到符合條件的資料時回傳 404。
    """

summarytagsPath/Querydescriptionexamples,加上函式本身的 docstring,FastAPI 會自動組成這樣的畫面:

https://ithelp.ithome.com.tw/upload/images/20260806/20182549lB6HknjoQg.png

▲ 展開後的 /stats/{lot_id}:完整說明文字、每個參數的用途與範例值、回應格式與錯誤碼一次到位

這樣做的好處很直接:新加入的人不用讀原始碼,也不用來問你,只要打開 /docs 點開這支 API,就知道 lot_id 是路徑參數、parameter 預設是 Thickness、404 代表查無資料——文件跟程式碼綁在一起,程式改了文件就跟著改,不會有「文件寫的是三個月前的版本」這種常見的老問題。而且右上角那顆 Try it out 按鈕還能直接在瀏覽器裡打真的請求測試,不用另外開 Postman。

4. Dockerfile 最佳化實踐

容器化 Python 應用時,最在乎的是 Image 大小跟依賴安裝速度。在 services/wafer-bi/Dockerfile 中,我們用輕量的 python:3.10-slim 當基底:

FROM python:3.10-slim

WORKDIR /app

# 先複製 requirements 並安裝,善用 Docker Layer 緩存機制
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 再複製原始碼與資料產生腳本
COPY . .

# 確保容器啟動前,若無資料則自動生成測試集
RUN python data_generator.py

EXPOSE 8000

# 使用 uvicorn 啟動 ASGI 伺服器
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

跟 Day 3 的 Java 一樣,COPY requirements.txtCOPY . . 要分開寫——pip install 是最貴的一層(pandas + pyarrow + deltalake 全家桶),只要 requirements 沒變就走快取。

實際建置的完整過程如下,pip 安裝約 34 秒、資料生成約 1.2 秒:

https://ithelp.ithome.com.tw/upload/images/20260806/20182549VUe0d0aFFx.png

▲ docker build Python Image:pip install 與 data_generator 資料生成完整過程

5. 踩坑實錄:SchemaMismatchError

哭啊,說好的坑馬上就來。某次重新建置 Image 時,RUN python data_generator.py 突然炸出這個:

_internal.SchemaMismatchError: Cannot cast schema, number of fields does not match: 8 vs 6

當下真的一頭霧水,程式碼明明沒動啊?

原因:先回頭看 Dockerfile 那行 COPY . .——意思是「把目前目錄下的所有東西都複製進 Image」,而不是逐一列出要複製哪些檔案。這樣寫是為了省事:以後新增原始碼檔案,不用回頭改 Dockerfile,COPY . . 自動就會帶到。但代價是「所有東西」不分青紅皂白全部照單全收,包括你根本不希望進 Image 的東西。這個服務目前沒有 .dockerignore,於是本機資料夾裡舊版跑 data_generator.py 殘留下來的 wafer_delta_table/(6 個欄位的舊 Schema)也被一起複製進了 Image。build 到 RUN python data_generator.py 這一步時,新版腳本要寫入 8 個欄位(新增了 product_idyield),但 Image 裡的同一個路徑已經卡著一份 6 欄位的舊資料,Delta Lake 的 mode="overwrite" 預設只覆寫資料、不覆寫 Schema,於是直接拒絕寫入。

解法:明確告訴 Delta Lake「我要連 Schema 一起演進」:

write_deltalake(delta_path, full_df, mode="overwrite", schema_mode="overwrite")

這個修法解決了「這次」的症狀,但沒解決「COPY . . 沒有把關」這個根本原因——本機資料夾以後長出任何其他不該進 Image 的東西(__pycache__.venv、測試用的暫存檔),一樣會被原封不動複製進去。更徹底的做法是補一份 .dockerignore,把 wafer_delta_table/__pycache__/ 這類本機產物明確排除在 build context 之外,讓 COPY . . 的「省事」不用拿「乾淨」去換。

事後想想,Schema 校驗擋下這次寫入,其實就是 Delta Lake ACID 特性的體現——它寧可讓你的寫入失敗,也不會默默把資料表弄壞。開發時被它擋下來會覺得煩,但在生產環境這種「嚴格」就是保命符。

6. 總結

把 Delta Lake 的存儲路徑 /app/wafer_delta_table 獨立出來之後,後續在 K8S 部署時(Day 12 會講),就能很輕鬆地用 PV/PVC 把這個目錄掛成持久化存儲,容器重啟資料也不會消失。

後端引擎就緒,明天來看前端——React + ECharts 怎麼流暢渲染上萬個資料點的晶圓熱圖。


上一篇
[Day 3] 容器化實踐 (一):Spring Boot 3 的 Docker 化與最佳化 —— 從多階段構建到 JVM 參數,優化 Java 容器體質。
系列文
從零到一:使用 K8S + GitOps 打造異構技術棧的資料分析平台4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言