Hoàn thành bài "Sử dụng Socket.IO cho Phòng (Rooms), Namespaces và Tự động kết nối lại" trước để mở khoá bài này.Đến bài trước

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ý.

đồ thị cho thấy hai máy chủ Node.js phía sau một bộ cân bằng tải, mỗi máy chủ có các khách hàng kết nối riêng, và Redis nằm ở giữa như một bus pub/sub

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:

bash
npm install socket.io redis socket.io-redis

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:

javascript
const http = require('http');
const { Server } = require('socket.io');
const { createAdapter } = require('@socket.io/redis-adapter');
const { createClient } = require('redis');
const httpServer = http.createServer();
const io = new Server(httpServer);
const pubClient = createClient({ url: 'redis://localhost:6379' });
const subClient = pubClient.duplicate();
Promise.all([pubClient.connect(), subClient.connect()]).then(() => {
io.adapter(createAdapter(pubClient, subClient));
io.listen(3000);
});

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ủ:

javascript
io.on('connection', (socket) => {
socket.on('join', (room) => {
socket.join(room);
});
socket.on('message', (room, text) => {
io.to(room).emit('message', {
user: socket.data.username,
text,
time: Date.now()
});
});
});

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.

flowchart cho thấy một tin nhắn bắt nguồn từ máy chủ 1, đi qua Redis Pub/Sub, sau đó phát tới máy chủ 2 và máy chủ 3 mỗi máy chủ chuyển nó đến các client địa phương của mình

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`.

Luyện tập sau buổi học

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.

0/1
Đã trả lời · 0/1