ถ้าใครเคยออกแบบ URL Shortener มาก่อน เราคงรู้ว่าบริการอย่าง TinyURL หรือ Bitly ทำงานอย่างไร

นั่น คือ รับ URL ยาวๆ มาสร้างเป็น URL สั้นๆ เก็บ mapping ระหว่าง 2 อันไว้ แล้ว redirect ผู้ใช้กลับไปยัง URL ต้นทางเมื่อมีคนคลิก

ฟังดูตรงไปตรงมา

หลายๆ คนชอบหยุดไว้แค่นี้ แต่โจทย์ระบบจริงมันไม่ได้หยุดอยู่แค่นั้น สิ่งที่คนออกแบบระบบต้องตอบให้ได้ คือ ระบบจะเป็นอย่างไรเมื่อ traffic พุ่งขึ้นแบบไม่ทันตั้งตัว

ระบบจะรอดไหม ถ้าจู่ๆ traffic เพิ่มขึ้น 1000 เท่า จะรับมือได้ไหมถ้ามี URL เดียวกลายเป็นไวรัลข้ามคืน และจะยังให้บริการผู้ใช้หลักล้านได้ต่อเนื่องโดยไม่ล่มหรือเปล่า

บทความนี้ถอดสถานการณ์จริงที่มักถูกยกมาเป็นโจทย์สัมภาษณ์ system design ออกมาเป็นบทวิเคราะห์ ตั้งแต่การหาสาเหตุ ไปจนถึงการออกแบบสถาปัตยกรรมใหม่ให้เหตุการณ์แบบนี้ไม่เกิดซ้ำ


สถานการณ์: URL เดียวรับ 25 ล้านคลิกใน 5 นาที

ลองนึกภาพว่าทีมของเราสร้าง URL Shortener แบบเดียวกับ TinyURL ขึ้นมา ระบบให้บริการ URL หลายล้านตัวต่อวัน โดยทั่วไปเรามักจะเห็นการออกแบบไว้ประมาณนี้

การออกแบบนี้มันสามารถทำงานได้อย่างราบรื่น ไม่มีปัญหาอะไร จนถึงคืนหนึ่งดันมีคนเซเลปที่มีผู้ติดตามหลายล้านคนได้แชร์ URL สั้นๆ ตัวหนึ่งของเราลงโซเชียลมีเดีย ภายในห้านาที URL ตัวเดียวนั้นรับ redirect request เกือบ 25 ล้านครั้ง จน dashboard เริ่มโชว์ latency ที่สูงขึ้น ผู้ใช้เริ่มรายงานว่าใช้งานไม่ได้ และบริการ redirect เริ่ม timeout

คำถาม คือ จะไล่หาสาเหตุอย่างไร และ จะออกแบบระบบใหม่อย่างไร ให้ทนต่อสถานการณ์แบบนี้


อย่ารีบแก้ ให้หา bottleneck ก่อน

ข้อผิดพลาดที่ควรเลี่ยงที่สุด คือ การกระโดดไปหาทางแก้ทันที เมื่อระบบ production ล่มเพราะ traffic หนัก สิ่งแรกที่ควรทำ คือ ทำความเข้าใจว่า bottleneck อยู่ตรงไหน

ในกรณีของ use case นี้ การ redirect ของ URL Shortener เป็น operation ที่เรียบง่ายมาก ไม่มี business logic ซับซ้อน ทุก request ถามคำถามเดียวกันหมด คือ "URL สั้นๆ ตัวนี้ ชี้ไปที่ URL ต้นทางอะไร"

ด้วยความเรียบง่ายนี้เอง เมื่อระบบล่มภายใต้ traffic หนัก สาเหตุมักมาจากคอขวดของ infrastructure ไม่ใช่ตรรกะของแอปพลิเคชัน ถ้าเรา workaround โดยการ scale เครื่องปัญหานี้ก็ยังไม่ได้หายไป หรือ การออกแบบก็ดูดีไม่น่าผิดพลาดอะไร

ดังนั้น ก่อนจะรื้อออกแบบอะไร ควรไล่ดู request lifecycle ก่อน

จากเส้นทางของ request เมื่อผู้ใช้คลิก URL สั้น ก็ตรงไปตรงมา ทุก redirect คือ การ lookup ครั้งเดียวเท่านั้น ไม่มี logic อื่นเจือปน ยิ่งเราตอบ lookup นี้ได้เร็วเท่าไรก็ยิ่งดีเท่านั้น


Root cause คือ Hot Key

แล้วทำไมสถาปัตยกรรมที่ดูดีนี้ถึงล่ม คำตอบ คือ ทุก request ถามหา key เดียวกันเป๊ะๆ เช่น abc123

