11 ธ.ค. 2568·อ่าน 2 นาที

การเปลี่ยนสคีมาโดยไม่หยุดให้บริการ: มิเกรชันแบบเพิ่มที่ปลอดภัย

เรียนรู้การเปลี่ยนสคีมาแบบไม่มี downtime ด้วยมิเกรชันแบบเพิ่ม การเติมข้อมูลย้อนหลังอย่างปลอดภัย และการปล่อยเป็นเฟสที่ช่วยให้ไคลเอนต์เก่ายังคงทำงานได้ระหว่างปล่อย

การเปลี่ยนสคีมาโดยไม่หยุดให้บริการ: มิเกรชันแบบเพิ่มที่ปลอดภัย

ความหมายที่แท้จริงของ "ไม่มีเวลาหยุดทำงาน" สำหรับการเปลี่ยนสคีมา

การเปลี่ยนสคีมาแบบไม่มีเวลาหยุดทำงานไม่ได้หมายความว่าไม่มีอะไรเปลี่ยนเลย แต่มันหมายความว่าผู้ใช้ยังทำงานต่อได้ในขณะที่คุณอัปเดตฐานข้อมูลและแอป โดยไม่เกิดความล้มเหลวหรือกระบวนการที่ติดขัด

เวลา "downtime" คือช่วงเวลาที่ระบบหยุดทำงานตามปกติ อาการอาจเป็น 500 errors, การตอบ API ช้า, หน้าจอโหลดแล้วว่างหรือแสดงค่าผิด, งานแบ็กกราวด์ล้ม หรือฐานข้อมูลที่ยอมอ่านแต่บล็อกการเขียนเพราะมิเกรชันยาวกำลังถือล็อกอยู่

การเปลี่ยนสคีมาอาจทำให้ส่วนอื่นของระบบพังได้ไม่ใช่แค่ UI หลัก จุดพังที่พบบ่อยได้แก่ไคลเอนต์ API ที่คาดหวังรูปร่างการตอบแบบเก่า งานแบ็กกราวด์ที่อ่าน/เขียนคอลัมน์เฉพาะ รายงานที่คิวรีโดยตรง ระบบเชื่อมต่อภายนอก และสคริปต์แอดมินภายในที่ "ยังทำงานได้เมื่อวาน"

แอปมือถือเก่าและไคลเอนต์ที่แคชไว้เป็นปัญหาพบบ่อยเพราะคุณไม่สามารถอัปเดตพวกมันได้ทันที บางคนเก็บเวอร์ชันเก่าเป็นสัปดาห์ บางคนมีการเชื่อมต่อเป็นช่วงๆ และ retry คำขอเก่าในภายหลัง แม้แต่ไคลเอนต์เว็บก็อาจทำตัวเป็น "เวอร์ชันเก่า" เมื่อ service worker, CDN หรือ proxy cache ยังคงเก็บโค้ดหรือสมมติฐานที่ล้าสมัย

เป้าหมายที่แท้จริงไม่ใช่ "มิเกรชันครั้งเดียวให้เสร็จเร็ว" แต่เป็นลำดับของก้าวเล็กๆ ที่แต่ละก้าวทำงานได้ด้วยตัวเอง แม้ว่าจะมีไคลเอนต์คนละเวอร์ชันอยู่พร้อมกัน

คำนิยามเชิงปฏิบัติ: คุณควรสามารถดีพลอยโค้ดใหม่และสคีมาใหม่ในลำดับใดก็ได้ แล้วระบบยังทำงานได้

วิธีคิดนี้ช่วยให้คุณหลีกเลี่ยงกับดักคลาสสิก: ดีพลอยแอปใหม่ที่คาดหวังคอลัมน์ใหม่ก่อนที่คอลัมน์จะมีอยู่จริง หรือเพิ่มคอลัมน์ใหม่ที่โค้ดเก่าไม่สามารถจัดการได้ วางแผนการเปลี่ยนให้เป็นแบบเพิ่มก่อน ปล่อยเป็นเฟส แล้วค่อยเอาทางเก่าออกเมื่อมั่นใจว่าไม่มีใครใช้อีกต่อไป

เริ่มด้วยการเปลี่ยนแบบเพิ่มที่ไม่ทำให้โค้ดเดิมพัง

วิถีที่ปลอดภัยที่สุดเพื่อให้ไม่มี downtime คือต้องเพิ่ม ไม่ใช่แทนที่ การเพิ่มคอลัมน์ใหม่หรือเทเบิลใหม่มักไม่ทำให้เกิดปัญหาเพราะโค้ดเดิมยังอ่าน/เขียนรูปร่างเดิมได้

การเปลี่ยนชื่อและการลบนั้นมีความเสี่ยง การเปลี่ยนชื่อเท่ากับ "เพิ่มใหม่ + เอาเก่าออก" และส่วนที่เป็นปัญหาคือการเอาเก่าออก ซึ่งทำให้ไคลเอนต์เก่าล้ม หากต้องการเปลี่ยนชื่อ ให้ทำเป็นสองขั้นตอน: เพิ่มฟิลด์ใหม่ก่อน เก็บฟิลด์เก่าไว้สักพัก แล้วเอาออกเมื่อแน่ใจว่าไม่มีอะไรพึ่งพา

