การดู : 0

12/04/2026 18:16น.

Golang The Series EP 126: วิธีทำ DDoS Protection และ Rate Limiting เพื่อระบบที่ไม่มีวันตาย

Golang The Series EP 126: วิธีทำ DDoS Protection และ Rate Limiting เพื่อระบบที่ไม่มีวันตาย

#Token Bucket

#DDoS Protection

#Rate Limiting

#Go

#Golang

ยินดีต้อนรับชาว Gopher ทุกท่านกลับเข้าสู่ซีรีส์ที่เข้มข้นที่สุดครับ!

 

ใน EP 125 เราได้วางรากฐานเรื่องความปลอดภัยในการสื่อสารผ่าน TLS และ WSS ไปแล้ว ซึ่งเปรียบเสมือนการสร้าง "กำแพงเมือง" ที่แข็งแกร่งและปกปิดความลับได้ดีเยี่ยม

 

แต่ทว่า... กำแพงที่แข็งแกร่งก็อาจพังทลายได้หากมีคนนับล้านพยายามวิ่งกรูเข้าประตูเมืองพร้อมกันจนเหยียบกันตาย หรือที่เรียกว่า DDoS (Distributed Denial of Service) นั่นเองครับ ในโลกของ Backend ต่อให้โค้ดคุณจะเขียนมาดีแค่ไหน (Optimize มาเทพเพียงใด) แต่ถ้าไม่มีระบบจัดการ Traffic ที่ดี ระบบก็ "ล่ม" ได้ง่ายๆ เพียงแค่เจอ Botnet กระหน่ำส่ง Request เข้ามา

 

วันนี้เราจะมาสร้าง "ด่านตรวจคนเข้าเมือง" ที่ชาญฉลาดด้วยการทำ Rate Limiting และ DDoS Protection ในระดับ Application Layer (Layer 7) ด้วยภาษา Go ครับ

 

1. ทำไมระบบของคุณต้องมี Rate Limiting? (The Why)

 

หากคุณปล่อยให้ทุก Request เข้าถึงทรัพยากร (Database, CPU, Memory) ได้อย่างอิสระ คุณกำลังเผชิญกับความเสี่ยง 3 อย่าง:

  1. Resource Exhaustion: ผู้ใช้เพียงคนเดียวอาจเขียน Loop ยิง Request จน Database Connection เต็ม
  2. Cost Management: หากคุณใช้ Cloud Services (เช่น Lambda หรือ API ที่คิดเงินตามจำนวนครั้ง) กระเป๋าคุณอาจฉีกได้ถ้าโดน Bot ถล่ม
  3. Security Risks: การปล่อยให้คนยิง Login รัวๆ คือช่องโหว่ในการทำ Brute-force Attack

 

2. เจาะลึก Rate Limiting Algorithms: เลือกใช้อะไรดี?

 

ก่อนจะเขียนโค้ด เราต้องเข้าใจก่อนว่า "การกัก" มีหลายวิธีที่นิยมใน Go:

  • Token Bucket (นิยมที่สุดใน Go): เปรียบเสมือนถังที่มีเหรียญ (Token) เติมเข้ามาเรื่อยๆ ตามเวลาที่กำหนด หากใครจะเข้าต้องหยิบเหรียญไป 1 อัน ถ้าเหรียญหมดก็ต้องรอ เหมาะมากสำหรับระบบที่ยอมให้มี "Burst" (การใช้งานที่พุ่งสูงขึ้นชั่วคราว) ได้บ้าง
  • Leaky Bucket: คล้ายกับถังน้ำที่เจาะรูด้านล่าง น้ำที่เทเข้ามาจะไหลออกในอัตราที่คงที่เสมอ หากเทน้ำเร็วเกินไปจนล้นถัง น้ำส่วนเกินจะถูกทิ้งทันที เหมาะสำหรับระบบที่ต้องการความเสถียรของ Traffic แบบสุดๆ
  • Fixed Window Counter: นับจำนวน Request ในช่วงเวลาที่กำหนด (เช่น 100 ครั้งต่อนาที) แต่มักมีปัญหาเรื่อง "Edge Case" ตรงช่วงรอยต่อนาทีที่อาจทำให้ Request ทะลักเข้ามาเป็น 2 เท่า

 

3. Implementation: การใช้ golang.org/x/time/rate

 

ใน Go Standard Library (Sub-repository) มี Package ที่ชื่อว่า rate ซึ่งใช้ Algorithm Token Bucket ที่ทรงพลังมากครับ

 

หัวใจสำคัญคือ NewLimiter(r, b)

 

  • r (Limit): อัตราการเติม Token ต่อวินาที
  • b (Burst): จำนวน Token สูงสุดที่ถังเก็บได้ (ความจุสูงสุดที่ยอมให้เข้าได้พร้อมกันในเสี้ยววินาที)

 

ตัวอย่าง Code: Middleware ระดับ Advance

 

เราจะไม่เขียน Limit ไว้ใน Handler ตรงๆ แต่เราจะทำเป็น Middleware เพื่อให้ใช้ซ้ำได้กับทุก Endpoint ครับ

 

Go
package main

import (
	"net/http"
	"sync"
	"time"

	"golang.org/x/time/rate"
)

// IPlimiter เก็บสถานะ Limiter ของแต่ละ IP แยกกัน
type IPlimiter struct {
	ips map[string]*rate.Limiter
	mu  sync.RWMutex
}

func NewIPlimiter() *IPlimiter {
	return &IPlimiter{
		ips: make(map[string]*rate.Limiter),
	}
}

