Centrifugo & Go: Bí kíp xử lý triệu kết nối Real-time không lo ‘sập’ server

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Nỗi ám ảnh mang tên WebSocket khi hệ thống phình to

Lúc mới bắt đầu làm tính năng chat hay thông báo thời gian thực, mình thường chọn giải pháp ‘nhà làm’: dùng thư viện gorilla/websocket trong Go. Chạy trên máy cá nhân với vài user thì mượt mà vô cùng. Tuy nhiên, khi hệ thống chạm mốc 10.000 user online cùng lúc, những rắc rối thực sự mới bắt đầu lộ diện.

Vấn đề lớn nhất của WebSocket thuần túy nằm ở Trạng thái (State). Mỗi kết nối là một đường ống liên tục giữa client và server. Nếu bạn chạy 3-4 server để chia tải, làm sao server A biết user đang ở server B để gửi tin nhắn? Bạn sẽ phải tự xây thêm một lớp Pub/Sub bằng Redis để đồng bộ. Chưa kể việc xử lý tự động kết nối lại (reconnection), quản lý channel, hay bảo mật JWT cũng cực kỳ ngốn thời gian. Thay vì tập trung vào tính năng, bạn lại sa lầy vào việc bảo trì hạ tầng WebSocket.

Ba hướng đi phổ biến cho hệ thống Real-time

Dưới đây là những lựa chọn mà anh em kỹ sư thường cân nhắc khi cần scale hệ thống:

  • Tự build (Go + Redis): Bạn kiểm soát 100% code nhưng tốn cực nhiều công sức để xử lý các lỗi phát sinh khi mở rộng ngang (horizontal scaling).
  • Dùng Cloud (Firebase, Pusher): Triển khai siêu tốc. Tuy nhiên, hóa đơn cuối tháng có thể lên tới hàng nghìn USD nếu lượng user tăng vọt, kèm theo nỗi lo bị phụ thuộc vào nhà cung cấp.
  • Centrifugo: Một giải pháp chuyên biệt đứng độc lập như một server riêng. Nó lo hết từ kết nối, Pub/Sub đến khả năng mở rộng. Backend của bạn (Go, Python, Node.js) chỉ việc gọi API là xong.

Centrifugo chính là điểm giao thoa hoàn hảo. Nó mang lại hiệu năng cực cao của Go nhưng lại dễ tích hợp như các dịch vụ Cloud trả phí.

Tại sao Centrifugo là lựa chọn hàng đầu cho Production?

Được viết bằng Go, Centrifugo tận dụng tối đa khả năng xử lý đa luồng (concurrency). Điểm sáng giá nhất là nó tách biệt hoàn toàn logic real-time khỏi server ứng dụng của bạn.

Mô hình dữ liệu sẽ thay đổi thành:
Client <–> Centrifugo <– (API/GRPC) –> Go Backend

Với mô hình này, Go backend trở nên ‘stateless’ (không giữ trạng thái). Nó chỉ làm nhiệm vụ xác thực và đẩy tin nhắn đi. Mọi gánh nặng về RAM và CPU để duy trì hàng triệu kết nối đã có Centrifugo gánh vác thay bạn.

Hướng dẫn triển khai thực tế

1. Khởi tạo Centrifugo với Docker

Để bắt đầu nhanh nhất, mình sẽ dùng Docker. Bạn chỉ cần một file config.json để thiết lập secret key bảo mật.

# Tạo file cấu hình nhanh
echo '{"token_hmac_secret_key": "my-secret", "api_key": "my-api-key"}' > config.json

# Chạy Centrifugo chỉ với một dòng lệnh
docker run -p 8000:8000 -v `pwd`/config.json:/centrifugo/config.json centrifugo/centrifugo centrifugo

Sau khi chạy, bạn có thể truy cập ngay vào dashboard quản trị tại http://localhost:8000.

2. Phía Backend: Cấp quyền và Gửi tin nhắn

Client muốn kết nối vào Centrifugo cần một mã xác thực (JWT). Go backend sẽ chịu trách nhiệm tạo ra mã này. Trong lúc phát triển, mình thường dùng toolcraft.app để kiểm tra nhanh cấu trúc JSON trả về, giúp việc debug diễn ra trơn tru hơn.

func GenerateCentrifugoToken(userID string) (string, error) {
	claims := jwt.MapClaims{
		"sub": userID,
		"exp": time.Now().Add(time.Hour * 24).Unix(),
	}
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	return token.SignedString([]byte("my-secret"))
}

Để gửi thông báo từ server xuống client, bạn chỉ cần gọi API đơn giản qua thư viện gocent:

c := gocent.New(gocent.Config{
	Addr: "http://localhost:8000/api",
	Key:  "my-api-key",
})

// Gửi tin nhắn đến channel "notifications"
ctx := context.Background()
_, err := c.Publish(ctx, "notifications", []byte(`{"message": "Chào anh em!"}`))

3. Phía Frontend: Nhận tin thời gian thực

Ở phía client, thư viện centrifuge-js rất nhẹ và dễ dùng. Nó tự động xử lý các việc phức tạp như tự động kết nối lại khi mất mạng.

const centrifuge = new Centrifuge('ws://localhost:8000/connection/websocket', {
    token: 'JWT_TU_BACKEND'
});

const sub = centrifuge.newSubscription('notifications');
sub.on('publication', (ctx) => {
    console.log('Tin nhắn mới:', ctx.data.message);
});

sub.subscribe();
centrifuge.connect();

Kinh nghiệm ‘xương máu’ khi vận hành thực tế

Khi đưa hệ thống lên quy mô lớn, bạn cần lưu ý 3 điểm mấu chốt sau:

Luôn sử dụng Redis Engine

Mặc định Centrifugo lưu dữ liệu trong bộ nhớ (Memory). Để chạy nhiều node Centrifugo phía sau Load Balancer, bạn bắt buộc phải dùng Redis làm cầu nối. Nếu không, tin nhắn gửi đến node này sẽ không thể tới được user đang kết nối ở node kia.

Xác thực Proxy (Proxy Authentication)

Nếu JWT chưa đủ linh hoạt, hãy thử Proxy Authentication. Mỗi khi có user connect, Centrifugo sẽ hỏi Go backend: “User này có hợp lệ không?”. Cách này giúp bạn kiểm soát quyền truy cập cực kỳ chặt chẽ theo logic nghiệp vụ riêng.

Giám sát chặt chẽ các chỉ số

Hãy bật endpoint /metrics và tích hợp với Grafana. Bạn cần đặc biệt chú ý đến num_clients (số kết nối) và messages_sent. Nếu thấy CPU tăng cao khi số kết nối vượt ngưỡng 100.000, đó là lúc bạn cần scale thêm node Centrifugo mới.

Tự xây dựng hệ thống WebSocket chuẩn production là một bài toán khó và tốn kém. Bằng cách kết hợp Centrifugo với Go, bạn sẽ có một hạ tầng vững chắc để tập trung hoàn toàn vào việc phát triển tính năng cho người dùng.

Share: