广播风暴架构优化方案思考

背景:

现在,假设"国战军情"需要向全服数千玩家广播时触发的场景。而当前的架构是"一对一"推送模型,其在广播场景下存在设计瓶颈。如果是你,你会怎么优化这个架构。

方案思考的角度

  • 异步
  • 削峰
  • 解耦

新架构核心

  • 中心化广播服务 + 推拉结合

目标

  • 将集中的、计算密集的推送压力,转化为分散的、可平滑处理的拉取请求。

核心组件

  • 广播中心 (BroadcastCenter):接收所有需要广播的军情事件,作为唯一入口。

  • 频道管理器 (ChannelManager):管理订阅关系。玩家登录时,其所在的大厅服务会向管理器订阅相关频道(如联盟:1001、全服)。

  • 多级内存队列 (Multi-level Queue):

    • 高优先级队列:用于国战指挥、系统公告等实时性要求高的消息。
    • 普通优先级队列:用于普通军情、聊天广播等。
    • 队列处理器:独立的消费者协程/线程,从队列中取出消息执行推送。

数据流

军情产生\] -\> \[广播中心\] -\> \[按优先级入队\] -\> \[队列处理器异步消费\] -\> \[遍历频道订阅列表推送

关键优化点

  1. 解耦与异步化:将"事件生产"与"消息推送"解耦。军情生成后立即返回,推送任务异步执行,避免阻塞主逻辑。

  2. 流量削峰:队列将瞬间的脉冲式广播,转化为平滑的匀速处理,保护 CPU。

  3. 优先级保障:确保关键消息(如战斗指令)优先送达。

  4. 轻量推送:推送内容仅为通知(如 {"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、社交等服务独立部署,内存队列无法跨进程。

  • 水平扩展:单个广播中心成为瓶颈,需支持多节点。

  • 可靠性要求:需消息持久化,防止服务重启丢失。

组件升级

  1. 广播中心 -> 军情事件服务:无状态,只负责接收和标准化事件。
  2. 内存队列 -> 分布式消息队列 (MQ):如 Kafka 或 Pulsar。负责持久化、解耦、削峰。
  3. 频道管理器 -> 注册中心:如 ZooKeeper/Etcd/Nacos,管理动态订阅关系。
  4. 大厅服务集群:作为消费者,从 MQ 拉取消息,并管理玩家连接。

数据流

军情事件\] -\> \[军情服务\] -\> \[发布至MQ Topic\] -\> \[多个Lobby服务并发消费\] -\> \[轻量通知玩家\] -\> \[客户端延迟拉取详情

核心演进点

  • 完全解耦:军情服务与玩家连接管理彻底分离,可独立伸缩。

  • 消息持久化:MQ 保证消息不丢,支持重播。

  • 推拉结合 (Hybrid Push-Pull):

    • 推:Lobby 向玩家推送轻量通知(几十字节)。

    • 拉:玩家客户端随机延迟后,主动 HTTP 拉取详情。此设计彻底消除了"拉取风暴"。

  • 动态发现:Lobby 服务启动时向注册中心注册,军情事件按需分发。

收益与代价

  • 收益:

    • 高可用:无单点,组件可水平扩展。

    • 高可靠:消息不丢失。

    • 极致解耦:各服务技术栈可独立演进。

  • 代价:

    • 架构复杂度:引入 MQ、注册中心,运维成本增加。

    • 延迟微增:从内存操作变为网络 RPC,但通常在可接受范围(毫秒级)。

    • 最终一致性:消息可能有微小延迟。

相关推荐
zhangrelay2 小时前
三分钟云课实践速通--大学物理--python 版
linux·开发语言·python·学习·ubuntu·lubuntu
探物 AI2 小时前
【感知·医学分割】当 YOLOv11 杀入医学赛道:先检测后分割的级联架构
算法·yolo·计算机视觉·架构
uzong2 小时前
软件架构设计的考虑:如构建一个长生周期的系统
后端·架构
炽烈小老头2 小时前
【每日天学习一点算法 2026/04/27】缺失的第一个正数
学习·算法
handler012 小时前
Linux 进程探索:从 PCB 管理到 fork() 的写时拷贝
linux·c语言·c++·笔记·学习
原则猫2 小时前
曝光埋点
架构
QiZhang | UESTC2 小时前
从基础 RoPE 到 YaRN:源码学习路线揭秘
pytorch·深度学习·学习
xuhaoyu_cpp_java3 小时前
MyBatis学习(五)
经验分享·笔记·学习·mybatis
ECT-OS-JiuHuaShan3 小时前
整体论体系定理,全球开放,无法绕过
人工智能·科技·学习·算法·生活