Vấn đề với một máy chủ đơn
Mọi kết nối WebSocket đều tồn tại trong bộ nhớ của một quá trình Node.js. Nếu bạn chạy một máy chủ đơn, tất cả người dùng kết nối với nó và bạn có thể phát đi một thông điệp với một vòng lặp trong bộ nhớ đơn giản. Khoảng thời gian bạn chạy một phiên bản thứ hai đằng sau bộ cân bằng tải, khách hàng sẽ bị phân chia giữa các tiến trình. Một thông điệp đến được máy chủ A sẽ không thể đến được một khách hàng kết nối tới máy chủ B, vì hai tiến trình không chia sẻ bộ nhớ.
Việc mở rộng theo chiều ngang do đó cần một cách để các máy chủ giao tiếp với nhau. Redis Pub/Sub là lựa chọn nhẹ nhất: mỗi máy chủ đăng ký vào một tập các kênh, và khi một máy chủ công bố, Redis đẩy thông điệp tới mọi người đăng ký.
Kết nối Adapter Redis
Adapter `socket.io-redis` ghép vào API Adapter tích hợp của Socket.IO và thay thế phát sóng trong bộ nhớ mặc định bằng Redis Pub/Sub. Cài đặt nó trên mọi phiên bản:
Sau đó ở điểm vào máy chủ của bạn, tạo một adapter ràng buộc với một Redis client và gắn nó vào máy chủ Socket.IO:
Khi đã gắn, mọi lời gọi tới `io.emit`, `io.to(room).emit`, hoặc `socket.broadcast.emit` sẽ đi qua Redis. Các máy chủ được đăng ký với cùng một Redis instance sẽ chuyển giao sự kiện đến các socket đã kết nối của chúng, bất kể máy chủ nguồn gốc phát ra là máy chủ nào.
Phát sóng Across Instances
Với adapter được đặt ở đó, mã phát broadcast của bạn không đổi. Một trình xử lý chat room hoạt động trên một máy chủ sẽ hoạt động trên mười máy chủ:
Khi client A ở máy chủ 1 phát `message`, Socket.IO tuần tự hóa sự kiện, công bố nó lên Redis trên một kênh theo phạm vi phòng, và mọi máy chủ khác bắt được và phát nó ra tới các socket địa phương trong phòng đó. Từ góc nhìn của client, cuộc trò chuyện là thời gian thực và toàn cầu.
Mở rộng Adapter vượt quá Hai Nút
Mô hình ở trên mở rộng theo cấp số nhân. Một vài quy tắc vận hành giúp nó khỏe mạnh ở số lượng nút cao hơn.
Đầu tiên, chạy Redis với độ bền ở mức hợp lý. Các tin nhắn Pub/Sub không được lưu trữ, nhưng adapter cũng dùng các khóa Redis cho trạng thái handshake. Việc flush khi gặp sự cố sẽ buộc client kết nối lại, điều này chấp nhận được đối với chat nhưng không phải cho các workload có trạng thái.
Thứ hai, áp dụng sessions dính (sticky sessions) trên load balancer của bạn cho chỉ phần chuyển đổi WebSocket, hoặc chấp nhận chi phí của việc kết nối lại. Không có sự dính, một client kết nối lại có thể ghé vào một máy chủ khác và phải tham gia lại các phòng. Socket.IO xử lý điều này một cách minh bạch vì thành viên phòng là địa phương trên máy chủ, nhưng client sẽ bỏ lỡ các sự kiện trong quá trình bắt tay.
Thứ ba, theo dõi số kênh Pub/Sub Redis và số người đăng ký. Mỗi namespace của Socket.IO thêm các kênh, nên một deployment đơn lẻ với nhiều namespace sẽ nhân đôi diện tích kênh.
Cuối cùng, nếu Redis trở thành nút cổ chai, bạn có thể phân mảnh Pub/Sub trên Redis Cluster. Gói `@socket.io/redis-adapter` hỗ trợ chế độ cluster ngay từ khi xuất xưởng bằng cách chấp nhận nhiều lựa chọn kết nối.
Những rủi ro phổ biến
Một sai lầm phổ biến là tạo adapter trước khi cả hai Redis client đều kết nối. Xuất bản trên một client chưa kết nối sẽ ném lỗi hoặc bỏ qua tin nhắn. Luôn `await` các lời gọi kết nối, hoặc bọc chúng trong một callback sẵn sàng như ở trên.
Một sai lầm khác là dùng một Redis client duy nhất cho both publish và subscribe. Redis yêu cầu một kết nối người đăng ký riêng vì một client ở chế độ subscribe không thể chạy các lệnh bình thường. Lời gọi `pubClient.duplicate()` trong ví dụ tạo ra kết nối thứ hai cho bạn.
Một vấn đề tinh vi hơn là dựa vào `io.local` cho logic across-server. `io.local.emit` chỉ kích hoạt trên máy chủ hiện tại và bỏ qua Redis, điều này hữu ích cho chẩn đoán theo phiên bản nhưng sẽ thất bại ngay khi bạn cho rằng thông điệp đã tới mọi nút. Sử dụng plain `io.emit` cho mọi thứ phải tới tất cả client.
Tóm tắt
Mở rộng horizontal cho các WebSocket server phụ thuộc vào hai phần: một load balancer ở phía trước N instance Node.js và Redis Pub/Sub như kênh bus cho các tin nhắn giữa chúng. Bộ điều hợp `socket.io-redis` bọc bus để mã ứng dụng vẫn giữ nguyên. Các Rooms, broadcast và namespaces đều làm việc across instances theo cùng cách chúng làm trên một máy. Các mối quan tâm vận hành chính là sự sẵn sàng của Redis, sticky sessions cho nâng cấp WebSocket, và đừng bao giờ nhầm lẫn `io.local.emit` với `io.emit`.
Bài kiểm tra cuối bài
Ôn lại kiến thức, thử sức và nhận góp ý cho bài làm.