เมื่อเพิ่มคอลัมน์ ให้เริ่มจากฟิลด์ที่เป็น nullable คอลัมน์ที่เป็น nullable ทำให้โค้ดเก่ายังสามารถแทรกแถวโดยไม่รู้จักคอลัมน์ใหม่ได้ ถ้าคุณต้องการในท้ายที่สุดเป็น NOT NULL ให้เพิ่มเป็น nullable ก่อน เติมข้อมูลย้อนหลัง แล้วค่อยบังคับ NOT NULL ภายหลัง ค่า default ก็ช่วยได้ แต่ระวัง: การเพิ่ม default อาจกระทบแถวจำนวนมากในบางฐานข้อมูล และทำให้การเปลี่ยนช้าลงได้

ดัชนี (index) เป็นการเพิ่มที่ "ปลอดภัยแต่ไม่ฟรี" พวกมันทำให้การอ่านเร็วขึ้น แต่การสร้างและรักษา index อาจชะลอการเขียน ให้เพิ่ม index เมื่อคุณแน่ใจว่าคิวรีไหนจะใช้มัน และพิจารณาเพิ่มในช่วงเวลาที่เบากว่าถ้าฐานข้อมูลยุ่ง

ชุดกฎง่ายๆ สำหรับมิเกรชันฐานข้อมูลแบบเพิ่ม:

  • เพิ่มตารางหรือคอลัมน์ใหม่ก่อน เก็บของเก่าไว้
  • ทำให้ฟิลด์ใหม่เป็น optional (nullable) จนกว่าจะเติมข้อมูลครบ
  • ให้ query และ payload เก่ายังคงทำงานจนกว่าไคลเอนต์จะอัปเดต
  • เลื่อนการบังคับ constraint (NOT NULL, unique, foreign keys) ไปหลังการเติมข้อมูลย้อนหลัง

แผนการปล่อยแบบเป็นขั้นตอนที่ช่วยให้ไคลเอนต์เก่ายังทำงานได้

มองการเปลี่ยนสคีมาแบบไม่มี downtime เป็นการ rollout ไม่ใช่การดีพลอยครั้งเดียว เป้าหมายคือปล่อยให้เวอร์ชันเก่าและใหม่ทำงานร่วมกันได้ในขณะที่ฐานข้อมูลย้ายไปยังรูปร่างใหม่อย่างค่อยเป็นค่อยไป

ลำดับปฏิบัติได้จริง:

  1. เพิ่มสคีมาแบบเข้ากันได้ก่อน สร้างคอลัมน์หรือเทเบิลใหม่ อนุญาตให้เป็น null และหลีกเลี่ยง constraint เข้มงวดที่โค้ดเก่าไม่สามารถเติมได้ ถ้าต้องมี index ให้เพิ่มโดยไม่บล็อกการเขียน
  2. ดีพลอยการเปลี่ยนแปลงฝั่ง backend ที่พูดได้ทั้ง "ภาษา" อัปเดต API ให้รับคำร้องแบบเก่าและแบบใหม่ เริ่มเขียนฟิลด์ใหม่ขณะยังคงดูแลความถูกต้องของฟิลด์เก่า ระยะ "dual write" นี้ทำให้ไคลเอนต์หลายเวอร์ชันปลอดภัย
  3. เติมข้อมูลย้อนหลังเป็นแบตช์เล็กๆ เติมคอลัมน์ใหม่สำหรับแถวเก่าอย่างค่อยเป็นค่อยไป จำกัดขนาดแบตช์ พักเมื่อจำเป็น และติดตามความคืบหน้าเพื่อหยุดถ้าพบโหลดเพิ่ม
  4. สลับการอ่านเมื่อ coverage สูงพอ เมื่อแถวส่วนใหญ่ถูกเติมและคุณมั่นใจ เปลี่ยน backend ให้ใช้ฟิลด์ใหม่เป็นค่าเริ่มต้น แต่เก็บ fallback ไปยังฟิลด์เก่าสักพัก
  5. เอาฟิลด์เก่าออกเป็นขั้นตอนสุดท้ายเมื่อไม่มีใครใช้จริง รอจนแอปมือถือเก่าหมดช่วงส่วนใหญ่ ล็อกไม่มีการอ่านฟิลด์เก่าในล็อก และมีแผน rollback ชัดเจน จากนั้นลบคอลัมน์เก่าและโค้ดที่เกี่ยวข้อง

ตัวอย่าง: คุณเพิ่ม full_name แต่ไคลเอนต์เก่ายังคงส่ง first_name และ last_name ในช่วงหนึ่ง backend สามารถสร้าง full_name ขณะเขียน เติมข้อมูลผู้ใช้เก่า แล้วค่อยอ่าน full_name เป็นค่าเริ่มต้น แต่ยังรองรับ payload เก่า จนกว่า adoption ชัดเจนแล้วจึงลบฟิลด์เก่า

เติมข้อมูลย้อนหลังโดยไม่ประหลาดใจ: วิธีเติมข้อมูลอย่างปลอดภัย

Backfill คือการเติมคอลัมน์หรือเทเบิลใหม่ให้กับแถวที่มีอยู่ มักเป็นส่วนที่เสี่ยงที่สุดเพราะอาจสร้างโหลดหนัก ล็อกยาว และพฤติกรรม "กึ่งมิเกรต" ที่ทำให้สับสน