แทนที่จะเป็น request หลายล้านตัวที่กระจายไปหา key ต่างกัน กลับกลายเป็น request หลายล้านตัวถล่มเข้าหา key ตัวเดียว ปรากฏการณ์นี้เรียกว่า Hot Key

แม้ Redis จะเร็วมาก แต่เพราะ key แต่ละตัวถูก map ไปอยู่ที่ node เดียว

ผล ก็คือ node ที่ถือ abc123 โดนถล่มจนล้น

ในขณะที่ Redis node อื่นๆ แทบว่างงาน traffic ไม่ได้กระจาย มันไปกองอยู่ที่จุดเดียว


ฐานข้อมูล คือ เหยื่อ ไม่ใช่ต้นเหตุ

เมื่อ Redis node นั้นล้น การ lookup ก็ช้าลง บาง request เริ่ม timeout request ที่ timeout เหล่านั้นก็ตกลงไปหาฐานข้อมูลแทน

ผลลัพธ์ คือ จากที่ฐานข้อมูลเคยรับ query แค่หลักพัน จู่ๆ มันต้องรับ query หลักล้าน แล้วทุกอย่างก็เริ่มพังไปเป็นลูกโซ่

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

ถ้าเราแก้ที่ปลายเหตุ (ฐานข้อมูล) โดยไม่แตะต้นเหตุ (Hot Key ที่ cache) ปัญหาก็จะกลับมาอีก


ดัน data ให้ใกล้ผู้ใช้ที่สุดด้วย CDN

ชั้นแรกที่ควรปรับปรุง คือ caching แต่แทนที่จะให้ Redis ตอบทุก request เราควรดันข้อมูลออกไปให้ใกล้ผู้ใช้มากขึ้นอีก

นั่นคือ การใช้ CDN

หลายคนคิดว่า CDN มีไว้ cache แค่รูปภาพกับวิดีโอ แต่จริงๆ แล้ว response ของ redirect ก็ cache ได้ สำหรับ URL ยอดนิยม CDN edge server สามารถส่ง HTTP redirect กลับให้ผู้ใช้ได้โดยตรง โดยไม่ต้องติดต่อ backend ของเราเลย

ผล ก็คือ request หลายล้านตัวไม่มีวันวิ่งมาถึงเซิร์ฟเวอร์ของเราด้วยซ้ำ traffic ก้อนใหญ่ที่สุดถูกกันไว้ตั้งแต่ขอบเครือข่าย


ถ้า redirect cache ไปที่ CDN ไม่ได้ Redis ก็กลายเป็นตัวชี้ขาด

ในกรณีที่ redirect บางตัว cache ที่ CDN ไม่ได้ Redis ก็กลายเป็นชั้นสำคัญ ทางเลือกแรกที่หลายคนนึกถึง คือ เปลี่ยนจาก Redis instance เดียวมาเป็น Redis Cluster

แต่ Redis Cluster เพียงอย่างเดียว ไม่ได้แก้ปัญหา Hot Key เพราะ key ตัวเดิมก็ยังถูก map ไปอยู่ shard เดียวอยู่ดี shard นั้นจึงยังคงถูกถล่มเหมือนเดิม การเพิ่ม shard ช่วยเรื่องกระจาย key หลายตัว แต่ไม่ช่วยเมื่อ traffic ทั้งหมดกระจุกอยู่ที่ key ตัวเดียว


3 กลยุทธ์จัดการ Hot Key

เมื่อ Redis Cluster เพียวๆ เอาไม่อยู่ มี 3 กลยุทธ์ที่ใช้จัดการ Hot Key ได้

1. Local In-Memory Cache — ให้ redirect service แต่ละ instance เก็บ URL ยอดนิยมไว้ใน memory ของตัวเอง เมื่อ request วิ่งเข้ามาที่ instance ไหน ก็ตอบได้จาก memory ในเครื่องเลย ทำให้ Redis ถูกเรียกใช้เพียงเป็นครั้งคราวเท่านั้น เป็นการลดภาระที่ตัวกลางลงอย่างมาก

2. Cache Replication — ทำสำเนา key ยอดนิยมไปไว้บน Redis หลาย node แทนที่จะให้มันอยู่บน shard เดียว เมื่อ key เดียวกันมีอยู่หลายที่ traffic ที่เคยกองอยู่จุดเดียวก็กระจายออกไปได้

