標籤: Kubernetes

  • 多通路電商 OMS 系統實戰:系列導讀

    2026 年:AI 衝擊下的軟體產業

    這是一個特殊的時代。

    SaaS 股票從 2021 年的高點崩跌 70-80%。曾經被視為「永遠成長」的軟體訂閱模式,現在面臨嚴峻的質疑。AI 能夠寫程式碼、能夠自動化客服、能夠生成內容——很多人問:軟體工程師還有未來嗎?

    現實的聲音:

    • 「AI 可以寫 80% 的 CRUD,還需要工程師嗎?」
    • 「Copilot 已經能自動補全程式碼了」
    • 「低代碼平台會取代傳統開發」
    • 「SaaS 已死,ARR 不再性感」

    在這個背景下,為什麼我還要寫這系列文章?為什麼 OMS 這種「傳統」的企業系統還有存在價值?


    AI 無法取代的部分

    讓我們誠實面對:AI 確實改變了很多事。但有些東西,AI 要做到需要企業級的整合架構

    AI 能做的 AI 要做到這些,需要什麼
    生成 CRUD 程式碼 整合架構讓 AI 知道該改哪裡
    寫單元測試 系統設計讓 AI 理解業務邏輯
    補全程式碼片段 標準化介面讓 AI 能套用模式
    呼叫 API OMS 提供統一的 API 讓 AI 操作
    分析數據 系統整合好的數據讓 AI 分析
    關鍵洞察:AI Agent、MCP 等技術正在讓 AI 能操作系統。但 AI 要發揮作用,需要標準化的底層架構。OMS 不是被 AI 取代的對象,而是讓 AI 能發揮的基礎設施

    SaaS 崩跌教會我們什麼?

    2021 年,SaaS 公司用 30-50 倍 ARR 估值。2024 年,同樣的公司只剩 5-8 倍。這不是 SaaS 模式有問題,而是市場重新認識了什麼是真正的價值

    泡沫時期的迷思 現實
    用戶增長就是一切 能留住用戶、能收到錢才是
    燒錢換市佔 正向現金流才能活下去
    功能越多越好 解決核心痛點比較重要
    技術債以後再還 技術債會讓你跑不動
    OMS 系統的價值:它不是「酷炫的新創產品」,而是「讓電商能正常運作的基礎設施」。每天處理訂單、同步庫存、產生出貨單——這些boring but essential的事情,才是真正的商業價值。

    為什麼 OMS 在 2026 年仍然重要?

    1. 電商只會更複雜,不會更簡單

    2020 年:蝦皮、Momo、PChome
    2024 年:加上 TikTok Shop、Shopee Food、跨境電商
    2026 年:更多新平台、更多整合需求

    平台越多,整合的需求越大。AI 可以幫你寫對接程式碼,但決定該怎麼對接、如何處理例外,還是需要人。

    2. 「無聊」的系統反而更持久

    一個觀察:最持久的軟體系統,往往是那些「不性感」的:

    • 銀行核心系統(COBOL 跑了 50 年)
    • ERP 系統(SAP 依然是企業標配)
    • 訂單管理系統(每個電商都需要)

    這些系統不會上 TechCrunch 頭條,但它們每天都在創造價值

    3. AI 是工具,不是替代品

    我現在寫程式碼,確實會用 AI 輔助。但 AI 給我的是草稿,我需要:

    • 理解業務需求,決定架構方向
    • 審核 AI 生成的程式碼是否符合系統設計
    • 處理 AI 不知道的「公司內部潛規則」
    • 跟平台 API 變更搏鬥(AI 不知道蝦皮上週改了什麼)

    這系列文章的價值

    這不是一篇「如何用某個框架」的教學。這是真實企業系統的設計思考

    你能學到的 為什麼重要
    工廠模式整合多平台 AI 時代更需要好的架構設計
    事件驅動處理高併發 擴展性是系統長期價值的關鍵
    多租戶認證 SaaS 模式的技術基礎
    分散式系統的觀測性 系統越複雜,可觀測性越重要
    Kubernetes 部署 雲原生是現在的標配
    核心觀點:技術會變,但解決問題的思維方式不會變。理解為什麼這樣設計、權衡了什麼、踩過什麼坑——這些經驗在 AI 時代反而更珍貴。

    回到最初的問題:為什麼需要 OMS?

    想像你是一個電商賣家,同時在蝦皮、Momo、Yahoo、PChome、樂天等 17 個平台上銷售商品。

    每天的噩夢:

    • 登入 17 個後台看訂單
    • 在 17 個平台更新庫存
    • 處理 17 種不同格式的出貨單
    • 應付 17 種不同的 API 規格變更

    AI 能幫你自動回覆客服訊息,但它不會幫你把蝦皮的訂單自動同步到你的倉儲系統。這種系統整合的工作,需要專門設計的軟體。


    商業價值(數字說話)

    70%
    人力成本降低
    10x
    處理速度提升
    99%
    庫存準確率
    80%
    擴展成本降低

    這些數字怎麼來的?讓我拆解給你看。

    計算基礎:一個中型電商的假設

    參數 數值 說明
    日均訂單量 500 單 中型電商規模
    銷售平台數 5 個 蝦皮、Momo、Yahoo、PChome、官網
    SKU 數量 2,000 個 中型商品數
    客服/營運人員月薪 35,000 元 含勞健保約 42,000

    1. 人力成本降低 70%:怎麼算的?

    沒有 OMS 的人力配置:

    工作內容 每日耗時 需要人力
    登入 5 個平台抓訂單 2 小時 × 3 次/天 1 人
    手動輸入訂單到 ERP 500 單 × 2 分鐘 2 人
    更新 5 個平台庫存 2,000 SKU × 5 平台 1 人
    產生出貨單(各平台格式) 500 單 × 3 分鐘 1 人
    合計 5 人

    有 OMS 的人力配置:

    工作內容 每日耗時 需要人力
    訂單自動同步 0(系統自動) 0
    審核異常訂單 約 5% 需人工,50 單 × 2 分鐘 0.2 人
    庫存自動同步 0(系統自動) 0
    批次產生出貨單 點一下,500 單 × 0.1 分鐘 0.1 人
    系統維運/例外處理 每天約 2-3 小時 0.5 人
    客服(不變) 需處理客戶問題 0.7 人
    合計 1.5 人
    計算:(5 – 1.5) / 5 = 70% 人力減少

    年省成本:3.5 人 × 42,000 元 × 12 月 = 176 萬元/年


    2. 處理速度提升 10 倍:怎麼算的?

    沒有 OMS(手動流程):

    步驟 耗時 說明
    平台抓單 等待下次人工抓取 平均等 2-4 小時
    人工輸入 ERP 2-5 分鐘/單 容易出錯要重做
    列印出貨單 3-5 分鐘/單 各平台格式不同
    撿貨出貨 實際作業時間 約 30 分鐘
    總計 4-8 小時 從下單到出貨

    有 OMS(自動流程):

    步驟 耗時 說明
    訂單自動同步 5 分鐘內 Webhook 或輪詢
    自動寫入系統 即時 無需人工
    批次產生出貨單 1 分鐘/批 統一格式
    撿貨出貨 實際作業時間 約 20 分鐘(有優化動線)
    總計 25-35 分鐘 從下單到出貨
    計算:6 小時 / 30 分鐘 = 12 倍(取保守值 10 倍)

    3. 庫存準確率 85% → 99%:怎麼算的?

    沒有 OMS 的庫存問題:

    問題來源 發生頻率 影響
    平台 A 賣掉,平台 B 還沒更新 每天 5-10 次 超賣
    人工輸入錯誤 約 2% 錯誤率 庫存不準
    更新延遲(人力不足) 下班後無人更新 隔夜超賣
    多平台庫存加總錯誤 每週發生 帳實不符

    估算:2,000 SKU,每天約 300 個有異動,其中 15% 會有某種不準確 = 約 85% 準確率

    有 OMS 的庫存管理:

    • 單一庫存來源,自動同步到所有平台
    • 賣出立即扣庫存,無延遲
    • 異常(負庫存)自動警示
    • 剩餘的 1% 誤差來自:實體盤點差異、退貨處理時間差
    結果:系統化管理後,準確率提升到 99%

    減少損失:假設超賣一次平均損失 500 元(退款 + 負評 + 平台罰款),每天減少 5 次 = 每月省 75,000 元


    4. 擴展成本降低 80%:怎麼算的?

    沒有 OMS,新增一個平台:

    工作項目 耗時 成本
    研究平台 API 文件 1-2 週 工程師人力
    開發訂單同步 2-3 週 工程師人力
    開發庫存同步 1-2 週 工程師人力
    開發出貨單格式 1 週 工程師人力
    測試、修 bug、上線 2-4 週 工程師 + QA
    營運人員培訓 1 週 培訓成本
    總計 8-12 週 約 50-80 萬

    有 OMS,新增一個平台:

    工作項目 耗時 成本
    研究平台 API 文件 3-5 天 工程師人力
    實作 ChannelAction 介面 1 週 套用現有架構
    DTO 轉換器 3-5 天 格式對照
    測試、上線 1 週 已有測試框架
    營運人員培訓 1 天 介面相同,只是多一個選項
    總計 2-3 週 約 10-15 萬
    計算:(60萬 – 12萬) / 60萬 = 80% 成本降低

    時間:從 2-3 個月縮短到 2-3 週


    ROI 總結

    效益項目 年度價值 計算方式
    人力成本節省 176 萬 3.5 人 × 42K × 12 月
    超賣損失減少 90 萬 75K × 12 月
    新通路快速上線(假設年增 2 個) 96 萬 48 萬 × 2 個通路
    出貨效率提升(減少延遲出貨罰款) 36 萬 估算
    年度總效益 約 400 萬
    注意:以上數字基於「日均 500 單、5 個平台」的中型電商假設。實際效益會因公司規模、產業特性而異。但數量級是對的——OMS 的 ROI 通常在 1-2 年內就能回本。

    投資成本與風險揭露

    講完效益,也要誠實談成本和風險。

    導入成本估算

    項目 自建開發 SaaS 方案
    初期建置 300-800 萬 0-50 萬
    月費/維護 5-15 萬(團隊) 2-10 萬(訂閱)
    導入期 6-12 個月 1-3 個月
    客製化程度 完全可控 受限於平台
    回本期 1-2 年 3-6 個月

    常見失敗風險

    導入 OMS 可能失敗的原因:

    • 需求不明確:沒釐清要解決什麼問題就開始開發
    • 低估平台複雜度:每個電商平台的 API 都有坑
    • 團隊能力不足:沒有足夠的開發和維運資源
    • 變更管理失敗:使用者不願意改變工作流程
    • 過度客製化:追求完美導致永遠做不完

    市場方案比較

    自建 OMS 不是唯一選擇。以下是常見方案的比較:

    方案類型 代表產品 適合誰 優點 缺點
    SaaS OMS 91APP、CYBERBIZ、EasyStore 中小型電商 快速上線、低成本、有人維護 客製化受限、數據在別人手上
    平台原生工具 蝦皮賣家中心、Momo 後台 單平台賣家 免費、原生整合 只能管單一平台、功能受限
    ERP 模組 SAP、Oracle、鼎新 大型企業 財務整合、企業級穩定 重、貴、慢、電商功能弱
    自建系統 本文討論的方式 有 IT 團隊的中大型電商 完全客製、數據自主、可深度整合 開發成本高、需要團隊維護
    選擇建議:

    • 日均 < 100 單、1-2 平台:用平台原生工具就好
    • 日均 100-500 單、3-5 平台:考慮 SaaS OMS
    • 日均 > 500 單、5+ 平台、有特殊需求:考慮自建或深度客製

    實際案例參考

    以下是幾個匿名化的導入案例:

    案例 A:服飾品牌(成功)

    規模 日均 800 單、7 個平台
    原本痛點 6 人團隊處理訂單、每天加班、超賣頻繁
    導入方式 自建 OMS,開發期 8 個月
    結果 縮減到 2 人、超賣降到每月 1-2 次、14 個月回本
    關鍵成功因素 老闆全力支持、IT 主管有電商經驗

    案例 B:3C 經銷商(部分成功)

    規模 日均 300 單、4 個平台
    原本痛點 ERP 和電商平台脫鉤、手動對帳
    導入方式 先用 SaaS,後來部分自建
    結果 訂單處理自動化成功,但庫存同步仍有問題
    教訓 低估了 ERP 整合的複雜度

    案例 C:食品電商(失敗重來)

    規模 日均 200 單、3 個平台
    原本痛點 想提升效率、老闆看到別人有就想要
    導入方式 外包開發
    結果 做了 6 個月、花了 150 萬、最後沒上線
    失敗原因 需求一直變、外包商不懂電商、內部沒人能接手
    案例啟示:導入 OMS 不是花錢就會成功。需要明確的需求有能力的團隊管理層支持三者缺一不可。

    OMS 可能不適合你

    誠實說,不是每家公司都需要 OMS:

    如果你符合以下情況,可能不需要(或還不需要)OMS:

    • 只在 1-2 個平台銷售:平台原生工具夠用
    • 日均訂單 < 50 單:人工處理得過來,ROI 不划算
    • 沒有 IT 資源:系統會變成另一個維護包袱
    • 業務模式還在摸索:需求不穩定,系統做了也會一直改
    • 現金流緊張:有更急迫的事情要處理

    反思:如果人工作業者也用 AI 呢?

    這是 2026 年必須面對的問題:如果營運人員也會用 ChatGPT、Copilot 等 AI 工具,還需要 OMS 嗎?

    AI 能幫助人工作業的部分

    工作內容 AI 能幫的 效率提升
    填寫表格 自動補全、格式轉換 2x
    回覆客戶訊息 生成範本回覆 3x
    整理 Excel 資料 公式生成、格式處理 2x
    查詢訂單狀態 整理多平台資訊(需人工複製貼上) 1.5x
    產生報表 數據分析、圖表建議 2x

    AI 無法幫助的部分

    關鍵限制:AI 是「對話工具」,不是「系統整合工具」

    • 無法登入平台後台:AI 不能幫你登入蝦皮抓訂單
    • 無法即時同步:你睡覺時,AI 也不會幫你更新庫存
    • 無法批次操作:AI 一次處理一個問題,不能同時處理 500 張訂單
    • 無法跨系統傳遞:AI 不能自動把訂單從蝦皮寫進你的 ERP
    • 無法 24/7 運作:沒有人下指令,AI 就不會動

    調整後的人力需求比較

    情境 需要人力 年人事成本
    純人工(無 AI) 5 人 252 萬
    人工 + AI 輔助 3 人(效率提升約 40%) 151 萬
    OMS 系統 1.5 人 76 萬
    OMS + AI 輔助 1 人(例外處理更快) 50 萬

    重新計算 ROI

    比較基準改變:「人工 + AI」vs「OMS」

    效益項目 人工+AI vs 純人工 OMS vs 人工+AI
    人力成本節省 省 101 萬/年 再省 75 萬/年
    超賣損失 無改善(還是會漏) 省 90 萬/年
    處理速度 快 1.5 倍 快 10 倍
    24/7 運作 不可能 可以
    新通路擴展 一樣要重新訓練人 2-3 週上線
    結論:即使人工作業者使用 AI,OMS 仍然每年省下約 200 萬(75 萬人力 + 90 萬超賣損失 + 其他效益)。

    原因:AI 讓「人」更有效率,但 OMS 解決的是「系統整合」問題——這兩者是不同層次的事情。

    真正的比較:「人+AI」vs「系統+AI」

    情境 A:人工 + AI
    ─────────────────────────────────
    人 → ChatGPT 幫忙寫回覆 → 人複製貼上到平台
    人 → 登入蝦皮看訂單 → 人手動輸入 ERP → 人更新其他平台庫存
    人 → AI 幫忙整理 Excel → 人複製貼上到各系統

    瓶頸:每個動作都需要「人」當中介

    情境 B:OMS 系統 + AI
    ─────────────────────────────────
    訂單 → OMS 自動同步 → 自動進 ERP → 自動更新所有平台庫存
    人 → AI 幫忙處理例外 → 在 OMS 一個介面完成
    報表 → 系統自動產生 → AI 幫忙分析

    瓶頸:只有「例外」需要人處理

    思考框架:

    • AI 提升「點」的效率:讓單一任務做得更快
    • OMS 解決「線」的問題:讓流程自動串連
    • 兩者結合才是最佳解:自動化流程 + AI 處理例外

    系統架構全景圖

    這 10 篇技術文章涵蓋了 OMS 系統的各個層面,以下是它們在系統中的位置:

    ┌─────────────────────────────────────────────────────────────────────────┐
    外部平台層
    │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
    │ │ 蝦皮 │ │ Momo │ │ Yahoo │ │ PChome │ │ … │ │
    │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │
    │ │ │ │ │ │ │
    │ └───────────┴───────────┴───────────┴───────────┘ │
    │ │ │
    │ ┌──────────▼──────────┐ │
    │ │ [6] HTTP 客戶端 │ ← OkHttp 連線池、重試機制 │
    │ │ [8] JSON 序列化 │ ← 時區處理、格式轉換 │
    │ └──────────┬──────────┘ │
    └───────────────────────────────┼─────────────────────────────────────────┘

    ┌───────────────────────────────▼─────────────────────────────────────────┐
    整合層
    │ │
    │ ┌─────────────────────────────────────────────────────────────────┐ │
    │ │ [1] 工廠模式 + 策略模式 │ │
    │ │ ChannelFactory → ShopeeAction / MomoAction / YahooAction … │ │
    │ └─────────────────────────────────────────────────────────────────┘ │
    │ │
    │ ┌─────────────────────────────────────────────────────────────────┐ │
    │ │ [5] DTO 設計 │ │
    │ │ 外部 DTO ←→ Converter ←→ 內部 DTO │ │
    │ └─────────────────────────────────────────────────────────────────┘ │
    │ │
    └───────────────────────────────┬─────────────────────────────────────────┘

    ┌───────────────────────────────▼─────────────────────────────────────────┐
    核心服務層
    │ │
    │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
    │ │ OrderService │ │InventorySync│ │ ShippingServ │ │
    │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
    │ │ │ │ │
    │ └───────────────────┼───────────────────┘ │
    │ │ │
    │ ┌──────────────────────────▼──────────────────────────────────────┐ │
    │ │ [3] 多租戶認證 │ │
    │ │ Token 驗證 → SecurityContext → 商戶隔離 │ │
    │ └─────────────────────────────────────────────────────────────────┘ │
    │ │
    │ ┌──────────────────────────────────────────────────────────────┐ │
    │ │ [7] PDF 生成 ← 出貨單、撿貨單、對帳單 │ │
    │ └──────────────────────────────────────────────────────────────┘ │
    │ │
    └───────────────────────────────┬─────────────────────────────────────────┘

    ┌───────────────────────────────▼─────────────────────────────────────────┐
    訊息層
    │ │
    │ ┌─────────────────────────────────────────────────────────────────┐ │
    │ │ [2] Kafka 事件驅動 │ │
    │ │ Producer → Topics (per channel) → Consumer Jobs │ │
    │ └─────────────────────────────────────────────────────────────────┘ │
    │ │
    └───────────────────────────────┬─────────────────────────────────────────┘

    ┌───────────────────────────────▼─────────────────────────────────────────┐
    基礎設施層
    │ │
    │ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
    │ │ [4] 健康檢查 │ │ [9] 分散式追蹤 │ │ [10] K8s 部署 │ │
    │ │ Actuator │ │ OpenTracing │ │ Helm Charts │ │
    │ │ 自訂 Indicator │ │ Jaeger │ │ ConfigMap │ │
    │ └────────────────┘ └────────────────┘ └────────────────┘ │
    │ │
    └─────────────────────────────────────────────────────────────────────────┘
    閱讀建議:

    • 從頭開始:按順序讀,理解系統如何從外到內設計
    • 解決特定問題:直接跳到對應章節
    • 架構設計師:重點看 [1] 工廠模式、[2] Kafka、[10] K8s
    • 後端工程師:重點看 [3] 認證、[5] DTO、[6] HTTP

    系列文章導覽

    # 主題 解決什麼問題 AI 時代的價值
    1 工廠模式 整合 17 個平台的不同 API 架構設計思維
    2 Kafka 事件驅動 高併發訂單處理 分散式系統設計
    3 多租戶認證 一套系統服務多商戶 SaaS 技術基礎
    4 健康檢查 自動監控系統狀態 可觀測性
    5 DTO 設計 管理數百個資料物件 程式碼組織
    6 HTTP 客戶端 穩定呼叫外部 API 整合實務
    7 PDF 生成 統一出貨標籤格式 API 設計
    8 JSON 序列化 處理不同平台的時區 資料處理
    9 分散式追蹤 追蹤請求在各服務的流向 除錯能力
    10 K8s 部署 管理 17+ 個服務部署 雲原生技能

    適合讀者

    角色 可以學到
    正在焦慮的工程師 AI 時代什麼能力還有價值
    想轉型的技術人 企業系統的實戰經驗
    技術主管 如何設計可擴展的系統
    電商從業者 技術如何解決業務痛點

    關於作者

    Tom|10+ 年軟體工程經驗

    經歷過幾個時代:

    • 2010s:傳統 SI、CRM 系統
    • 2015+:電商爆發、系統整合
    • 2020s:雲原生、微服務
    • 2024+:AI 衝擊、重新定位

    這系列文章來自在精誠開發多通路電商 OMS 系統的實戰經驗。不是教科書理論,是真正上線運營、處理過各種奇怪問題的心得。

    寫這些文章的原因:在 AI 時代,我相信「理解系統為什麼這樣設計」比「會寫程式碼」更有價值。希望這些經驗對你有幫助。


    下一步

    如果你是工程師:

    如果你是技術主管或決策者:

    • 想評估貴公司是否適合導入 OMS?歡迎來信交流
    • 需要技術諮詢或系統規劃?我提供電商系統架構顧問服務
    聯絡方式:


    這是「多通路電商 OMS 系統實戰」系列的導讀篇。點擊上方表格中的連結,深入每個技術主題。

  • K8s 部署實戰:Helm Charts 與服務編排

    商業價值:標準化部署讓「17 個通路獨立升級不互相影響」,確保 導讀篇提到的「系統穩定性」——單一通路故障不會拖垮整個系統。

    為什麼不用其他方案?

    方案 優點 缺點 適用場景
    Helm Charts(本文) 標準化、版本控制、可重用 學習曲線 多服務微服務架構
    純 YAML 簡單直接 重複多、難維護 單一服務
    Kustomize 原生支援 複雜覆寫較難 簡單環境差異
    Docker Compose 開發方便 不適合生產環境 本地開發

    前言:微服務部署的挑戰

    多通路 OMS 系統包含:

    服務類型 數量 說明
    Consumer Job 17+ 個 每個通路一個
    API 服務 10+ 個 訂單、商品、物流等
    Web 前端 3 個 商戶、管理、OpenAPI
    問題:如何有效管理這麼多服務的部署?

    解決方案:Helm Charts

    Chart 目錄結構

    helm/
    └── oms-consumer-shopee/
    ├── Chart.yaml # Chart 基本資訊
    ├── values.yaml # 預設變數
    ├── values-dev.yaml # 開發環境
    ├── values-prod.yaml # 正式環境
    ├── config/ # 應用程式配置
    │ ├── application.yml
    │ └── logback.xml
    └── templates/ # K8s 資源模板
    ├── deployment.yaml
    ├── service.yaml
    └── configmap.yaml

    Deployment 設定

    # templates/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: {{ .Chart.Name }}
    namespace: oms
    labels:
    app: {{ .Chart.Name }}

    spec:
    replicas: {{ .Values.replicas }}
    selector:
    matchLabels:
    app: {{ .Chart.Name }}

    template:
    metadata:
    annotations:
    # ConfigMap 變更時自動觸發滾動更新
    checksum/config: {{ include (print $.Template.BasePath “/configmap.yaml”) . | sha256sum }}
    # Prometheus 監控
    prometheus.io/scrape: “true”
    prometheus.io/path: /metrics
    prometheus.io/port: “8080”

    labels:
    app: {{ .Chart.Name }}
    version: {{ .Values.image.version }}

    spec:
    containers:
    – name: {{ .Chart.Name }}
    image: “{{ .Values.image.repository }}:{{ .Values.image.version }}”
    imagePullPolicy: {{ .Values.image.pullPolicy }}

    # 資源限制
    resources:
    requests:
    cpu: {{ .Values.resources.requests.cpu }}
    memory: {{ .Values.resources.requests.memory }}
    limits:
    cpu: {{ .Values.resources.limits.cpu }}
    memory: {{ .Values.resources.limits.memory }}

    # 健康檢查
    livenessProbe:
    httpGet:
    path: /health/liveness
    port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10

    readinessProbe:
    httpGet:
    path: /health/readiness
    port: 8080
    initialDelaySeconds: 20
    periodSeconds: 5

    # 優雅關閉
    lifecycle:
    preStop:
    exec:
    command: [“curl”, “-XPOST”, “http://localhost:8080/shutdown”]


    values.yaml:環境變數化

    # values.yaml(預設值)
    replicas: 2

    image:
    repository: registry.example.com/oms/consumer-shopee
    version: latest
    pullPolicy: Always

    resources:
    requests:
    cpu: 100m
    memory: 256Mi
    limits:
    cpu: 1000m
    memory: 1Gi

    # values-prod.yaml(正式環境覆寫)
    replicas: 5

    image:
    version: v1.2.3
    pullPolicy: IfNotPresent

    resources:
    requests:
    cpu: 500m
    memory: 512Mi
    limits:
    cpu: 2000m
    memory: 2Gi

    部署指令

    # 開發環境
    helm upgrade –install consumer-shopee ./helm/oms-consumer-shopee \
    -f values-dev.yaml

    # 正式環境
    helm upgrade –install consumer-shopee ./helm/oms-consumer-shopee \
    -f values-prod.yaml \
    –set image.version=v1.2.3


    Secret 管理

    # 敏感資訊不放在 values.yaml
    env:
    – name: DB_USERNAME
    valueFrom:
    secretKeyRef:
    name: db-credentials
    key: username

    – name: DB_PASSWORD
    valueFrom:
    secretKeyRef:
    name: db-credentials
    key: password

    # 建立 Secret
    kubectl create secret generic db-credentials \
    –namespace oms \
    –from-literal=username=admin \
    –from-literal=password=secret123

    ConfigMap 管理

    # templates/configmap.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
    name: {{ .Chart.Name }}-config
    data:
    {{ (.Files.Glob “config/*”).AsConfig | indent 2 }}
    效果:config/ 目錄下的檔案會自動打包成 ConfigMap

    健康檢查整合

    Probe 類型 端點 失敗行為
    liveness /health/liveness 重啟 Pod
    readiness /health/readiness 從 Service 移除
    # 分離 liveness 和 readiness
    livenessProbe:
    httpGet:
    path: /health/liveness
    port: 8080
    initialDelaySeconds: 30 # 啟動後等待時間
    periodSeconds: 10 # 檢查間隔
    timeoutSeconds: 5 # 超時時間
    failureThreshold: 3 # 失敗幾次後重啟

    readinessProbe:
    httpGet:
    path: /health/readiness
    port: 8080
    initialDelaySeconds: 20
    periodSeconds: 5
    failureThreshold: 3


    多通路部署策略

    17 個通路,每個都有獨立的 Helm Chart:

    helm/
    ├── oms-consumer-shopee/
    ├── oms-consumer-momo/
    ├── oms-consumer-yahoo/
    ├── oms-consumer-pchome/
    ├── oms-consumer-rakuten/
    ├── oms-consumer-shopify/
    ├── oms-consumer-shopline/
    └── … (共 17 個)

    批次部署腳本

    #!/bin/bash

    CHANNELS=(
    “shopee”
    “momo”
    “yahoo”
    “pchome”
    “rakuten”
    “shopify”
    “shopline”
    )

    VERSION=$1

    for channel in “${CHANNELS[@]}”; do
    echo “Deploying $channel…”

    helm upgrade –install “consumer-${channel}” \
    “./helm/oms-consumer-${channel}” \
    –set image.version=$VERSION \
    –namespace oms

    done

    echo “All channels deployed!”


    Service 與 Ingress

    # templates/service.yaml
    apiVersion: v1
    kind: Service
    metadata:
    name: {{ .Chart.Name }}
    namespace: oms
    spec:
    type: ClusterIP
    ports:
    – port: 80
    targetPort: 8080
    name: http
    selector:
    app: {{ .Chart.Name }}
    # Ingress 設定(API Gateway)
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
    name: oms-api-gateway
    annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    spec:
    rules:
    – host: api.example.com
    http:
    paths:
    – path: /orders
    pathType: Prefix
    backend:
    service:
    name: order-service
    port:
    number: 80

    CI/CD 整合

    # .gitlab-ci.yml
    stages:
    – build
    – deploy

    build:
    stage: build
    script:
    – docker build -t registry.example.com/oms/consumer-shopee:$CI_COMMIT_SHA .
    – docker push registry.example.com/oms/consumer-shopee:$CI_COMMIT_SHA

    deploy-dev:
    stage: deploy
    script:
    – helm upgrade –install consumer-shopee ./helm/oms-consumer-shopee
    –set image.version=$CI_COMMIT_SHA
    -f values-dev.yaml
    only:
    – develop

    deploy-prod:
    stage: deploy
    script:
    – helm upgrade –install consumer-shopee ./helm/oms-consumer-shopee
    –set image.version=$CI_COMMIT_TAG
    -f values-prod.yaml
    only:
    – tags
    when: manual


    實戰踩坑

    踩坑 1:ConfigMap 改了但 Pod 沒更新
    情境:改了 application.yml 但服務行為沒變
    原因:K8s 不會自動重啟 Pod,舊的 ConfigMap 還在記憶體中
    解法:在 Deployment 加 checksum annotation,ConfigMap 變化時觸發滾動更新
    踩坑 2:OOM Killed 但沒收到告警
    情境:服務突然重啟,查了半天才發現是記憶體不足
    原因:JVM heap 設定超過 container limits,被 K8s 強制 kill
    解法:JVM heap 設為 limits 的 70%,並設定 Prometheus 告警監控 OOM 事件
    踩坑 3:滾動更新時服務中斷
    情境:部署新版本時用戶收到 502 錯誤
    原因:新 Pod 還沒 ready 就把舊 Pod 砍掉,或 preStop 沒有 graceful shutdown
    解法:設好 readinessProbe + preStop hook,確保流量先切換再關閉

    總結

    設計 效果
    Helm Chart 標準化 每個服務結構一致,易於維護
    values 分環境 同一 Chart 部署不同環境
    ConfigMap 動態載入 配置與程式碼分離
    Secret 管理 敏感資訊安全儲存
    健康檢查整合 K8s 自動管理故障
    多通路獨立部署 故障隔離,獨立升級

    上一篇 系列目錄 完結
    OpenTracing分散式追蹤 系列導讀 本篇為系列最終篇

    這是「多通路電商 OMS 系統實戰」系列的最終篇。感謝閱讀,希望對你的系統設計有所幫助!

  • 分散式追蹤:OpenTracing 整合實戰

    商業價值:分散式追蹤讓「問題定位從小時變分鐘」,直接支援 導讀篇提到的「系統穩定性」——當訂單卡在某個環節時,能快速找到是哪個服務出問題。

    為什麼不用其他方案?

    方案 優點 缺點 適用場景
    B3 + Jaeger(本文) 業界標準、視覺化強 需要額外部署 微服務架構
    Log 關聯查詢 簡單 跨服務追蹤困難 單體應用
    Zipkin 輕量 功能較少 小型微服務
    商業 APM 功能完整 費用高 預算充足團隊

    前言:微服務的可觀測性挑戰

    在多通路系統中,一個訂單請求的流程可能是:

    使用者 → API Gateway → Order Service → Kafka → Consumer Job → 蝦皮 API

    資料庫更新

    回呼通知
    問題:當某個環節出問題時,如何追蹤請求在各服務之間的流向?

    這就是分散式追蹤(Distributed Tracing)要解決的問題。


    追蹤標準:B3 Propagation

    我們採用 B3 標準,定義一組 HTTP Header 來傳遞追蹤資訊:

    Header 說明 範例
    x-request-id 請求唯一識別碼 uuid-xxxx
    x-b3-traceid 追蹤 ID(整個請求鏈共用) abc123
    x-b3-spanid 當前操作 ID def456
    x-b3-parentspanid 父操作 ID abc123
    x-b3-sampled 是否取樣 1

    實作:傳遞追蹤 Header

    定義追蹤 Header 清單

    public class TracingHeaders {

    public static final List<String> HEADERS = List.of(
    “x-request-id”,
    “x-b3-traceid”,
    “x-b3-spanid”,
    “x-b3-parentspanid”,
    “x-b3-sampled”,
    “x-b3-flags”
    );
    }

    從 Request 提取追蹤 Header

    @Component
    public class TracingExtractor {

    /**
    * 從 HTTP Request 提取追蹤 Header
    */

    public Map<String, String> extract(HttpServletRequest request) {
    Map<String, String> tracingHeaders = new HashMap<>();

    for (String headerName : TracingHeaders.HEADERS) {
    String value = request.getHeader(headerName);
    if (value != null && !value.isEmpty()) {
    tracingHeaders.put(headerName, value);
    }
    }

    return tracingHeaders;
    }
    }


    使用情境

    情境 1:API 呼叫外部服務

    @RestController
    public class OrderController {

    @Autowired private TracingExtractor tracingExtractor;
    @Autowired private HttpClientService httpClient;

    @GetMapping(“/api/orders/{orderId}”)
    public Order getOrder(
    @PathVariable String orderId,
    HttpServletRequest request) {

    // 1. 提取追蹤 Header
    Map<String, String> tracingHeaders = tracingExtractor.extract(request);

    // 2. 呼叫外部 API,傳遞追蹤 Header
    HttpResult result = httpClient.get(
    “https://api.platform.com/orders/” + orderId,
    tracingHeaders
    );

    return parseOrder(result);
    }
    }

    情境 2:Kafka 訊息傳遞

    // Producer:將追蹤資訊放入訊息
    public String buildMessage(Object body, Map<String, String> tracingHeaders) {
    Map<String, Object> message = new HashMap<>();

    // Header 包含追蹤資訊
    Map<String, Object> header = new HashMap<>();
    header.put(“version”, 1);
    header.put(“tracing”, tracingHeaders);

    message.put(“header”, header);
    message.put(“body”, body);

    return JsonUtil.toJson(message);
    }

    // Consumer:從訊息取出追蹤資訊
    public void processMessage(String message) {
    Map<String, Object> data = JsonUtil.toMap(message);
    Map<String, Object> header = (Map) data.get(“header”);

    // 取出追蹤 Header
    Map<String, String> tracingHeaders = (Map) header.get(“tracing”);

    // 呼叫外部 API 時繼續傳遞
    HttpResult result = httpClient.get(platformApiUrl, tracingHeaders);
    }


    自動傳遞:Filter + ThreadLocal

    @Component
    public class TracingFilter implements Filter {

    @Autowired
    private TracingExtractor extractor;

    @Override
    public void doFilter(
    ServletRequest request,
    ServletResponse response,
    FilterChain chain) throws IOException, ServletException {

    HttpServletRequest httpRequest = (HttpServletRequest) request;

    // 提取追蹤 Header 放入 ThreadLocal
    Map<String, String> tracingHeaders = extractor.extract(httpRequest);
    TracingContext.set(tracingHeaders);

    try {
    chain.doFilter(request, response);
    } finally {
    TracingContext.clear();
    }
    }
    }

    // ThreadLocal 儲存
    public class TracingContext {

    private static final ThreadLocal<Map<String, String>> CONTEXT =
    new ThreadLocal<>();

    public static void set(Map<String, String> headers) {
    CONTEXT.set(headers);
    }

    public static Map<String, String> get() {
    Map<String, String> headers = CONTEXT.get();
    return headers != null ? headers : Collections.emptyMap();
    }

    public static void clear() {
    CONTEXT.remove();
    }
    }

    效果:HTTP 客戶端自動帶入追蹤 Header,開發者不用手動處理。

    整合 Jaeger

    Jaeger 是常用的分散式追蹤系統,可以視覺化追蹤鏈。

    Spring Boot 設定

    # application.yml
    opentracing:
    jaeger:
    enabled: true
    udp-sender:
    host: jaeger-agent
    port: 6831
    log-spans: true
    probabilistic-sampler:
    sampling-rate: 1.0 # 100% 取樣(生產環境調低)

    追蹤視覺化

    ┌─────────────────────────────────────────────────────────────────┐
    │ Trace: abc123 │
    ├─────────────────────────────────────────────────────────────────┤
    │ ▼ api-gateway [45ms] │
    │ ├─ order-service [30ms] │
    │ │ ├─ kafka-produce [5ms] │
    │ │ └─ db-query [10ms] │
    │ └─ consumer-job [25ms] │
    │ └─ platform-api [20ms] │
    └─────────────────────────────────────────────────────────────────┘

    錯誤追蹤

    @Aspect
    @Component
    public class TracingAspect {

    @Autowired
    private Tracer tracer;

    @Around(“execution(* com.example..*Service.*(..))”)
    public Object trace(ProceedingJoinPoint joinPoint) throws Throwable {
    Span span = tracer.buildSpan(joinPoint.getSignature().getName()).start();

    try {
    return joinPoint.proceed();

    } catch (Exception e) {
    // 記錄錯誤到追蹤系統
    span.setTag(“error”, true);
    span.log(Map.of(
    “event”, “error”,
    “error.message”, e.getMessage(),
    “error.class”, e.getClass().getName()
    ));
    throw e;

    } finally {
    span.finish();
    }
    }
    }


    實戰踩坑

    踩坑 1:ThreadLocal 洩漏到其他請求
    情境:偶發看到 traceId 對不上,追蹤鏈混亂
    原因:Filter 的 finally 沒有正確清理 ThreadLocal,執行緒池重用導致污染
    解法:確保 TracingContext.clear() 一定會執行,可用 try-finally 或 Spring 的 RequestContextHolder
    踩坑 2:Kafka Consumer 追蹤斷裂
    情境:Producer 到 Consumer 的追蹤連不起來
    原因:只在 HTTP 層傳遞 Header,忘了在 Kafka 訊息中嵌入追蹤資訊
    解法:Producer 把 tracingHeaders 放進訊息 header,Consumer 取出後設回 ThreadLocal
    踩坑 3:取樣率設 100% 撐爆 Jaeger
    情境:Jaeger 儲存空間暴增,查詢變慢
    原因:生產環境忘了調低 sampling-rate,每個請求都記錄
    解法:生產環境設 0.1(10%)或更低,搭配動態取樣根據錯誤率調整

    總結

    設計 效果
    B3 標準 Header 相容主流追蹤系統
    Filter 自動提取 開發者不需手動處理
    ThreadLocal 儲存 任何地方都能取得追蹤資訊
    Kafka 嵌入 異步訊息也能追蹤
    Jaeger 整合 視覺化追蹤鏈

    上一篇 系列目錄 下一篇
    JSON處理與時區管理 系列導讀 Kubernetes Helm Charts 部署

    這是「多通路電商 OMS 系統實戰」系列的第九篇。下一篇會介紹 Kubernetes 部署。

  • 分佈式健康檢查:自定義 Spring Boot Actuator

    商業價值:健康檢查讓系統「自動發現問題、自動恢復」,直接支撐 導讀篇提到的 99% 庫存準確率——系統不穩定就不可能有準確的庫存。

    前言:為什麼需要健康檢查?

    在微服務架構中,一個服務可能依賴多個外部元件:

    元件 用途 掛掉的影響
    PostgreSQL 主資料庫 無法讀寫訂單
    Redis 快取 效能下降
    Kafka 訊息佇列 無法非同步處理
    Solr 搜尋引擎 無法搜尋訂單
    問題:Kubernetes 預設只檢查 HTTP 回應,無法知道資料庫是否正常。

    Spring Boot Actuator 健康檢查

    基本設定

    # application.yml
    management:
    endpoints:
    web:
    base-path: /
    exposure:
    include: health, info, metrics

    endpoint:
    health:
    show-details: always
    show-components: always

    health:
    # 啟用各元件的健康檢查
    db:
    enabled: true
    redis:
    enabled: true

    健康檢查端點

    端點 用途 使用場景
    /health 完整健康狀態 監控系統
    /health/liveness 存活檢查 K8s liveness probe
    /health/readiness 就緒檢查 K8s readiness probe

    自定義健康檢查指標

    Kafka 健康檢查

    @Component
    public class KafkaHealthIndicator implements HealthIndicator {

    @Value(“${kafka.bootstrap-servers}”)
    private String bootstrapServers;

    private AtomicReference<Health> cachedHealth =
    new AtomicReference<>(Health.unknown().build());

    @Override
    public Health health() {
    return cachedHealth.get();
    }

    /**
    * 背景執行緒定期檢查,避免阻塞健康檢查端點
    */

    @Scheduled(fixedRate = 30000) // 每 30 秒檢查一次
    public void checkHealth() {
    try {
    Properties props = new Properties();
    props.put(“bootstrap.servers”, bootstrapServers);
    props.put(“request.timeout.ms”, “5000”);

    try (AdminClient admin = AdminClient.create(props)) {
    admin.listTopics().names().get(5, TimeUnit.SECONDS);
    }

    cachedHealth.set(Health.up()
    .withDetail(“servers”, bootstrapServers)
    .build());

    } catch (Exception e) {
    cachedHealth.set(Health.down()
    .withDetail(“error”, e.getMessage())
    .build());
    }
    }
    }

    Solr 健康檢查

    @Component
    public class SolrHealthIndicator implements HealthIndicator {

    @Autowired
    private SolrClient solrClient;

    private AtomicReference<Health> cachedHealth =
    new AtomicReference<>(Health.unknown().build());

    @Override
    public Health health() {
    return cachedHealth.get();
    }

    @Scheduled(fixedRate = 30000)
    public void checkHealth() {
    try {
    SolrPingResponse response = solrClient.ping();
    int status = response.getStatus();

    if (status == 0) {
    cachedHealth.set(Health.up()
    .withDetail(“responseTime”, response.getQTime())
    .build());
    } else {
    cachedHealth.set(Health.down()
    .withDetail(“status”, status)
    .build());
    }

    } catch (Exception e) {
    cachedHealth.set(Health.down()
    .withDetail(“error”, e.getMessage())
    .build());
    }
    }
    }


    健康檢查回應範例

    {
    “status”: “UP”,
    “components”: {
    “db”: {
    “status”: “UP”,
    “details”: {
    “database”: “PostgreSQL”,
    “validationQuery”: “isValid()”
    }
    },
    “kafka”: {
    “status”: “UP”,
    “details”: {
    “servers”: “kafka:9092”
    }
    },
    “redis”: {
    “status”: “UP”,
    “details”: {
    “version”: “7.0.0”
    }
    },
    “solr”: {
    “status”: “UP”,
    “details”: {
    “responseTime”: 5
    }
    }
    }
    }

    Kubernetes 整合

    # deployment.yaml
    spec:
    containers:
    – name: oms-service
    # 存活檢查:程式是否還活著
    livenessProbe:
    httpGet:
    path: /health/liveness
    port: 8080
    initialDelaySeconds: 30
    periodSeconds: 10
    timeoutSeconds: 5
    failureThreshold: 3

    # 就緒檢查:是否可以接受流量
    readinessProbe:
    httpGet:
    path: /health/readiness
    port: 8080
    initialDelaySeconds: 20
    periodSeconds: 5
    timeoutSeconds: 3
    failureThreshold: 3

    Probe 類型 失敗後行為 使用場景
    liveness 重啟 Pod 程式死當、無回應
    readiness 從 Service 移除 暫時無法服務(如 DB 斷線)

    設計考量

    為什麼用背景執行緒 + 快取?

    • 健康檢查端點需要快速回應(< 1秒)
    • 外部元件檢查可能很慢(網路延遲)
    • Kubernetes 頻繁呼叫(每 5-10 秒)
    設計 說明
    背景檢查 每 30 秒執行一次,不阻塞端點
    結果快取 AtomicReference 儲存最新狀態
    逾時設定 檢查逾時 5 秒,避免卡住
    狀態詳情 包含時間、錯誤訊息等資訊

    監控整合

    將健康狀態匯出到 Prometheus:

    # 健康狀態指標
    health_check_status{component=”kafka”} 1
    health_check_status{component=”solr”} 1
    health_check_status{component=”redis”} 1
    health_check_status{component=”db”} 1

    # 檢查執行時間
    health_check_duration_seconds{component=”kafka”} 0.023
    health_check_duration_seconds{component=”solr”} 0.005


    總結

    設計 效果
    自定義 HealthIndicator 檢查所有依賴元件
    背景執行 + 快取 端點回應快速
    K8s Probe 整合 自動重啟/移除故障 Pod
    Prometheus 匯出 歷史趨勢監控

    為什麼不用其他方案?

    方案 優點 缺點 結論
    只靠 K8s 預設檢查 零設定 只檢查 HTTP 回應,不知道 DB 狀態 不夠
    外部監控工具打 API 不侵入程式碼 只知道 API 回應,不知道內部狀態 補充用
    自己寫健康檢查 API 完全控制 要自己處理快取、超時 重複造輪子
    Actuator + 自訂 整合好、可擴展 要學 Spring 生態 Spring 專案首選

    實戰踩坑

    坑 1:健康檢查太慢導致 Pod 被殺

    最初健康檢查直接連 Kafka,網路慢時要 10 秒才回應。K8s 以為 Pod 死了,不斷重啟。解法:改成背景執行緒定期檢查,健康端點只回傳快取結果。

    坑 2:Liveness 和 Readiness 混用

    最初兩個 Probe 用同一個端點。結果 Kafka 斷線時,所有 Pod 都被重啟(Liveness 失敗)。正確做法:Liveness 只檢查「程式還活著」,Readiness 檢查「能不能接流量」。Kafka 斷線應該是 Readiness 失敗(從 Service 移除),不是 Liveness 失敗(重啟)。

    坑 3:忘記設定 initialDelaySeconds

    應用程式啟動要 30 秒,但健康檢查 10 秒就開始。結果 Pod 永遠起不來,一直被重啟。


    系列導航

    ◀ 上一篇
    多租戶認證
    📚 返回目錄 下一篇 ▶
    DTO 設計
  • 成本預估故事

    Cost

    所有服務

    服務 VM VCPU Memory size(GB) Hard disk size(GB)
    K8s(Run 69 Pods) 3 16 128 200
    Solr 2 16 16 200
    PostgreSQL 2 16 32 600
    Kafka 3 4 4 80
    ZooKeeper(zk01) 1 16 8 20
    ZooKeeper(zk02,zk03) 2 4 4 20
    Infinispan 2 2 4 20
    HAProxy 2 4 4 20
    Nginx 2 2 4 50
    GitLab 1 8 8 100
    Jenkins 1 2 4 50
    Harbor Registry (IMG Hub) 1 2 2 100
    Elasticsearch 1 8 8 750
    Logstash 1 4 4 20
    Kibana 1 4 8 100
    DNS 1 2 2 16
    MAIL Server 1 4 4 20
    Object Storage (Ceph) 3 4 4 150

    故事

    2021

    5月 我加入精誠,非Oneec身分,但是閒暇時會與Ethan進行相關的討論,並且不時會看SHOPEE跟東森的API文件思考架構
    8月 infra加入精誠,非Oneec身分,但是Ethan已經準備好了技術選型並且請這位Infra整理機器,清理空間
    9月 PM加入精誠 ,Oneec身分,Ethan請她進行思考
    10月 最強的全端RD入場,Oneec身分,Ethan請他跟Infra準備K8S環境底下的高可用環境程式
    12月 還在討論Topic,12月中 全端RD回報,準備好了,開工

    (閱讀全文…)