เริ่มจากการเลือกวิธีรัน backfill สำหรับ dataset เล็ก run แบบแมนนวลตาม runbook ก็พอ แต่สำหรับ dataset ใหญ่ ควรใช้ background worker หรือ scheduled task ที่รันซ้ำได้และหยุดปลอดภัย

แบ่งงานเป็นแบตช์เพื่อควบคุมแรงกดบนฐานข้อมูล อย่าอัปเดตล้านแถวในทรานแซกชันเดียว ตั้งขนาดชิ้นงานที่คาดการณ์ได้และพักสั้นๆ ระหว่างแบตช์เพื่อให้ทราฟิกผู้ใช้ปกติราบรื่น

รูปแบบปฏิบัติได้จริง:

  • เลือกแบตช์เล็ก (เช่น แถวต่อไป 1,000 แถว) โดยใช้คีย์ที่มีดัชนี
  • อัปเดตเฉพาะสิ่งที่ยังขาดอยู่ (หลีกเลี่ยงการเขียนซ้ำแถวที่เติมแล้ว)
  • คอมมิทเร็ว แล้วพักสั้นๆ
  • บันทึกความคืบหน้า (ID ล่าสุดหรือ timestamp)
  • รีทรายเมื่อล้มเหลวโดยไม่ต้องเริ่มใหม่ทั้งหมด

ทำให้ job รันซ้ำได้โดยไม่พัง เก็บตัวชี้ความคืบหน้าในตารางเฉพาะ และออกแบบให้การรันซ้ำไม่ทำให้ข้อมูลเสียหาย การอัปเดตแบบ idempotent (เช่น update where new_field IS NULL) จะช่วยได้มาก

ตรวจสอบขณะทำงาน ติดตามจำนวนแถวที่ยังขาดค่า และเพิ่มการตรวจสอบความสมเหตุสมผล เช่น ยอดเงินไม่เป็นลบ, timestamp อยู่ในช่วงที่คาดไว้, สถานะอยู่ในชุดที่อนุญาต ตัวอย่างจริงๆ ให้สุ่มตรวจดูแถวบางส่วน

ตัดสินใจพฤติกรรมแอปในขณะที่ backfill ยังไม่เสร็จ ทางเลือกปลอดภัยคือ fallback reads: ถ้าฟิลด์ใหม่เป็น null ให้คำนวณหรืออ่านค่าจากช่องทางเก่า เช่น ถ้าคุณเพิ่ม preferred_language จนกว่าจะเติมเสร็จ API อาจคืนค่าภาษาจากการตั้งค่า profile เมื่อตัวใหม่ว่าง แล้วค่อยเริ่มบังคับหลังเติมเสร็จ

กฎการเข้ากันได้ของ API สำหรับไคลเอนต์หลายเวอร์ชัน

รักษาสคีมาและโค้ดให้อยู่ในแนวเดียวกัน
Regenerate โค้ดต้นฉบับที่สะอาดเมื่อสคีมาเปลี่ยนแปลง โดยไม่ต้องสะสมแพตช์รกๆ
Generate Code

เมื่อคุณปล่อยการเปลี่ยนสคีมา คุณไม่คุมทุกไคลเอนต์เสมอ ผู้ใช้เว็บมักอัปเดตเร็ว ในขณะที่แอปมือถือเก่าอาจอยู่เป็นสัปดาห์ นั่นคือสาเหตุที่ API ที่เข้ากันได้ย้อนหลังสำคัญแม้มิเกรชันฐานข้อมูลจะปลอดภัยแล้วก็ตาม

ปฏิบัติต่อข้อมูลใหม่เป็น optional ก่อน เพิ่มฟิลด์ใหม่ในคำขอและการตอบ แต่ไม่บังคับตั้งแต่วันแรก ถ้าไคลเอนต์เก่าไม่ส่งฟิลด์ใหม่ เซิร์ฟเวอร์ควรรับคำขอและทำงานเหมือนเมื่อวาน

หลีกเลี่ยงการเปลี่ยนความหมายของฟิลด์เดิม การเปลี่ยนชื่ออาจโอเคถ้าคุณยังคงให้ชื่อเก่าทำงานต่อ การเอาฟิลด์เดิมมาใช้ความหมายใหม่คือจุดที่เกิดปัญหาได้ง่าย

ค่า default ฝั่งเซิร์ฟเวอร์คือเน็ตนิทรรศน์ความปลอดภัย เมื่อเพิ่มคอลัมน์ใหม่เช่น preferred_language ให้เซ็ตค่าเริ่มต้นฝั่งเซิร์ฟเวอร์เมื่อขาดหาย การตอบ API อาจรวมฟิลด์ใหม่และไคลเอนต์เก่าสามารถละเลยมันได้

