3 นาที

เปลี่ยนสคีมาโดยไม่หยุดให้บริการด้วยรูปแบบ expand/contract

วางแผนและปล่อยการเปลี่ยนสคีมาโดยไม่หยุดให้บริการด้วยรูปแบบ expand/contract, backfill ที่ปลอดภัย, รีลีสที่เข้ากันได้, การตรวจสอบ และการย้อนกลับ

เปลี่ยนสคีมาโดยไม่หยุดให้บริการด้วยรูปแบบ expand/contract

เหตุใดการเปลี่ยนสคีมาจึงทำให้ระบบหยุดให้บริการ

การเปลี่ยนสคีมาทำให้ระบบหยุดให้บริการเมื่อแอปแต่ละเวอร์ชัน, worker เบื้องหลัง และฐานข้อมูลไม่เห็นพ้องกันอีกต่อไปว่าโครงสร้างและค่าใดใช้ได้ ความล้มเหลวอาจชัดเจน เช่น ทุก request ตอบกลับด้วยข้อผิดพลาด หรือค่อยๆ ปรากฏ เช่น query latency สูงขึ้น การเขียนล้มเหลว replica lag และคิวงานที่ต้องนำกลับมารันใหม่

การ deploy บนระบบจริงแทบไม่เคยเปลี่ยนทุก process พร้อมกัน การปล่อยแบบทยอยทำให้ instance ของแอปเก่าและใหม่ทำงานร่วมกันได้ worker ที่อยู่มานานอาจยังใช้ build เก่านานหลายชั่วโมง ไคลเอนต์มือถืออาจยังทำงานอยู่หลายเดือน และงานรายงานหรืองานเชื่อมต่ออาจใช้ตารางโดยไม่ผ่านแอปหลัก ทั้งหมดใช้ฐานข้อมูลเดียวกัน

รูปแบบความล้มเหลวที่พบบ่อย ได้แก่

  • โค้ดใหม่เขียนคอลัมน์ก่อน migration ที่สร้างคอลัมน์นั้นจะเสร็จ
  • โค้ดเก่าอ่านตารางหรือคอลัมน์ที่รีลีสหลังจากนั้นเปลี่ยนชื่อหรือลบไปแล้ว
  • การเขียนตารางใหม่, backfill หรือสร้างดัชนีใช้ I/O และ CPU มากพอจะทำให้ทราฟฟิกปกติช้าลง
  • คำสั่งสคีมารอ lock ขณะที่ request สะสมอยู่ด้านหลัง
  • constraint ใหม่ปฏิเสธการเขียนจาก process ที่ยังไม่ได้อัปเกรด

ส่วนที่อันตรายมักเป็นการได้ lock ไม่ใช่เวลาในการรันตามปกติ ALTER TABLE ที่เร็วอาจต้องรอธุรกรรมที่ยาวนาน ระหว่างรอ query ถัดมาอาจต่อคิวอยู่หลัง schema lock ที่กำลังรออยู่ เปลี่ยน migration เล็กๆ ให้กลายเป็นการหยุดชะงักทั้งแอป

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

วิธีนี้เหมาะกับระบบที่มีทราฟฟิกจริง, deploy แบบทยอย, เป้าหมายด้านความพร้อมใช้งานที่เข้มงวด หรือมีขั้นตอนกู้คืนที่มีต้นทุนสูง เครื่องมือภายในขนาดเล็กที่ฐานข้อมูลไม่ค่อยมีงานอาจเหมาะกับช่วงบำรุงรักษาที่ผ่านการทดสอบแล้วมากกว่า การตัดสินใจควรสะท้อนต้นทุนจากการหยุดให้บริการและความซับซ้อนในการปฏิบัติการของ migration

Expand/contract อธิบายแบบง่ายๆ

รูปแบบ expand/contract เปลี่ยนการแก้ไขที่เข้ากันไม่ได้ครั้งเดียวให้เป็นลำดับของรีลีสที่เข้ากันได้ ฐานข้อมูลรองรับข้อมูลสองรูปแบบชั่วคราว ขณะที่โค้ดและข้อมูลค่อยๆ ย้ายจากรูปแบบเดิมไปยังรูปแบบใหม่

ลำดับมีสามส่วน

  • Expand ด้วยการเพิ่มคอลัมน์ ตาราง ดัชนี หรือ constraint โดยไม่ลบสิ่งที่โค้ดปัจจุบันต้องใช้
  • Transition ด้วยการ deploy โค้ดที่เข้ากันได้ ย้ายข้อมูลเก่า และชี้การอ่านกับการเขียนไปยังรูปแบบใหม่
  • Contract ด้วยการลบโค้ดและอ็อบเจ็กต์ฐานข้อมูลเก่าหลังตรวจสอบว่าไม่มีใครใช้งานแล้ว

สมมติว่าตาราง PostgreSQL เก็บชื่อบุคคลไว้ใน full_name แต่แอปต้องการฟิลด์ first_name และ last_name แยกกัน ขั้น expand จะเพิ่มคอลัมน์ที่ยอมรับค่าว่างโดยยังเก็บ full_name ไว้ รีลีสที่เข้ากันได้จะเขียนข้อมูลในรูปแบบที่ต้องใช้ระหว่างเปลี่ยนผ่าน backfill จะแยกค่าที่มีอยู่ โดยกำหนดนโยบายชัดเจนสำหรับชื่อที่แยกอย่างน่าเชื่อถือไม่ได้ การอ่านจะย้ายเมื่อฟิลด์ใหม่มีข้อมูลครบเพียงพอ และค่อยลบ full_name ในขั้น contract

ลำดับนี้เหมาะกับการ deploy แบบทยอย เพราะ build เก่ายังเจอ full_name และ build ใหม่เจอทั้งสามคอลัมน์ อีกทั้งยังคงทางเลือกในการย้อนกลับแอปไว้ หากรีลีสใหม่ทำงานผิดปกติ ก็รัน build ก่อนหน้าได้เพราะยังไม่ได้ลบสิ่งที่สคีมาต้องพึ่งพา

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

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

