☕ 5-Min Tech Story #31

เพิ่งจ่ายเงินแต่ประวัติว่างเปล่า: หายนะ Replication Lag ที่ทำให้ลูกค้านึกว่าโดนโกง

⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 20 วินาที

ลูกค้ากดยืนยันการสั่งซื้อสำเร็จ... หน้าเว็บเด้ง Redirect พาไปที่หน้า "ประวัติคำสั่งซื้อของฉัน" ทันที

สิ่งที่ลูกค้าเห็นคือ: "คุณยังไม่มีคำสั่งซื้อใดๆ" ความเงียบเข้าครอบงำ ลูกค้าเริ่มใจเสีย ตกใจว่าเงินถูกตัดไปแล้วแต่ของไม่ได้ เลยรีบกระหน่ำกด F5 รัวๆ...

และในวินาทีที่ 3 คำสั่งซื้อเจ้ากรรมก็โผล่ขึ้นมาหน้าตาเฉยราวกับไม่มีอะไรเกิดขึ้น! ลูกค้าคิดว่าเป็นบั๊กผีหลอก แต่ความจริงเบื้องหลังคือระบบเพิ่งโดนพิษของ Replication Lag เล่นงานเข้าเต็มเปา

🔀 สถาปัตยกรรมแยกงาน: Master รับเขียน - Replica รับอ่าน

เมื่อระบบมีคนใช้งานเยอะ สถาปัตยกรรมยอดฮิตคือการทำ Read/Write Splitting:

เพื่อให้เครื่อง Master ทำงานได้เร็วที่สุด กลไกการส่งข้อมูลไปหา Replica ส่วนใหญ่จึงทำงานแบบ Asynchronous Replication:

"เมื่อ Master บันทึกข้อมูลเสร็จ จะตอบกลับ Client ทันทีว่า 'สำเร็จ!' แล้วค่อยส่งสำเนา Binlog หรือ WAL ข้ามเครือข่ายไปให้เครื่อง Replica ไล่ตามบันทึกตามหลังอย่างใจเย็น"

⏱️ เสี้ยววินาทีมรณะ: The Lag Window

ระยะเวลาที่ Replica วิ่งตาม Master ทัน ปกติอาจจะเร็วแค่ 50 - 200 มิลลิวินาที

แต่พฤติกรรมของเว็บสมัยใหม่คือ:

  1. ผู้ใช้กดสั่งซื้อ ➔ ยิง POST /orders บันทึกลง Master DB (เสร็จใน 10ms)
  2. แอปพลิเคชันตอบกลับ Frontend ➔ Frontend สั่ง Redirect ยิง GET /orders ไปขอดูข้อมูลทันที
  3. คำขอ GET ถูก Load Balancer ส่งไปดึงข้อมูลจาก Read Replica...
  4. แต่ในเวลานั้น Replica ยังไม่ได้รับ Binlog จาก Master เลยด้วยซ้ำ!

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

🛡️ ท่ากู้ชีพ: Read-Your-Own-Writes Consistency

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

1. ล็อกเป้าอ่าน Master ชั่วคราว (Sticky Routing Window):

เมื่อผู้ใช้คนใดก็ตามเพิ่งกระทำการ Write (สร้าง/แก้ไขข้อมูล) ให้เซฟ Cookie หรือ Session Flag ไว้สั้นๆ เช่น 5 วินาที:

// ตรรกะการเลือก DataSource
if (user.hasRecentlyModifiedData(withinSeconds = 5)) {
  // บังคับให้อ่านจาก Master DB โดยตรง ชัวร์ 100% ว่าเห็นของใหม่แน่!
  return masterDataSource
} else {
  // ผู้ใช้ทั่วไปที่เข้ามาดูเฉยๆ ส่งไปอ่าน Read Replica ตามปกติ
  return readReplicaDataSource
}

2. แอบรอด้วย GTID / Log Sequence Number (Causal Read):

ระบบขั้นสูงจะคืนค่า Version Token (เช่น MySQL GTID) กลับมาพร้อมคำตอบตอน Save เมื่อหน้าบ้านจะอ่านต่อ จะแนบ Token นี้ไปด้วย... ถ้า Replica เช็กแล้วพบว่าตัวเองยังเล่นข้อมูลไม่ถึงเวอร์ชันนี้ มันจะหน่วงเวลารออีก 50ms ให้ทัน หรือเตะคำขอนั้นกลับไปถาม Master แทน

💡 5-Min Takeaway

"ผู้ใช้งานยอมรับได้ถ้าเห็นข้อมูลของคนอื่นช้าไป 1 วินาที แต่จะรับไม่ได้ทันทีถ้าเห็นข้อมูลที่ 'ตัวเองเพิ่งทำเอง' หายไป" การแบ่งเบาโหลดด้วย Read Replica ต้องมาคู่กับกลยุทธ์ Read-Your-Own-Writes เสมอ อย่าปล่อยให้ช่วงเวลาเหลื่อมล้ำระดับเสี้ยววินาทีของ Replication กลายเป็นฝันร้ายที่ทำให้ผู้ใช้เข้าใจผิดว่าระบบทำงานผิดพลาด