กฎเข้ากันได้ที่ป้องกันการล่มส่วนใหญ่:

  • เพิ่มฟิลด์ใหม่เป็น optional ก่อน แล้วค่อยบังคับเมื่อ adoption สูง
  • รักษาพฤติกรรมเก่าให้มั่นคง แม้จะเพิ่มพฤติกรรมใหม่ใต้ฟีเจอร์แฟล็ก
  • ใช้ค่า default ฝั่งเซิร์ฟเวอร์เพื่อให้ไคลเอนต์เก่าละเลยฟิลด์ใหม่ได้
  • คาดหวังทราฟิกผสมและทดสอบทั้งสองเส้นทาง: “ไคลเอนต์ใหม่ส่ง” และ “ไคลเอนต์เก่าไม่ส่ง”
  • รักษาข้อผิดพลาดและรหัสข้อผิดพลาดให้คงที่เพื่อไม่ให้การมอนิเตอร์เป็นเสียงดังขึ้นทันที

ตัวอย่าง: คุณเพิ่ม company_size ใน flow ลงทะเบียน backend สามารถเซ็ตค่า default เป็น "unknown" เมื่อฟิลด์ขาด ไคลเอนต์ใหม่ส่งค่าจริง ไคลเอนต์เก่าทำงานต่อได้ แดชบอร์ดยังอ่านได้

เมื่อแพลตฟอร์มของคุณ regenerate: รักษาสคีมาและตรรกะให้สอดคล้อง

ถ้าแพลตฟอร์มของคุณ regenerate แอป คุณจะได้การสร้างโค้ดและคอนฟิกใหม่ที่สะอาด การนี้ช่วยกับการเปลี่ยนสคีมาแบบไม่มี downtime เพราะคุณสามารถทำก้าวเล็กๆ เพิ่มและดีพลอยบ่อยๆ แทนการค้างแพตช์นานๆ

กุญแจคือต้นทางความจริงแหล่งเดียว (one source of truth) ถ้าสคีมาเปลี่ยนที่ที่หนึ่งและตรรกะธุรกิจเปลี่ยนที่อีกที่ ความแตกต่างจะเกิดเร็ว กำหนดที่เดียวว่าเป็นที่นิยามการเปลี่ยน และจัดการส่วนอื่นเป็น output ที่ถูกสร้าง

การตั้งชื่อตรงไปตรงมาช่วยลดอุบัติเหตุในช่วง rollout ถ้าคุณเพิ่มฟิลด์ใหม่ ให้ตั้งชื่อที่ชัดเจนว่าอันไหนปลอดภัยสำหรับไคลเอนต์เก่าและอันไหนเป็นทางใหม่ ตัวอย่างเช่น ตั้งคอลัมน์ใหม่เป็น status_v2 ปลอดภัยกว่าตั้งเป็น status_new เพราะยังมีความหมายในระยะยาว

ควรทดสอบอะไรหลังการ regenerate แต่ละครั้ง

แม้การเปลี่ยนจะเป็นแบบเพิ่ม การ rebuild อาจเปิดเผย coupling ที่ซ่อนอยู่ หลังการ regenerate และดีพลอยทุกครั้ง ให้ตรวจสอบ flow สำคัญบางอย่าง:

  • ลงทะเบียน, เข้าสู่ระบบ, รีเซ็ตรหัสผ่าน, รีเฟรชโทเค็น
  • การสร้างและอัปเดตหลักๆ (ที่ใช้มากที่สุด)
  • การตรวจสิทธิ์และ admin
  • การจ่ายเงินและ webhook (เช่น เหตุการณ์ Stripe)
  • การแจ้งเตือนและการส่งข้อความ (อีเมล/SMS, Telegram)

วางแผนมิเกรชันก่อนแก้ไขใน editor: เพิ่มฟิลด์ใหม่, ดีพลอยรองรับทั้งสองฟิลด์, เติมข้อมูลย้อนหลัง, สลับการอ่าน, แล้วเก็บทางเก่าออกทีหลัง ลำดับนี้ช่วยให้สคีมา ตรรกะ และโค้ดที่ regenerate เคลื่อนไปด้วยกัน ทำให้การเปลี่ยนเล็ก ทบทวนได้ และย้อนกลับได้

ข้อผิดพลาดที่พบบ่อยที่ทำให้เกิดการล่ม (และวิธีหลีกเลี่ยง)

ทดสอบ playbook การมิเกรชันของคุณ
จำลองแผนมิเกรชันเป็นขั้นตอนในแอปจริงที่ deploy ไปยังผู้ให้บริการคลาวด์ได้
Get Started

การล่มส่วนใหญ่ตอนมิเกรชันแบบไม่มี downtime ไม่ได้เกิดจากงานฐานข้อมูลหนัก แต่มาจากการเปลี่ยนสัญญาระหว่างฐานข้อมูล API และไคลเอนต์ในลำดับที่ผิด

