背景:
现在,假设"国战军情"需要向全服数千玩家广播时触发的场景。而当前的架构是"一对一"推送模型,其在广播场景下存在设计瓶颈。如果是你,你会怎么优化这个架构。
方案思考的角度
- 异步
- 削峰
- 解耦
新架构核心
- 中心化广播服务 + 推拉结合
目标
- 将集中的、计算密集的推送压力,转化为分散的、可平滑处理的拉取请求。
核心组件
-
广播中心 (BroadcastCenter):接收所有需要广播的军情事件,作为唯一入口。
-
频道管理器 (ChannelManager):管理订阅关系。玩家登录时,其所在的大厅服务会向管理器订阅相关频道(如联盟:1001、全服)。
-
多级内存队列 (Multi-level Queue):
- 高优先级队列:用于国战指挥、系统公告等实时性要求高的消息。
- 普通优先级队列:用于普通军情、聊天广播等。
- 队列处理器:独立的消费者协程/线程,从队列中取出消息执行推送。
数据流
军情产生\] -\> \[广播中心\] -\> \[按优先级入队\] -\> \[队列处理器异步消费\] -\> \[遍历频道订阅列表推送
关键优化点
-
解耦与异步化:将"事件生产"与"消息推送"解耦。军情生成后立即返回,推送任务异步执行,避免阻塞主逻辑。
-
流量削峰:队列将瞬间的脉冲式广播,转化为平滑的匀速处理,保护 CPU。
-
优先级保障:确保关键消息(如战斗指令)优先送达。
-
轻量推送:推送内容仅为通知(如 {"type": "march_update", "id": 1001}),而非完整数据。
方案不足点
在单服务内,用异步队列将同步风暴转为平滑任务流,无法满足分布式场景的需求。
容灾
场景:当消息队列挂了,广播系统系统会怎样?
lua
-- 文件:task_dispatcher.lua (采用工作池的完整版)
local skynet = require "skynet"
local queue = require "skynet.queue"
local TASK_QUEUE = {} -- 任务队列
local WORKER_COUNT = 8 -- 工作协程数量,根据CPU和业务调整
local cv_full = skynet.condition() -- 条件变量,用于队列满时等待
local cv_empty = skynet.condition() -- 条件变量,用于队列空时等待
local MAX_QUEUE_SIZE = 10000 -- 队列最大长度,实现背压
function CMD.dispatch_march(march_data)
-- 【背压控制】如果队列满了,阻塞等待,避免内存爆炸
while #TASK_QUEUE >= MAX_QUEUE_SIZE do
skynet.wait(cv_full)
end
-- 将任务放入队列
table.insert(TASK_QUEUE, march_data)
skynet.wakeup(cv_empty) -- 唤醒一个可能空闲的工作协程
return true
end
-- 工作协程的主循环函数
local function worker_loop(worker_id)
skynet.error(string.format("Worker %d started.", worker_id))
while true do
-- 从任务队列中取任务,队列空则休眠等待
while #TASK_QUEUE == 0 do
skynet.wait(cv_empty)
end
local task = table.remove(TASK_QUEUE, 1)
skynet.wakeup(cv_full) -- 取出一个任务,通知队列有空位
-- 处理任务:调用MQ服务
local ok, err = pcall(skynet.call, MQ_SERVICE_ADDR, "lua", "push", task)
if not ok then
skynet.error(string.format("Worker %d failed: %s", worker_id, err))
-- 处理失败逻辑,如重试、放入死信队列等
handle_failure(task, err)
end
end
end
-- 在服务启动时,创建固定数量的工作协程
skynet.init(function()
for i = 1, WORKER_COUNT do
skynet.fork(worker_loop, i)
end
end)
优化方向
-
服务拆分:军情、Lobby、社交等服务独立部署,内存队列无法跨进程。
-
水平扩展:单个广播中心成为瓶颈,需支持多节点。
-
可靠性要求:需消息持久化,防止服务重启丢失。
组件升级
- 广播中心 -> 军情事件服务:无状态,只负责接收和标准化事件。
- 内存队列 -> 分布式消息队列 (MQ):如 Kafka 或 Pulsar。负责持久化、解耦、削峰。
- 频道管理器 -> 注册中心:如 ZooKeeper/Etcd/Nacos,管理动态订阅关系。
- 大厅服务集群:作为消费者,从 MQ 拉取消息,并管理玩家连接。
数据流
军情事件\] -\> \[军情服务\] -\> \[发布至MQ Topic\] -\> \[多个Lobby服务并发消费\] -\> \[轻量通知玩家\] -\> \[客户端延迟拉取详情
核心演进点
-
完全解耦:军情服务与玩家连接管理彻底分离,可独立伸缩。
-
消息持久化:MQ 保证消息不丢,支持重播。
-
推拉结合 (Hybrid Push-Pull):
-
推:Lobby 向玩家推送轻量通知(几十字节)。
-
拉:玩家客户端随机延迟后,主动 HTTP 拉取详情。此设计彻底消除了"拉取风暴"。
-
-
动态发现:Lobby 服务启动时向注册中心注册,军情事件按需分发。
收益与代价
-
收益:
-
高可用:无单点,组件可水平扩展。
-
高可靠:消息不丢失。
-
极致解耦:各服务技术栈可独立演进。
-
-
代价:
-
架构复杂度:引入 MQ、注册中心,运维成本增加。
-
延迟微增:从内存操作变为网络 RPC,但通常在可接受范围(毫秒级)。
-
最终一致性:消息可能有微小延迟。
-