จัดประเภทการเปลี่ยนก่อนเลือกขั้นตอน

แผน migration ควรสอดคล้องกับความเสี่ยงจริงเรื่อง lock, การเขียนตารางใหม่, ความเข้ากันได้ และการแปลงข้อมูลของแต่ละคำสั่ง การมอง ALTER TABLE ทุกแบบว่าเท่ากันทำให้ขั้นตอนยุ่งยากเกินจำเป็น หรือปล่อยงานอย่างไม่ปลอดภัย

การเปลี่ยนแบบเพิ่มมักง่ายที่สุด คอลัมน์ที่ยอมรับค่าว่าง ตารางแยก หรือดัชนีที่สร้างด้วยวิธีออนไลน์ มักเพิ่มได้ก่อนโค้ดแอปจะใช้งาน คำสั่งยังต้องใช้ lock จึงควรทดสอบพฤติกรรมกับตารางและภาระธุรกรรมที่ใกล้ระบบจริง

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

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

การตรวจทานก่อนเริ่มที่มีประโยชน์ครอบคลุมห้าคำถาม

  • แต่ละ statement ขอ lock แบบใด และรอหรือถือ lock ได้นานแค่ไหน?
  • งานนี้จะเขียนตารางใหม่ สร้าง WAL ปริมาณมาก หรือเพิ่ม replica lag หรือไม่?
  • แอป งาน รายงาน และผู้บริโภค change-data-capture ตัวใดใช้อ็อบเจ็กต์ที่ได้รับผลกระทบ?
  • รีลีสปัจจุบันและรีลีสที่เสนอทำงานกับทุกสถานะระหว่างเปลี่ยนผ่านได้หรือไม่?
  • สัญญาณใดจะสั่งพักงาน และเมื่อหยุดแล้วจะเหลือสถานะอะไร?

รัน migration เดิมกับข้อมูลที่มีปริมาณและการกระจายใกล้เคียงจริง ตารางทดสอบที่มีแถวเป็นระเบียบหนึ่งพันแถวบอกได้น้อยมากเกี่ยวกับตารางจริงที่มีหลายร้อยล้านแถว tuple กว้าง แถวที่ตายแล้ว ค่ากระจุกตัว และธุรกรรมที่รันนาน

Expand อย่างปลอดภัยใน PostgreSQL

การ expand PostgreSQL ที่ปลอดภัยใช้การเปลี่ยน metadata ระยะสั้น จำกัดเวลารอ lock และแยกงานออนไลน์ตามที่ฐานข้อมูลกำหนด เพิ่มโครงสร้างใหม่ก่อน deploy โค้ดที่ต้องพึ่งพามัน

การเพิ่มคอลัมน์ที่ยอมรับค่าว่างและไม่มีค่าเริ่มต้นมักเป็นงาน metadata ระยะสั้น

BEGIN;
SET LOCAL lock_timeout = '2s';
SET LOCAL statement_timeout = '30s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

timeout ช่วยไม่ให้รีลีสรอธุรกรรมที่เปิดค้างไว้อย่างไม่มีกำหนด หากได้ lock ไม่ทัน ให้ migration ล้มเหลว ตรวจดูตัวที่ขวางอยู่ แล้วลองใหม่ในเวลาที่ปลอดภัยกว่า อย่าลองใหม่อัตโนมัติในลูปถี่ๆ เพราะการขอ lock ซ้ำอาจรบกวนทราฟฟิกจริงอยู่เรื่อยๆ

PostgreSQL เวอร์ชันใหม่สามารถเพิ่มคอลัมน์ที่มีค่าเริ่มต้นคงที่ได้ โดยไม่ต้องเขียนค่านั้นลงทุกแถวเดิมทันที แต่การเพิ่มประสิทธิภาพนี้ไม่ได้ทำให้ค่าเริ่มต้นทุกแบบปลอดภัย expression ที่เปลี่ยนแปลงได้อาจต้องเขียนตารางใหม่ และ ALTER TABLE ยังต้องใช้ ACCESS EXCLUSIVE lock ชั่วครู่ ยืนยันพฤติกรรมสำหรับเวอร์ชัน PostgreSQL ที่ deploy และ expression ที่ใช้จริง แทนการอาศัยกฎทั่วไป

CREATE INDEX ปกติอาจขวางการเขียน ใช้การสร้างพร้อมกันเมื่อตารางต้องยังเขียนได้

CREATE INDEX CONCURRENTLY idx_customers_phone_e164
ON customers (phone_e164);

CREATE INDEX CONCURRENTLY รันใน transaction block ไม่ได้ ใช้เวลานานกว่า มีงานเพิ่ม และอาจรอธุรกรรมเก่า แต่การ insert, update และ delete ปกติทำต่อได้ มันยังใช้ CPU, I/O และ WAL จึงติดตาม database latency และ replica ระหว่างทำงาน

การสร้างแบบพร้อมกันที่ล้มเหลวอาจทิ้งดัชนีที่ใช้ไม่ได้ไว้ ตรวจสถานะดัชนีก่อนลองใหม่ แล้วลบหรือสร้างอ็อบเจ็กต์ที่ใช้ไม่ได้ขึ้นใหม่โดยตั้งใจ เครื่องมือ migration ที่ห่อทุกไฟล์ใน transaction ต้องมีโหมด non-transactional ที่รองรับสำหรับงานดัชนีแบบพร้อมกัน

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

