การดู : 0

12/04/2026 18:16น.

Golang The Series EP 130: Multi-instance WebSocket – สเกลระบบ Real-time ด้วย Redis

Golang The Series EP 130: Multi-instance WebSocket – สเกลระบบ Real-time ด้วย Redis

#WebSocket

#Go

#Golang

#Redis Pub/Sub

#Multi-instance

ยินดีต้อนรับเข้าสู่บทความที่เข้มข้นที่สุดตอนหนึ่งในซีรีส์ครับ! หากคุณกำลังสร้างระบบ Chat, Real-time Notification หรือ Dashboard ที่ต้องรองรับผู้ใช้งานพร้อมกันหลักแสน คุณจะพบว่า WebSocket แบบดั้งเดิมที่เก็บสถานะไว้ในหน่วยความจำเครื่องใครเครื่องมัน (Stateful) นั้นคืออุปสรรคสำคัญในการขยายระบบ (Scaling)

 

วันนี้เราจะมาสร้างสถาปัตยกรรมที่ช่วยให้ WebSocket ของเราขยายตัวออกไปได้ไม่จำกัดแบบ Horizontal Scaling ครับ

 

1. โจทย์สุดหิน: เมื่อ "กำแพง" กั้นระหว่าง Instance

 

โดยปกติ HTTP Request จะเป็น Stateless (ขอมา-ตอบไป-จบงาน) แต่ WebSocket ต่างออกไป มันคือการเปิดท่อ TCP ค้างไว้ (Stateful) ลองจินตนาการปัญหาที่เกิดขึ้นเมื่อเรามี Server มากกว่า 1 ตัว:

  1. นายก้อง เชื่อมต่อเข้า Instance A
  2. นางสาวแก้ว เชื่อมต่อเข้า Instance B (ผ่าน Load Balancer ตัวเดียวกัน)
  3. เมื่อก้องส่งข้อความหาแก้ว... Instance A จะมองหา "แก้ว" ใน Memory ของตัวเองไม่เจอ! เพราะแก้วไปเปิดท่อค้างไว้อีกเครื่องหนึ่ง

 

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

 

2. Redis Pub/Sub: กระดูกสันหลังของระบบ Real-time

 

ทำไมต้องเป็น Redis Pub/Sub? ในการสื่อสารระดับ Microservices เราต้องการความเร็วระดับ Microsecond และ Redis คือคำตอบที่ลงตัวที่สุด:

  • Performance: ทำงานบน RAM ทั้งหมด Latency จึงต่ำมาก
  • Pub/Sub Pattern: สนับสนุนการกระจายข้อมูลแบบ Broadcast (หนึ่งเครื่องประกาศ ทุกเครื่องที่ติดตามอยู่ได้รับทันที)
  • Lightweight: ติดตั้งและดูแลรักษาง่ายกว่า Message Broker ขนาดใหญ่อย่าง Kafka ในโจทย์ที่เน้นความเร็วของการส่งข้อความสั้นๆ

 

3. สถาปัตยกรรม "The Message Backplane"

 

เราจะใช้ Redis เป็น "Backplane" (แผงวงจรกลาง) เพื่อส่งต่อข้อมูลระหว่าง Instance ดังนี้:

  1. Publish: เมื่อ Instance A ได้รับข้อความจาก Client ของตัวเอง มันจะ Publish ข้อความนั้นลงใน Redis Channel (เช่น chat_room_1)
  2. Subscribe: ทุกๆ Instance ที่รันอยู่จะทำการ Subscribe รอที่ Channel นั้นไว้ตั้งแต่วินาทีแรกที่เริ่มทำงาน
  3. Dispatch: เมื่อ Redis กระจายข้อความมาให้ ทุก Instance จะได้รับพร้อมกัน และตรวจสอบว่า "คนรับปลายทาง (Target User) อยู่ที่เครื่องฉันไหม?" ถ้าอยู่ ก็จะส่งผ่านท่อ WebSocket ให้ทันที

 

4. Implementation: โค้ด Go ระดับ Production-Ready

 