กับดักที่พบบ่อยและการตัดสินใจที่ปลอดภัยกว่า:

  • เปลี่ยนชื่อคอลัมน์ขณะที่โค้ดเก่ายังอ่านมันอยู่. เก็บคอลัมน์เก่าไว้ เพิ่มคอลัมน์ใหม่ และแม็ปทั้งสองไว้สักพัก (เขียนทั้งสองหรือใช้ view) เปลี่ยนชื่อก็ต่อเมื่อยืนยันได้ว่าไม่มีใครพึ่งพาชื่อเก่า
  • ทำให้ฟิลด์ nullable เป็น required เร็วเกินไป. เพิ่มคอลัมน์เป็น nullable ก่อน ดีพลอยโค้ดให้เขียนทุกที่ เติมแถวเก่า แล้วค่อยบังคับ NOT NULL ในมิเกรชันสุดท้าย
  • เติมข้อมูลย้อนหลังในทรานแซกชันมโหฬารที่ล็อกตาราง. เติมเป็นแบตช์เล็กๆ มีข้อจำกัดและพักระหว่างแบตช์ ติดตามความคืบหน้าเพื่อให้สามารถ resume ได้
  • สลับการอ่านก่อนที่การเขียนจะผลิตข้อมูลใหม่. สลับการเขียนก่อน แล้วเติมข้อมูลย้อนหลัง แล้วค่อยสลับการอ่าน ถ้าสลับการอ่านก่อนจะเจอหน้าจอว่าง ยอดรวมผิด หรือ error ว่า "ฟิลด์หาย"
  • ลบฟิลด์เก่าโดยไม่มีหลักฐานว่าไคลเอนต์เก่าหมดไปแล้ว. เก็บฟิลด์เก่านานกว่าที่คิด ลบเมื่อ metrics แสดงว่าเวอร์ชันเก่าแทบไม่ได้ใช้งาน และประกาศช่วงเวลาการเลิกใช้อย่างชัดเจน

ถ้าคุณ regenerate แอป มักจะอยาก "ทำความสะอาด" ชื่อและ constraint ในครั้งเดียว ให้ต้านแรงกระตุ้นนั้นไว้ การล้างทำความสะอาดควรเป็นก้าวสุดท้าย ไม่ใช่ก้าวแรก

กฎที่ดี: ถ้าการเปลี่ยนไม่สามารถ rollback และ roll forward ได้อย่างปลอดภัย แสดงว่าไม่พร้อมลง production

การมอนิเตอร์และแผน rollback สำหรับมิเกรชันแบบเป็นเฟส

สร้างแอปที่เปลี่ยนแปลงได้
ใช้ AppMaster เพื่อสร้างเว็บและโมบายแอปที่ตรรกะ backend สามารถวิวัฒนาการได้อย่างปลอดภัย
Start a Project

ความสำเร็จของการเปลี่ยนสคีมาแบบไม่มี downtime ขึ้นกับสองอย่าง: สิ่งที่คุณมอนิเตอร์ และความเร็วที่คุณหยุดได้

ติดตามสัญญาณที่สะท้อนผลกระทบต่อผู้ใช้จริง ไม่ใช่แค่ "การดีพลอยเสร็จ" เช่น:

  • อัตราข้อผิดพลาดของ API (พุ่งของ 4xx/5xx บน endpoint ที่อัปเดต)
  • คำสั่งช้า (p95 หรือ p99 ของเวลา query สำหรับตารางที่แตะ)
  • ความหน่วงการเขียน (เวลาที่ใช้ในการ insert/update ระหว่างทราฟิกสูง)
  • ความลึกของคิว (งานสะสมสำหรับ backfill หรือการประมวลผลอีเวนต์)
  • แรงใช้ CPU/IO ของฐานข้อมูล (การเพิ่มขึ้นฉับพลันหลังการเปลี่ยน)

ถ้าคุณทำ dual writes ให้เพิ่ม logging ชั่วคราวเพื่อเปรียบเทียบทั้งสองค่า เก็บเฉพาะเมื่อค่าต่างกัน รวม ID ระเบียนและรหัสเหตุผลสั้นๆ และ sample ถ้าปริมาณมาก ตั้งเตือนให้ลบ logging นี้หลังการมิเกรชันเพื่อไม่ให้กลายเป็นเสียงรบกวนถาวร

Rollback ต้องเป็นจริงจัง ในความเป็นจริงมักไม่ rollback สคีมา แต่ rollback โค้ดและเก็บสคีมาแบบเพิ่มไว้

Runbook การ rollback ที่ปฏิบัติได้จริง:

  • ย้อนโค้ดแอปเป็นเวอร์ชันที่รู้จักว่ายอดเยี่ยม
  • ปิดการอ่านจากทางใหม่ก่อน แล้วค่อยปิดการเขียนทางใหม่
  • เก็บตาราง/คอลัมน์ใหม่ไว้แต่หยุดใช้งาน
  • หยุด backfill จนกว่าเมตริกจะนิ่ง

สำหรับ backfill ให้มีสวิตช์หยุดที่ปิดได้ในไม่กี่วินาที (feature flag, ค่าคอนฟิก, หยุด job) และสื่อสารเฟสล่วงหน้า: เมื่อ dual writes เริ่ม, เมื่อ backfill รัน, เมื่อสลับการอ่าน และนิยามว่า "หยุด" คืออะไร เพื่อให้ไม่มีใคร improvises ในภาวะกดดัน

เช็คลิสต์ด่วนก่อนดีพลอย