การเปลี่ยนชนิดข้อมูลต้องระวังเป็นพิเศษ บางแบบเป็นเพียง metadata ขณะที่บางแบบเขียนทุกแถวใหม่หรือได้ lock ที่เข้มงวดนานเกินไป หากการแปลงมีความเสี่ยง ให้เพิ่มคอลัมน์ชนิดเป้าหมาย เติมข้อมูลเป็น batch สลับการเข้าถึงของแอป แล้วค่อยลบของเดิม วิธีนี้ยังเปิดพื้นที่ให้ทีมบันทึกความล้มเหลวในการแปลง แทนการให้ ALTER COLUMN TYPE ขนาดใหญ่สำเร็จหรือล้มเหลวทั้งก้อน

Deploy โค้ดที่ยังเข้ากันได้

โค้ดแอปที่เข้ากันได้ต้องรับมือกับค่าระหว่างเปลี่ยนผ่านที่อาจไม่มี และต้องไม่ต้องการ destructive migration ใน rollout เดียวกัน การ expand ฐานข้อมูลควรเสร็จก่อน instance แรกของแอปจะเริ่มใช้อ็อบเจ็กต์ใหม่

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

ตรรกะ dual-write ต้องมีแหล่งอ้างอิงหนึ่งเดียวด้วย หาก phone_e164 คำนวณจาก phone ให้กำหนดว่าค่าใดชนะเมื่อส่งมาทั้งคู่ และใช้ normalization เดียวกันใน API handler, worker, งานนำเข้า และเครื่องมือผู้ดูแล มิฉะนั้นเส้นทางโค้ดสองเส้นที่ดูถูกต้องอาจเก็บผลลัพธ์ต่างกัน

ควรย้ายการอ่านช้ากว่าการเขียน ให้การอ่านอยู่ที่ฟิลด์เดิม ขณะที่การเขียนใหม่เติมทั้งสองรูปแบบและ backfill จัดการแถวเก่า หลังตรวจสอบแล้ว deploy เส้นทางอ่านที่เลือกฟิลด์ใหม่ก่อน และใช้ค่าเก่าตามกฎ fallback ที่กำหนดไว้เท่านั้น วัดการใช้ fallback เพราะ fallback ที่เงียบอยู่สามารถปกปิดข้อมูลที่ไม่ครบไปตลอดได้

ลำดับรีลีสโดยทั่วไปคือ

  • รีลีส 1 เพิ่มอ็อบเจ็กต์ฐานข้อมูลใหม่โดยไม่เปลี่ยนพฤติกรรมแอป
  • รีลีส 2 เขียนข้อมูลระหว่างเปลี่ยนผ่าน แต่ยังอ่านข้อมูลรูปแบบเดิม
  • รีลีส 3 สลับการอ่านหลัง backfill และการตรวจความสอดคล้องผ่านแล้ว
  • รีลีส 4 หยุดดูแลข้อมูลรูปแบบเดิมหลังเกณฑ์การย้อนกลับหมดอายุ
  • รีลีส 5 ลบการอ้างอิงจากโค้ดเดิม และค่อยทำความสะอาดฐานข้อมูลภายหลัง

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

รวบรวมผู้เขียนข้อมูลทุกตัว HTTP handler เป็นเพียงแหล่งหนึ่งของการเปลี่ยนแปลง queue consumer, scheduled job, script นำเข้า, เครื่องมือซ่อมข้อมูล, database trigger และการจัดการโดยตรงอาจยังสร้างแถวรูปแบบเก่าอยู่ ตั้งชื่อแอปให้ connection ฐานข้อมูลเมื่อเหมาะสม และบันทึกการใช้เส้นทางระหว่างเปลี่ยนผ่าน เพื่อให้ process ที่ตกหล่นปรากฏขึ้น

process ที่มีอายุยาวอาจคงสมมติฐานเก่าไว้ผ่าน prepared statement, metadata ที่ cache หรือชั้น object-relational mapping ทดสอบการ restart แบบทยอยและพฤติกรรม connection pool ก่อน contract process ที่ยังไม่ส่งทราฟฟิกอาจยังล้มเหลวเมื่อ rare job รันครั้งแรก

ทำ backfill โดยไม่ทำให้ฐานข้อมูลรับภาระเกิน

ทำ backfill เป็น batch เล็กๆ
สร้างงาน backfill แบบเรียบง่าย แล้วปรับขนาด batch โดยไม่ทำให้ทีมช้าลง

backfill ที่ปลอดภัยอัปเดตเป็น batch เล็กๆ ที่กลับมาทำต่อได้ และชะลอเมื่อสุขภาพของระบบจริงแย่ลง เริ่มทำหลังผู้เขียนข้อมูลที่ใช้งานจริงดูแลข้อมูลรูปแบบใหม่ได้แล้วเท่านั้น

เลือก batch จากเวลาที่ใช้และผลกระทบต่อฐานข้อมูล ไม่ใช่จำนวนแถวตายตัว แถวแคบหนึ่งพันแถวอาจเสร็จในไม่กี่มิลลิวินาที ขณะที่หนึ่งพันแถวที่มีค่าขนาดใหญ่หรือการแปลงหนักอาจสร้าง I/O มาก เริ่มอย่างระมัดระวังและตั้งเป้าธุรกรรมที่จบในไม่กี่วินาที commit ระหว่าง batch เพื่อไม่ให้ lock และ row version เก่าสะสมในธุรกรรมเดียว

PostgreSQL ไม่รองรับ ORDER BY และ LIMIT โดยตรงบน UPDATE ธรรมดา เลือก batch ใน common table expression แล้วอัปเดตแถวเหล่านั้น

WITH batch AS (
    SELECT id
    FROM my_table
    WHERE id > $1
      AND new_col IS NULL
    ORDER BY id
    LIMIT 1000
)
UPDATE my_table AS target
SET new_col = transform_expression(target.old_col)
FROM batch
WHERE target.id = batch.id
  AND target.new_col IS NULL
RETURNING target.id;

