🎯 什麼情境該想到我

當你要改資料格式/schema/API,又擔心「新舊版本同時在線」會壞掉時(滾動升級、多服務並存)。

⚙️ 怎麼用

  1. 顧好雙向相容:
    • 向後相容:新程式能讀舊資料。
    • 向前相容:舊程式能讀新資料(忽略不認得的欄位)。
      兩者都要,才能新舊版本共存、滾動部署。
  2. 選編碼格式:JSON/XML(人可讀、無 schema 演進保證)vs Protobuf/Avro/Thrift(緊湊、有明確的 schema 演進規則)。
  3. 加欄位用「可選 + 預設值」,別移除或改動既有欄位的標籤/型別。

★ 兩種相容性(方向很容易搞反)

向後相容 backward compatibility:★ 新程式能讀「舊」資料
  → 往回相容過去。新版服務上線後,還讀得懂資料庫裡的舊資料。

向前相容 forward compatibility:★ 舊程式能讀「新」資料
  → 往前相容未來。★ 這一條才是滾動部署的關鍵。

★ 為什麼向前相容最難也最重要:
滾動部署期間,新舊版本同時在跑。新版寫進去的資料,舊版必須讀得懂(至少不能崩)。
而「舊程式」是已經部署出去的,你無法修改它 —— 只能靠格式本身的設計。

★ 格式對照

格式自描述體積演進規則適合
JSON✅大★ 靠約定,沒有強制對外 API、設定、debug
Protobuf❌(要 .proto)小★ 欄位編號是契約內部 RPC、高吞吐
Avro❌(要 schema)最小★ 讀寫 schema 分離,靠欄位名匹配大資料、Kafka
MessagePack / CBOR✅中同 JSON想要 JSON 語義但更小

★ Protobuf 與 Avro 的差別值得知道:

  • Protobuf 用「欄位編號」:改名字沒關係,改編號就毀了
  • Avro 用「欄位名」+ 讀寫 schema 解析:★ 可以改編號(沒有編號),但需要 schema registry
    → Avro 特別適合「資料存很久、schema 一直演進」的資料湖場景

★ Protobuf 的演進規則(照做就安全)

message User {
  string id    = 1;
  string email = 2;
  // reserved 3;                 ★ 刪掉欄位時一定要 reserve,防止編號被重用
  // reserved "old_name";
  optional int32 age = 4;        ★ 新欄位一律 optional(或有預設值)
}
動作安全嗎
加一個 optional 欄位✅ 舊程式忽略它
改欄位名稱✅(編號才是契約)
★ 改欄位編號❌ 災難
★ 改欄位型別❌ 多數情況不安全(少數相容組合除外)
刪欄位⚠️ 要 reserved,且確認沒人在讀
加 required(proto2)❌ 舊資料沒有它 → 直接失敗。這就是 proto3 拿掉 required 的原因

★ 資料庫 schema 的演進:擴張—收縮(expand-contract)

「改欄位名」不能一步到位,要分三次部署:

① 擴張:加新欄位(可為 null),★ 程式「同時寫兩邊、讀舊的」
② 遷移:背景把舊資料補進新欄位;切換成「寫兩邊、讀新的」
③ 收縮:確認無人讀舊欄位後,停止寫舊的,最後才 DROP

★ 每一步之間都要能安全回滾。 一次做完 = 部署到一半就壞掉,而且回滾也壞掉。
這是零停機資料庫遷移的標準做法(見 工具-低風險發布模式)。

★ 其他高風險的 DDL:

操作風險
ALTER TABLE ADD COLUMN NOT NULL DEFAULT ...舊版 PG 會重寫整張表並鎖住(PG 11+ 已優化)
CREATE INDEX★ 會鎖表 → 用 CREATE INDEX CONCURRENTLY
ALTER COLUMN TYPE通常重寫整張表
加外鍵會驗證全表 → 先 NOT VALID 再 VALIDATE CONSTRAINT

★ Schema 放哪裡

做法評價
各服務各自複製一份 .proto❌ 一定會不同步
集中的 schema repo + CI 相容性檢查★ 推薦(buf breaking 可以在 PR 時擋下破壞性改動)
Schema Registry(Confluent 等)★ Kafka/Avro 生態的標準做法,執行期強制相容性

★ 關鍵不是「放哪裡」,是「有沒有自動化的相容性檢查」——
靠人記得不要改編號,遲早會出事。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★ JSON 沒有任何演進保證。它只是「舊程式傾向忽略不認得的欄位」——
    但很多解析器預設會報錯(Go 的 json.Decoder.DisallowUnknownFields、zod 的 strict mode)。
    ★ 對外 API 用 JSON 時,要明確在文件裡寫「客戶端必須忽略未知欄位」。
  • ★ 「忽略未知欄位」有一個陷阱:舊程式讀進來、改了一個欄位、寫回去 →
    它不認得的欄位就被吃掉了。這是「讀-改-寫」場景下的靜默資料遺失。
    → Protobuf 有 unknown fields 保留機制;JSON 要自己處理(保留原始 map)。
  • ★ 資料存越久,演進規則越重要。
    RPC 的資料活幾毫秒(雙方版本接近),資料庫與資料湖的資料活好幾年(要能被未來的程式讀懂)。
    保存期限越長,越該用 Avro/Protobuf 這種有明確規則的格式。
  • 不要用語言內建的序列化(Java Serializable、Python pickle、Go gob 跨版本)——
    綁死語言、有安全漏洞、演進規則不明。
  • 列舉(enum)的演進很麻煩:舊程式遇到新的 enum 值怎麼辦?一定要有 UNKNOWN = 0 的預設值。
  • 加欄位是安全的,但「加一個必填欄位」不是。必填等於破壞相容性。

🔗 相關工具