《Designing Data-Intensive Applications(DDIA)》Podcast 準備稿:編碼與演進
書名: 設計資料密集型應用 Designing Data-Intensive Applications(The Big Ideas Behind Reliable, Scalable, and Maintainable Systems) 作者: Martin Kleppmann 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|DDIA.資料活得比程式碼久:編碼與相容性 (4/12) 涵蓋範圍: 第5章 編碼與演進(JSON/XML、Thrift/Protobuf/Avro、前後向相容、資料庫/REST-RPC/訊息傳遞三種資料流)
背景速覽
這是 Part I 的收尾,也是一個「常被低估、實戰上卻天天踩雷」的主題:資料怎麼編碼成位元組,以及系統演化時新舊版本怎麼共存。大型系統不可能一次升級到位——伺服器滾動升級、客戶端看使用者心情更新,於是新舊版本的程式碼與資料格式必然同時存在。這集講清楚兩個方向的相容性,以及三種資料流模式各自的相容性考量。這章是「微服務時代的生存技能」。
一句話重點
資料活得比程式碼久——程式碼幾分鐘就能全部換掉,但資料庫裡躺著五年前和五毫秒前寫入的資料;所以真正的功夫不在「選哪個編碼格式」,而在維護雙向相容:向後相容(新 code 讀得懂舊資料)和向前相容(舊 code 讀得懂新資料、且不會把不認識的新欄位搞丟)。
值得討論的重點
1. 兩個方向的相容性,先把名詞釘死
- 向後相容(backward):新版程式碼能讀舊版寫的資料。通常較容易——你知道舊格式長怎樣。
- 向前相容(forward):舊版程式碼能讀新版寫的資料。較棘手——舊 code 得「優雅地忽略」它不認識的新東西。
整章所有討論都繞著這兩個詞。記住:滾動升級期間,這兩個方向你都需要,因為新舊節點同時在線、同時讀寫。
2. 語言內建序列化是陷阱,文字格式有暗坑
Java 的 Serializable、Python 的 pickle 方便但有毒:鎖死單一語言、解碼可實例化任意類別(遠端執行攻擊面)、版本控制薄弱、效能差。只適合極短暫用途,絕不可拿來持久化或跨服務。
JSON/XML/CSV 跨語言、人類可讀,但有微妙坑:JSON 不分整數浮點、大於 2^53 的整數在 JS 會掉精度(Twitter 因此 API 同時給數字與字串兩種 tweet ID)、不支援二進位字串(Base64 膨脹 33%)。不過——作為「組織之間」的交換格式,它們仍然無敵,因為「讓不同組織同意用同一格式」本身就是最大的挑戰。
3. Thrift / Protobuf / Avro:用 schema 換掉欄位名
組織內部可以用更緊湊的二進位編碼。核心思想:用 schema 定義取代在編碼裡嵌入欄位名。Thrift(Facebook)和 Protobuf(Google)的關鍵設計是欄位標籤(field tag)——schema 裡的數字(1, 2, 3),編碼後只存標籤號不存欄位名,資料大幅縮小。
演化規則的精髓:
- 可改欄位名(編碼裡只有標籤號)、不可改標籤號(會讓所有舊資料失效)。
- 新增欄位給新標籤號,舊 code 遇到不認識的標籤直接跳過(向前相容);新欄位必須 optional 或有預設值(否則新 code 讀舊資料會因缺欄位而爆,破壞向後相容)。
4. Avro 的精妙:writer’s schema 與 reader’s schema 不必相同,只要相容
Avro(為 Hadoop 而生)跟前兩者最大的不同:schema 裡沒有標籤號,編碼後只是值的連續排列,最緊湊(同一筆記錄只要 32 bytes)。代價是解碼必須知道寫入時用的 schema。
它的核心設計是 writer’s schema vs reader’s schema——兩者不必相同,Avro 會自動比對、轉換差異(靠欄位名匹配、忽略多的、補預設值給少的)。這讓 Avro 特別適合動態生成 schema:把關聯式資料庫匯出成 Avro 時,schema 一變就自動重產,不用人工維護標籤號對應。
5. 三種資料流模式,各有相容性考量
資料從一個程序流到另一個,三種路徑:
- 透過資料庫:寫者編碼、讀者解碼。最大的坑是舊 code 讀到含新欄位的記錄、更新後寫回時把新欄位弄丟——應用層要記得保留未知欄位。
- 透過服務(REST / RPC):REST 適合公開 API(好除錯、生態豐富),RPC 框架(gRPC)適合組織內部高效通訊。RPC 的根本問題:它想讓網路呼叫看起來像本地呼叫,但網路會不可預測地失敗、超時、重試導致重複——這個抽象本質上是漏的。
- 透過非同步訊息(message broker):介於 RPC 和資料庫之間,靠 broker 暫存、解耦發送與接收。只要編碼格式支援前後向相容,publisher 和 consumer 就能獨立升級。
注意事項
⚠️ 「向前相容」是最容易被忽略、卻最常出事的方向。 大家寫 migration 時習慣想「新 code 怎麼讀舊資料」(向後),卻忘了滾動升級時舊 code 還在線、正在讀新 code 寫的資料。那個「舊 code 讀到不認識的新欄位、更新後寫回卻把它弄丟」的 bug(書中 Figure 4-7),是實戰最常見的資料遺失來源之一。這條一定要重點講。
⚠️ RPC 不是「讓遠端呼叫像本地呼叫」那麼美好。 位置透明性(location transparency)是個誘人但有缺陷的抽象。網路請求會超時且「你不知道對方到底處理了沒」,所以重試需要冪等性保護。這個「冪等性」會在後面講分散式系統、講串流容錯時反覆出現——這裡先埋下種子。
⚠️ 別為了「最緊湊」就無腦選 Avro。 編碼格式的選擇要看場景:Avro 適合 Hadoop、適合 schema 動態生成;Protobuf 在靜態型別語言、需要程式碼生成時很順;而跨組織的公開介面,老老實實用 JSON 反而最不會出錯。緊湊度不是唯一指標,相容性語意的清晰度和生態才是。
專家補充
💡 這一章其實在講「微服務能獨立部署」的底層前提。 微服務的核心賣點是「每個服務獨立部署、獨立演化」——但這只有在「服務間的資料編碼跨版本相容」時才成立。所以這章看似在講編碼,實則在講微服務架構的可行性條件。沒有前後向相容,所謂的獨立部署只是災難的前奏。這條直接接到系統設計域的服務拆分主題。
💡 「schema 演化提供了 schemaless 的彈性,又多了保證與工具。」 這是章末的關鍵句。很多人以為要彈性就得放棄 schema(用 JSON 隨便塞),DDIA 反駁:基於 schema 的二進位格式(Thrift/Protobuf/Avro)一樣能改結構(加 optional 欄位),但額外換來:更緊湊、schema 即文件(永遠與實際格式一致)、部署前可驗證相容性、靜態語言可程式碼生成。「彈性」和「保證」不是只能二選一。
💡 會計式的「資料壽命超越程式碼」是一個深刻的心智模型。 Schema 演化讓整個資料庫「看起來像是用單一 schema 編碼的」,即使底層躺著各個歷史版本。這個「用相容性把時間差異抹平」的思想,到第11、12集講 event sourcing、不可變事件日誌時會升級成更大的架構哲學——真相是日誌,當前狀態只是日誌的快取。
討論問題
🎙️ 開場鉤子:「你有沒有遇過——上線一個新版本,結果舊版本的服務開始默默把資料寫壞?這不是 bug 寫錯,而是你沒處理好一件事:新舊程式碼,正在同一個時刻讀寫同一批資料。」
🎙️ 自問自答:「為什麼 gRPC 再快、再方便,也救不了『網路呼叫不等於本地呼叫』這件事?」用 RPC 的漏抽象回答:本地呼叫穩定可靠、遠端呼叫會超時且你不知道對方做了沒——把這兩件事假裝成一樣,遲早會在重試時把資料弄重複。
🎙️ 帶走的一題:「下次你改資料庫 schema 或改 API 時,別只問『新版本讀得懂舊資料嗎』,要多問一句——『升級到一半時,還活著的舊版本,讀得懂新資料、而且不會把它弄丟嗎?』」
更大範圍關聯
- 與 Building Microservices(Sam Newman)的分工:Newman 講「服務怎麼拆、團隊怎麼配」,DDIA 這章講「拆開之後,服務間的資料怎麼才能各自獨立演化」。前者是組織與邊界,後者是讓邊界成立的技術前提。 兩本互補。(見 [軟體工程領域切分藍圖] §5「軟體架構演進」)
- 承上啟下:這集收掉 Part I(單機資料系統)。下一集進入 Part II(分散式資料),第一個主題就是複製——而複製傳的東西(replication log)正是這集講的「編碼後的變更流」。Part I 是地基,Part II 開始蓋分散式的高樓。
- 思想連線:「資料活得比程式碼久」這句,是貫穿全書後半的暗線。它在這裡是相容性問題,到 Part III 會升華成「不可變事件日誌才是系統的真相」——同一個洞見,越講越大。
錄製建議
- 建議時長:約 20–22 分鐘。配比:兩種相容性+文字格式的坑約 6 分;Thrift/Protobuf/Avro 與演化規則約 8 分(主菜,但別陷進編碼細節,講「為什麼這樣設計」就好);三種資料流模式約 6 分。
- 討論策略:這章容易講得太技術、太枯燥。對策是緊扣「實戰會踩什麼雷」:開場用「上線新版本卻把舊資料寫壞」的痛感,中段把演化規則翻譯成「加欄位要 optional、別改標籤號」這種可操作的口訣,結尾把它拉高到「這就是微服務能獨立部署的前提」。
- 不要在節目裡唸二進位編碼的 byte 細節(59 bytes vs 34 bytes 那些),那是書上看的;口頭只要講「Avro 最緊湊、靠的是連欄位名都不存」這個結論性的對比就夠了。