แอปบันทึก id ที่ทำเสร็จสูงสุดเป็น cursor การอัปเดตแบบมีเงื่อนไขทำให้การรันซ้ำ idempotent ดังนั้นการล่มหลัง commit จะไม่ทำให้แถวที่ประมวลผลแล้วเสียหาย เก็บความคืบหน้าอย่างระวังพอที่จะไม่ให้ cursor ข้าม batch ที่ยังไม่ได้ commit

cursor id ที่เพิ่มขึ้นหลีกเลี่ยงการสแกนต้นตารางซ้ำๆ แต่ไม่ครอบคลุมการแก้ไขที่มาทีหลังหรือแถวที่แทรกต่ำกว่า cursor ปิดงานด้วยการไล่ตรวจค่า NULL ที่เหลือทั้งหมด หาก identifier ไม่ได้เรียงลำดับหรือแถวเปลี่ยนสถานะการเข้าเกณฑ์ได้ ให้ใช้ work table หรือ checkpoint ที่ชัดเจนแบบอื่น แทนการคิดว่าการสแกนไปข้างหน้าครั้งเดียวครบแล้ว

worker หลายตัวอาจรับแถวด้วย FOR UPDATE SKIP LOCKED แต่การทำงานขนานเพิ่มแรงกดดันจากการเขียนและทำให้ติดตามความคืบหน้ายากขึ้น อย่ารวมแถวที่ข้ามไว้กับ cursor ที่เลื่อนไปไกลจนไม่กลับมาดูอีก คิวของ identifier ที่รับงานแล้ว หรือการสแกนตามเงื่อนไขซ้ำ ปลอดภัยกว่าสำหรับ worker หลายตัว

จำกัดความเร็วตามค่าจากระบบจริง เช่น query latency, active connection, lock wait, การสร้าง WAL, replica replay delay และจำนวน dead row พักงานเมื่อเกินเกณฑ์ แล้วกลับมาทำต่อจาก checkpoint การพักเป็นช่วงเวลาคงที่ทำง่าย แต่ feedback จากฐานข้อมูลตอบสนองต่อการเปลี่ยนแปลงของทราฟฟิกได้ดีกว่า

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

Autovacuum และ replica ต้องรองรับงานที่ตามมาหลังแต่ละ update backfill อาจสำเร็จบน primary แต่ replica ล่าช้ามาก หรือตารางบวมจน query หลังจากนั้นแย่ลง การจำกัดความเร็วควรคำนึงถึงต้นทุนที่ตามมานี้ ไม่ใช่แค่เวลารัน batch ทันที

ตรวจสอบข้อมูลและทราฟฟิกจริง

migration พร้อมเข้าสู่ขั้น contract ก็ต่อเมื่อการตรวจข้อมูล telemetry ของแอป และหลักฐานเรื่อง dependency ตรงกันว่าข้อมูลรูปแบบใหม่เป็นแหล่งอ้างอิง งานที่ตัวนับบอกว่าเสร็จไม่ได้พิสูจน์ความถูกต้องเพียงลำพัง

เริ่มจากความครบถ้วนและความสอดคล้อง IS DISTINCT FROM ของ PostgreSQL เปรียบเทียบค่าโดยจัดการ NULL อย่างชัดเจน ต่างจาก <> ที่ให้ผลไม่ทราบเมื่อด้านใดด้านหนึ่งเป็น NULL

SELECT count(*)
FROM customers
WHERE normalize_phone(phone) IS DISTINCT FROM phone_e164;

อย่ารัน count ทั้งตารางที่ไม่มีดัชนีซ้ำๆ บนตารางขนาดใหญ่มากที่กำลังใช้งาน ใช้การตรวจสอบแบบควบคุมครั้งเดียว ช่วง identifier ที่จำกัด ตัวอย่าง หรือ process ตรวจสอบชั่วคราวที่ค่อยๆ เดินผ่านตาราง วิธีที่เหมาะสมขึ้นอยู่กับต้นทุนของความผิดพลาดและกำลังสำรองของฐานข้อมูล

การตรวจสอบควรครอบคลุม

  • ไม่มีค่าที่ขาดหายโดยไม่คาดคิดในแถวที่ต้องใช้ฟิลด์ใหม่
  • ค่าใหม่ตรงกับการแปลงที่ตกลงกัน รวมถึงข้อมูลผิดรูปแบบและค่าว่าง
  • แถวใหม่และการอัปเดตยังสอดคล้องกันหลังจัดการข้อมูลเก่าเสร็จ
  • การใช้ fallback ในการอ่านถึงเกณฑ์ที่วางไว้ โดยทั่วไปควรเป็นศูนย์สำหรับทราฟฟิกที่เซิร์ฟเวอร์ควบคุม
  • อัตราข้อผิดพลาด query latency lock และ replica delay ยังอยู่ในขีดจำกัดของรีลีส

เปรียบเทียบผลลัพธ์ทางธุรกิจควบคู่กับคอลัมน์ หาก migration เปลี่ยนราคา สิทธิ์ สถานะบัญชี หรือ identifier ให้ตรวจยอดรวมและ invariant ที่ผู้ใช้ต้องพึ่งพา สองคอลัมน์อาจตรงกันเชิงกลไกแต่ทั้งคู่เข้ารหัสกฎธุรกิจผิด

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

ทำ canary กับการสลับการอ่านหากสถาปัตยกรรมแอปรองรับ ส่งทราฟฟิกส่วนเล็กไปยังเส้นทางอ่านใหม่ เปรียบเทียบผล แล้วค่อยขยายอย่างค่อยเป็นค่อยไป ให้การย้อนกลับเรียบง่าย คือเปลี่ยนการอ่านกลับไปยังข้อมูลรูปแบบเดิมโดยไม่ย้อน backfill

เพิ่ม constraint หลังข้อมูลพร้อมแล้ว

