☕ 5-Min Tech Story #40

Thundering Herd: เมื่อคีย์ทองคำหมดอายุเพียง 1 วินาที ทำไมคำขอหลักหมื่นถึงกรูลงไปรุมเหยียบ DB จนแบนติดดิน?

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

หน้าแรกของแอปช้อปปิ้งออนไลน์ มีแบนเนอร์แสดงโปรโมชัน Flash Sale ที่ทุกคนต้องเปิดดู... คีย์นี้ถูกแคชไว้ใน Redis สวยงาม มีผู้ใช้งานเปิดดูพร้อมกัน 15,000 ครั้งต่อวินาที

ทุกอย่างราบรื่น Hit Rate แตะ 100% จนกระทั่งถึงเวลา 12:00:00 น. ซึ่งเป็นเวลาที่แคชคีย์นี้หมดอายุ (TTL Expire) ลงตามที่ตั้งไว้...

ในเสี้ยววินาทีถัดมา: แทนที่จะมีแค่ 1 คำขอแอบเดินลงไปดึงข้อมูลใหม่จาก Database เงียบๆ... คำขอทั้ง 15,000 คนที่วิ่งมาไม่เจอแคชพร้อมกัน ดันพร้อมใจกันแปลงร่างเป็น "ฝูงควายป่าแตกตื่น" วิ่งกรูทะลุลงไป Query หน้าตารางเดียวกันที่ Database ในมิลลิวินาทีเดียวกันเป๊ะ!

CPU ของฐานข้อมูลพุ่งกระแทกเพดานทันที Connection Pool ตันสนิท และหน้าเว็บทั้งระบบล่มสลายในพริบตา!

🐃 ฝูงสัตว์แตกตื่น: The Thundering Herd Problem

คำว่า Thundering Herd (ฝูงสัตว์แตกตื่น) เดิมทีมาจากโลกของระบบปฏิบัติการ UNIX เมื่อมี Process จำนวนมากหลับรอคอย Event ชิ้นเดียวกัน พองานมาถึง OS กลับส่งสัญญาณปลุกทุกคนพร้อมกัน ทำให้ Process ทั้งหมดตื่นขึ้นมาแย่งของชิ้นเดียวจนกินแรง CPU เปล่าประโยชน์

ในโลกของ Web Architecture ยุคใหม่ ปรากฏการณ์นี้เกิดขึ้นเมื่อ:

"ทรัพยากรยอดนิยม (Hot Key) หมดอายุลงกะทันหัน ส่งผลให้ทราฟฟิกมหาศาลที่เคยถูกดูดซับด้วย Cache กรูลงไปยังบริการหลังบ้าน (Origin/Database) พร้อมเพรียงกันโดยไม่มีใครยอมเบรก!"

สิ่งที่น่าเจ็บปวดคือ: ทั้ง 15,000 คำขอต้องการผลลัพธ์หน้าตาเหมือนกันเป๊ะทุกไบต์! แต่ระบบกลับสั่งให้ Database ทำงานซ้ำเดิมหมื่นห้าพันรอบในเสี้ยววินาทีเดียว

🛡️ 3 คาถาสยบฝูงสัตว์: Singleflight, Probabilistic Early Expiration & Lock

1. ไม้ตายของ Go: golang.org/x/sync/singleflight (รวมงานเป็นคำขอเดียว):

Singleflight คือกลไกขั้นเทพที่ทำงานในระดับ Application Memory: ถ้ามี 10,000 Goroutine พยายามขอดึงคีย์ banner:flashsale ในจังหวะเดียวกัน...

// ตัวอย่าง Go Singleflight
var requestGroup singleflight.Group

val, err, _ := requestGroup.Do("banner:flashsale", func() (interface{}, error) {
  // คำสั่งนี้จะถูกรันแค่ 1 ครั้งเท่านั้น ไม่ว่าจะมีคนเรียกพร้อมกันกี่หมื่นคน!
  return fetchBannerFromDatabase()
})

2. ชิงชาร์จพลังก่อนหมดเวลา (XFetch / Probabilistic Early Expiration):

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

3. ปิดกั้นด้วย Distributed Mutex:

ในภาษาอย่าง Java หรือ Node.js ให้ใช้ Redis SET NX EX เพื่อให้เฉพาะ Worker ที่แย่งกุญแจได้ตัวแรกเป็นคนลงไปดึงข้อมูล ส่วนคนอื่นที่ไม่ได้กุญแจ ให้ยอมส่ง Stale Cache (ข้อมูลเก่าที่ค้างอยู่) กลับไปให้ผู้ใช้ชั่วคราว ดีกว่าปล่อยให้หน้าเว็บค้าง

💡 5-Min Takeaway

"งานที่ซ้ำซ้อนกันในเสี้ยววินาทีเดียวกัน ไม่ควรถูกทำซ้ำเป็นครั้งที่สอง" เมื่อระบบมี Hot Key ที่คนเปิดดูมหาศาล การมีแค่ Cache เพียงอย่างเดียวไม่พอ ต้องมีกลไก Request Collapsing / Singleflight หรือการทำ Background Refresh คอยคุมบังเหียนเสมอ เพื่อป้องกันไม่ให้คลื่นมนุษย์หลุดเข้าไปเหยียบฐานข้อมูลจนล้มทั้งระบบ