iT邦幫忙

2026 iThome 鐵人賽

DAY 13
0

到目前為止,PokeThreads 已經具備了 Load Balancer、Read Replica、Cache、Stateless Server、CDN 與 Message Queue,是時候回頭想想,這些元件之間該怎麼有效率地互相溝通了。

不過在談溝通方式之前,先看一眼現在的 Application Server 到底裝了多少東西。

一個 Server,裝了整個 PokeThreads

到目前為止,不管是發文、看 Feed、Follow,還是接收 Message Queue 的 Notification Worker,全部都活在同一包程式碼裡:

             PokeThreads Application
        ┌─────────────────────────┐
        │ User                    │
        │ Post                    │
        │ Feed                    │
        │ Follow                  │
        │ Notification            │
        └─────────────────────────┘
                    ↓
                Database

這種「所有功能都打包在同一個部署單位」的做法,叫做 Monolith

Monolith 代表整個 Application 以一個主要部署單位存在,就像是一棟「獨立賣場」,例如 3C 家電連鎖通路(全國電子、黃色鬼屋),所有的服務(會員、訂單、金流、UI介面)都在同一棟建築物內,共用同一個地基與水電系統。

這在系統還小的時候完全適用,兩個模組要合作直接呼叫彼此的函式就好,不用處理任何網路問題。

但 PokeThreads 走到今天這一步,Monolith 開始出現幾個明顯的痛點。

痛點一:沒辦法單獨 Scaling

假設 Feed 的流量暴增,需要更多運算資源,但 Notification Worker 平常負載很輕。

在 Monolith 架構下,Server 裡什麼功能都有,只能整包一起複製、一起擴展,沒辦法只加強 Feed 那一部分。

痛點二:牽一髮動全身

假設只是想調整 Notification 的文案,理論上跟 Feed、Follow 一點關係都沒有。

但因為所有程式碼綁在同一個部署單位裡,任何一次上線都是整個 Application 重新部署一次,風險跟影響範圍都被放大了。

痛點三:不同功能想要不同的技術棧

  • Feed 想要的是 Cache + Read Replica
  • Notification 想要的是 Message Queue + Worker
  • Search(之後會聊到)可能想要專門的 Search Engine

硬把這些完全不同性質的工作塞進同一個 Runtime,只會讓每個功能都無法用最適合自己的方式運作。

Microservices:把賣場拆成專門店

既然 Monolith 卡在「所有東西綁在一起」,那就反過來把系統拆成多個服務,每個都各自獨立部署、各自負責一個明確業務範圍的服務,這就是 Microservices

如果 Monolith 是一間大賣場,Microservices 更像百貨公司,每一家專櫃各自有自己的店面、自己的人力配置,彼此透過走廊(也就是 API)交換東西,但誰也不用管誰的內部庫存怎麼擺。

回到 PokeThreads,第一個值得拆出去的候選者很明顯:Notification

它和跟主要的 Post/Feed API 完全不同、依賴的元件(Message Queue、Worker)也不同,拆分之後,Notification Service 可以:

  • 依照 Queue 的堆積程度,獨立決定要開幾個 Worker
  • 獨立部署,不會因為改了 Feed 的程式碼而被迫一起重新上線
  • 未來要換 Message Queue 的實作,也不會牽連到主要 API

拆開之後,Service 之間要怎麼溝通?

一旦 Notification 變成一個獨立的 Service,跑在自己的 Process、甚至自己的機器上,原本一行函式呼叫就能解決的事,現在得改成一次網路請求。

對外(Client 端)我們已經用 REST 溝通了,但如果 Service 之間內部溝通也用 REST + JSON,會有一些不太划算的地方:JSON 是文字格式,序列化/反序列化比較慢;而且 REST 沒有強制的 Schema,A 服務改了欄位名稱,B 服務可能要等到執行期才會發現出錯。

內部服務之間更常見的選擇是 gRPC

API Service ⇄ gRPC ⇄ Notification Service

拆分為 Microservices 並透過 gRPC 溝通的架構圖

gRPC 有幾個特性特別適合「內部」溝通:

  • Protocol Buffers(Protobuf):先用 .proto 檔定義好訊息結構跟型別,再產生對應語言的程式碼。跟 JSON 比起來,訊息是二進位格式,體積更小、解析更快,而且欄位型別在編譯期就會檢查,不會等到 Production 才發現「這個欄位應該是數字,結果傳了字串」。
  • 建立在 HTTP/2 上:支援同一條連線上多路複用(Multiplexing)多個請求,不像傳統 HTTP/1.1 那樣容易被單一個慢請求卡住連線。
  • 原生支援 Streaming:不只「一問一答」,也能做 Server 持續推送、雙向串流這類模式。

值得注意的是:gRPC 適合的是服務之間的內部溝通,不是用來取代 Day 4 討論的 REST(或 GraphQL)。

對外面對瀏覽器、App 的 API,仍然會維持 REST/GraphQL 這類對前端友善、方便除錯的格式;gRPC 負責的是 Server 與 Server 之間看不到的那一段。

Microservices 的代價

拆分不是沒有成本,把 Monolith 打散成多個 Service,等於把原本「函式呼叫」的問題,換成了「網路呼叫」的問題:

  • Network 不可靠:Timeout、Connection Failure、Latency 隨時可能發生,這也是為什麼前面要先講 Message Queue、Retry、Idempotency,這些正是應付 Service 間通訊失敗的工具。
  • 資料一致性變複雜:以前一個 Transaction 就能搞定的事,現在可能橫跨兩個 Service、兩個 Database,沒辦法再靠單一個 DB Transaction 保證全部成功或全部失敗。
  • 除錯變困難:一個 Request 可能經過好幾個 Service 才完成,出錯的時候,到底是哪一段壞了?這會讓後面要談的 Observability 變得不只是「加分項」,而是「必需品」。
  • 維運成本上升:每多一個 Service,就多一組要部署、監控、設定 Health Check、處理 Alerting 的東西。

什麼時候該拆?

回頭看,如果 PokeThreads 從第一天就拆成 Notification Service、Feed Service、Search Service……那我們大概還在處理跨服務 bug/debug,根本還沒空理會使用者真正在乎的功能。

比較實際的做法是:先讓系統用 Monolith 跑起來,隨著規模成長,持續觀察

  • 哪些功能已經有清楚的業務邊界?
  • 哪些功能需要獨立的擴展節奏?
  • 哪些功能的部署頻率跟其他部分明顯不同?

當答案越來越明確,再逐步、一個一個把它們拆出去,而不是一口氣把整個系統打散。

PokeThreads 拆出 Notification Service,正是因為它符合上面所有條件;Feed、Post、Follow 這些彼此高度相關的功能,暫時還是留在同一個 Service 裡比較划算。

小結

比較維度 Monolith Microservices
結構 單一程式碼庫,功能都在一起 拆成多個獨立部署的 Service
溝通方式 函式呼叫 網路請求(gRPC / Message Queue)
Scaling 整包一起放大 各自獨立擴展
除錯難度 相對單純 需要良好的 Observability
適合階段 早期、規模小 規模夠大、邊界夠清楚之後

PokeThreads 現在多了一個 Notification Service,也多了一種內部溝通方式(gRPC)。

但只要 Service 數量開始變多,馬上會冒出下一個問題:

Client 要怎麼知道該打哪一個 Service?


上一篇
Day 12 Message Queue 不讓客人等
系列文
系統設計就像九頭蛇:打造社群網站的 30 天13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言