เพิ่งจ่ายเงินแต่ประวัติว่างเปล่า: หายนะ Replication Lag ที่ทำให้ลูกค้านึกว่าโดนโกง
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 20 วินาที
ลูกค้ากดยืนยันการสั่งซื้อสำเร็จ... หน้าเว็บเด้ง Redirect พาไปที่หน้า "ประวัติคำสั่งซื้อของฉัน" ทันที
สิ่งที่ลูกค้าเห็นคือ: "คุณยังไม่มีคำสั่งซื้อใดๆ" ความเงียบเข้าครอบงำ ลูกค้าเริ่มใจเสีย ตกใจว่าเงินถูกตัดไปแล้วแต่ของไม่ได้ เลยรีบกระหน่ำกด F5 รัวๆ...
และในวินาทีที่ 3 คำสั่งซื้อเจ้ากรรมก็โผล่ขึ้นมาหน้าตาเฉยราวกับไม่มีอะไรเกิดขึ้น! ลูกค้าคิดว่าเป็นบั๊กผีหลอก แต่ความจริงเบื้องหลังคือระบบเพิ่งโดนพิษของ Replication Lag เล่นงานเข้าเต็มเปา
🔀 สถาปัตยกรรมแยกงาน: Master รับเขียน - Replica รับอ่าน
เมื่อระบบมีคนใช้งานเยอะ สถาปัตยกรรมยอดฮิตคือการทำ Read/Write Splitting:
- Primary DB (Master): รับคำสั่ง
INSERT,UPDATE,DELETEเพื่อการันตีความถูกต้องของข้อมูล - Read Replicas (Slaves): ก๊อปปี้ข้อมูลจำลองไว้ 2-3 เครื่อง คอยรับคำสั่ง
SELECTเพื่อแบ่งเบาภาระการอ่าน
เพื่อให้เครื่อง Master ทำงานได้เร็วที่สุด กลไกการส่งข้อมูลไปหา Replica ส่วนใหญ่จึงทำงานแบบ Asynchronous Replication:
"เมื่อ Master บันทึกข้อมูลเสร็จ จะตอบกลับ Client ทันทีว่า 'สำเร็จ!' แล้วค่อยส่งสำเนา Binlog หรือ WAL ข้ามเครือข่ายไปให้เครื่อง Replica ไล่ตามบันทึกตามหลังอย่างใจเย็น"
⏱️ เสี้ยววินาทีมรณะ: The Lag Window
ระยะเวลาที่ Replica วิ่งตาม Master ทัน ปกติอาจจะเร็วแค่ 50 - 200 มิลลิวินาที
แต่พฤติกรรมของเว็บสมัยใหม่คือ:
- ผู้ใช้กดสั่งซื้อ ➔ ยิง
POST /ordersบันทึกลง Master DB (เสร็จใน 10ms) - แอปพลิเคชันตอบกลับ Frontend ➔ Frontend สั่ง Redirect ยิง
GET /ordersไปขอดูข้อมูลทันที - คำขอ
GETถูก Load Balancer ส่งไปดึงข้อมูลจาก Read Replica... - แต่ในเวลานั้น Replica ยังไม่ได้รับ Binlog จาก Master เลยด้วยซ้ำ!
Replica จึงตอบกลับมาด้วยความสัตย์จริงว่า "ยังไม่มีออเดอร์นี้ในตารางเลยครับ" เกิดเป็นภาวะที่เรียกว่า Inconsistent State ที่ทำลายความเชื่อมั่นของผู้ใช้ลงในพริบตา
🛡️ ท่ากู้ชีพ: Read-Your-Own-Writes Consistency
เราไม่จำเป็นต้องบังคับให้ระบบทั้งหมดกลายเป็น Synchronous ที่ช้าเป็นเต่า แต่ใช้เทคนิคจำเพาะเจาะจงเพื่อคุ้มครองประสบการณ์ของผู้ใช้:
1. ล็อกเป้าอ่าน Master ชั่วคราว (Sticky Routing Window):
เมื่อผู้ใช้คนใดก็ตามเพิ่งกระทำการ Write (สร้าง/แก้ไขข้อมูล) ให้เซฟ Cookie หรือ Session Flag ไว้สั้นๆ เช่น 5 วินาที:
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 กลายเป็นฝันร้ายที่ทำให้ผู้ใช้เข้าใจผิดว่าระบบทำงานผิดพลาด