【從一台 Server 到分散式架構】第 11 篇:單體開始痛苦——一改 code 全站都要 deploy

小明他們的線上課程平台,現在已經跨出了台灣,變成一個橫跨美、亞兩洲的全球化系統了。

為了應付全球流量,他們在上一篇(第 10 篇)討論了跨區的 CAP 取捨:在台灣部署了 Primary DB,在美國部署了資料複本(Replica),並且根據業務重要性決定了 CP(強一致)與 AP(最終一致)的策略。在「基礎建設」與「資料一致性」上,這套系統已經相當成熟。

但在開發效率部署穩定性上,小明和他的團隊卻開始感到前所未有的痛苦。

原因很簡單:雖然基礎設施已經分散在全球各區,但核心的後端程式仍然是一個巨大的 單體架構(Monolith)

🏗️ 本篇開始前的架構 — 全球部署的「單體大程式」

雖然我們在多個地區部署了伺服器和資料庫,但每個地區跑的「後端 API」仍然是同一份巨大的程式碼。


什麼叫「單體痛苦」?

一開始,小明只有三、四個工程師。大家改同一份 code、跑同一個 CI/CD 流程,覺得很方便。但當團隊成長到二十人、三十人,負責的功能切成「使用者組」、「課程組」、「金流組」時,問題就來了:

1. 「一帶全」的部署壓力(All or Nothing)

金流組只是想改一個退款的小 bug,理論上只需要動到幾行 code。但因為所有功能都包在一起,他們必須部署整個網站

  • 每次 deploy 都要把整包幾百 MB 的程式重傳、重新編譯。
  • 部署時間從幾分鐘變成幾十分鐘,開發效率大幅下降。

2. 「連環爆」的連帶風險

某天課程組改壞了一個「取得推薦列表」的功能,沒想到這個邏輯被包裹在全站通用的 Utils 裡,結果導致連登入功能都壞掉。 在單體架構中,所有模組都在同一個 Process 裡跑,記憶體與 CPU 是共用的。一個模組發生記憶體洩漏(Memory Leak)或無窮迴圈,整台 Server 就會一起掛掉。你很難做到「局部隔離」

3. 程式碼的「萬能醬汁」(Big Ball of Mud)

隨著時間過去,課程、訂單、使用者、評論……這些邏輯開始互相依賴。

  • Order 類別裡直接呼叫了 CourseService 的私有 method。
  • User 類別被傳來傳去,到處都是 side effect。 邊界越來越模糊,新進工程師根本不敢改 code,因為不知道動了這裡會不會噴掉那裡。這就是俗稱的「大泥球(Big Ball of Mud)」。

生活化的比喻:大廚房 vs 獨立工作室

我們之前都用「廚房」來比喻後端,現在我們來看看廚房變大的問題。

單體架構就像一間超級巨大的廚房。這裡有幾十個廚師,有的烤麵包、有的炒青菜、有的切刺身、有的煮熱湯。

  • 改 code 的痛苦:如果你想把烤箱換掉(改金流邏輯),整間廚房必須停工清場,所有廚師都要停下手邊動作等烤箱換好。
  • 連環爆的風險:炒青菜的廚師不小心失火了,因為大家都在同一個開放空間,濃煙會立刻燻到刺身區和麵包區,導致整間餐廳今天都不能營業。
  • 管理的混亂:大家共用同一個洗碗槽、同一個冰箱。有人把生魚片放在甜點區,有人把洗手水噴到湯裡。

微服務架構則是把這間大廚房拆掉,變成一整條街上的多個專門工作室

  • 有專門的麵包店、專門的熱炒店、專門的壽司店。
  • 麵包店想換烤箱?他們自己關門半天就好,隔壁的熱炒店照樣可以炒餐。
  • 熱炒店失火了?只要門關好,不會影響到其他店。
  • 每間店有自己獨立的冰箱(資料庫)和領班。

為什麼現在才要拆?

你可能會問:「既然單體這麼痛苦,為什麼小明一開始不直接做微服務?」

這就是系統設計最重要的觀念:微服務不是免費的午餐。

拆分服務會帶來極高的維運複雜度

  • 你要維護多個 CI/CD 流程。
  • 你要處理服務之間的通訊(Network I/O 代替了 Function Call)。
  • 出問題時,你要追查這筆請求到底是卡在麵包店還是熱炒店(分散式追蹤)。

單體架構是「好開發、難擴大規模」;微服務是「難開發、好擴大規模」。 只有當你的團隊規模和流量大到「單體帶來的開發阻礙」已經超過「維運微服務的成本」時,拆分才是有意義的。


接下來的挑戰

小明團隊意識到,他們不能再讓「全站 deploy」成為日常。他們決定開始挑選最獨立的功能,嘗試切出第一個「微服務」。

🏗️ 本篇結束後的架構 — 識別出高度耦合,準備切分

雖然目前物理部署(台灣/美國)沒變,但我們在意識上已經要把 Monolith 內部的「功能群」標示出來,準備進行手術。

要把這些「黏在一起」的功能分開,並不是把程式碼剪剪貼貼到新專案就好,小明他們馬上就面臨了幾個實務上的「大坑」:

  1. 資料庫的「連理枝」:目前的單體共用同一個資料庫、同一個 Schema。如果把課程邏輯拆出去,那課程資料表(Course Table)要不要也搬走?如果搬走了,訂單要查詢課程名稱時,該怎麼做 Join?
  2. 呼叫方式的改變:在單體裡,A 功能找 B 功能只是內部的 Function Call,速度極快且保證成功。拆開後,變成跨網路的 HTTP CallgRPC。網路可能會斷、可能會慢,小明必須開始處理「逾時(Timeout)」與「重試(Retry)」的問題。
  3. 交易(Transaction)的遺失:過去「下單 + 扣課程配額」可以在同一個資料庫 Transaction 裡完成,要嘛一起成功,要嘛一起失敗。拆成兩個服務後,如果錢扣了但課程配額沒扣到,該怎麼辦?

劃分服務邊界(Service Boundary),並且在拆分後還能讓各個工作室順暢溝通,就是小明下一個階段要修煉的課題。


小結與預告

今天我們聊了單體架構在系統長大與團隊擴編後的三大痛苦:

  1. 部署壓力:一改 code 就要整站重新 deploy。
  2. 風險連帶:一處出錯,全站掛掉,缺乏故障隔離。
  3. 代碼耦合:邊界模糊,變成牽一髮而動全身的「大泥球」。

雖然微服務是解藥,但它也是一劑強效藥,伴隨著昂貴的維運代價。 下一篇,我們將跟著小明拆出第一個服務,看看「API Gateway」這個關鍵帶位員如何登場。