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)"]
กลไกที่ทำให้เป็นแบบนี้ได้คือสิ่งที่คุณรู้อยู่แล้ว: field ถูกระบุด้วย number server เก่าที่เจอ field number ที่ไม่รู้จักก็แค่ข้ามไป ส่วน server ใหม่ที่ไม่ได้รับ field ที่คาดหวังก็อ่านค่า default การระบุ field ด้วย number ไม่ใช่ชื่อ คือสิ่งที่รักษาความสงบไว้
change ที่ปลอดภัยเสมอ
หัวข้อที่มีชื่อว่า “change ที่ปลอดภัยเสมอ”พวกนี้ไม่มีทางทำให้ client ที่ deploy แล้วพัง:
- เพิ่ม field ใหม่ ด้วย number ที่สดใหม่ไม่เคยใช้ code เก่าจะข้ามไป ส่วน code ใหม่อ่านค่าได้ (และได้ default เมื่อไม่มีค่ามา)
- ลบ field — ตราบใดที่คุณ
reservednumber นั้นไว้ (ดูด้านล่าง) client เก่าที่ยังส่งมาก็ถูกข้ามไปเฉย ๆ - rename field wire ถือ number ดังนั้นชื่อเปลี่ยนได้ฟรี
- เพิ่ม value ใหม่ให้ enum หรือ field ใหม่ให้
oneofclient เก่ามอง unknown เป็น unknown - เพิ่ม RPC method ใหม่ ให้ service เดิม ตัว client ที่มีอยู่ไม่ได้เรียก method ใหม่นั้น ไม่มีอะไรพัง
change ที่พัง — แบบเงียบ ๆ
หัวข้อที่มีชื่อว่า “change ที่พัง — แบบเงียบ ๆ”พวกนี้ compile ผ่านแล้วไปทำข้อมูลเสียหรือทำ client crash บน production:
- reuse number ของ field ที่ลบไป กับ field อื่น bytes เก่าถูก decode เป็น type ใหม่ นี่คือหายนะคลาสสิก
- เปลี่ยน type ของ field แบบเข้ากันไม่ได้ (เช่น
int32→string) bytes บน wire ไม่ตรงกับที่ผู้อ่านคาดหวัง - renumber field ที่มีอยู่ client ที่ deploy แล้วทุกตัวยังใช้ number เดิม คุณเพิ่งสลับความหมายของ field สองตัว
- เปลี่ยน field ระหว่าง
repeatedกับ singular หรือย้าย field เข้า/ออกจากoneof
จุดร่วม: อะไรก็ตามที่เปลี่ยนความหมายของ number คือ breaking change ถึงแม้ .proto จะยัง compile ได้ก็ตาม
วินัยเรื่อง reserved
หัวข้อที่มีชื่อว่า “วินัยเรื่อง reserved”เพราะอันตรายคือการ 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 ตอนตีสอง
version ที่ package ไม่ใช่ด้วยการทำพัง
หัวข้อที่มีชื่อว่า “version ที่ package ไม่ใช่ด้วยการทำพัง”เมื่อคุณจำเป็นต้องทำ change ที่เข้ากันไม่ได้จริง ๆ อย่าไปแก้ message เดิม ให้ publish package version ใหม่ (user.v2) ควบคู่กับ user.v1 แล้วค่อย ๆ migrate client นี่คือเหตุผลที่คุณเห็น package user.v1; อยู่ทุกที่ — version ถูกฝังไว้ใน type path เพื่อให้ v1 กับ v2 อยู่ด้วยกันได้