Cache เร็ว… จนกว่าจะหมดอายุ: ว่าด้วยเรื่อง Cache Stampede | MCT
Engineering Culture

Cache เร็ว… จนกว่าจะหมดอายุ

ว่าด้วยเรื่อง Cache Stampede – ปัญหาที่ซ่อนอยู่หลังตัวเลข Response Time สวย ๆ

MCT Engineering อ่าน 8 นาที Performance Engineering · Load Test · Caching
TTL
Cache เร็ว… จนกว่าจะหมดอายุ
Cache Stampede a.k.a. Thundering Herd
5 มิลลิวินาทีคือ Response Time หลังใส่ Cache
แต่วินาทีที่ Cache หมดอายุ ระบบเร็วที่สุดของคุณอาจกลายเป็นระบบที่ช้าที่สุดได้ในพริบตา
5 มิลลิวินาที

นั่นคือ Response Time ของ API หน้าสินค้า หลังจากทีมใส่ Cache เข้าไป จากเดิมที่ Query ฐานข้อมูลทีละ 400 มิลลิวินาที ตอนนี้เร็วขึ้นถึง 80 เท่า Load Test ผ่านฉลุย กราฟเรียบเป็นเส้นตรง ทุกคนพอใจ แล้วระบบก็ขึ้น Production

แต่มีคำถามข้อหนึ่งที่แทบไม่มีใครถามในห้องประชุมวันนั้น

“แล้ว 5 มิลลิวินาทีนี้ จะเป็นยังไง ในวินาทีที่ Cache หมดอายุ?”
⚠️Cache แทบทุกตัวต้องมีวันหมดอายุ เพื่อไม่ให้ข้อมูลเก่าค้างอยู่นานเกินไป และวินาทีที่มันหมดอายุนี่เอง คือจุดที่ ระบบเร็วที่สุดของคุณอาจกลายเป็นระบบที่ช้าที่สุดได้ในพริบตา

015 นาทีในชีวิตของ Cache หนึ่งตัว

สมมติว่า Cache ของหน้าสินค้ายอดนิยมตั้งอายุไว้ 5 นาที และมีคนเปิดหน้านี้ 500 ครั้งต่อวินาที เรามาลองไล่ดูทีละช่วงเวลากัน

  • นาทีที่ 00:00 — Request แรกวิ่งไปฐานข้อมูล ใช้เวลา 400 มิลลิวินาที ได้ข้อมูลกลับมาแล้วเก็บลง Cache
  • นาทีที่ 00:01 ถึง 04:59 — ช่วงเวลาทอง ทุก Request ได้ Response จาก Cache ภายในไม่กี่มิลลิวินาที ฐานข้อมูลแทบไม่ต้องทำอะไรเลย นี่คือช่วงที่ Load Test ส่วนใหญ่วัดได้
  • นาทีที่ 05:00 — Cache หมดอายุ Request ถัดไปหาข้อมูลไม่เจอ จึงวิ่งไปถามฐานข้อมูล
  • อีก 400 มิลลิวินาทีถัดมา — ระหว่างที่ Request แรกยังรอ Response มี Request ใหม่เข้ามาอีกราว 200 รายการ ทุกรายการหา Cache ไม่เจอเหมือนกัน ทุกรายการจึงวิ่งไปถามฐานข้อมูลด้วยคำถามเดียวกันพร้อม ๆ กัน
  • วินาทีต่อจากนั้น — ฐานข้อมูลรับงานซ้ำซ้อนหลายร้อยงานจนตอบช้าลง ยิ่งช้า Request ที่ค้างก็ยิ่งกองสูง Connection Pool เริ่มเต็ม บาง Request Timeout แล้ว Retry ซ้ำ ลูกค้าเห็นหน้าเว็บหมุนค้าง

แล้วทุกอย่างก็วนกลับไปเริ่มใหม่ในอีก 5 นาทีข้างหน้า

อาการนี้มีชื่อว่า Cache Stampede หรือ Thundering Herd เหมือนฝูงสัตว์ที่วิ่งกรูไปทางเดียวกันพร้อมกัน บนกราฟจะเห็นเป็นยอดแหลมพุ่งขึ้นเป็นจังหวะ ตรงกับรอบหมดอายุพอดี