ก่อนจะปล่อยการเปลี่ยนสคีมา หยุดสักครู่แล้วตรวจเช็คลิสต์นี้ มักจับสมมติฐานเล็กๆ ที่กลายเป็นการล่มเมื่อมีไคลเอนต์หลายเวอร์ชัน

  • การเปลี่ยนเป็นแบบเพิ่ม ไม่ทำลาย. มิเกรชันเพิ่มเฉพาะตาราง คอลัมน์ หรือ index เท่านั้น ไม่มีการลบ เปลี่ยนชื่อ หรือทำให้เข้มงวดจนรับการเขียนเก่าไม่ได้
  • การอ่านทำงานทั้งสองรูปร่าง. โค้ดเซิร์ฟเวอร์ใหม่จัดการทั้ง "มีฟิลด์ใหม่" และ "ไม่มีฟิลด์ใหม่" โดยไม่ error ค่า optional มี default ที่ปลอดภัย
  • การเขียนยังเข้ากันได้. ไคลเอนต์ใหม่ส่งข้อมูลใหม่ได้ ไคลเอนต์เก่ายังส่ง payload เก่าแล้วสำเร็จ หากต้องอยู่ร่วมกัน เซิร์ฟเวอร์รับทั้งสองฟอร์แมตและตอบแบบที่ไคลเอนต์เก่าอ่านได้
  • Backfill หยุดและเริ่มได้อย่างปลอดภัย. งานรันเป็นแบตช์ รีสตาร์ทโดยไม่ซ้ำซ้อนหรือทำให้ข้อมูลเสีย และมีตัวเลข "แถวคงเหลือ" ที่วัดได้
  • คุณรู้วันลบ. มีเกณฑ์ชัดเจนว่าเมื่อไรปลอดภัยที่จะลบฟิลด์หรือโค้ดเก่า (เช่น หลัง X วัน และยืนยันว่า Y% ของคำขอจากไคลเอนต์อัปเดตแล้ว)

ถ้าคุณใช้แพลตฟอร์มที่ regenerate ให้เพิ่มเช็คลิสต์อีกข้อ: สร้างและดีพลอยบิลด์ที่มาจากโมเดลที่คุณกำลังมิเกรต แล้วยืนยันว่า API และตรรกะที่ generate ยังคงทนต่อเรคอร์ดเก่า ความล้มเหลวที่พบบ่อยคือคิดว่าสคีมาใหม่เท่ากับตรรกะใหม่ที่เป็น required โดยทันที

เขียนสองการกระทำด่วนที่คุณจะทำถ้าพบปัญหาหลังดีพลอย: จะมอนิเตอร์อะไร (errors, timeouts, ความคืบหน้าการ backfill) และจะย้อนกลับอะไรก่อน (ปิดฟีเจอร์แฟล็ก, หยุด backfill, revert release) เพื่อให้จาก "เราจะตอบเร็ว" เป็นแผนที่ใช้งานได้จริง

ตัวอย่าง: เพิ่มฟิลด์ใหม่ขณะที่แอปมือถือเก่ายังใช้งานอยู่

ฝึกมิเกรชันแบบเพิ่มอย่างรวดเร็ว
ออกแบบสคีมาแบบเพิ่มและ regenerate backend กับ API อย่างปลอดภัยเมื่อความต้องการเปลี่ยนแปลง
ลอง AppMaster

คุณมีแอปสั่งของ ต้องการฟิลด์ใหม่ delivery_window และมันจะเป็น required สำหรับกฎธุรกิจใหม่ ปัญหาคือเวอร์ชัน iOS และ Android เก่ายังคงใช้งาน และจะไม่ส่งฟิลด์นี้เป็นระยะเวลา วันหรือสัปดาห์ ถ้าคุณทำให้ฐานข้อมูลบังคับฟิลด์นี้ทันที ไคลเอนต์เหล่านั้นจะเริ่มล้มเหลว

เส้นทางปลอดภัย:

  • Phase 1: เพิ่มคอลัมน์เป็น nullable ไม่มี constraint ใดๆ เก็บการอ่านและการเขียนเดิมไว้
  • Phase 2: Dual write ไคลเอนต์ใหม่ (หรือ backend) เขียนฟิลด์ใหม่ ไคลเอนต์เก่ายังคงทำงานเพราะคอลัมน์รับ null ได้
  • Phase 3: Backfill เติม delivery_window สำหรับแถวเก่าโดยใช้กฎ (อนุมานจากวิธีการจัดส่ง หรือค่า default เป็น "anytime" จนกว่าลูกค้าจะแก้ไข)
  • Phase 4: สลับการอ่าน อัปเดต API และ UI ให้อ่าน delivery_window ก่อน แต่ fallback ไปยังค่าที่อนุมานไว้เมื่อยังว่าง
  • Phase 5: บังคับภายหลัง เมื่อ adoption และ backfill เสร็จ ให้เพิ่ม NOT NULL และเอา fallback ออก

ประสบการณ์ผู้ใช้ในแต่ละเฟสคงที่ (นั่นคือเป้าหมาย):

  • ผู้ใช้มือถือเก่ายังสั่งของได้เพราะ API ไม่ปฏิเสธข้อมูลที่ขาด
  • ผู้ใช้มือถือใหม่เห็นฟิลด์ใหม่และการเลือกของพวกเขาจะถูกบันทึกอย่างสม่ำเสมอ
  • ทีมซัพพอร์ตและปฏิบัติการเห็นฟิลด์ค่อยๆ ถูกเติมโดยไม่เกิดช่องว่างกะทันหัน

