实战篇:分布式游戏服务器集群搭建
概述
本篇把第 10 周的内容拼成一套完整集群:网关层 + 多逻辑节点 + 房间分片 + RPC 通信 + 消息队列解耦。以"匹配对战 → 房间分片 → 结算异步"为主链路,展示集群的部署结构、路由与通信的工程实现,以及跨节点协作的完整闭环。
一、集群架构
1.1 拓扑
集群拓扑:
网关层(2+):Netty WS 接入,无状态,负载均衡
逻辑层(N):玩家/房间分片(一致性哈希)
服务发现:Nacos(节点注册/摘除)
全局会话:Redis(playerId → 节点)
消息队列:RocketMQ(结算/统计/邮件异步)
数据层:MySQL(分片)+ Redis(缓存/会话)
主链路:
客户端 → 网关 → 逻辑节点(分片)→ 结算 → MQ → 各系统1.2 工程结构
com.game.cluster
├── gateway
│ ├── GatewayServer.java // 网关启动(Netty WS)
│ └── GatewayRouter.java // 全局会话 + 路由转发
├── logic
│ ├── LogicServer.java // 逻辑节点启动(分片)
│ ├── RoomShard.java // 房间分片(一致性哈希)
│ └── PlayerHandler.java // 玩家业务
├── rpc
│ ├── GameRpcClient.java // gRPC 客户端
│ └── GameRpcServer.java // gRPC 服务端
├── mq
│ ├── MqProducer.java // 事件发布
│ └── SettleConsumer.java // 结算消费
└── common
└── NodeRegistry.java // Nacos 注册二、节点注册与发现
java
// 逻辑节点启动:注册 + 分片初始化
public class LogicServer {
public static void main(String[] args) {
String nodeId = args[0]; // 节点编号
// 注册到 Nacos(服务发现)
registry.register("game-logic", nodeId, ip, port);
// 初始化一致性哈希(带虚拟节点)
roomShard = new ConsistentHashRouter(nodeId, VIRTUAL_NODES);
// 启动 gRPC 服务(接收跨节点 RPC)
startRpcServer(port);
}
}三、网关转发
java
// 网关:登录绑定 + 按玩家路由转发
public class GatewayRouter {
// 玩家消息 → 对应逻辑节点
public void forward(long playerId, Object msg) {
String node = globalSession.lookup(playerId);
if (node == null) {
// 未上线,回错误
} else {
rpcClient.call(node, "HandlePlayerMsg", playerId, msg);
}
}
// 登录绑定(Redis 全局会话)
public void bind(long playerId, String node, String channelId) {
globalSession.set(playerId, node + ":" + channelId);
}
}四、房间分片与跨节点路由
java
// 逻辑节点:房间分片 + 跨节点路由
public class RoomShard {
private final String selfNode;
private final ConsistentHashRouter router;
// 玩家请求房间操作:定位房间所在节点
public void roomAction(long roomId, Object action) {
String node = router.route(roomId); // 一致性哈希
if (node.equals(selfNode)) {
localHandle(roomId, action); // 本地直接处理
} else {
rpcClient.call(node, "HandleRoomAction", roomId, action);
}
}
}java
// 玩家匹配 → 创建房间(房间归属节点决定)
public void onMatchFound(long a, long b) {
long roomId = idGen.nextId();
String owner = router.route(roomId); // 分片决定节点
rpcClient.call(owner, "CreateRoom", roomId, a, b);
notifyPlayer(a, roomId);
notifyPlayer(b, roomId); // 通知 → 网关 → 客户端
}五、消息队列解耦
java
// 结算事件 → MQ → 各系统异步消费
public class MqProducer {
// 对局结束(房间所在节点调用)
public void publishSettle(BattleResult result) {
mq.send("battle-settle", result);
}
}
// 消费方:货币/任务/成就/排行榜各自处理
public class SettleConsumer {
@MessageListener(topic = "battle-settle")
public void onSettle(BattleResult result) {
// 发货币(幂等:battleId)
currencyService.change(result.winnerId, GOLD,
result.goldReward, "BATTLE", "battle:" + result.battleId);
// 任务进度
questService.onBattleEnd(result);
// 排行榜异步更新(合并)
rankService.addScore("rank:power:daily", result.winnerId, result.score);
}
}六、故障处理
节点故障:
逻辑节点宕机 → Nacos 摘除 → 分片重新路由
该节点房间 → 其他节点重建(快照恢复)
在线玩家 → 重连其他节点(全局会话更新)
网关故障:
连接断 → 客户端重连其他网关
逻辑节点路由不变(玩家状态在逻辑层)
MQ 积压:
消费限速 + 扩容消费者
积压告警(见监控章节)// 故障恢复:玩家重连时恢复房间
public void onReconnect(long playerId, long roomId) {
String node = router.route(roomId);
rpcClient.call(node, "ReconnectRoom", roomId, playerId);
// 目标节点返回房间快照 → 客户端恢复
}七、关键要点回顾
工程要点:
网关无状态(全局会话路由)
逻辑节点分片(一致性哈希 + 虚拟节点)
跨节点 RPC(gRPC + 超时重试)
MQ 解耦(结算/统计/邮件异步)
服务发现(Nacos 自动注册摘除)
故障恢复(分片重建 + 重连快照)
验证清单:
玩家消息正确路由到所在节点
房间创建落在分片节点且可被路由到
跨节点匹配成功并通知双方
结算事件被各系统消费且幂等
节点下线后房间可恢复
网关故障不影响逻辑层状态链路总览:
客户端 → 网关 → 全局会话 → 逻辑节点(分片)
→ 房间处理 → 结算事件 → MQ
→ 货币/任务/排行榜异步
→ 结果经网关回推客户端