// GetLimiter ค้นหาหรือสร้าง Limiter ใหม่สำหรับ IP นั้นๆ
func (i *IPlimiter) GetLimiter(ip string) *rate.Limiter {
	i.mu.Lock()
	defer i.mu.Unlock()

	limiter, exists := i.ips[ip]
	if !exists {
		// อนุญาต 2 requests ต่อวินาที และ Burst ได้ 5
		limiter = rate.NewLimiter(rate.Every(500*time.Millisecond), 5)
		i.ips[ip] = limiter
	}

	return limiter
}

func limitMiddleware(next http.Handler, iplimiter *IPlimiter) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// ดึง IP ของผู้ใช้ (ในความเป็นจริงอาจต้องเช็ค X-Forwarded-For ถ้าอยู่หลัง Proxy)
		ip := r.RemoteAddr

		limiter := iplimiter.GetLimiter(ip)
		if !limiter.Allow() {
			w.Header().Set("X-RateLimit-Limit", "2")
			w.Header().Set("X-RateLimit-Remaining", "0")
			http.Error(w, "Too Many Requests: พักก่อนนะจ๊ะ ระบบกำลังประมวลผล", http.StatusTooManyRequests)
			return
		}

		next.ServeHTTP(w, r)
	})
}

func main() {
	iplimiter := NewIPlimiter()
	mux := http.NewServeMux()

	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("ยินดีต้อนรับสู่ Superdev Academy API!"))
	})

	// หุ้ม Handler ด้วย Middleware
	http.ListenAndServe(":8080", limitMiddleware(handler, iplimiter))
}

 

ทำไมต้องใช้ sync.RWMutex? เพราะใน Go http.Server ทำงานเป็นแบบ Concurrency (Goroutines) หากมี Request เข้ามาพร้อมกันจากหลาย IP และเราพยายามเขียนลง map ตัวเดียวกันโดยไม่มี Lock ระบบจะเกิด Race Condition และ Panic ได้ทันทีครับ

 

4. ยกระดับการป้องกัน DDoS ในระดับ Application (L7)

 

การทำ Rate Limit แค่จำนวนครั้งอาจไม่พอสำหรับการป้องกัน DDoS ที่มุ่งเน้นการทำให้ Resource เต็ม เราควรเพิ่มกลยุทธ์เหล่านี้:

 

A. การตั้ง Timeouts ที่รัดกุม

 

DDoS บางประเภทอย่าง Slowloris จะพยายามเปิด Connection ค้างไว้ให้นานที่สุดจน Server รับคนใหม่ไม่ได้ เราต้องตั้งค่า http.Server ให้เข้มงวด:

 

Go
server := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  2 * time.Second,  // ต้องอ่าน Header ให้เสร็จใน 2 วิ
    WriteTimeout: 5 * time.Second,  // ต้องส่งข้อมูลกลับให้เสร็จใน 5 วิ
    IdleTimeout:  30 * time.Second, // ตัด Connection ที่ไม่ได้ใช้งาน
}

 

B. การทำ Distributed Rate Limiting (ด้วย Redis)

 

โค้ดด้านบนมีจุดอ่อนคือ มันเก็บไว้ใน Memory ถ้าคุณรัน Server 3 ตัว (Scaling) แต่ละตัวจะนับแยกกัน ซึ่งอาจทำให้ User ส่ง Request รวมกันได้เกินจริง

 

ทางแก้: ใช้ Redis เป็นตัวนับกลาง (Centralized Counter) โดยใช้ Library อย่าง go-redis เพื่อทำ setnx หรือใช้ redis-cell module ครับ

 

5. ข้อควรระวังและ Best Practices

 

  1. Don't Block Legitimate Users: การตั้ง Limit ที่ตึงเกินไปอาจทำให้ User ปกติใช้งานไม่ได้ (เช่น การโหลดไฟล์รูปภาพเยอะๆ ในหน้าเดียว) ควรใช้ระบบ Burst ให้เหมาะสม
  2. Graceful Response: เมื่อโดนบล็อก ควรส่ง HTTP Status 429 Too Many Requests พร้อมระบุ Header Retry-After เพื่อบอกว่าเขาจะกลับมาใช้งานได้อีกเมื่อไหร่
  3. Whitelisting: อย่าลืมเว้นที่ให้ IP ของบริการภายใน (Internal Services) หรือพาร์ทเนอร์ที่เชื่อถือได้ ไม่ให้โดนบล็อกไปด้วยนะครับ

 


 

สรุป

 

ใน EP 126 นี้ เราได้สร้างระบบป้องกันที่ช่วยให้ Server ของเรา "หายใจออก" ในช่วงที่มี Traffic พุ่งสูง การเข้าใจเรื่อง Token Bucket และการจัดการ State ในระดับ IP คือทักษะสำคัญที่แยก "คนเขียนโปรแกรมได้" ออกจาก "คนออกแบบระบบเป็น"

 

แต่... การบล็อก Traffic เป็นเพียงจุดเริ่มต้นครับ ในสถานการณ์ที่ระบบภายในของเรา (เช่น Microservices อื่นๆ) เริ่มทำงานช้าลงหรือตายไป เราจะรับมืออย่างไรไม่ให้ระบบทั้งหมดล่มตามกันไป?

 

เตรียมตัวให้พร้อม เพราะใน EP 127 เราจะก้าวเข้าสู่เรื่อง Connection Management & Circuit Breaker - ระบบตัดไฟอัตโนมัติที่จะช่วยรักษาชีวิตของ Architecture ของคุณไม่ให้พังทลายเป็นโดมิโน่ ห้ามพลาดครับ!

 

บทความโดย: Superdev Academy "สอนให้คุณเก่งโค้ด ด้วยโจทย์โลกความจริง" หากคุณพบว่าบทความนี้มีประโยชน์ อย่าลืมแชร์ให้เพื่อนชาว Dev และกดติดตามเราในทุกช่องทางนะครับ!