3. Request Coalescing — ถ้ามี backend request หนึ่งกำลังดึงข้อมูล URL อยู่แล้ว request อื่นๆ ที่ถามหา key เดียวกันจะ "รอ" ผลลัพธ์ของ request นั้นร่วมกัน แทนที่ต่างคนต่างยิง query ซ้ำ ๆ ลงฐานข้อมูล เทคนิคนี้ป้องกันไม่ให้เกิด query ซ้ำซ้อนมหาศาลเมื่อ cache miss พร้อมกัน

Request Coalescing

Stateless คือ หัวใจของการ horizontal scale

เมื่อ traffic เพิ่มขึ้นอีกเท่าตัว สิ่งที่ต้อง scale คือ redirect service และหลักการสำคัญ คือ redirect service ต้องเป็น stateless เสมอ

เมื่อ service ไม่เก็บ state ใดๆ ไว้ในตัว การ scale แนวนอนก็ทำได้ง่าย เมื่อ CPU สูงขึ้น Kubernetes หรือ Auto Scaling Group ก็แค่เปิด redirect instance เพิ่ม โดยไม่ต้องย้าย session ไปมา ทุก instance เท่าเทียมกันหมด รับ request ตัวไหนก็ตอบได้เหมือนกัน


ฐานข้อมูล scale ทีหลัง และควร source of truth เท่านั้น

การ scale ฐานข้อมูลช่วยได้ก็จริง แต่ควรทำ หลัง ปรับ cache ให้ดีแล้วเท่านั้น เพราะ URL Shortener เป็นระบบที่ read-heavy อย่างมาก อุดมคติของระบบแบบนี้ คือ

  • request ราว 99.9% ไม่ควรแตะฐานข้อมูลเลย
  • Redis รับภาระเกือบทั้งหมด
  • ฐานข้อมูลทำหน้าที่เป็นแหล่งความจริง (source of truth) เท่านั้น

ถ้าหลังปรับ cache แล้ว การอ่านจากฐานข้อมูลยังสูงอยู่ ค่อยเพิ่ม read replica โดยให้การเขียนวิ่งไปที่ primary ส่วนการอ่านกระจายไปยัง replica ต่าง ๆ ซึ่งเข้ากับธรรมชาติ read-heavy ของระบบพอดี


ตรวจจับก่อนผู้ใช้บ่น: ตัวชี้วัดที่ต้องเฝ้า

การรอให้ผู้ใช้รายงานปัญหานั้นสายเกินไป ทางที่ดี คือ เฝ้าตัวชี้วัดเหล่านี้ เพื่อจับ URL ที่กำลังจะไวรัลให้ทันก่อน

  • Redis Hot Key metrics
  • Cache hit ratio
  • Redirect latency (P95/P99)
  • Database QPS
  • Connection pool usage
  • CDN cache hit percentage
  • Error rate
  • Auto Scaling events

ทันทีที่ URL ตัวใดตัวหนึ่งเริ่มสร้าง traffic ผิดปกติ เราสามารถ replicate key นั้นหรือ preload cache ล่วงหน้าได้ (เชิงรุก) ก่อนที่มันจะลุกลามเป็นปัญหา


ลำดับการไล่แก้ปัญหา

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

  1. เช็ก CDN hit ratio
  2. เช็ก Redis Hot Keys
  3. เช็ก cache hit rate
  4. เช็ก database read QPS
  5. เช็ก connection pool
  6. scale redirect service ที่เป็น stateless

การไล่ตามลำดับนี้ช่วยให้เห็นว่า traffic ทะลุมาถึงชั้นไหน และควรอุดรอยรั่วที่จุดใดก่อน


บทสรุป

แก่นของเรื่องนี้ คือ URL Shortener เป็นระบบที่ read-heavy อย่างท่วมท้น ในเหตุการณ์ที่มี URL กลายเป็นไวรัล ความท้าทายไม่ได้อยู่ที่ "การเก็บข้อมูล" แต่อยู่ที่ "การเสิร์ฟข้อมูลก้อนจิ๋วก้อนเดิมซ้ำๆ หลายล้านครั้งให้เร็วที่สุด"

สถาปัตยกรรมที่รับมือได้จึงเกิดจากการวางหลายชั้นทำงานร่วมกัน ได้แก่ CDN edge caching เพื่อกัน traffic ไว้ที่ขอบเครือข่าย, Redis เป็น cache ชั้นกลาง, local in-memory cache เพื่อจัดการ hot key ที่ระดับ service, การ scale แนวนอนแบบ stateless และการใช้ฐานข้อมูลเป็นเพียงแหล่งความจริง เมื่อทุกชั้นเสริมกันแบบนี้ ระบบก็ยังตอบสนองได้ต่อเนื่อง แม้ link ตัวเดียวจะดังเป็นพลุแตกทั่วอินเทอร์เน็ตในชั่วข้ามคืนก็ตาม