constraint ควรเข้มงวดหลังผู้เขียนข้อมูลทุกตัวทำตามกฎและตรวจข้อมูลเดิมแล้ว การบังคับ NOT NULL, check หรือ foreign key ระหว่าง expand อาจขวางทราฟฟิกหรือปฏิเสธการเขียนจาก process เก่า

PostgreSQL เพิ่ม check constraint แบบ NOT VALID ได้ ซึ่งบังคับกฎกับแถวใหม่หรือแถวที่เปลี่ยนโดยไม่สแกนข้อมูลเก่าทั้งหมดทันที ตรวจสอบแยกหลัง backfill

ALTER TABLE customers
ADD CONSTRAINT customers_phone_e164_present
CHECK (phone_e164 IS NOT NULL) NOT VALID;

ALTER TABLE customers
VALIDATE CONSTRAINT customers_phone_e164_present;

เมื่อการตรวจสอบสำเร็จ PostgreSQL เวอร์ชันที่รองรับใช้หลักฐานนี้ตอนตั้งคอลัมน์เป็น NOT NULL ได้ จึงหลีกเลี่ยงการสแกนทั้งตารางอีกครั้ง การแก้ไขสุดท้ายยังต้องใช้ table lock ที่เข้มงวด จึงกำหนด lock timeout ที่จำกัดและแผนลองใหม่

ALTER TABLE customers
ALTER COLUMN phone_e164 SET NOT NULL;

ALTER TABLE customers
DROP CONSTRAINT customers_phone_e164_present;

จะเก็บ check ชั่วคราวไว้ก็ได้หากยังมีประโยชน์ แต่การเก็บ constraint ที่เทียบเท่ากันเพิ่มความรกใน catalog โดยไม่เปลี่ยนกฎ

Foreign key ใช้ลำดับคล้ายกันได้ด้วย NOT VALID และ VALIDATE CONSTRAINT การเขียนใหม่จะถูกตรวจหลังสร้าง constraint ส่วนการตรวจข้อมูลเก่าทำภายหลัง เพิ่ม supporting index อย่างตั้งใจ หากพฤติกรรม delete หรือ update ของความสัมพันธ์ที่อ้างถึงอาจทำให้สแกนหนัก

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

Contract เส้นทางเดิมอย่างปลอดภัย

ใช้ Planning Mode สำหรับการย้ายข้อมูล
วางแผนรีลีส การ backfill และคิวรีตรวจสอบใน Koder.ai Planning Mode

ขั้น contract ควรลบ dependency ของแอปก่อนลบอ็อบเจ็กต์ฐานข้อมูล เมื่อ telemetry และการตรวจสอบยืนยันว่าข้อมูลรูปแบบใหม่เป็นแหล่งอ้างอิงแล้ว การเก็บกวาดทำผ่านหลายรีลีสแยกกันได้

เริ่มจากหยุดอ่านฟิลด์เก่าและลบตรรกะ fallback แล้วปิดการเขียนของมันและสังเกตระบบจริงนานพอจะจับเส้นทางที่เกิดไม่บ่อย ลบ feature flag, trigger, compatibility view, script ซ่อม และ scheduled job ที่กล่าวถึงข้อมูลรูปแบบเก่า ค้นหาซอร์สที่ export และโค้ด migration แต่ตรวจรายงาน query ของระบบเชื่อมต่อ และการตั้งค่า change-data-capture นอก repository หลักด้วย

ลำดับการเก็บกวาดที่ปลอดภัยคือ

  • ลบการอ่านแบบ fallback และยืนยันว่าไม่ปรากฏใน telemetry อีก
  • หยุดการเขียนแบบเก่าและลบโค้ด synchronization
  • ลบการอ้างอิงจากแอปทุกเวอร์ชันที่ deploy ได้
  • ลบดัชนีและ constraint ที่เลิกใช้ด้วยวิธีออนไลน์ที่เหมาะสม
  • ลบคอลัมน์หรือตารางเดิมในรีลีสฐานข้อมูลที่ตามมา

การลบคอลัมน์ PostgreSQL ส่วนใหญ่เป็นการเปลี่ยน catalog แต่ยังต้องใช้ ACCESS EXCLUSIVE lock ดังนั้น statement สั้นๆ อาจรอธุรกรรมที่ยาวและขวางงานถัดมา กำหนด lock timeout ตรวจธุรกรรมที่รันนานล่วงหน้า และกำหนดเวลาลองทำในช่วงที่มีความเสี่ยงต่ำกว่า

ใช้ DROP INDEX CONCURRENTLY กับดัชนีที่เลิกใช้ เมื่อยอมให้ขวางการเขียนไม่ได้ เช่นเดียวกับการสร้างพร้อมกัน คำสั่งนี้รันใน transaction block ไม่ได้ และมีข้อจำกัดที่เครื่องมือ migration ต้องรองรับ

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

ก่อนลบตาราง ตรวจ ownership ของ sequence, view, function, grant, trigger, replication publication และ query ภายนอก หลีกเลี่ยง CASCADE เพื่อเป็นทางลัดใน migration ระบบจริง เพราะมันอาจลบ dependency ที่ไม่อยู่ในขอบเขตของการเปลี่ยนครั้งนี้

รับมือการย้อนกลับและขั้นตอนที่ล้มเหลว

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

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

หาก backfill สร้างภาระ ให้พักงาน batch ที่ commit แล้วและ idempotent สามารถคงอยู่ได้ ลดขนาดหรืออัตรา batch แก้การแปลงที่แพง แล้วทำต่อจาก checkpoint การย้อน update ที่ถูกต้องหลายล้านแถวมักเพิ่มความเสี่ยงโดยไม่ช่วยให้ระบบจริงฟื้นตัว