☕หากจะเทียบให้เห็นภาพ: ร้านกาแฟที่ชงชาเย็นไว้เป็นเหยือก ขายได้เร็วมาก จนชาหมดตอน 8 โมงตรง พนักงานทุกคนหันไปต้มชาเหยือกใหม่พร้อมกัน คนละเหยือกให้ลูกค้าคนละคน เตาไม่พอ คิวทั้งร้านหยุดนิ่ง
💡 Fun Fact

ในโลก .NET เมธอด GetOrCreateAsync ของ IMemoryCache ที่ใช้กันแพร่หลาย ไม่ได้กันปัญหานี้ให้ ถ้าหลาย Request เข้ามาพร้อมกันตอน Cache ว่าง โค้ดดึงข้อมูลก็อาจถูกเรียกซ้ำพร้อมกันได้หลายครั้ง ชื่อเมธอดดูเหมือนจะจัดการให้ครบ แต่ความเป็นจริงไม่ใช่แบบนั้น

02Check List: Load Test ของคุณมองเห็น Stampede ไหม?

ลองติ๊กดูว่าการทดสอบรอบล่าสุดของทีมตรงกับข้อไหนบ้าง ยิ่งติ๊กเยอะ ยิ่งมีโอกาสที่ปัญหานี้ซ่อนอยู่โดยที่ไม่มีใครเห็น

  • Data Test ชุดเล็กๆ และวนซ้ำ — Script ใช้รหัสสินค้าไม่กี่สิบตัวหมุนไปเรื่อย ๆ ไม่กี่วินาทีทุกอย่างก็อยู่ใน Cache หมด ที่เหลือคือการวัดความเร็วของ Cache ไม่ใช่ของระบบ
  • รอบรันสั้นกว่ารอบหมดอายุ Cache — อาจจะถูกตั้งให้หมดอายุทุก 180 นาที แต่ Load Test รันแค่ 60 นาที เท่ากับไม่เคยเห็นช่วงหมดอายุเลยสักครั้ง
  • ตัดช่วง Warm-up ทิ้ง — ช่วยให้ตัวเลขนิ่งก็จริง แต่ช่วงที่ตัดทิ้งคือช่วงที่ Cache ว่างเปล่าพอดี
  • ดูแค่ค่าเฉลี่ยหรือตัวเลขสรุป — ยอดแหลมไม่กี่วินาทีจะหายไปเมื่อเฉลี่ยรวมกับ Request เร็ว ๆ อีกเป็นแสนรายการ
  • ไม่ได้ดูฝั่งฐานข้อมูลระหว่างทดสอบ — วัดแค่ Response Time ที่ปลายทาง ไม่ได้ดูจำนวน Query และ Connection Pool
💡ถ้าติ๊กตั้งแต่สองข้อขึ้นไป ไม่ได้แปลว่าระบบมีปัญหาแน่นอน แต่แปลว่า การทดสอบของคุณยังตอบไม่ได้ ว่ามีหรือไม่มี

033 วิธีรับมือ เทียบกันให้เห็นชัด ๆ

ปัญหานี้มีวิธีแก้ที่รู้จักกันดี แต่ละวิธีมีข้อแลกเปลี่ยนต่างกัน ทีมส่วนใหญ่ใช้หลายวิธีร่วมกัน

วิธี หลักการ ถ้าเป็นร้านชา ข้อแลกเปลี่ยน
Single-flight
(ล็อกต่อ Key)
เมื่อ Cache หมดอายุ ให้ Request แรกไปดึงข้อมูลใหม่คนเดียว Request อื่นรอรับคำตอบชุดเดียวกัน ใน .NET ใช้ได้ผ่าน package HybridCache (เปิดตัวในยุค .NET 9 และรองรับ .NET รุ่นเก่าด้วย) ให้พนักงานคนเดียวต้มเหยือกใหม่ คนอื่นรอเทให้ลูกค้า ลูกค้าที่มาช่วงนั้นยังต้องรอหนึ่งรอบ และกลไกนี้ทำงานแยกในแต่ละ Server ถ้ามี 10 Server ฐานข้อมูลยังโดนได้สูงสุด 10 Query ต่อ Key ซึ่งดีกว่า 200 มาก แต่ไม่ใช่ 1
Background Refresh /
Stale-While-Revalidate
รีเฟรช Cache ล่วงหน้าก่อนหมดอายุ หรือส่งข้อมูลเก่าไปก่อนระหว่างที่ดึงข้อมูลใหม่ ต้มเหยือกใหม่ตั้งแต่เหยือกเก่ายังเหลือครึ่ง ข้อมูลอาจเก่ากว่าเดิมไม่กี่วินาที ต้องดูว่าธุรกิจยอมรับได้ไหม และ HybridCache ไม่ได้ทำส่วนนี้ให้อัตโนมัติ ต้องเขียนเองหรือใช้ไลบรารีอย่าง FusionCache
Jitter
(สุ่มเวลาหมดอายุ)
บวกลบเวลาหมดอายุเล็กน้อย ไม่ให้ Cache ทุกตัวหมดอายุในวินาทีเดียวกัน ไม่ให้ทุกเหยือกหมดพร้อมกันตอน 8 โมงตรง ไม่ได้แก้ Stampede ของ Key ยอดนิยมตัวเดียว แค่กระจายภาระโดยรวม

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

