下面是工作过程中,不断的优化 Socket.IO 的集群大小时的一些总结,本部分是关注的 Redis Adapter 的配置和跨服务器通信的优化。
1. Socket.IO 分布式架构概述
在多服务器部署中,Socket.IO 需要一个适配器来同步不同服务器实例之间的消息和状态。Redis Adapter 是最常用的解决方案,它使用 Redis 作为中间件来协调多个 Socket.IO 服务器实例。
基本架构图
plaintext
客户端 A -----> 服务器实例 1 ----+
|
客户端 B -----> 服务器实例 2 ----+--> Redis <--+
| |
客户端 C -----> 服务器实例 3 ----+ |
|
v
跨服务器通信和状态同步
2. Redis Adapter 的关键配置选项
2.1 publishOnSpecificResponseChannel
这是一个重要的性能优化选项,特别是在大规模部署中。
作用 :当设置为 true 时,跨服务器请求的响应只会发送给发起请求的服务器实例,而不是所有服务器实例。
默认值 : false (响应会发送给所有服务器实例)
配置示例 :
javascript
import { createAdapter } from "@socket.io/redis-adapter";
import { pubClient, subClient } from "../config/redis.js";
// 创建 Socket.IO 服务器
const io = new Server({
cors: {
origin: '*',
},
});
// 配置 Redis Adapter 并启用优化选项
io.adapter(createAdapter(pubClient, subClient, {
publishOnSpecificResponseChannel: true
}));
2.2 默认行为与优化行为对比
默认行为(不启用 publishOnSpecificResponseChannel) :
- 服务器 A 执行 fetchSockets() 查询设备状态
- 服务器 B 和 C 收到请求,检查自己是否有匹配的 socket
- 服务器 B 找到了匹配的 socket,将结果发布到 Redis
- 服务器 A、B 和 C 都会收到这个响应
- 服务器 A 处理响应,B 和 C 接收到后会忽略它.
虽然这样也能正常工作,但流量比较大,然后 B 和 C 接收会还是不要这些信息。
优化行为(启用 publishOnSpecificResponseChannel) :
- 服务器 A 执行 fetchSockets() ,并在请求中包含自己的唯一标识符
- 服务器 B 和 C 收到请求,检查自己是否有匹配的 socket
- 服务器 B 找到了匹配的 socket, 只将结果发送回服务器 A
- 服务器 C 不会收到这个响应
3. 跨服务器请求优化
3.1 fetchSockets 方法的优化参数
fetchSockets() 方法可以接受参数来优化请求行为:
参数说明 :
- timeout : 设置请求超时时间(毫秒)。如果在指定时间内没有收到足够的响应,请求会自动完成。
- expectResponses : 期望收到的响应数量。一旦收到这么多响应,请求就会立即完成,不再等待其他响应。
4. 响应机制详解
4.1 响应消息格式
当服务器实例响应跨服务器请求时,它会发布一个类似这样的消息到 Redis:
json
{
"requestId": "req123456",
"uid": "server-A-uid",
"responseChannel": "socket.io#/#request#response",
"response": [
{
"id": "socket-xyz",
"handshake": { ... },
"rooms": [ ... ],
"data": { ... }
}
]
}
4.2 响应处理流程
- 每个服务器实例都有一个唯一的 uid
- 发起请求时,服务器会生成一个唯一的 requestId
- 响应中包含 requestId 和发起请求的服务器 uid
- 服务器收到响应后会检查:
- 这是否是自己发起的请求(通过 uid 判断)
- 这个请求是否仍在等待响应(通过 requestId 判断)
- 如果不是自己的请求,服务器会直接忽略这个响应
5. 性能影响与优化建议
5.1 不启用 publishOnSpecificResponseChannel 的性能影响
- 网络流量增加 :所有服务器都接收所有响应
- 处理开销 :每个服务器都需要解析响应并判断是否需要处理
- 内存使用 :处理不必要的消息会占用内存
5.2 优化建议
-
启用 publishOnSpecificResponseChannel :
javascriptio.adapter(createAdapter(pubClient, subClient, { publishOnSpecificResponseChannel: true }))
-
使用 fetchSockets 的优化参数 :
javascriptconst sockets = await this.io.in(roomName).fetchSockets({ flags: { timeout: 1000, // 合理的超时时间 expectResponses: 1 // 只需要最少数量的响应 } });
-
避免不必要的跨服务器请求 :先检查本地,再查询远程
-
使用 Redis 存储共享状态 :减少跨服务器查询的需求
6. 常见问题与解决方案
6.1 为什么 Socket.IO 不默认启用这些优化?
Socket.IO 的默认配置倾向于简单性和可靠性,而不是最大性能。这是为了:
- 向后兼容性 :保持 API 的稳定性
- 简单性优先 :使初学者更容易上手
- 通用性 :适应各种使用场景
- 渐进式优化 :让开发者根据需求逐步优化
6.2 如何判断是否需要这些优化?
如果您的应用符合以下条件,应该考虑启用这些优化:
- 使用多个 Socket.IO 服务器实例
- 有大量的客户端连接
- 频繁使用跨服务器操作(如 fetchSockets() 、 serverSideEmit() )
- 对实时性和性能有较高要求
7. 实际案例分析
7.1 设备登录场景
在我们的系统中,当设备登录时,需要确保同一设备ID只有一个活跃连接。这需要跨服务器检查和操作:
javascript
// 1. 先检查本地服务器
const localMembers = await this.io.sockets.adapter.rooms.get(deviceId);
if (localMembers && localMembers.size > 0) {
// 处理本地重复连接...
} else {
// 2. 再检查其他服务器
try {
const remoteSockets = await this.io.in(deviceId).fetchSockets({
flags: { timeout: 1500, expectResponses: 1 }
});
if (remoteSockets.length > 0) {
// 处理远程重复连接...
}
} catch (error) {
logger.error(`跨服务器查询失败: ${error.message}`);
}
}
8. 总结
在 Socket.IO 分布式系统中,正确配置 Redis Adapter 和优化跨服务器请求可以显著提高系统性能和可扩展性。关键优化点包括:
- 启用 publishOnSpecificResponseChannel 选项
- 使用 fetchSockets() 的优化参数
- 先检查本地再查询远程
- 使用 Redis 存储共享状态 这些优化对于大规模部署尤为重要,可以减少网络流量、降低处理开销,并提高系统响应速度。