หากเส้นทางอ่านใหม่ให้ผลผิด ให้ชี้การอ่านกลับไปยังข้อมูลรูปแบบเก่าโดยเก็บข้อมูลใหม่ไว้เพื่อวินิจฉัย ทำ dual-write ต่อเมื่อมั่นใจว่ามันถูกต้องเท่านั้น หากตัวเขียนเองมีปัญหา ให้ปิดมันหรือย้อนแอปก่อนซ่อมแถวที่ได้รับผลกระทบ

หลัง contract การกู้คืนอาจต้องคืนข้อมูล ไม่ใช่เพียง deploy build เก่า กำหนดจุดที่ย้อนกลับไม่ได้อย่างชัดเจน ทำ backup หรือ snapshot ตามนโยบายกู้คืนของระบบ ทดสอบการกู้คืนก่อนรีลีส และเก็บอ็อบเจ็กต์เก่าไว้ตามระยะเวลาที่ตกลงเมื่อค่าใช้พื้นที่ยอมรับได้

คำสั่งสคีมาอาจอยู่ใน transaction ได้ แต่ผลกระทบภายนอกไม่ได้ครอบคลุมเสมอไป งานดัชนีแบบพร้อมกัน ข้อความในคิว การเปลี่ยน cache และการ deploy แอปไม่ได้อยู่ใน transaction เดียวกัน runbook ควรอธิบายสถานะที่สังเกตได้หลังความล้มเหลวบางส่วน และคำสั่งที่ทำต่อจากจุดนั้นได้อย่างปลอดภัย

หลีกเลี่ยงกับดัก migration ที่พบบ่อย

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

  • เพิ่ม NOT NULL ขณะที่ instance ของแอปเก่ายังละเว้นฟิลด์ได้
  • รัน backfill ขนาดใหญ่ใน transaction เดียว ทำให้ lock และ row version ค้างนานเกินไป
  • เปลี่ยนชื่อคอลัมน์ราวกับเป็นการเพิ่ม ทั้งที่โค้ดเก่ายังใช้ชื่อเดิม
  • สลับการอ่านก่อนทุกเส้นทางการเขียนและแถวเก่าจะเติมข้อมูลรูปแบบใหม่
  • ถือว่า deploy สำเร็จเป็นหลักฐานว่ารายงาน worker replica และระบบเชื่อมต่อเข้ากันได้

ความล้มเหลวที่ละเอียดอ่อนอีกแบบมาจากการ sync สองทิศทาง trigger คัดลอก old_col ไป new_col ขณะที่โค้ดแอปคัดลอก new_col กลับไป old_col ความต่างของ normalization หรือลำดับ trigger อาจทำให้เกิดลูป เขียนทับค่าที่ตั้งใจไว้ หรือไม่ชัดว่าใครเป็นเจ้าของข้อมูล เลือกทิศทางเดียวและบันทึกว่ารูปแบบใดเป็นแหล่งอ้างอิงในแต่ละรีลีส

ค่าเริ่มต้นอาจซ่อนปัญหาที่ผู้เขียนข้อมูลยังไม่ได้อัปเดต หากคอลัมน์บังคับใหม่ได้รับค่าเริ่มต้นว่างหรือทั่วไป โค้ดเก่าจะดูเหมือนเข้ากันได้ทั้งที่เก็บข้อมูลผิดความหมาย ใช้ช่วงเปลี่ยนผ่านแบบยอมรับค่าว่างเมื่อการไม่มีค่าช่วยวินิจฉัยได้ แล้วค่อยบังคับกฎจริงหลังผู้เขียนทุกตัวส่งค่าที่มีความหมาย

feature flag ไม่ได้ทำให้คำสั่งสคีมาที่เข้ากันไม่ได้ปลอดภัยขึ้นเอง เส้นทางโค้ดที่ปิดไว้อาจยังถูกโหลด เตรียมไว้ หรือรันโดย process เก่า ฐานข้อมูลต้องเก็บอ็อบเจ็กต์ไว้จนกว่าไม่มีเวอร์ชันที่ deploy ได้หรือกำลังทำงานอ้างถึงมัน

ownership ของ migration ก็สำคัญ มอบหมายบุคคลหรือทีมหนึ่งให้ดูแลการเปลี่ยนผ่านจนถึง contract รวมถึงวันที่ตรวจสอบและลบ มิฉะนั้นคอลัมน์ชั่วคราว flag และงาน synchronization อาจค้างอยู่นานหลายเดือน เพิ่มต้นทุนให้การเปลี่ยนครั้งถัดไป

แทนที่คอลัมน์โทรศัพท์โดยไม่หยุดให้บริการ

สร้างต้นแบบด้วย PostgreSQL อย่างรวดเร็ว
สร้างแอปที่ใช้ Postgres และปรับสคีมาอย่างปลอดภัยระหว่างปล่อยงาน

การแทนที่ customers.phone ด้วย customers.phone_e164 ที่ทำให้เป็นมาตรฐาน ต้องมีคอลัมน์เพิ่ม นโยบายการแปลงที่กำหนดไว้ โค้ดที่เข้ากันได้ backfill ที่มีขอบเขต การสลับการอ่าน และการเก็บกวาดภายหลัง นโยบายการแปลงต้องมาก่อน SQL เพราะข้อมูลที่เก็บไว้ไม่ได้ทำให้เป็นมาตรฐานอัตโนมัติได้ทุกค่า

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

เพิ่มคอลัมน์พร้อม lock timeout สั้นๆ

BEGIN;
SET LOCAL lock_timeout = '2s';

ALTER TABLE customers
ADD COLUMN phone_e164 text;

COMMIT;

Deploy โค้ดที่ทำ input ใหม่ให้เป็นมาตรฐานและเขียน phone กับ phone_e164 ใน transaction เดียวกัน ให้การอ่านอยู่ที่ phone ในช่วงแรก อัปเดตผู้เขียนข้อมูลทุกตัว รวมถึงงานนำเข้าบัญชี เครื่องมือช่วยเหลือ worker job และ test ที่สร้าง customer fixture