044 Scenario ที่ควรเพิ่มในการทดสอบรอบหน้า

ไม่ต้องเปลี่ยนเครื่องมือ k6, JMeter, NeoLoad หรือ LoadRunner ก็ยังทำได้ทั้งหมด สิ่งที่เปลี่ยนคือการออกแบบ Scenario

  1. Cold Start — ล้าง Cache ก่อนเริ่ม แล้วยิงโหลดเต็มทันทีโดยไม่มีช่วงอุ่นเครื่อง จำลองวินาทีหลัง Deploy หรือหลัง Server Restart
  2. Expiry Window — รันให้ยาวกว่ารอบหมดอายุอย่างน้อยสองถึงสามรอบ แล้วดูกราฟ Response Time ตามเวลา ถ้ามียอดแหลมเกิดเป็นจังหวะสม่ำเสมอ นั่นคือสัญญาณ
  3. Realistic Data Mix — ใช้ Data Test ที่มีทั้งสินค้ายอดนิยมและสินค้าที่นาน ๆ ทีจะมีคนเปิด ให้ได้ Cache Hit และ Cache Miss ในสัดส่วนใกล้ของจริง
  4. Hot Key Burst — ยิง Request จำนวนมากเข้า Key เดียวกันพร้อมกันในจังหวะที่ Key นั้นเพิ่งหมดอายุ เพื่อพิสูจน์ว่ากลไกป้องกันในตารางด้านบนทำงานจริง

ระหว่างรันทุก Scenario ให้เฝ้าดู Cache Hit Ratio, จำนวน Query ที่เข้าฐานข้อมูล และ Connection Pool ไปพร้อมกับ Response Time ถ้า Query พุ่งขึ้นหลายร้อยรายการในวินาทีเดียว คุณเจอ Stampede แล้ว

💡 Tip

ถ้าอยากเห็นผลเร็ว ให้ลดอายุ Cache ใน Test Environment ลงชั่วคราว เช่น จาก 30 นาทีเหลือ 1 นาที จะได้เห็นหลายรอบหมดอายุภายใน Load Test ที่สั้นลง แค่อย่าลืมว่าตัวเลขที่ได้เป็นการจำลอง ไม่ใช่ค่าจริงของ Production

05คำถามที่ควรถามในประชุมครั้งหน้า

กลับไปที่ห้องประชุมวันที่ทุกคนปรบมือให้ตัวเลข 5 มิลลิวินาที ถ้าย้อนเวลาได้ คำถามที่ควรถามคือ

  • Cache ของเราหมดอายุทุกกี่นาที และ Load Test รันนานกว่านั้นหรือเปล่า?
  • ถ้า Cache ว่างทั้งหมดในวินาทีเดียว ฐานข้อมูลรับได้กี่ Request พร้อมกัน?
  • เรามีกลไกอะไรกันไม่ให้หลายร้อย Request วิ่งไปถามเรื่องเดียวกัน และเคยพิสูจน์แล้วหรือยังว่ามันทำงาน?

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

“ตัวเลขที่เร็วที่สุดใน Load Test ไม่ได้บอกว่าระบบแข็งแรงแค่ไหน ตัวเลขในวินาทีที่ทางลัดหายไปต่างหากที่บอก“

ถ้าบทความนี้ทำให้นึกถึงระบบไหนขึ้นมา ลองส่งต่อให้ทีมที่ดูแลระบบนั้น แล้วตั้งคำถามสามข้อด้านบนในการประชุมรอบหน้าดู

อ่านเพิ่มเติม