Skip to content

[Daily Questions Challenge 45]
部署邊界與責任:從三條線看懂 API / Worker / MQ / DB 的切分

[Daily Questions Challenge 45] 部署邊界與責任:從三條線看懂 API / Worker / MQ / DB 的切分

前一篇 雲端部署基礎架構 談的是「VPC、Subnet、Security Group、Load Balancer 各自是什麼」。這一篇要換一個角度:拿到 API、Worker、MQ、Cache、DB 這幾個組件,你該沿著哪些線把它們切開部署

如果把部署當成一張要背下來的架構圖,一旦組件換成別的、或規模變了,就不知道該怎麼擺。真正能遷移的是切分的判斷準則——三條邊界,每條背後都有一個可以複用的提問。

整體架構:三條分界線

這張圖有三條分界線,就是本篇的全部重點:

  1. API / Worker 的橫切線:同步與非同步的時間特性不同。
  2. Stateless / Stateful 的縱切線:有沒有保管資料,決定能不能被隨意砍換。
  3. Public / Private 的網路線:誰真的需要被外部主動連入。

以下逐條拆開。

邊界一:API vs Worker —— 時間特性不同就該分開

為什麼不能塞在同一個 process?

直覺上「同一份程式碼跑在一起最省事」,但這忽略了兩種工作的時間特性根本不同:

工作同步/非同步延遲要求使用者在等嗎?
HTTP 請求(送出訂單)同步幾百 ms 內回應在螢幕前等
Queue job(寄確認信)非同步幾秒也沒關係根本不在等

如果塞在同一個 process,一個慢吞吞的寄信 job 就會占住本來該去服務 HTTP 請求的資源——這就是「web process 被 queue 卡住」。

The Twelve-Factor App 把這件事講得很直接:把不同類型的工作交給不同的 process type,HTTP 請求由 web process 處理、長時間的背景任務由 worker process 處理。

分開部署帶來的營運價值:獨立伸縮

兩個角色的負載曲線不一樣,就該各自獨立伸縮:

  • 平常 API 請求穩定,但整點湧入大量批次 job(例如結算 10 萬筆)。
  • 如果 API 和 worker 是分開部署的兩組機器,這時可以只 scale worker,完全不動 API

API 和 worker 通常共用同一份 codebase,但用不同的進入點、部署成不同的 process。API 由 web server 驅動、對外開 port 收 HTTP;worker 是常駐迴圈(例如 Laravel 的 php artisan queue:work),不對外開 port,只主動往 queue 拉 job。因為負載特性不同,兩者的伸縮依據也不同——API 看 request/秒,worker 看 queue 積壓量。

小心過度概化:上圖把 worker 標為「主動拉 job(pull)」,是以 Laravel Queue、SQS、Redis list 的 pull 模型為例。push 還是 pull 依 broker 而異——RabbitMQ 是 broker 主動 push 給 consumer,Kafka 則是 consumer 主動 pull。不要把「worker 一律主動拉」當成所有 MQ 的通則。

核心:API 與 worker 常共用 codebase,但因為同步/非同步的時間特性不同,要用不同進入點部署成不同 process,才能各自獨立伸縮——混在一起時,慢 job 會卡住本該服務請求的資源。

邊界二:Stateless vs Stateful —— 部署的第一刀

分水嶺不是「省不省事」,而是「有沒有保管資料」

判斷一個組件該怎麼部署,真正該問的是這一句:

把這台機器直接砍掉、換一台新的頂上,使用者會不會感覺到資料不見了?

  • 不會 → Stateless(無狀態):API、Worker。它們不保管長期資料,處理完請求/job 就結束,狀態寫回別處。正因為無狀態,才能被自由地砍、換、加——這是「scale API/scale worker」能成立的前提。
  • 會 → Stateful(有狀態):MQ、Cache、DB。它們保管資料,砍掉就掉資料,必須是獨立、被妥善管理、會持久化的組件。

這正是基礎建設圈那句「牲口不是寵物(cattle, not pets)」的意思:無狀態機器是可替換、可拋棄的牲口,壞了就換一台一樣的頂上;有狀態資料則是需要細心照顧的寵物。

Cache 與 MQ 都是 stateful,但存的資料性質不同

三個有狀態組件雖然都不能亂砍,但「掉了會怎樣」差很多:

組件存什麼掉了會怎樣
Redis Cache掉了也能從 DB 重算回來的資料效能下降,資料不會真的少
MQ還沒被處理的 job/訊息真的少做事(訊息遺失)
DB唯一真實資料源(source of truth)資料永久遺失

在小型 Laravel 系統裡,常用同一個 Redis 同時兼任 cache 和 queue——這很常見,但它們是兩種不同責任,規模大了會拆開(例如 cache 用 ElastiCache、queue 改用 SQS 或專用 MQ)。

核心:雲端部署的第一刀,沿著 stateless/stateful 切開。無狀態層(API、worker)追求可拋棄性、靠水平擴展抗流量;有狀態層(DB、MQ、Cache)追求持久化與資料安全,要有備份、複製、故障轉移,通常交給雲端託管服務(RDS、ElastiCache、SQS/managed Kafka)來扛,而不是裝在 app 機器上。API 和 worker 都只是這些託管服務的客戶端

邊界三:Public vs Private —— 誰需要被外部主動連入

分法:只有「流量入口」需要 public

四個角色沿著「誰需要被外部主動連入」來分:

  • API:需要接收外部請求 → 靠近 public。
  • Worker / MQ / DB:都是被 API 或 worker 從內部去連的,沒有任何理由對全世界開放 → private。

但連 API 機器本身都可以退進 private

如果有 5 台 API 機器,外部請求要怎麼知道連哪一台?中間需要一個 Load Balancer 分流。而這個 LB 一出現,它就變成真正頂在 public 最前線的東西——API 機器反而可以全部退進 private

這樣一來,暴露在網路上的攻擊面只剩「一個 LB + 它開的 443 port」,後面所有東西全躲在 private,外面完全連不到。層層往內收,愈珍貴、愈有狀態的東西藏得愈深

核心:網路邊界不是沿著「誰要對外服務」切,而是沿著「誰真正需要被外部主動連入」切。真正需要 public 的只是流量入口(Load Balancer,通常只開 443),不是運算節點本身。API/worker/MQ/DB 全退進 private,只透過內部網路互連,再用 Security Group 限制「誰能連我」。

責任邊界總表

把三條邊界合起來看,每個角色的定位就一目了然:

角色狀態網路層對外開 port?伸縮依據
Load Balancer無狀態public是(443)通常託管、自動
API無狀態private否(只收 LB)request/秒
Worker無狀態private否(主動外連)queue 積壓量
MQ有狀態private(內層)否(只收內部)託管/水平分區(partition)
Cache有狀態private(內層)否(只收內部)一般垂直/託管擴展
DB有狀態private(最內層)否(只收內部)讀寫分離;寫入靠 sharding

總結

三條邊界,三個提問,三句話就能記住:

  1. 時間特性不同就分開:API 同步、worker 非同步,塞一起慢 job 會卡住請求,分開才能獨立伸縮(API 看 request/秒,worker 看 queue 積壓量)。
  2. 有沒有保管資料決定能不能砍:問「砍掉換一台,使用者會不會覺得資料不見」——不會的(API/worker)可拋棄、水平擴展;會的(MQ/Cache/DB)要持久化、交給託管服務。
  3. 只有流量入口需要 public:LB 當唯一對外門面(只開 443),其餘全退進 private,愈珍貴藏愈深。

面對一個新系統時,別急著畫圖——先照這三條線切,圖自然就會浮現。

參考