☕ 5-Min Tech Story #45

The Retry Storm: เมื่อ "ความพยายามขอยิงซ้ำ" กลายร่างเป็นอาวุธ DDoS ถล่มเซิร์ฟเวอร์ตัวเองจนฟื้นไม่ขึ้น

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

คุณตั้งค่าระบบ Microservices ไว้อย่างรอบคอบ: "ถ้า Service ปลายทางตอบกลับช้าหรือ Error ให้ระบบทำการ Retry ยิงซ้ำอัตโนมัติ 3 ครั้งทันที เพื่อความทนทาน (Resilience)"

บ่ายวันหนึ่ง Payment Service เกิดสะดุดไปเพียง 2 วินาที จากขยะหน่วยความจำ (GC Pause) ชั่วคราว...

แต่แทนที่เมื่อหายสะดุดแล้วทุกอย่างจะกลับมาปกติ กราฟ Request ขาเข้ากลับพุ่งทะยานจาก 5,000 req/s กลายเป็น 25,000 req/s ในพริบตา! Payment Service สำลักตายสนิท ทราฟฟิกหน้าเว็บกลายเป็น 504 Gateway Timeout กราวรูด

วิศวกร On-call รีบกดรีสตาร์ตเซิร์ฟเวอร์ใหม่ แต่ทันทีที่เครื่องบูตติด มันก็โดนคลื่นทราฟฟิกลูกเดิมซัดจนดับวูบซ้ำรอบสอง (Cascading Failure)... ใครกันแน่ที่กำลังยิง DDoS ใส่เรา? คำตอบสุดเจ็บปวดคือ: "โค้ด Retry ที่พวกเราเขียนขึ้นมาเองนั่นแหละ!"

🌪️ กำเนิดพายุหมุน: The Retry Amplification

กลไก Retry เกิดขึ้นจากความหวังดีเพื่อแก้ปัญหา Transient Fault (ข้อผิดพลาดชั่วคราว เช่น เน็ตเวิร์กกระตุก)

แต่ลองดูคณิตศาสตร์จำลองเมื่อระบบเผชิญกับ สภาวะโอเวอร์โหลด (Overload State):

// ลูกโซ่ขยายทราฟฟิก (Amplification Factor)
1. ปลายทางทำงานช้า ➔ คำขอ 5,000 รายการหมดเวลา (Timeout พร้อมกัน)
2. ฝั่งต้นทางสั่ง Retry ทันที ➔ ยิงซ้ำรอบที่ 1 (+5,000 req)
3. ผู้ใช้งานหน้าเว็บเห็นปุ่มค้าง ➔ กดปุ่มคลิกซ้ำ (+5,000 req)
4. Request ใหม่ของวินาทีถัดไปไหลเข้ามาสมทบ (+5,000 req)
-------------------------------------------------------------
รวมโหลดที่ประดังใส่เครื่องปลายทาง = 20,000+ คำขอต่อวินาที!
"เหมือนคนที่กำลังจมน้ำสำลักอากาศ... แทนที่เราจะยื่นมือไปดึงขึ้นช้าๆ โค้ด Retry กลับพร้อมใจกันกระโดดลงไปกดหัวเขาซ้ำให้อยู่ใต้น้ำลึกขึ้นไปอีก!"

🥁 จังหวะกลองนรก: Thundering Retry (คลื่นการซ้ำที่พร้อมเพรียง)

ข้อผิดพลาดร้ายแรงที่สุดของการเขียน Retry คือการใช้ Fixed Interval (รอเวลาคงที่) เช่น:

// หายนะ: รอนิ่งๆ 1 วินาทีเท่ากันเป๊ะทุกคำขอ
Thread.sleep(1000);
retry();

ถ้ามี 3,000 คำขอที่ล้มเหลวในวินาทีเดียวกัน... ทั้ง 3,000 คำขอนั้นจะนับ 1, 2, 3 แล้ว วิ่งกรูพุ่งชนเซิร์ฟเวอร์ปลายทางในมิลลิวินาทีเดียวกันเป๊ะเหมือนจังหวะกลอง เกิดเป็นคลื่นกระแทกแบบสไปก์ (Traffic Pulses) ที่ทำให้คิวรับงานของเซิร์ฟเวอร์แตกกระจุย

🛡️ 3 คาถาสยบพายุ: Backoff, Jitter และ Circuit Breaker

1. ถอยทีละก้าวแบบทวีคูณ (Exponential Backoff):

ยิ่งยิงไม่ผ่าน ยิ่งต้องเว้นระยะห่างให้นานขึ้น เพื่อเปิดโอกาสให้ระบบปลายทางได้มีเวลาหายใจและเคลียร์คิวเดิม:

สูตร: Wait Time = Base Delay * 2^(attempt) ➔ 1s, 2s, 4s, 8s...

2. ไม้ตายลับของ AWS: โรยเกลือสุ่มเวลา (Full Jitter):

Exponential Backoff อย่างเดียวยังไม่พอ เพราะถ้าเริ่มพร้อมกัน มันก็จะขยายเวลาไปชนพร้อมกันอยู่ดี! วิศวกรของ AWS จึงคิดค้นเทคนิค Jitter (การสุ่มค่ารบกวน) ลงไปในสมการ:

// สูตร Full Jitter จาก AWS Architecture Blog
sleep = random_between(0, min(cap, base * 2 ** attempt))

ผลลัพธ์คือ คลื่นกระแทก 3,000 คำขอจะถูก กระจายเฉลี่ยแบนราบ (Smoothed Out) ออกไปตลอดช่วงเวลา ทำให้เซิร์ฟเวอร์ปลายทางค่อยๆ ทยอยกลืนคำขอได้อย่างนุ่มนวล

3. สับคัตเอาต์ตัดตอนด้วย Circuit Breaker:

ถ้าเซิร์ฟเวอร์ปลายทางล้มเหลวติดต่อกันเกินเกณฑ์ (เช่น ล้มเหลว 50% ใน 10 วินาที) ตัว Circuit Breaker (Resilience4j / Envoy) จะต้องสั่ง "ดีดสวิตช์เปิดวงจร (Open)" เพื่อปฏิเสธทุกการส่งคำขอและตัดสิทธิ์การ Retry ทันทีที่หน้าประตู ปล่อยให้ระบบปลายทางได้ฟื้นคืนชีพเต็มร้อยก่อนเริ่มทดสอบส่งคำขอใหม่อีกครั้ง

💡 5-Min Takeaway

"ความพยายามแก้ปัญหาแบบทู่ซี้ในระบบกระจายศูนย์ มีค่าเท่ากับการโจมตีระบบตัวเอง" อย่าทำ Retry แบบไม่จำกัดและห้ามใช้ Fixed Delay เด็ดขาด จงใช้ Exponential Backoff ร่วมกับ Jitter เพื่อสลายแรงสไปก์ และติดตั้ง Circuit Breaker เสมอ เพื่อให้ระบบรู้จักการ 'ยอมแพ้ชั่วคราว' แทนที่จะดื้อดึงจนพากันกอดคอตายทั้งระบบ