เกตมอนิเตอร์ง่ายๆ สำหรับแต่ละขั้น: ติดตามสัดส่วนของคำสั่งใหม่ที่ delivery_window ไม่เป็น null เมื่อตัวเลขนี้คงที่สูง และ validation error เกี่ยวกับ "ขาดฟิลด์" อยู่ใกล้ศูนย์ มักปลอดภัยที่จะไปจาก backfill เป็นการบังคับ constraint

ขั้นตอนต่อไป: สร้าง playbook มิเกรชันที่ทำซ้ำได้

การ rollout ระมัดระวังครั้งเดียวไม่ใช่กลยุทธ์ ปฏิบัติต่อการเปลี่ยนสคีมาเป็นกิจวัตร: ขั้นตอนเหมือนเดิม การตั้งชื่อเหมือนเดิม การเซ็นรับเหมือนเดิม แล้วการเปลี่ยนครั้งต่อไปจะยังคงน่าเบื่อแม้ระบบยุ่งและไคลเอนต์ต่างเวอร์ชัน

เก็บ playbook ให้สั้น ตอบคำถาม: เราเพิ่มอะไร, จะปล่อยอย่างปลอดภัยอย่างไร, และจะเอาอะไรเก่าออกเมื่อไร

เทมเพลตเรียบง่าย:

  • เพิ่มเท่านั้น (คอลัมน์/ตาราง/index ใหม่, ฟิลด์ API ใหม่ที่ optional)
  • ดีพลอยโค้ดที่อ่านทั้งสองรูปร่าง
  • เติมข้อมูลย้อนหลังเป็นแบตช์เล็ก ๆ พร้อมสัญญาณ "เสร็จ"
  • สลับพฤติกรรมด้วย feature flag หรือค่าคอนฟิก ไม่ใช่ redeploy
  • เอาฟิลด์/endpoint เก่าออกเฉพาะหลัง cutoff date และยืนยัน

เริ่มจากตารางความเสี่ยงต่ำ (สถานะใหม่ที่เป็น optional หรือฟิลด์โน้ต) และรัน playbook ทั้งหมดตั้งแต่ต้นถึงท้าย: การเปลี่ยนแบบเพิ่ม, backfill, ไคลเอนต์หลายเวอร์ชัน, แล้ว cleanup การฝึกแบบนี้จะเปิดช่องโหว่ในมอนิเตอร์ การแบ่งแบตช์ และการสื่อสารก่อนคุณจะทำ redesign ใหญ่

นิสัยหนึ่งที่ป้องกันความยุ่งเหยิงระยะยาว: ติดตามรายการ "ต้องลบทีหลัง" เป็นงานจริง เมื่อคุณเพิ่มคอลัมน์ชั่วคราว โค้ดรองรับความเข้ากัน หรือ dual-write logic ให้สร้าง ticket cleanup ทันที พร้อมเจ้าของและวันที่ เก็บ "หนี้ความเข้ากัน" ไว้ในเอกสาร release เพื่อให้มองเห็นได้

ถ้าคุณสร้างด้วย AppMaster, คุณสามารถถือว่า regeneration เป็นส่วนหนึ่งของกระบวนการความปลอดภัย: model สคีมาแบบเพิ่ม, อัปเดตตรรกะธุรกิจให้รองรับทั้งฟิลด์เก่าและใหม่ระหว่างการเปลี่ยน, แล้ว regenerate เพื่อให้โค้ดต้นฉบับสะอาดเมื่อความต้องการเปลี่ยน หากต้องการดูว่าเวิร์กโฟลว์นี้เข้ากับการตั้งค่า no-code ที่ยังผลิตซอร์สโค้ดจริงได้อย่างไร AppMaster (appmaster.io) ถูกออกแบบให้สอดคล้องกับสไตล์การปล่อยแบบซ้ำๆ และเป็นขั้นตอน

เป้าหมายไม่ใช่ความสมบูรณ์แบบ แต่เป็นการทำซ้ำได้: ทุกมิเกรชันมีแผน, ตัวชี้วัด, และทางออกชัดเจน

คำถามที่พบบ่อย

คำว่า “zero-downtime” สำหรับการเปลี่ยนสคีมาหมายความว่าอย่างไรจริงๆ?

Zero-downtime หมายความว่าผู้ใช้งานยังสามารถทำงานได้ปกติในขณะที่คุณเปลี่ยนสคีมาและดีพลอยโค้ด นั่นรวมถึงการหลีกเลี่ยงการล่มชัดเจน แต่ยังรวมถึงการหลีกเลี่ยงปัญหาเงียบๆ เช่น หน้าจอว่าง ค่าผิดพลาด งานแบ็กกราวด์ล้ม หรือตัวเขียนถูกบล็อกด้วยล็อกยาวๆ

ทำไมการเปลี่ยนสคีมาถึงทำให้ระบบพังได้แม้มิเกรชันจะสำเร็จ?

เพราะหลายส่วนของระบบขึ้นกับรูปร่างของฐานข้อมูล ไม่ใช่แค่ UI หลัก งานแบ็กกราวด์ รายงาน สคริปต์แอดมิน การเชื่อมต่อภายนอก และแอปมือถือเวอร์ชันเก่า อาจยังคงส่งหรือคาดหวังฟิลด์เก่าๆ นานหลังจากที่คุณดีพลอยโค้ดใหม่ไปแล้ว

