ข้ามไปยังเนื้อหา

Evolving Schemas

ในระบบจริง client กับ server deploy คนละเวลา server ใหม่จะได้รับ message จาก client เก่า และ server เก่าจะได้รับ message จาก client ใหม่ schema ของคุณต้องทำให้ ทั้งสองทิศทางยังทำงานได้ — นั่นคือ backward และ forward compatibility และ protobuf ถูกออกแบบมาให้ทำได้ถ้าคุณทำตามกฎ

flowchart TB
  newc["client ใหม่"] -->|field เพิ่ม| olds["server เก่า
(ข้าม field ที่ไม่รู้จัก)"]
  oldc["client เก่า"] -->|ไม่มี field ใหม่| news["server ใหม่
(อ่าน default)"]
schema change ต้องรอดใน fleet ที่ปนกัน

กลไกที่ทำให้เป็นแบบนี้ได้คือสิ่งที่คุณรู้อยู่แล้ว: field ถูกระบุด้วย number server เก่าที่เจอ field number ที่ไม่รู้จักก็แค่ข้ามไป ส่วน server ใหม่ที่ไม่ได้รับ field ที่คาดหวังก็อ่านค่า default การระบุ field ด้วย number ไม่ใช่ชื่อ คือสิ่งที่รักษาความสงบไว้

พวกนี้ไม่มีทางทำให้ client ที่ deploy แล้วพัง:

  • เพิ่ม field ใหม่ ด้วย number ที่สดใหม่ไม่เคยใช้ code เก่าจะข้ามไป ส่วน code ใหม่อ่านค่าได้ (และได้ default เมื่อไม่มีค่ามา)
  • ลบ fieldตราบใดที่คุณ reserved number นั้นไว้ (ดูด้านล่าง) client เก่าที่ยังส่งมาก็ถูกข้ามไปเฉย ๆ
  • rename field wire ถือ number ดังนั้นชื่อเปลี่ยนได้ฟรี
  • เพิ่ม value ใหม่ให้ enum หรือ field ใหม่ให้ oneof client เก่ามอง unknown เป็น unknown
  • เพิ่ม RPC method ใหม่ ให้ service เดิม ตัว client ที่มีอยู่ไม่ได้เรียก method ใหม่นั้น ไม่มีอะไรพัง

พวกนี้ compile ผ่านแล้วไปทำข้อมูลเสียหรือทำ client crash บน production:

  • reuse number ของ field ที่ลบไป กับ field อื่น bytes เก่าถูก decode เป็น type ใหม่ นี่คือหายนะคลาสสิก
  • เปลี่ยน type ของ field แบบเข้ากันไม่ได้ (เช่น int32string) bytes บน wire ไม่ตรงกับที่ผู้อ่านคาดหวัง
  • renumber field ที่มีอยู่ client ที่ deploy แล้วทุกตัวยังใช้ number เดิม คุณเพิ่งสลับความหมายของ field สองตัว
  • เปลี่ยน field ระหว่าง repeated กับ singular หรือย้าย field เข้า/ออกจาก oneof

จุดร่วม: อะไรก็ตามที่เปลี่ยนความหมายของ number คือ breaking change ถึงแม้ .proto จะยัง compile ได้ก็ตาม

เพราะอันตรายคือการ reuse การป้องกันจึงง่ายและเป็นกลไก: เมื่อลบ field ให้ reserved ทั้ง number และชื่อไว้ทันที

message User {
reserved 3, 7; // field number ที่ปลดระวาง — ห้าม reuse
reserved "email", "phone"; // ชื่อที่ปลดระวาง — ดักการ re-add โดยพลาด
int64 id = 1;
string name = 2;
bool verified = 4;
}

ตอนนี้ compiler เป็นตาข่ายกันตกให้คุณ: ความพยายามในอนาคตที่จะ reuse number 3 หรือชื่อ email จะ build ไม่ผ่าน เครื่องมืออย่าง buf ไปไกลกว่านั้นอีก คือตรวจจับ breaking change อัตโนมัติใน CI ได้ด้วยการเทียบ .proto ของคุณกับ version ที่ publish ไปก่อนหน้า — เปลี่ยน “ฉันเพิ่งทำ client พังหมดเลยหรือเปล่า?” ให้เป็น pull request ที่ fail แทน incident ตอนตีสอง

เมื่อคุณจำเป็นต้องทำ change ที่เข้ากันไม่ได้จริง ๆ อย่าไปแก้ message เดิม ให้ publish package version ใหม่ (user.v2) ควบคู่กับ user.v1 แล้วค่อย ๆ migrate client นี่คือเหตุผลที่คุณเห็น package user.v1; อยู่ทุกที่ — version ถูกฝังไว้ใน type path เพื่อให้ v1 กับ v2 อยู่ด้วยกันได้

อะไรที่ทำให้ protobuf schema evolution เป็นไปได้ตั้งแต่แรก?
change ไหนที่การันตีว่าปลอดภัย?
ทำไม reuse field number ที่ลบไปแล้วถึงอันตรายนัก?
เมื่อต้องทำ change ที่เข้ากันไม่ได้จริง ๆ วิธีที่แนะนำคืออะไร?