เราจะใช้ gorilla/websocket และ go-redis/v9 (เวอร์ชันล่าสุด) โดยเน้นความปลอดภัยและประสิทธิภาพครับ

 

A. โครงสร้าง Hub และ Redis Client

 

Go
type Hub struct {
    // เก็บรายการ Client ที่เชื่อมต่อกับเครื่องนี้เท่านั้น (Local Clients)
    clients    map[*Client]bool
    broadcast  chan []byte
    register   chan *Client
    unregister chan *Client
    
    // Redis สำหรับสื่อสารข้ามเครื่อง
    rdb        *redis.Client
}

 

B. การจัดการ Redis Subscription (The Listener)

 

เราต้องรัน Goroutine แยกออกมาเพื่อฟังข้อความจาก Redis ตลอดเวลา:

 

Go
func (h *Hub) listenToRedis(ctx context.Context) {
    pubsub := h.rdb.Subscribe(ctx, "global_chat")
    defer pubsub.Close()

    ch := pubsub.Channel()

    for msg := range ch {
        // เมื่อได้รับข้อความจาก Redis ให้ส่งเข้าช่องทาง broadcast ของเครื่องตัวเอง
        // เพื่อกระจายให้ Client ทุกคนที่เชื่อมต่ออยู่กับเครื่องนี้
        h.broadcast <- []byte(msg.Payload)
    }
}

 

C. การรับและส่งข้อความ (Publishing)

 

เมื่อ Client ส่งข้อความเข้ามา แทนที่จะวน Loop ส่งให้คนอื่นในเครื่องตัวเองตรงๆ เราจะโยนให้ Redis จัดการ:

 

Go
func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()

    for {
        _, message, err := c.conn.ReadMessage()
        if err != nil {
            break
        }
        // Publish ลง Redis เพื่อให้ทุก Instance (รวมถึงเครื่องตัวเอง) ได้รับข้อมูล
        err = c.hub.rdb.Publish(context.Background(), "global_chat", message).Err()
        if err != nil {
            slog.Error("Redis Publish Failed", "error", err)
        }
    }
}

 

5. เทคนิคการจัดการ Scaling ให้มีเสถียรภาพ

 

  • Sticky Sessions: แม้จะมี Redis มาช่วย แต่การตั้งค่า Load Balancer (เช่น Nginx) ให้ใช้ ip_hash ยังคงสำคัญ เพื่อลดการ Re-handshake บ่อยๆ ซึ่งช่วยลดภาระ CPU ของ Server
  • JSON Structured Data: ห้ามส่งแค่ Text เปล่าๆ ควรส่งเป็น JSON ที่มี target_id หรือ room_id เพื่อให้แต่ละ Instance กรองข้อมูล (Filtering) ได้รวดเร็วโดยไม่ต้องเสียเวลาประมวลผลมาก
  • Graceful Shutdown: (ย้อนกลับไป EP 129) ก่อนปิด Instance ต้องทำการส่งสัญญาณแจ้ง Client ให้ Reconnect และ Unsubscribe จาก Redis ให้เรียบร้อยเพื่อป้องกันอาการ "ข้อความค้าง"

 


 

สรุป

 

การนำ Redis Pub/Sub มาเป็นตัวกลาง เปลี่ยนจาก WebSocket Server เดี่ยวๆ ให้กลายเป็น Distributed Real-time System ที่ทรงพลัง คุณสามารถเพิ่ม Instance (Scale Out) ได้ไม่จำกัดเพื่อรองรับผู้ใช้งานที่เพิ่มขึ้น โดยที่พวกเขายังสามารถสื่อสารกันได้ลื่นไหลเหมือนอยู่ในเครื่องเดียวกัน

 

"Stateful doesn't have to mean Unscalable." การจัดการ State ที่ดี คือหัวใจของระบบระดับโลกครับ

 

ในตอนหน้า (EP 131): เราจะยกระดับความซับซ้อนไปอีกขั้น กับการนำ WebSocket เข้าสู่โลกของ Microservices Architecture เต็มรูปแบบ ว่าจะจัดการเรื่อง Authentication, API Gateway และ Service Mesh อย่างไร ห้ามพลาดครับ!