DDIA 導讀 CH5 — 編碼與演化 (下)
2026年6月28日
在 DDIA 導讀 CH5 — 編碼與演化 (上) 我們談了第五章節前半段關於編碼格式的比較,從 JSON、XML,一路到 Protocol Buffers 和 Avro。如前半部談到的,我們之所以需要編碼與解碼,是因為當資料離開目前程式的記憶體,就需要被轉換成其他的程式能夠讀取的形式。當資料進到另一個伺服器、被寫入資料庫,或者被放到訊息佇列,在這些不同的地方,都要能夠解讀該資料,所以格式的轉換很重要。
不僅是不同系統的格式要統一,當系統改版時,新系統跟舊系統如果沒有統一,就會有相容性的問題。因此,在做資料格式演化時,需要同時考慮向前相容 (舊程式要能處理新版資料) 與向後相容 (新程式要能讀懂舊資料),這樣才能確保不同版本的程式,在面對資料時都能有效應對。
前半部談了不同的格式,後半部作者則進一步討論資料實際會流動過的地方,從資料庫流動、不同的服務、工作流程引擎,以及非同步訊息的訊息傳遞。在章節最後,作者也談了事件驅動架構 (event-driven architecture) 的資料流動方式。
資料庫需要相容性
很多人在看資料庫時,會把資料庫當成單純儲存的地方,但如果換個角度看,資料庫其實類似一個資料的中繼站。對於不同服務的程式來說,會在某個時間點把資料留在資料庫,然後在未來的另一個時間點讀取。在讀取時,可能是新版本的程式來讀取,因此先前談到的相容性,對於資料庫來說很重要。
舉例來說,假如資料庫沒有做到向後相容,未來新版的程式碼就讀不懂舊版本程式碼寫下的資料;這會導致新版本部署後,舊的資料讀不出來,從軟體的角度來看,這是相當嚴重的事故。
此外,在現代的資料系統中,同時多個不同的服務對資料庫發讀寫請求,並不是罕見的狀況。而這些服務的實例,可能會因為需要擴容所以要做實例升級;在升級時多半會是漸進式,先升一台,完成後再升下一台,逐步把舊版本換掉。這時就可能出現一個微妙的狀況,已經升級的實例寫入新版本的資料格式,而還沒升的舊實例也還在運行所以會碰到新資料。在這種狀況下,就必須透過向前相容來確保能順利運行。
例如原本訂單系統有 pending、paid,以及 cancelled 三種狀態,後來支援退款後加入 refunded。這時新版服務開始寫入 refunded,還沒升級的舊服務不認識這個狀態,處理起來就會出錯。這時如果有做好向前相容,能夠避免系統因為無法處理而直接崩潰。
除了這種狀況,在資料庫中也很常會有不同時間點寫入的資料。因此可能有某筆剛寫入的資料,同時也有另一筆多年前寫入的。而這些不同時期寫入的資料,可能因為版本不同而有問題 (例如上面的訂單系統的例子)。
要處理這類問題,可以透過資料遷移,一次性把舊資料轉換成新格式。但在資料量大的狀況下,遷移的成本會很高。如果要避免這種全量重寫的狀況,透過上述提到的預設 null 來處理,會是更簡便的方法。當然,在比較複雜的變更,可能就需要從程式層級來處理資料遷移,來確保資料能被順利處理。
客戶端與伺服器端如何溝通
除了資料庫可能需要跟多個伺服器溝通外,在現代的系統架構中,往往會有伺服器與客戶端溝通,以及伺服器之間彼此溝通的需求。在這些不同端點溝通的過程,也會需要確保資料傳遞的格式是彼此能理解的。
舉例來說,在網頁開發中,瀏覽器是客戶端,網站伺服器則是伺服器端。當使用者進到網頁中,瀏覽器會向伺服器發送 GET 請求,來拿到 HTML、CSS、JavaScript、圖片等靜態資源,以渲染網頁的內容並讓網頁能夠互動。
而在一個微服務架構中,某台伺服器可能會是另一個伺服器的客戶端。例如訂單服務會呼叫付款微服務,這時訂單服務對付款服務來說,是客戶端的角色。除了內部架構外,也很常會有呼叫第三方服務的狀況,例如後端系統呼叫第三方金流 API,這時自家的後端系統就是扮演客戶端的角色。
在網頁開發中,客戶端會透過 API 來向伺服器端發送請求,而在眾多形式中,最熱門的莫過於 REST 的設計理念。REST 經常搭配 HTTP 使用,把 URL 當作資源表示,搭配 HTTP 方法 (例如 GET 與 POST 等等),同時用標準的 HTTP 功能處理快取、內容類型協商等議題。
不過即使用了 REST,客戶端在發送請求時,仍需要知道很多細節。例如每個端點用什麼 HTTP 方法、請求時要帶什麼、伺服器端回應長什麼樣。對此,開發者之間會通過介面定義語言 (IDL) 來描述 API。我們可以把介面定義語言理解成 API 的規格書,在 REST/HTTP API 中常見的是 OpenAPI;在 RPC 類型的 API 中,常見的是 gRPC 搭配 Protocol Buffers 的 .proto 檔描述服務介面。
舉例來說,書中提供了以下的 OpenAPI 範例,在下面的範例中定義了 API 的名稱叫 Ping, Pong ,在本地伺服器 http://localhost:8080 中有個叫 /ping 的端點,可以用 GET 來呼叫,成功時會回傳某個 JSON 格式的物件,內容會帶有一個 message。
openapi: 3.0.0
info:
title: Ping, Pong
version: 1.0.0
servers:
- url: http://localhost:8080
paths:
/ping:
get:
summary: Given a ping, returns a pong message
responses:
"200":
description: A pong
content:
application/json:
schema:
type: object
properties:
message:
type: string
example: Pong!
透過這種定義格式,開發者彼此溝通起來就會容易許多。現在也有許多框架,可以根據介面定義語言,來自動化產生相對應的型別驗證,讓開發者只需要透過一個指令,就可以直接拉取並自動化更新,不擔心在呼叫 API 時會出錯。
遠端程序呼叫 (RPC) 的問題
當談到客戶端與伺服器端溝通,就不能不提 1970 年代就存在的遠端程序呼叫 (RPC) 的作法。這種作法是讓對遠端的程序呼叫,變得像本地的函式一樣。這種把分散式系統包裝成像本地的普通程式,能讓程式碼看起來很簡單,但作者也提醒,由於網路呼叫與本地函式仍有區別,所以需要特別小心。
RPC 很容易讓人忘記自己正在跨網路發請求,而請求中間一旦隔著網路,就有可能出現各種失敗的狀況,也會需要面對延遲、資料與版本相容的問題。而這中間出的問題,許多可能不在自己的控制範圍,因此需要在寫程式時特別注意可能出現的例外狀況,然後在程式碼中做相關的防禦。
舉例來說,因為有網路斷線的可能,所以程式碼中要有重試邏輯;而當程式碼中有重試邏輯,就必須要考量重複可能造成的重複執行問題 (備註:我們在 API 設計 — 如何設計穩定可預測的 API 一文有詳談)。
從作者的觀點來看,把遠端服務當成本地物件的意義不大,因為本質上來看是不同東西。不過這不代表就不要用 RPC。以我們的經驗來看,像是 gRPC 等工具仍是非常好用,只是如作者所述,在用的時候不要忘記背後是通過網路做請求,所以在程式碼中要明確地處理超時、重試、版本相容等狀況。
加入 E+ 會員方案
對更深入了解這個主題,以及其他前後端開發、軟體工程、AI 工程主題感興趣的讀者,歡迎加入 E+ 一起成長 (連結)。