跨邊界的語意流失,與 monolith 一直有吸引力的原因

每次接手一個拆得很細的系統,過一陣子都會冒出同一句抱怨:明明每個服務都寫得不差,組起來卻像一坨難動的東西。直覺會去怪某個寫爛的 service,但多數時候根因不在那。真正掉的東西是語意,它在跨服務邊界的那一刻被壓扁了。

邊界把語意壓成 bytes

在同一個 process 裡,一筆 Order 是一個有型別的東西。它的欄位、不變條件、跟 Payment 的關係,全都活在編譯器看得到的範圍內。你改了它的結構,工具會立刻告訴你哪幾個 caller 跟著爆。重構、改名、把一個欄位從 optional 改成 required,這些動作之所以敢做,是因為語意有人幫你盯著。

跨過服務邊界,這個世界就消失了。Order 出門前會被序列化成 JSON、protobuf 或某種 wire format,到對面再被反序列化回去。中間那段傳輸,它就是一坨 bytes,不帶任何意義。對面服務怎麼解讀這坨 bytes,靠的是一份隱含契約:可能是一份不一定同步的 OpenAPI、一段 README、或某個工程師三年前在 Slack 講過的話。型別系統在這條線上完全使不上力,它看不到對面,對面也看不到它。

於是所有原本由工具自動扛住的事,全退回成人工。改一個欄位名,編譯器不會報錯,下游要等到 runtime 才在某筆資料上炸開。想知道某個欄位還有沒有人在用,沒有 IDE 的 find usages,只能去翻 log、問人、賭一把。遷移一個資料結構,不再是一次有把握的重構,而是一連串跨團隊協調加上線上小心翼翼地補洞。語意沒有消失,只是從工具能驗證的地方,搬到了只有人腦能追的地方。

monolith 的吸引力不是懶

理解了這件事,monolith 為什麼一直有人想回去,就不是什麼神秘現象。它不是技術上偷懶,也不是不懂分散式。monolith 真正給你的,是讓語意一直活在同一套型別系統跟同一條工具鏈裡。

在一個 monolith 裡,從 HTTP handler 一路到資料層,Order 都還是那個 Order。改它的定義,編譯器陪你走完每一個 caller;想拔掉一個欄位,find usages 告訴你還有誰在碰;要把一段邏輯搬家,IDE 的 refactor 一鍵到位。這些不是小確幸,是大型系統還能持續演進的根本。系統會老化,不是因為它大,而是因為你逐漸不敢動它。monolith 把「敢動」這件事的成本壓得很低,因為工具一直站在你這邊。

這也解釋了為什麼有些團隊拆成 microservices 之後反而更慢。他們以為買到的是獨立部署跟團隊自治,實際上付掉的,是原本白白享有的那層語意驗證。每一條服務邊界,都是一個工具不再幫你把關的地方。邊界開得越多,落回人工協調的面積就越大。一個 well-factored 的 monolith,常常比一套邊界亂畫的 microservices 好維護得多,差別就在語意還在不在工具的射程內。

拆之前先問值不值

講這些不是要你別拆服務。有些邊界該拆,理由很硬:團隊要獨立部署不想互相卡、某段工作負載的 scaling 特性跟其他人完全不同、某塊東西的故障必須隔離不能拖垮全站。這些都是好理由,該拆就拆。

重點是順序。拆之前先直說一句:這條邊界,要我犧牲多少語意換?如果一條邊界把兩個高度耦合、天天一起改的東西切開,你等於是把編譯器原本幫你頂住的工作,整碗倒回給跨團隊的人工協調,這筆帳通常不划算。

就算決定要拆,語意也不是只能認賠。它可以靠 shared model 接回來一部分:跨邊界共用的 schema、生出多語言型別的 IDL、還有契約測試,讓 provider 一改壞掉原有結構,CI 就先替 consumer 喊痛,而不是等 production 的某筆資料去發現。這些東西不會讓跨服務變得跟同 process 一樣安全,但它們是在邊界上重新長出一點工具支援,把語意從純人工的深淵拉回來一些。差別在於,你是有意識地在補這層,還是假裝邊界不用成本。

所以邊界該畫在哪?畫在語意損失最小的地方,不是組織圖上最好看的地方。沿著耦合最弱、契約最穩定的那條縫去切,跨邊界要交換的意義就少,工具接不住的部分自然也少。反過來,如果一條邊界只是為了對齊團隊結構或讓架構圖好看,卻硬把兩塊在語意上難分難捨的東西拆開,你買到的不是模組化,是一套永遠在補洞的分散式系統。

重點整理

  • 大型系統難組合,根因常是語意在跨邊界被壓成 bytes 與隱含契約,不是某個元件爛。
  • 過了服務邊界,型別系統就失效,驗證、重構、遷移全退回人工,錯誤被推遲到 runtime。
  • monolith 的吸引力不是偷懶,是語意還活在同一套工具鏈裡,工具能幫你驗證與重構。
  • 拆服務前先問這條邊界值得犧牲多少語意;shared schema 與契約測試能接回一部分。
  • 邊界要畫在語意損失最小的地方,沿最弱的耦合縫切,不是照組織圖好看的地方切。

觀點 Sheng,內文 Claude 協助 · 列入 20260613 blog 翻新計劃,新漆未乾。