背景

如果几万人同时发言,服务器怎么扛住?

痛点

  • 所有人发消息,所有人都要收到。如果10万人同时发,消息量是 N×N,这是巨大的广播风暴。

核心策略

限流、消息队列、分频道

具体设计

① 接入层(限流与风控):

首先,我不会让所有消息都透传到逻辑服。在网关层(Gate Server)做频率限制(Rate Limit),比如每秒最多发1条。同时做敏感词过滤和禁言检测,拦截非法请求,减少后端压力。

② 逻辑层(服务拆分与广播):

我会把聊天做成一个独立的微服务(Chat Server)。

  • 普通世界频道:考虑到世界频道人多,我倾向于写扩散,即消息只发一份到消息队列,由消息队列推送给在线的人。

  • 分治思想:如果在线人数超过5万,我会按 “频道分片”​ 部署多个Chat Node,每个Node负责一部分玩家,通过一致性哈希路由,避免单点瓶颈。

③ 存储层(Redis缓存):

聊天记录不需要入库,只需要在 Redis 里缓存最近 100 条(List结构),玩家上线或断线重连时拉取最近记录即可。

④ 降级方案:

如果服务器负载过高,我会自动关闭世界频道的广播,或者开启“VIP发言”模式,保证核心用户体验。

为何采取写扩散

在游戏服务器中,全服聊天通常是“伪全服”(比如只显示最近100条),且对实时性要求极高。

  • 如果用读扩散:玩家每次滑动聊天框,都要去DB/Redis读一次全服最新的100条。如果10万人同时读,DB压力巨大。

  • 如果用写扩散:玩家发消息时,直接把消息写入到每个玩家的“未读消息列表”或“公共频道缓存”中。玩家上线时,直接从自己的缓存里拿,速度快,压力小。

写扩散的具体实现步骤

  1. 数据结构设计(Redis)

我们需要两个核心结构:

  • Channel List (Sorted Set):存全服最近的100条消息(作为数据源)。

  • User Inbox (List):存每个玩家私人的消息队列(可选,用于离线推送)。

# 1. 全服频道(Key: chat:world)
# Score 是时间戳,Member 是消息内容(JSON)
ZADD chat:world 1678888888 "{uid:1, name:'A', msg:'你好'}"

# 2. 玩家B的收件箱(Key: inbox:uid:2)
# 只存消息ID,或者存完整消息(看内存大小)
LPUSH inbox:uid:2 "msg_id_123"
  1. 发送消息流程
  • 入口校验:玩家A发消息,GateServer 先校验频率(1秒1条)和敏感词。

  • 写入公共池:逻辑服收到消息,生成一个唯一 MsgID,把消息内容(JSON)存入 Redis 的 chat:world有序集合。同时用 ZREMRANGEBYRANK删除排名100以后的消息,保证只存最新的100条。

  • 触发扩散:

    • 方案A(简单粗暴):直接查询当前全服在线玩家列表(比如从 SkyNet 的 Agent 里拿),循环给每个人发 Socket 推送。

      • 缺点:如果在线5万人,这一瞬间的 CPU 和网络 IO 会爆表。
    • 方案B(推荐:结合消息队列):不直接推,而是把“广播任务”丢进消息队列。

MQ的作用

  • 削峰填谷(Peak Shaving):春节活动,1秒内来了1000条全服喊话。如果没有 MQ,服务器直接卡死。有了 MQ,Worker 可以按每秒处理200条的速度慢慢推,虽然玩家看到消息有1秒延迟,但服务器活下来了。

  • 解耦(Decoupling):发消息的逻辑(写)和推消息的逻辑(推)分开。万一推送服务挂了,消息还在 MQ 里,重启后能继续推,不会丢消息。

  • 失败重试:如果推送给玩家B失败了(网络波动),Worker 可以从 MQ 里重新拉取这条消息再试一次。

频道分片

[Gate Server 1] \
[Gate Server 2] ---> [Router/Proxy] ---> [Chat Node A] (负责玩家 1~16666)
[Gate Server 3] /                     \-> [Chat Node B] (负责玩家 16667~33333)
                                        \-> [Chat Node C] (负责玩家 33334~50000)

分片策略选择

在这里插入图片描述
这里我采取一致性哈希

核心难点:跨分片聊天

如果分片了,玩家A在 Node A,玩家B在 Node B,A说话B看不见,这就不是“世界频道”了。
解决方案:引入“路由层(Router/Proxy)
流程

[Node A] -> [Router] -> [Kafka/RabbitMQ] -> [Node B Consumer] -> [Push to Players]
                                      \--> [Node C Consumer] -> [Push to Players]
  1. 玩家A​ 在 Node A 发消息。
  2. Node A​ 处理完本节点的广播后,把消息发送给 Router。
  3. Router​ 知道全服所有的 Chat Node 列表(A, B, C)。
  4. Router​ 把消息复制MQ。
  5. MQ异步转发给 Node B 和 Node C。
  6. Node B/C​ 收到消息后,再推送给各自负责的玩家。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