Cache Avalanche: ตั้ง TTL เท่ากันเป๊ะ ทำไมกลายเป็นกับดักตั้งเวลาระเบิดถล่ม Database?
⏱️ ใช้เวลาอ่านประมาณ: 4 นาที 25 วินาที
เที่ยงคืนตรง ระบบรัน Batch Job วอร์มข้อมูลแคชสินค้าสำหรับแคมเปญวันใหม่ 200,000 รายการยัดใส่ Redis พร้อมคำสั่งมาตรฐานที่ดูเรียบร้อย:
redis.setEx(key, 7200, jsonPayload);
ตลอด 1 ชั่วโมง 59 นาทีแรก ระบบวิ่งฉลุย Response Time นิ่งสนิทที่ 15 มิลลิวินาที Hit Rate แตะ 99%...
แต่พอเข็มนาฬิกาเดินแตะ 02:00:00 น. เป๊ะ กราฟ CPU ของ PostgreSQL พุ่งทะยานจาก 12% ขึ้นไปแตะ 100% ในเวลาเพียง 3 วินาที! หน้าเว็บกลายเป็น 504 Gateway Timeout ทั้งระบบ และ Database เริ่มปฏิเสธทุกการเชื่อมต่อราวกับโดนยิง DDoS Attack ขนาดมหึมา!
❄️ หิมะถล่มพร้อมเพรียง: ปรากฏการณ์ Cache Avalanche
ในตอนที่ 30 เราพูดถึง Cache Penetration (การถามหาของที่ไม่มีอยู่จริง) และ Cache Breakdown (คีย์ยอดฮิตคีย์เดียวหมดอายุ)...
แต่ Cache Avalanche (หิมะถล่ม) คือขั้นกว่าของความวินาศ: มันคือภาวะที่ คีย์ข้อมูลปริมาณมหาศาล (หลักแสนหรือล้านคีย์) พร้อมใจกัน "หมดอายุพร้อมกันในวินาทีเดียวกัน"!
"เพราะทุกคีย์ถูกสร้างใน Batch เดียวกัน และได้อายุขัย 7,200 วินาทีเท่ากันเป๊ะ... ในวินาทีที่ 7,201 เกราะป้องกันที่ชื่อว่า Redis จึงอันตรธานหายไปพร้อมกันทั้งผืน เหลือเพียงความว่างเปล่า!"
เมื่อผู้ใช้งานยังคงคลิกดูสินค้าอย่างต่อเนื่อง ทราฟฟิกมหาศาลที่ไม่เจอแคช (Cache Miss) จึง พร้อมใจกันพุ่งตกลงไปที่ Database โดยตรงพร้อมกัน เหมือนกำแพงเขื่อนแตก ส่งผลให้ Thread Pool และ Connection Pool ของ Database ตันสนิทในพริบตาเดียว
🧱 ล้มเป็นโดมิโน: แม้จะรีสตาร์ต Database ก็ไม่รอด
สิ่งที่เลวร้ายที่สุดของ Cache Avalanche คือ การกู้ระบบคืนทำได้ยากมาก:
- วิศวกร On-call ตื่นมากดรีสตาร์ต Database Engine...
- ทันทีที่ Database ฟื้นขึ้นมา คำขอที่คั่งค้างนับหมื่นรายการก็กรูซ้ำเข้าไปขุดข้อมูลทันที
- Database สำลักตายซ้ำรอบสอง (Cascading Failure)
- เพราะไม่มีใครมีโอกาสเขียนแคชกลับขึ้น Redis สำเร็จเลยสักตัว!
🛡️ 2 คาถาปราบพายุหิมะ: TTL Jitter & Mutual Exclusion
1. โรยเกลือสุ่มเวลา (TTL Jitter / Random Expiration):
กฎเหล็กข้อแรกของการเขียนแคชคือ: ห้ามตั้งเวลาหมดอายุเป็นค่าคงที่ตายตัวเด็ดขาด! ให้บวกเวลาสุ่ม (Random Delta) เข้าไปเสมอ:
val baseTtl = 7200
val randomJitter = Random.nextInt(0, 600) // สุ่ม 0 ถึง 10 นาที
redis.setEx(key, baseTtl + randomJitter, payload)
ผลลัพธ์คือ คีย์ 200,000 รายการจะ ค่อยๆ ทยอยหมดอายุแบบกระจายตัว ในช่วงเวลา 10 นาที แทนที่จะหลุดพร้อมกันในวินาทีเดียว ทำให้ภาระงานที่ไหลลง Database กระจายเป็นเส้นตรงเรียบเนียน ไม่เกิดยอดสไปก์
2. ล็อกคอคนลงไปตักน้ำ (Mutex Lock / SingleFlight):
ในกรณีที่คีย์บางกลุ่มหมดอายุลงพร้อมกันจริง อย่าปล่อยให้ทุก Thread กรูลงไปถาม Database ให้ใช้กลไก Distributed Lock หรือ Pattern อย่าง Go Singleflight / Mutex:
- อนุญาตให้เพียง 1 Thread แรก มีสิทธิ์ลงไป Query ที่ Database เพื่อนำข้อมูลมาเติมใส่แคช
- ส่วน Thread ตัวอื่นๆ ที่เหลือ ให้ยืนรอคอยผลลัพธ์ชั่วคราว หรืออ่านค่า Stale Data เก่าไปก่อนชั่วครู่
💡 5-Min Takeaway
"ความสม่ำเสมอเป๊ะๆ คือความสวยงามในโค้ด แต่คือหายนะในโลก Distributed Cache" เมื่อใดก็ตามที่มีการกำหนดเวลาหมดอายุของข้อมูลปริมาณมาก จงใส่ Random Jitter เพื่อกระจายความเสี่ยงเสมอ การปล่อยให้ข้อมูลหมดอายุเหลื่อมล้ำกันทีละนิด จะช่วยรักษาชีวิตของ Database ให้อยู่รอดปลอดภัยในยามที่คลื่นทราฟฟิกซัดกระหน่ำ