ทำ backfill แถวที่เข้าเกณฑ์ด้วยธุรกรรมสั้นๆ บันทึก identifier ล่าสุดที่ประมวลผล จำนวนที่แปลง จำนวนที่ข้าม และเหตุผลของแต่ละหมวดความล้มเหลว จำกัดอัตรางานตาม latency ระบบจริงและ replica delay เมื่อการไล่ไปข้างหน้าเสร็จ ให้สแกน NULL ที่เข้าเกณฑ์อีกครั้งเพื่อจับ insert ที่เกิดพร้อมกันหรือแถวที่พลาดหลัง restart

รัน consistency check ด้วยกฎ normalization เดียวกับแอป แล้วสุ่มตรวจ prefix ต่างประเทศ, extension, ค่าว่าง, ระเบียนผู้ติดต่อซ้ำ และข้อมูลนำเข้าเก่า จำนวนแถวพิสูจน์ความครอบคลุม ไม่ได้พิสูจน์ว่าได้หมายเลขโทรศัพท์ที่ถูกต้อง

Deploy เส้นทางอ่านที่คืน phone_e164 เมื่อมีค่า และใช้ phone เฉพาะกรณียกเว้นที่มีการบันทึก ติดตามการใช้ fallback และข้อผิดพลาด normalization แก้ข้อยกเว้นที่เหลือ แทนการปล่อยให้ fallback กลายเป็นพฤติกรรมถาวร

เมื่อฟิลด์ใหม่เป็นแหล่งอ้างอิงแล้ว ให้ลบ fallback และหยุดเขียน phone สังเกต rare job และทราฟฟิกจากระบบเชื่อมต่อตลอดรอบการทำงานที่เหมาะสม เพิ่ม constraint ที่ผ่านการตรวจสอบแล้วเฉพาะเมื่อกฎของผลิตภัณฑ์กำหนด

สุดท้าย ลบการอ้างอิง phone จากโค้ด ลบดัชนีหรือ constraint ของมันแยกต่างหาก แล้วลบคอลัมน์ใน migration ถัดไปพร้อมเวลารอ lock ที่จำกัด หากการสลับการอ่านล้มเหลวก่อนลบคอลัมน์นั้นเมื่อใด ให้ย้อนพฤติกรรมของแอปได้ เพราะทั้งสองคอลัมน์ยังใช้งานได้

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

ตรวจทุกรีลีสก่อนปล่อย

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

ก่อน deploy ให้ยืนยันว่า

  • เวอร์ชันแอปทำงานกับสถานะฐานข้อมูลก่อนและหลังรีลีสนี้ได้
  • ตั้ง lock และ statement timeout สำหรับคำสั่งสคีมาที่อาจรอทราฟฟิกแล้ว
  • งาน backfill หรืองานตรวจสอบมีการติดตามความคืบหน้า พัก ทำต่อ และจำกัดอัตรา
  • dashboard ครอบคลุมข้อผิดพลาด latency lock ภาระฐานข้อมูล WAL และ replica delay
  • ทดสอบการย้อนกลับแล้วโดยไม่ต้องพึ่งอ็อบเจ็กต์ที่ลบไปแล้ว

บันทึกเงื่อนไขเสร็จสิ้นให้ชัดเจน ตัวอย่างคือไม่มี consistency failure ใหม่ตลอดหนึ่งรอบของ job ไม่มี fallback read จากทราฟฟิกที่เซิร์ฟเวอร์ควบคุม ผู้ใช้ที่รู้จักทุกส่วนอัปเกรดแล้ว และคิวรีตรวจสอบแบบควบคุมสำเร็จ เปอร์เซ็นต์ความคืบหน้ามีประโยชน์ระหว่าง backfill แต่ประมวลผลครบ 100 เปอร์เซ็นต์ไม่เท่ากับถูกต้องครบ 100 เปอร์เซ็นต์

ตรวจลำดับ migration แยกจาก code review ชุด SQL และการเปลี่ยนแอปอาจถูกต้องแต่ยังล้มเหลวได้ หาก deployment รันตามลำดับผิด ระบุว่าขั้นใดเริ่มได้ก็ต่อเมื่ออีกขั้นเสร็จแล้ว

เงื่อนไขหยุดควรเป็นตัวเลขเมื่อทำได้ กำหนด query latency, lock wait, replica delay, อัตราข้อผิดพลาด และระยะเวลา batch ที่รับได้ เมื่อข้ามเกณฑ์ ผู้ปฏิบัติงานควรรู้ว่าต้องพักงาน ยกเลิก statement ที่รอ หรือชี้การอ่านกลับ โดยไม่ต้องขออนุมัติใหม่ระหว่างเหตุการณ์

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

ทำให้กระบวนการทำซ้ำได้

runbook migration ที่นำกลับมาใช้ได้ทำให้ expand/contract กลายเป็นงานปล่อยรีลีสตามปกติ พร้อมเจ้าของและเกณฑ์ที่วัดได้ มันควรสั้นพอให้ทำตามได้ระหว่าง deploy จริง และเฉพาะเจาะจงพอจะอธิบายสถานะเมื่อบางส่วนล้มเหลว

ใช้ห้าส่วนใน runbook

  • Expansion: งานสคีมาที่ต้องทำ lock ที่คาดไว้ timeout และข้อกำหนดของ transaction
  • Compatibility: โค้ด ผู้เขียน ผู้อ่าน flag ไคลเอนต์ และลำดับ deploy ที่ได้รับผลกระทบ
  • Backfill: นโยบายการแปลง batching checkpoint การจำกัดความเร็ว และการจัดการข้อยกเว้น
  • Verification: การตรวจ SQL business invariant telemetry และเกณฑ์เสร็จสิ้น
  • Contraction: การลบ dependency ช่วงสังเกต การเก็บกวาดจริง และขีดจำกัดการกู้คืน

