🎯 什麼情境該想到我
當你要改資料格式/schema/API,又擔心「新舊版本同時在線」會壞掉時(滾動升級、多服務並存)。
⚙️ 怎麼用
- 顧好雙向相容:
- 向後相容:新程式能讀舊資料。
- 向前相容:舊程式能讀新資料(忽略不認得的欄位)。
兩者都要,才能新舊版本共存、滾動部署。
- 選編碼格式:JSON/XML(人可讀、無 schema 演進保證)vs Protobuf/Avro/Thrift(緊湊、有明確的 schema 演進規則)。
- 加欄位用「可選 + 預設值」,別移除或改動既有欄位的標籤/型別。
★ 兩種相容性(方向很容易搞反)
向後相容 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的預設值。 - 加欄位是安全的,但「加一個必填欄位」不是。必填等於破壞相容性。
🔗 相關工具
- 工具-低風險發布模式 —— 擴張—收縮與零停機遷移
- 工具-共享庫版本與相容性 —— 同一套相容性思維用在程式碼上
- 工具-部署管線與持續交付 —— 相容性檢查該在 CI 裡跑
- 工具-服務邊界與演進式拆分 —— 服務邊界就是 schema 邊界
- 工具-型別推導還是解耦 —— 從 schema 推導型別(
z.infer、sqlc) - 命令模式 —— 序列化的命令需要版本相容
- 工具-Go嵌入機制 —— 嵌入對 JSON 序列化的影響
- 設計資料密集型應用