12/04/2026 18:16น.

Golang The Series EP 127: Connection Management & Circuit Breaker ป้องกันระบบล่มแบบโดมิโน่
#Connection Management
#Circuit Breaker
#Go
#Golang
ยินดีต้อนรับชาว Gopher ทุกท่านครับ! ใน EP 126 เราได้เรียนรู้วิธีการสร้าง "ด่านตรวจคนเข้าเมือง" เพื่อป้องกันการโจมตีจากภายนอกด้วย Rate Limiting ไปแล้ว ซึ่งช่วยให้ Server ของเราไม่โดน Traffic ถล่มจนตาย
แต่ทว่า... ความปลอดภัยภายนอกอย่างเดียวไม่พอครับ เพราะบางครั้ง "เนื้อร้าย" ก็เกิดจากภายในระบบของเราเอง เช่น Microservice ตัวหนึ่งทำงานช้าลง หรือ Database เกิดอาการคอขวด หากเราจัดการการเชื่อมต่อ (Connection Management) ไม่ดี ความเสียหายนี้จะลามไปหา Service อื่นๆ เหมือนกับโดมิโน่ที่ล้มทับกันจนพังทั้งระบบ
วันนี้เราจะมาเรียนรู้วิธีติดตั้ง "เซอร์กิตเบรกเกอร์" (Circuit Breaker) ให้กับโค้ด Go ของเราครับ
1. ปัญหาของ "ความใจดี" ที่เกินไป: Cascading Failure
ลองนึกภาพว่า Service A ต้องเรียกใช้ Service B:
- หาก Service B เริ่มทำงานช้า (High Latency) Service A จะต้องรอ (Wait)
- เมื่อ Service A รอ Connection ของ Service A ก็จะถูกเปิดค้างไว้
- ยิ่งมี Request เข้ามาที่ Service A มากเท่าไหร่ ทรัพยากร (Memory/Goroutines) ก็จะถูกกินไปเรื่อยๆ จนเต็ม
- สุดท้าย Service A ก็ล่มตาม Service B ไปด้วย และลามไปหา Service C, D, E...
นี่คือที่มาของคำว่า Cascading Failure ครับ การจัดการ Connection ที่ดีต้องไม่ใช่แค่การ "รอ" แต่ต้องรู้จัก "ตัด" เมื่อถึงเวลา
2. ทำความรู้จักกับ Circuit Breaker Pattern
แนวคิดนี้เหมือนกับเบรกเกอร์ไฟฟ้าในบ้านคุณครับ เมื่อเกิดไฟช็อต เบรกเกอร์จะ "ดีด" ออกเพื่อตัดวงจร ไม่ให้ไฟไหม้ทั้งบ้าน ในเชิง Software เบรกเกอร์ของเราจะมี 3 สถานะหลัก:
- Closed (สถานะปกติ): ข้อมูลไหลผ่านได้ปกติ เบรกเกอร์จะคอยนับจำนวนความผิดพลาด (Failure Rate)
- Open (สถานะตัดวงจร): เมื่อความผิดพลาดสูงเกินเกณฑ์ที่ตั้งไว้ เบรกเกอร์จะ "เปิด" ออก และหยุดส่ง Request ไปยัง Service ปลายทางทันที (ส่ง Error กลับทันที) เพื่อให้ Service ปลายทางมีเวลา "พักฟื้น"
- Half-Open (สถานะทดลอง): หลังจากเวลาผ่านไปช่วงหนึ่ง เบรกเกอร์จะลองปล่อย Request เล็กน้อยไปทดสอบดูว่าปลายทางหายดีหรือยัง ถ้าหายแล้วจะกลับไปสถานะ Closed แต่ถ้ายังจะกลับไป Open ใหม่
3. การจัดการ Connection ใน Go (The Foundation)
ก่อนจะไปถึง Circuit Breaker เราต้องเริ่มที่พื้นฐานคือการจัดการ Connection Pool และ Context Timeout ครับ
ใน Go เราควรใช้ context.WithTimeout ทุกครั้งที่มีการเรียก External Service เพื่อไม่ให้ Goroutine ของเราค้างเติ่ง:
Go
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "http://unstable-service/api", nil)
// ถ้าเกิน 2 วินาที ระบบจะตัดการทำงานทันที
4. Implementation: การใช้ sony/gobreaker ใน Go
หนึ่งใน Library ที่นิยมและเสถียรที่สุดคือ gobreaker จาก Sony ครับ เรามาดูวิธีการหุ้ม HTTP Client ของเราด้วย Circuit Breaker กัน
Go
package main
import (
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/sony/gobreaker"
)
var cb *gobreaker.CircuitBreaker
func init() {
settings := gobreaker.Settings{
Name: "External-API-Service",
MaxRequests: 3, // จำนวน request ที่ยอมให้ผ่านในสถานะ Half-Open
Interval: 5 * time.Second, // รอบเวลาในการล้างสถิติ
Timeout: 10 * time.Second, // เวลาที่เบรกเกอร์จะค้างอยู่ที่สถานะ Open ก่อนไป Half-Open
ReadyToTrip: func(counts gobreaker.Counts) bool {
// ถ้า Error เกิน 5 ครั้ง หรือ Failure Rate เกิน 60% ให้ตัดไฟ!
failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
return counts.Requests >= 5 && failureRatio >= 0.6
},
}
cb = gobreaker.NewCircuitBreaker(settings)
}
func CallService() ([]byte, error) {
// ใช้ cb.Execute เพื่อหุ้มการทำงาน
body, err := cb.Execute(func() (interface{}, error) {
resp, err := http.Get("http://potentially-failing-service.com")
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
return nil, fmt.Errorf("server error: %d", resp.StatusCode)
}
return ioutil.ReadAll(resp.Body)
})
if err != nil {
return nil, err
}
return body.([]byte), nil
}
func main() {
// จำลองการเรียกใช้
for i := 0; i < 20; i++ {
_, err := CallService()
if err != nil {
fmt.Printf("Request %d: Error - %v\n", i, err)
} else {
fmt.Printf("Request %d: Success\n", i)
}
time.Sleep(500 * time.Millisecond)
}
}
ทำไมโค้ดนี้ถึงช่วยชีวิตคุณ?
เมื่อ Service ปลายทางล่ม counts.TotalFailures จะเพิ่มขึ้นจนถึงเกณฑ์ที่กำหนด เบรกเกอร์จะเปลี่ยนเป็นสถานะ Open ทันที หลังจากนั้นทุก Request ที่เรียกเข้ามาที่ CallService() จะถูกตีกลับด้วย Error circuit breaker is open โดยที่ ไม่ต้องรอ HTTP Timeout เลย ช่วยรักษาทรัพยากรเครื่องได้มหาศาลครับ
5. กลยุทธ์การทำ Fallback (แผนสำรองเมื่อไฟตัด)
เมื่อเบรกเกอร์ตัดไฟ เราไม่ควรปล่อยให้ User เจอหน้าจอขาวเปล่าๆ เราควรมี Fallback Mechanism:
- Return Cached Data: ส่งข้อมูลล่าสุดที่เก็บไว้ใน Cache ให้แทน
- Return Default Value: ส่งค่าพื้นฐาน เช่น "บริการนี้ไม่พร้อมใช้งานชั่วคราว"
- Queueing: เก็บ Request ไว้ใน Message Queue (เช่น RabbitMQ) เพื่อประมวลผลทีหลังเมื่อระบบฟื้น
สรุป
การจัดการ Connection และการใช้ Circuit Breaker คือการยอมรับว่า "ความล้มเหลวเกิดขึ้นได้เสมอ" หน้าที่ของเราไม่ใช่การพยายามทำให้มันไม่ล้มเหลว 100% แต่คือการทำให้เมื่อมันล้มแล้ว "ไม่ลามไปที่อื่น" การออกแบบระบบแบบ Resilient Architecture ด้วย Go จะทำให้คุณนอนหลับได้สนิทขึ้น แม้ในวันที่ Service ของ Partner จะล่มพินาศก็ตามครับ
ใน EP ต่อไป (EP 128) เราจะไปดูเรื่องที่สำคัญไม่แพ้กัน นั่นคือ Logging, Monitoring และ Observability เพราะถ้าเราไม่มี "ตา" ไว้ดูว่าเบรกเกอร์มันดีดตอนไหน เราก็คงไม่มีวันรู้ว่าระบบของเรากำลังมีปัญหาอยู่ครับ!