ทีม Product เพิ่มฟิลด์ใหม่ในออเดอร์ แล้วโค้ด CAPI ที่ค้างอยู่ใน Queue พังทั้งชุด: ออกแบบ Schema ให้รองรับการเปลี่ยนแปลง

สรุปสั้น ๆ
การเพิ่มหรือแก้ไขฟิลด์ในโครงสร้าง event ดูเป็นเรื่องเล็กสำหรับทีม product แต่ถ้าระบบ CAPI มี event เก่าค้างอยู่ใน queue ระหว่างที่เปลี่ยนแปลง โค้ดที่ parse ตาม schema เก่าอาจพังหรือทำงานผิดเพี้ยนได้ทันที การออกแบบ schema ให้รองรับการเปลี่ยนแปลงแบบ backward compatible ตั้งแต่แรกคือสิ่งที่ป้องกันปัญหานี้ได้
บริษัทหนึ่งที่ผมเคยเข้าไปช่วยแก้ปัญหา มีเหตุการณ์ที่ทีม product เพิ่มฟิลด์ ‘ช่องทางการชำระเงิน’ เข้าไปในโครงสร้างข้อมูลออเดอร์ แล้วเปลี่ยนชื่อฟิลด์ ‘สถานะ’ เดิมจาก status เป็น order_status เพื่อให้สื่อความหมายชัดขึ้น ฟังดูเป็นการเปลี่ยนแปลงเล็กน้อยมาก แต่พอ deploy ขึ้นจริง ระบบยิง CAPI ที่ดึงค่าจากฟิลด์ status เดิมก็เริ่ม error เพราะฟิลด์นั้นหายไปแล้ว
ปัญหาที่ร้ายแรงกว่านั้นคือ event เก่าที่ยังค้างอยู่ใน queue รอ retry จาก schema แบบเก่า พอ worker หยิบมาประมวลผลหลัง deploy ก็ยังใช้โครงสร้างเก่าอยู่ ในขณะที่โค้ดใหม่คาดหวัง order_status ทำให้ event ชุดที่ค้างอยู่ล้มเหลวทั้งหมดและถูกส่งเข้า dead letter queue เป็นจำนวนมาก
สิ่งที่ทำให้เหตุการณ์นี้เกิดขึ้นได้ ไม่ใช่ความประมาทของทีม product แต่คือการไม่มีข้อตกลงร่วมกันเรื่อง schema versioning ระหว่างทีมที่สร้างข้อมูลกับทีมที่ใช้ข้อมูลนั้นไปยิง CAPI บทความนี้จะพาวางหลักการออกแบบ schema ให้เปลี่ยนแปลงได้โดยไม่พังของเก่าที่ค้างอยู่ในระบบ
ทำไม schema ของ event ถึงเปลี่ยนแปลงบ่อยกว่าที่ทีมพัฒนา CAPI คาดคิด
โครงสร้างข้อมูลออเดอร์หรือลูกค้าไม่ได้ถูกออกแบบมาเพื่อ CAPI โดยเฉพาะ มันคือโครงสร้างที่ทีม product ปรับเปลี่ยนตามความต้องการทางธุรกิจอยู่ตลอดเวลา เช่น เพิ่มฟิลด์ใหม่ตามฟีเจอร์ที่เพิ่งเปิดตัว หรือปรับโครงสร้างให้รองรับโมเดลธุรกิจใหม่ ทีมที่ดูแล CAPI มักเป็นผู้ ‘บริโภค’ ข้อมูลนี้ ไม่ใช่เจ้าของ ทำให้บ่อยครั้งไม่รู้ล่วงหน้าว่าโครงสร้างกำลังจะเปลี่ยน
ปัญหานี้ยิ่งซับซ้อนขึ้นเมื่อระบบมี queue ที่ทำให้ event ไม่ได้ถูกประมวลผลทันที event ที่สร้างขึ้นก่อนการเปลี่ยนแปลง schema อาจยังค้างอยู่ใน queue นานหลายนาทีถึงหลายชั่วโมง (โดยเฉพาะถ้าอยู่ระหว่าง retry หรือรอ backoff) ทำให้ในเวลาเดียวกันมี event ทั้งแบบเก่าและแบบใหม่ปะปนกันอยู่ในระบบจริง
กลยุทธ์ versioning ที่ใช้ได้จริงกับ event ที่ไหลผ่าน queue
| กลยุทธ์ | วิธีทำงาน | เหมาะกับ |
|---|---|---|
| Additive-only changes | อนุญาตให้เพิ่มฟิลด์ใหม่ได้เสมอ แต่ห้ามลบหรือเปลี่ยนชื่อฟิลด์เดิม | ทีมขนาดเล็กถึงกลางที่ต้องการความเรียบง่าย ไม่อยากดูแล version หลายชุด |
| Explicit version field | ใส่ฟิลด์ schema_version ในทุก event แล้วให้โค้ด parse ตามเวอร์ชันที่ระบุ | ระบบที่ต้องเปลี่ยนโครงสร้างแบบ breaking change บ่อยและมี event ค้างใน queue นาน |
| Schema registry กลาง | มีระบบกลางตรวจสอบว่า event ที่ส่งเข้ามาตรงตาม schema ที่ประกาศไว้ก่อนเข้า queue | องค์กรขนาดใหญ่ที่มีหลายทีมสร้าง event เข้าระบบเดียวกัน |
การเปลี่ยนแปลงที่ปลอดภัย เทียบกับที่อันตราย
- ปลอดภัย: เพิ่มฟิลด์ใหม่ที่เป็น optional — โค้ดเก่าที่ไม่รู้จักฟิลด์นี้จะแค่เพิกเฉยไป ไม่กระทบการทำงาน
- อันตราย: เปลี่ยนชื่อฟิลด์เดิม — โค้ดที่ยัง reference ชื่อเดิมจะหาค่าไม่เจอทันที ควรเพิ่มฟิลด์ชื่อใหม่ควบคู่ไปก่อน แล้วค่อยถอดฟิลด์เก่าออกทีหลังเมื่อมั่นใจว่าไม่มีที่ไหนใช้แล้ว
- อันตราย: เปลี่ยนชนิดข้อมูลของฟิลด์เดิม — เช่น จาก string เป็น number โค้ดที่ cast ค่าแบบเดิมอาจ error หรือแย่กว่านั้นคือได้ค่าที่ผิดแบบเงียบ ๆ โดยไม่ error เลย
- อันตราย: ลบฟิลด์ที่เคยมี — แม้จะดูไม่ได้ใช้แล้วในโค้ดปัจจุบัน แต่ event เก่าที่ค้างใน queue หรือ dead letter queue ที่รอ replay อาจยังพึ่งพาฟิลด์นั้นอยู่
ขั้นตอน migrate schema โดยไม่พังงานที่ค้างอยู่
- ประกาศการเปลี่ยนแปลงล่วงหน้าให้ทีมที่ดูแล CAPI รู้ก่อน deploy เสมอ แม้จะรู้สึกว่าเป็นการเปลี่ยนแปลงเล็กน้อยแค่ไหนก็ตาม เพราะทีมสองฝั่งมักไม่รู้ผลกระทบข้ามระบบของกันและกัน
- เขียนโค้ด parse ให้รองรับทั้ง schema เก่าและใหม่พร้อมกันในช่วงเปลี่ยนผ่าน เช่น เช็กว่ามีฟิลด์ order_status ไหม ถ้าไม่มีให้ fallback ไปใช้ฟิลด์ status แบบเดิมแทน
- รอให้ queue ระบายงานเก่าที่ค้างอยู่จนหมดก่อน (หรือมั่นใจว่า TTL ของงานเก่าหมดอายุไปแล้ว) ก่อนจะถอดโค้ดที่รองรับ schema เก่าออก อย่ารีบถอดทิ้งทันทีที่ deploy เสร็จ
- เก็บ log ไว้เสมอว่า event ไหนถูก parse ด้วย schema เวอร์ชันไหน เพื่อให้ตรวจสอบย้อนหลังได้ว่าการเปลี่ยนผ่านเสร็จสมบูรณ์แล้วจริงหรือยังมี event เก่าหลุดเข้ามาอยู่
ทดสอบการเปลี่ยนแปลง schema ก่อนปล่อยขึ้นจริงทุกครั้ง
วิธีที่ช่วยจับปัญหานี้ได้ก่อนขึ้น production คือมีชุดทดสอบใน sandboxที่จำลอง event ทั้งแบบเก่าและแบบใหม่ปนกัน แล้วยิงผ่าน pipeline เดียวกัน เพื่อดูว่าโค้ด parse รองรับทั้งสองแบบได้จริงหรือเปล่า ไม่ใช่ทดสอบแค่ด้วย event รูปแบบใหม่อย่างเดียว
ทีมที่ทำเรื่องนี้ได้ดีมักมีข้อตกลงง่าย ๆ ร่วมกันระหว่างทีม product กับทีมที่ดูแลการส่งข้อมูลเข้า CAPI คือทุกครั้งที่จะเปลี่ยนโครงสร้างข้อมูลออเดอร์หรือลูกค้า ต้องแจ้งล่วงหน้าอย่างน้อยหนึ่งรอบ deploy เพื่อให้อีกทีมเตรียมโค้ดรองรับได้ทัน ไม่ใช่มารู้ตัวตอนระบบพังแล้ว
สรุป
การเปลี่ยนแปลงโครงสร้างข้อมูลที่ทีม product มองว่าเล็กน้อย อาจส่งผลกระทบใหญ่กับระบบ CAPI ที่มี event ค้างอยู่ใน queue ถ้าไม่มีการสื่อสารหรือออกแบบให้รองรับการเปลี่ยนผ่านล่วงหน้า ปัญหานี้ไม่ได้แก้ด้วยเทคโนโลยีอย่างเดียว แต่ต้องมีข้อตกลงร่วมกันระหว่างทีมด้วย
หลักที่จำง่ายที่สุดคือ เพิ่มได้เสมอ แต่อย่าลบหรือเปลี่ยนชื่อฟิลด์เดิมโดยไม่มีช่วงเปลี่ยนผ่าน ถ้ายึดหลักนี้ไว้ ปัญหา event เก่าพังเพราะ schema เปลี่ยนจะเกิดขึ้นน้อยลงมาก แม้ทีม product จะปรับโครงสร้างข้อมูลบ่อยแค่ไหนก็ตาม
- event ที่ค้างอยู่ใน queue อาจมี schema เก่าปนกับ event ใหม่ที่ใช้ schema ล่าสุดในเวลาเดียวกัน
- เพิ่มฟิลด์ใหม่แบบ optional ได้อย่างปลอดภัย แต่ห้ามลบหรือเปลี่ยนชื่อฟิลด์เดิมโดยไม่มีช่วงเปลี่ยนผ่าน
- ให้โค้ด parse รองรับทั้ง schema เก่าและใหม่พร้อมกันจนกว่า queue จะระบายงานเก่าหมด ก่อนถอดโค้ดรองรับเก่าออก
คำถามที่พบบ่อย
จำเป็นต้องมี schema registry เต็มรูปแบบไหม ถ้าทีมยังเล็ก
ไม่จำเป็น ทีมเล็กใช้หลักการ additive-only changes ง่าย ๆ ก็เพียงพอแล้ว คือตกลงกันว่าจะไม่ลบหรือเปลี่ยนชื่อฟิลด์เดิม เพิ่มได้อย่างเดียว schema registry เหมาะกับองค์กรที่มีหลายทีมสร้าง event เข้าระบบเดียวกันจนดูแลด้วยข้อตกลงปากเปล่าไม่ไหวแล้ว
ถ้าจำเป็นต้องเปลี่ยนชื่อฟิลด์จริง ๆ ควรทำยังไงให้ปลอดภัยที่สุด
ใส่ฟิลด์ใหม่เข้าไปควบคู่กับฟิลด์เก่าก่อน ให้โค้ดอ่านได้ทั้งสองแบบในช่วงเปลี่ยนผ่าน รอจนมั่นใจว่าไม่มี event เก่าที่ใช้ฟิลด์เดิมค้างอยู่ในระบบแล้ว ค่อยถอดฟิลด์เก่าออกในภายหลัง ไม่ควรเปลี่ยนแบบตัดขาดทันทีในรอบ deploy เดียว
schema_version ควรใส่ไว้ตรงไหนของ event
นิยมใส่เป็นฟิลด์ระดับบนสุดของ payload เพื่อให้โค้ด parse อ่านค่านี้ได้ก่อนที่จะเริ่มแกะฟิลด์อื่น ๆ ตามเวอร์ชันที่ระบุ ทำให้ตัดสินใจได้เร็วว่าจะใช้ logic การ parse แบบไหนโดยไม่ต้องเดาจากโครงสร้างข้อมูลทั้งก้อน
ทีม product กับทีมที่ดูแล CAPI ควรสื่อสารกันแบบไหนให้ได้ผลจริง
วิธีที่ได้ผลคือมีช่องทางแจ้งเตือนที่ชัดเจนก่อนการเปลี่ยนแปลงโครงสร้างข้อมูลทุกครั้ง เช่น channel เฉพาะหรือ checklist ก่อน deploy ที่บังคับให้ระบุว่ากระทบระบบดาวน์สตรีมอะไรบ้าง ไม่ใช่หวังพึ่งการจำหรือความเกรงใจของแต่ละคน
event เก่าที่ parse ไม่ได้เพราะ schema เปลี่ยน ควรทำยังไงกับมัน
ถ้ามีจำนวนไม่มากและระบุที่มาได้ชัดเจน แนะนำให้เขียนสคริปต์แปลงข้อมูลเก่าให้ตรงกับ schema ใหม่แล้ว replay เข้าไปใหม่ แทนที่จะปล่อยทิ้งใน dead letter queue เพราะข้อมูล conversion ที่หายไปมีต้นทุนจริงต่อความแม่นยำของการวัดผล
การเปลี่ยนชนิดข้อมูลของฟิลด์ เช่น string เป็น number อันตรายแค่ไหน
อันตรายมากกว่าที่คิด เพราะบางภาษาโปรแกรมมิ่ง cast ชนิดข้อมูลแบบหลวม ๆ ให้อัตโนมัติโดยไม่ error ทำให้ได้ค่าที่ผิดแบบเงียบ ๆ แทนที่จะเห็น error ชัดเจน ควรมี validation ตรวจชนิดข้อมูลอย่างเข้มงวดก่อนนำค่าไปใช้งานต่อเสมอ
บทความที่เกี่ยวข้อง