มอบหมายเจ้าของและวันที่คาดว่าจะเสร็จให้ทุกอ็อบเจ็กต์ระหว่างเปลี่ยนผ่าน ติดตามคอลัมน์ ดัชนี flag trigger และ job ไว้ในที่เดียวกัน การเก็บกวาดเป็นส่วนหนึ่งของ migration ไม่ใช่งานบำรุงรักษาที่เลือกทำหรือไม่ทำก็ได้

สำหรับทีมที่สร้างด้วย Koder.ai สามารถใช้ Planning Mode เพื่อเขียนรายละเอียดของแต่ละช่วงและ checkpoint ก่อนเริ่มเปลี่ยนระบบจริง การ export ซอร์สโค้ดยังช่วยให้ SQL migration และตรรกะความเข้ากันได้ผ่านการตรวจทานแบบเดียวกับโค้ดแอปส่วนอื่น Koder.ai รองรับ deployment, hosting, snapshot และ rollback แต่ไม่ควรคิดว่าการย้อนแอปจะย้อนการแปลงข้อมูลที่ commit แล้วได้เสมอ รักษาความเข้ากันได้ของสคีมาไว้จนแผนกู้คืนฐานข้อมูลไม่ต้องพึ่งข้อมูลรูปแบบเดิมอีกต่อไป

กำหนดงานที่เขียนข้อมูลมากในช่วงทราฟฟิกต่ำเมื่อทำได้ แต่อย่าใช้ช่วงเวลาเป็นมาตรการความปลอดภัยเพียงอย่างเดียว ธุรกรรมที่มีขอบเขต การจำกัดความเร็วตาม feedback ความคืบหน้าที่มองเห็นได้ และวิธีพักงานที่ผ่านการทดสอบ คือสิ่งที่ทำให้ online migration จัดการได้เมื่อทราฟฟิกหรือข้อมูลแตกต่างจากที่คาดไว้

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

เหตุใดการเปลี่ยนสคีมาจึงทำให้ระบบหยุดให้บริการได้?

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

รูปแบบการย้ายข้อมูลแบบ expand/contract คืออะไร?

Expand/contract แบ่งการเปลี่ยนแปลงที่เข้ากันไม่ได้ออกเป็นช่วงที่ปลอดภัย เริ่มจากเพิ่มโครงสร้างใหม่ จากนั้นย้ายโค้ดและข้อมูลไปใช้โครงสร้างนั้น แล้วค่อยลบโครงสร้างเดิมเมื่อผู้ใช้ทุกส่วนเลิกใช้งานแล้ว

จะเปลี่ยนชื่อหรือแทนที่คอลัมน์ฐานข้อมูลโดยไม่หยุดให้บริการได้อย่างไร?

เพิ่มคอลัมน์ใหม่ก่อนและเก็บคอลัมน์เดิมไว้ Deploy โค้ดที่ทำงานกับทั้งสองฟิลด์ได้ ทำ backfill แถวเดิมเป็น batch เล็กๆ สลับการอ่านหลังตรวจสอบแล้ว และลบคอลัมน์เดิมในรีลีสถัดไป

เพิ่มคอลัมน์ PostgreSQL โดยไม่ขวางทราฟฟิกได้หรือไม่?

โดยทั่วไปทำได้ คอลัมน์ที่อนุญาตให้เป็นค่าว่างและไม่มีค่าเริ่มต้นมักเป็นการเปลี่ยน metadata ระยะสั้นใน PostgreSQL แต่ยังต้องล็อกตาราง กำหนด lock timeout ให้สั้นเพื่อให้ migration ล้มเหลวแทนการรอธุรกรรมที่ยาวนาน

สร้างดัชนีโดยไม่ขวางการเขียนได้อย่างไร?

ใช้ CREATE INDEX CONCURRENTLY เมื่อตารางต้องยังเขียนได้ คำสั่งนี้ใช้เวลานานกว่าและเพิ่มภาระฐานข้อมูล อีกทั้งรันใน transaction block ไม่ได้ จึงควรติดตาม latency, WAL และ replica delay ระหว่างทำงาน

ควรให้แอปเขียนทั้งฟิลด์เก่าและใหม่เมื่อใด?

เขียนทั้งสองค่าในธุรกรรมฐานข้อมูลเดียวกันเมื่อข้อมูลทั้งสองรูปแบบต้องทันสมัยเสมอ กำหนดว่าฟิลด์ใดมีผลเมื่อค่าไม่ตรงกัน และใช้กฎ normalization เดียวกันใน API, worker, งานนำเข้า และเครื่องมือช่วยเหลือ

จะทำ backfill ตาราง PostgreSQL ขนาดใหญ่อย่างปลอดภัยได้อย่างไร?

ประมวลผลเป็น batch สั้นๆ ที่กลับมาทำต่อได้ และ commit หลังแต่ละ batch บันทึก checkpoint อัปเดตเฉพาะแถวที่ยังต้องทำ และชะลอหรือพักงานเมื่อ query latency, lock wait, ปริมาณ WAL หรือ replica lag สูงขึ้น

จะรู้ได้อย่างไรว่า backfill เสร็จและถูกต้อง?

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

ควรเพิ่ม NOT NULL, check constraint หรือ foreign key เมื่อใด?

เพิ่ม constraint ที่เข้มงวดหลังข้อมูลเดิมผ่านการตรวจสอบและผู้เขียนข้อมูลทุกตัวส่งค่าที่ถูกต้อง PostgreSQL ช่วยให้เพิ่ม constraint บางชนิดเป็น NOT VALID บังคับใช้กับแถวใหม่ และตรวจสอบแถวเก่าแยกต่างหากได้

เมื่อใดจึงปลอดภัยที่จะลบเส้นทางสคีมาเดิม?

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

Related posts