ทำไมแอปมือถือเวอร์ชันเก่าถึงเสี่ยงมากเวลามิเกรชัน?

เวอร์ชันแอปมือถือเก่ามักอยู่ได้นานเป็นสัปดาห์ และบางไคลเอนต์อาจ retry คำขอเก่าเมื่อเครือข่ายกลับมา API ของคุณจึงต้องรับได้ทั้ง payload เก่าและใหม่ในช่วงเวลาหนึ่ง จึงจะไม่เกิดปัญหาเมื่อเวอร์ชันต่างกัน coexist กัน

การเปลี่ยนสคีมาประเภทไหนที่ปลอดภัยที่สุดโดยไม่ต้องหยุดระบบ?

การเปลี่ยนแบบเพิ่ม (additive) มักปลอดภัยที่สุด เพราะสคีมาเดิมยังอยู่ ฟีเจอร์ใหม่ที่เพิ่มตารางหรือคอลัมน์ไม่ค่อยทำให้โค้ดเดิมพัง การเปลี่ยนชื่อหรือการลบมีความเสี่ยงเพราะจะเอาสิ่งที่ไคลเอนต์เก่ายังอ่านหรือเขียนอยู่ออกไป

ฉันจะเพิ่มฟิลด์ที่ต้องการ (required) ใหม่โดยไม่ทำให้ไคลเอนต์เก่าพังได้อย่างไร?

เพิ่มคอลัมน์เป็น nullable ก่อน เพื่อที่โค้ดเก่าจะยังแทรกแถวได้เหมือนเดิม หลังจากนั้นเติมข้อมูลย้อนหลังเป็นชุดๆ แล้วเมื่อครอบคลุมดีและการเขียนใหม่สม่ำเสมอ จึงค่อยบังคับ NOT NULL เป็นขั้นตอนสุดท้าย

รูปแบบการเปิดตัวที่ปฏิบัติได้จริงสำหรับมิเกรชันแบบไม่มี downtime คืออะไร?

จัดการเป็น rollout: เพิ่มสคีมาแบบเข้ากันได้, ดีพลอยโค้ดที่รองรับสองแบบ, เติมข้อมูลย้อนหลังเป็นแบตช์เล็กๆ, สลับการอ่านโดยมี fallback, แล้วลบฟิลด์เก่าเมื่อแน่ใจว่าไม่มีใครใช้แล้ว ทุกขั้นตอนต้องทำงานได้เองได้แม้มีไคลเอนต์หลายเวอร์ชันอยู่พร้อมกัน

ฉันจะเติมข้อมูลย้อนหลังโดยไม่ทำให้ DB ถูกล็อกหรือช้าลงได้อย่างไร?

รันเป็นแบตช์เล็กๆ ในทรานแซกชันสั้นๆ อย่าอัปเดตรายล้านแถวในครั้งเดียว ทำให้ job restartable เก็บตัวชี้ความคืบหน้า เช่น ID ล่าสุด อัปเดตเฉพาะแถวที่ยังว่าง (new_field IS NULL) และมีการลองตรวจสอบตัวอย่างข้อมูลจริงเป็นระยะ

ฉันจะรักษา API ให้เข้ากันได้ในขณะที่สคีมาเปลี่ยนได้อย่างไร?

ทำให้ฟิลด์ใหม่เป็น optional ก่อน และเซ็ต default ฝั่งเซิร์ฟเวอร์เมื่อฟิลด์ขาดหาย รักษาพฤติกรรมเก่าไว้ อย่าเปลี่ยนความหมายของฟิลด์เดิม และทดสอบทั้งสองเส้นทาง: “ไคลเอนต์ใหม่ส่งมา” และ “ไคลเอนต์เก่าไม่ส่ง”

แผนการ rollback ที่ดีที่สุดในมิเกรชันแบบเป็นเฟสหน้าตาเป็นอย่างไร?

โดยปกติจะย้อนกลับโค้ด ไม่ยกเลิกสคีมา เพิ่มคอลัมน์/ตารางไว้ก่อน เลิกใช้การอ่านใหม่ก่อน แล้วค่อยหยุดเขียนใหม่ หยุด backfill จนสัญญาณเมตริกนิ่ง เพื่อให้กู้กลับได้โดยไม่เสียข้อมูล

ฉันควรมอนิเตอร์อะไรเพื่อรู้ว่าปลอดภัยพอที่จะไปขั้นถัดไป?

ดูสัญญาณที่มีผลต่อผู้ใช้จริง เช่น อัตราข้อผิดพลาดของ API (4xx/5xx), คำสั่งช้า (p95/p99), ความหน่วงการเขียน, ความลึกของคิวงาน, และการเพิ่มขึ้นของ CPU/IO ของฐานข้อมูล รอจนเมตริกนิ่งและเปอร์เซ็นต์การครอบคลุมฟิลด์ใหม่สูงก่อนจะไปขั้นต่อไป

ง่ายต่อการเริ่มต้น
สร้างบางสิ่งที่ น่าทึ่ง

ทดลองกับ AppMaster ด้วยแผนฟรี
เมื่อคุณพร้อม คุณสามารถเลือกการสมัครที่เหมาะสมได้

เริ่ม