Schema 會隨需求改變,但你不可能讓所有節點同時升級。相容性就是「新舊版本的程式碼與資料能否互通」的契約。核心洞見:資料比程式碼活得久,所以 schema 必須能向前向後演化。
🧠 Intuition
TIP
別記方向,記角色:相容性永遠是「誰來讀誰寫的資料」。向後相容 = 新程式讀舊資料(新人讀得懂前輩留的東西);向前相容 = 舊程式讀新資料(前輩讀得懂新人寫的、自動略過看不懂的部分)。
- 向後相容 (Backward):新版程式碼能讀舊版寫的資料。通常自然成立——新欄位舊資料沒有,給預設值即可。
- 向前相容 (Forward):舊版程式碼能讀新版寫的資料。較難——舊程式必須忽略它不認識的新欄位而不報錯。
- 欄位是契約:新增欄位必須 optional 或有預設值;移除欄位只能移 optional 欄位;識別子(tag / 名稱)不可重用。
- 資料比程式碼活得久:DB 裡的舊 bytes 可能是五年前的 schema 寫的,今天的程式仍得讀懂——這逼著相容性成為硬需求。
⚖️ Tradeoffs
向後相容 — 新讀舊
- 場景:你部署了 v2 程式,但 DB 裡還有 v1 寫的舊紀錄。
- 規則:新增欄位若 reader 期望但舊資料沒有,必須有預設值可補;不能新增「沒預設值的必填欄位」。
- 通常較容易達成,因為新程式作者知道舊 schema 長什麼樣。
向前相容 — 舊讀新
- 場景:v2 程式已開始寫新欄位,但還沒升級的 v1 程式得讀這些紀錄。
- 規則:舊程式必須安全略過不認識的欄位(Protobuf 靠未知 tag 略過、Avro 靠 schema resolution 略過 reader 沒有的欄位)。
- 較難,因為「未來的欄位」舊程式無從預期——格式本身得設計成可略過未知資料。
欄位增刪規則
- 新增:只能加 optional / 有預設值的欄位(同時保住向前與向後相容)。
- 刪除:只能刪 optional 欄位;刪必填欄位會讓舊資料 / 舊程式爆炸。
- 識別子 的 field tag、Avro 的欄位名一旦用過就不可改用途——改了等於把舊資料對應到錯欄位。
- 改型別,僅限可安全轉換者(如 int32 → int64),且可能損精度。
Avro 的 schema resolution
- Avro 解碼時同時看 writer schema(寫入時)與 reader schema(讀取時),逐欄位對齊。
- reader 有、writer 沒有的欄位 → 用 reader schema 的預設值補(撐起向後相容)。
- writer 有、reader 沒有的欄位 → 直接略過(撐起向前相容)。
- 因此 Avro 把「兩種相容性」都收斂成同一套 resolution 規則,但前提是 reader 必須能拿到 writer schema。
🔑 Takeaways
- 記角色不記方向:向後 = 新讀舊、向前 = 舊讀新;向前通常較難(要能略過未知欄位)。
- 安全演化的鐵則:新增只加 optional / 有預設值的欄位,刪除只刪 optional 欄位,識別子不可重用。
- Rolling upgrade 兩種相容性都要,因為新舊程式同時在線、互讀對方寫的資料。
- 根本理由是資料比程式碼活得久——schema 演化不是選配,是長壽資料系統的硬需求。
2026-07-01
- Q: rolling upgrade 為什麼向前、向後相容兩種都要?
- A: 因為滾動升級期間,新舊版本程式同時在線。已升級的新節點會讀到舊節點寫的紀錄(需要向後相容:新讀舊),而還沒升級的舊節點也會讀到新節點寫的紀錄(需要向前相容:舊讀新)。任何一個方向破了,升級窗口內就會有節點讀資料報錯。這跟「一次性全停機升級」不同——後者只需向後相容。也因此資料庫場景更嚴苛:DB 裡同時躺著好幾代 schema 寫的資料,程式必須對所有歷史版本都向後相容。