混合流量导致 RPC 消息队列堆积

面试案例:混合流量导致 RPC 消息队列堆积

真实经历:军情系统初版上线后,城战期间大量 RPC 涌入导致消息队列堆积,军情查询不刷新。

本文目标 :你用这个文档准备面试,重点不是记住结论,而是能一句一句把推理链讲清楚。


面试中最容易卡住的三个问题

面试官问你"怎么排查的",你大概率会卡在这三个地方:

卡点 你的回答不能只是 你需要能讲出来
"你怎么知道是消息队列堆积?" "我看了监控" 看了哪个监控?什么指标?数值多少?和正常值差多少?
"你怎么区分是 CPU 高导致的堆积,还是 RPC 阻塞导致的堆积?" "CPU 不高就说明不是 CPU 问题" CPU 和 MQ 的关联关系是什么?为什么这个组合能排除 CPU 瓶颈?
"你怎么证明是城战的 RPC 堵住了军情的 RPC?" "日志里能看到" 你统计了什么数据?两种 RPC 各占多少比例?为什么这个比例能说明问题?

下面每一步,都给你准备了可以原样说出口的"面试话术"。


一、故障背景(STAR - S 情境)

一句话概括

"某次版本上线了军情系统后,城战高峰期间,玩家反馈军情页面打开后一直转圈不刷新,同时也有玩家反馈聊天消息延迟好几秒。"

关键信息(面试时要主动说的)

  • 什么服务:军情服务(查询/更新玩家战报数据)
  • 什么时候:城战高峰期(大量玩家同时在线对战)
  • 什么变化:新上线的版本增加了军情系统
  • 什么现象:军情页面不刷新 + 聊天消息延迟

二、排查全过程(STAR - A 行动)

第一步:接到告警------确认"真的有问题,不是个别玩家网络卡"

面试话术:

"收到玩家反馈后,我第一步不是去看代码,而是先确认问题是否真实存在、影响范围多大。我们有一套自建的监控体系,其中最直接的是 alert_engine------它是一个独立的告警服务,每 10 秒从 metrics_collector 拉取所有指标,对照预设的阈值规则来判断是否触发告警。"

实际操作:

bash 复制代码
# alert_engine 每 10s 评估一次,输出 [ALERT] 或 [RECOVER] 日志
grep "\[ALERT\]" log/battle.log | tail -20

期望看到的输出:

复制代码
[ALERT][battle] military_svr mqlen 230 >= threshold 10
[ALERT][battle] military_svr mqlen 280 >= threshold 10
[ALERT][battle] military_svr mqlen 340 >= threshold 10

从这行日志你能得出什么:

信息 含义
[ALERT] 确认不是假阳性,告警已触发
military_svr 问题出在军情服务
mqlen 230 消息队列长度 230,远超阈值 10
连续 3 条 不是瞬时抖动,是持续恶化(230→280→340)

第二步:看 stat_poller------同时拿到 MQ 和 CPU,两条曲线对照

面试话术:

"alert_engine 告诉我 mqlen 高了,但这只是现象,不是根因。mqlen 高有两种可能:一是 CPU 跑满了处理不过来(计算瓶颈),二是处理被阻塞了在等别的东西(IO 阻塞)。这两种情况修复方向完全不同,所以第二步必须拿到 CPU 数据来区分。"

stat_poller 是什么 :它是一个独立的采集服务,每 5 秒对所有被监控的服务调用一次 skynet.call(addr, "debug", "STAT"),拿到 skynet 内置的 mqlen、cpu、message 三个指标,同时输出到日志和上报给 metrics_collector。

实际操作:

bash 复制代码
grep "stat_poller" log/battle.log | tail -6

期望看到的输出:

复制代码
[stat_poller] battle military_svr mqlen=180 cpu=15000000 msg_total=4200
[stat_poller] battle military_svr mqlen=230 cpu=14000000 msg_total=4600
[stat_poller] battle military_svr mqlen=280 cpu=16000000 msg_total=5100
[stat_poller] battle military_svr mqlen=340 cpu=14500000 msg_total=5600
[stat_poller] battle military_svr mqlen=420 cpu=15500000 msg_total=6100
[stat_poller] battle military_svr mqlen=510 cpu=15000000 msg_total=6700

面试时你要用手指着这三个数一个个解读:

"你看这三列数据------"

复制代码
mqlen:   180 → 230 → 280 → 340 → 420 → 510
         每 5 秒涨 50~80,持续上涨,不是抖动

cpu:     15000000 → 14000000 → 16000000 → 14500000 → 15500000 → 15000000
         稳定在 14~16ms/5s窗口 之间,没有明显上升

msg_total:  4200 → 4600 → 5100 → 5600 → 6100 → 6700
            每 5 秒新增 400~500 条消息,消息到达速率稳定

"关键判断 :mqlen 持续上涨,但 cpu 没有同步上涨。如果 CPU 是瓶颈,mqlen 涨的时候 cpu 应该也涨(处理每条消息都要消耗 CPU)。现在 cpu 稳定在低位,说明消息处理没有在密集计算,而是在等什么东西。"

这一步排除的假设:

假设 如果成立,应该看到的 实际看到的 结论
死循环 / 密集计算 CPU 持续高位(>80%) CPU 14~16ms 低位 ❌ 排除
内存泄漏导致 GC 频繁 memory_monitor 显示内存持续增长 + CPU 升高 CPU 稳定 ❌ 排除
消息处理被阻塞 CPU 低 + MQ 持续涨 ✅ 完全吻合 ⚠️ 重点怀疑

第三步:pstack------看清楚阻塞在等什么

面试话术:

"排除了 CPU 瓶颈后,下一步要搞清楚它在等什么。思路很简单:如果它在等网络,说明是在等 RPC 返回或跨节点通信;如果它在等锁,说明是死锁。pstack 可以零侵入地看进程此刻卡在哪个系统调用上。"

pstack 的原理 :它不需要暂停进程,只是读取 /proc/PID/stack,打印当前线程的内核态调用栈。对线上服务零影响。

实际操作:

bash 复制代码
PID=$(pgrep -f "config.battle" | head -1)

# 连续采样 5 次,间隔 2 秒,看栈顶是否稳定在同一位置
for i in 1 2 3 4 5; do
    echo "=== 采样 $i ==="
    sudo pstack $PID | head -30
    sleep 2
done

期望看到的关键行(5 次采样中反复出现):

复制代码
=== 采样 1 ===
#0  __epoll_wait(...)              ← 在等网络事件
#1  skynet_socket_poll(...)
#2  skynet_context_message_dispatch(...)
#3  luaM_pcall → military_svr:handle_query(...)

=== 采样 2 ===
#0  __epoll_wait(...)              ← 又是 epoll_wait
#1  skynet_socket_poll(...)
#2  ...
#3  luaM_pcall → rpc.call(...)     ← 在 RPC 调用内部等响应

=== 采样 3 ===
#0  __epoll_wait(...)              ← 还是 epoll_wait
...

=== 采样 4 ===
#0  __epoll_wait(...)
...

=== 采样 5 ===
#0  __epoll_wait(...)
...

面试话术:

"pstack 的结果非常明确:5 次采样,栈顶全部是 __epoll_wait。这说明什么?说明军情服务的消息处理线程不是在跑 CPU,而是在等网络 IO------大概率是在等 RPC 调用返回、或者在等跨节点的 cluster 消息传输完成。

如果栈顶是 luaV_execute 或 luaD_call,那说明在跑 Lua 代码,是计算问题。但这里 5 次全是 epoll_wait,排除了所有计算瓶颈的可能性,锁定根因在 RPC / 网络 IO 层面。"


第四步:debug console------确认是哪个服务的队列在堆积

面试话术:

"pstack 告诉我是网络 IO 阻塞,但还没回答一个问题:是军情服务自己的处理阻塞了,还是它调的下游服务阻塞了?这一步我用 skynet 自带的 debug console 来看所有服务的队列长度。"

实际操作:

bash 复制代码
telnet 127.0.0.1 8004
list

期望输出:

复制代码
:00000004  snlua cmaster
:00000005  snlua debug_console
:0000000a  snlua military_svr   (mqlen: 510)    ← 军情服务,队列炸了
:0000000b  snlua battle_data_svr (mqlen: 8)     ← 下游数据服务,正常
:0000000c  snlua match_mgr       (mqlen: 3)     ← 匹配服务,正常

面试话术:

"注意看------military_svr 的队列 510,但它的下游 battle_data_svr 队列只有 8。这说明不是下游服务慢导致的连锁阻塞。如果下游慢,下游的队列也应该高才对。

问题出在 military_svr 自身的消息处理------它在处理某些消息时做了阻塞操作,导致自己的队列越积越长。"


第五步:找证据------证明是城战的 RPC 堵住了军情的 RPC

面试话术:

"到这里,我已经确认三点:① mqlen 高 ② CPU 低(不是计算瓶颈)③ 阻塞在 epoll_wait(是 IO 等待)。但还有一个最关键的问题没回答------到底是什么消息堵住了队列?是军情自己的请求太多?还是别的系统的请求混进来了?

要回答这个问题,我需要看消息队列里混了哪些类型的消息。"

方法:agent 日志统计

agent 在收到客户端请求时,会打印 [agent] request name=xxx,记录了每个请求的协议名。通过统计城战高峰期的请求类型分布,就能还原当时消息队列里的"成分"。

bash 复制代码
# 统计城战高峰期(比如 20:15~20:20)的请求类型
grep "\[agent\] request name=" log/gate.log \
  | grep "20:1[5-9]:" \
  | sed 's/.*name=//' \
  | sort | uniq -c | sort -rn

期望输出:

复制代码
   1247 guild_war_turn_submit     ← 城战回合提交,占 47%
    623 military_query            ← 军情查询,占 23%
    412 guild_war_damage_report   ← 城战伤害上报,占 16%
    205 military_update           ← 军情数据更新,占 8%
    168 chat_public               ← 聊天,占 6%

面试话术(这是整个排查中最关键的一段推理):

"你看这个分布------城战相关的 RPC(guild_war_turn_submit + guild_war_damage_report)加起来占了 63%,而军情自己的 RPC(military_query + military_update)只占 31%。

问题不是军情的请求变多了,是城战的请求涌进了军情服务的消息队列。

Skynet 的消息队列是 FIFO(先进先出),所有消息按到达顺序处理。城战期间每秒涌入几十条城战 RPC,它们排在队列前面,每条都要处理几十到几百毫秒(因为涉及广播、数据写入等操作)。军情的查询请求排在后面,要等前面所有城战 RPC 处理完才能轮到。

这就是为什么玩家看到军情页面一直转圈------不是军情系统坏了,是军情系统的消息队列被城战 RPC 堵住了。"


第六步:完整推理链(面试时按这个讲)

复制代码
玩家投诉:军情页面不刷新
    │
    ▼
① alert_engine → military_svr mqlen=510(远超阈值 10)
    → 确认问题存在,队列严重堆积
    │
    ▼
② stat_poller → mqlen↑↑ + cpu 稳定低位(15ms/5s)
    → 排除 CPU 计算瓶颈和内存泄漏,锁定 IO 阻塞方向
    │
    ▼
③ pstack × 5 → 栈顶全是 __epoll_wait
    → 确认阻塞在网络 IO,不是 Lua 计算
    │
    ▼
④ debug console list → 只有 military_svr 队列高,下游服务正常
    → 排除下游服务慢导致的连锁阻塞,问题在 military_svr 自身
    │
    ▼
⑤ agent 日志统计 → 城战 RPC 占 63%,军情 RPC 只占 31%
    → 混合流量涌入同一队列,城战 RPC 堵住军情 RPC(Head-of-Line Blocking)
    │
    ▼
⑥ 根因:Skynet 单线程 Actor 的 FIFO 队列,
   慢消息(城战 RPC,含广播/写入)阻塞快消息(军情查询)

三、修复方案

止损(分钟级)

面试话术:

"我先做了一件最简单的事:在网关层临时拦截城战 RPC 转发到军情服务,把城战相关消息路由到独立的处理通道。这样军情服务的队列立刻从 510 降到 10 以内。这是止损,不是解决根本问题。"

根因修复(小时级)

面试话术:

"根本问题在于我把两类性质完全不同的消息放到了同一个 Actor 的消息队列里:

  • 城战 RPC:高频突发、单次耗时长(涉及广播、数据写入)、对延迟不敏感(玩家可以接受战报晚几秒到)
  • 军情 RPC:低频稳定、单次耗时短、对延迟极其敏感(玩家点开页面必须立刻看到)

修复方案是把城战 RPC 拆分到独立的服务 。军情服务只处理军情相关的查询和更新,城战相关的数据写入走单独的 guild_war_data_svr。两个服务各自独立的 FIFO 队列,互不阻塞。"

lua 复制代码
-- 修复前:两种消息混在同一个服务
-- military_svr 同时处理:
--   military_query(军情查询,轻量,要求即时响应)
--   guild_war_turn_submit(城战回合提交,重量,可异步)

-- 修复后:拆分成两个服务
-- military_svr     → 只处理 military_query / military_update
-- guild_war_data_svr → 只处理 guild_war_turn_submit / guild_war_damage_report

四、修复结果(STAR - R 结果)

指标 修复前 修复后
military_svr mqlen 510 6
军情查询 P99 8.5s(超时) 180ms
聊天消息 P99 5.2s 120ms
城战数据写入 P99 3.5s 3.8s(略增,可接受)
CPU(battle 节点) 15ms/5s 22ms/5s(略增,多了一个服务)

五、面试话术速记卡

30 秒版本(面试官问"你遇到过什么性能问题")

"我主导排查过一次军情系统的卡顿。现象是城战高峰期军情页面一直转圈不刷新。我通过 stat_poller 发现 mqlen 涨到 510 但 CPU 很低------这不是计算瓶颈,是 IO 阻塞。pstack 5 次全是 epoll_wait,确认是网络等待。最后统计 agent 日志发现城战 RPC 占了消息队列的 63%,军情查询排在后面被堵住了。根因是两类消息混在同一个 Actor 的 FIFO 队列里,慢消息拖死了快消息。修复方案是把城战 RPC 拆分到独立服务,mqlen 从 510 降到 6。"

2 分钟版本(面试官追问"你怎么查出来的")

按照上面第六步的推理链,①②③④⑤⑥ 挨个讲。

面试官可能追问

Q:为什么 CPU 低就能排除 CPU 瓶颈?

"因为 mqlen 上涨说明消息处理速度跟不上到达速度。如果 CPU 是瓶颈,处理每条消息消耗的 CPU 是固定的,处理速度跟不上意味着单位时间消耗的总 CPU 应该上升。但 stat_poller 显示 cpu 稳定在 15ms/5s 不变,说明处理速度没变慢,是单个消息的处理时间变长了------它在等 IO,不在计算。"

Q:pstack 看到 epoll_wait 就一定是 RPC 阻塞吗?

"不一定,epoll_wait 也可能是等客户端消息。但结合三个证据:① mqlen 持续涨(说明有消息在处理中)② alert_engine 显示是 military_svr 的队列高(不是 worker 线程空闲)③ agent 日志显示大量城战 RPC 涌入。三点交叉验证,锁定是 RPC 阻塞。"

Q:为什么不直接在 military_svr 里加个消息优先级?

"Skynet 的消息队列是框架 C 层实现的 FIFO,没有暴露优先级接口。如果硬要在 Lua 层实现(比如收到消息后判断类型、低优先级消息先放到 side queue),会破坏 skynet.dispatch 的语义,而且框架升级时可能不兼容。拆服务是 Skynet 官方推荐的做法------一个 Actor 只做一件事。"


六、面试策略:三个案例递进讲

顺序 案例 时长 展示什么
1 CPU 暴涨排查 3 分钟 工具链熟练度(perf 火焰图 + pstack 交叉验证)
2 内存泄漏排查 2 分钟 工程化方法(memory_monitor d10m 滑动窗口 + leak_detect)
3 混合流量 MQ 堆积 5 分钟 架构理解深度(Actor 模型边界认知 + 混合流量竞争)

第三个案例是杀手锏------前两个大部分 Skynet 开发者都会,第三个展示的是你对框架设计缺陷的清醒认知,面试官会记住你。

相关推荐
阿正的梦工坊1 天前
深入理解 JSON-RPC 2.0:轻量级远程调用协议详解
rpc·json
进化中的刀盾兽3 天前
从零实现 C++ Json-Rpc(十):把消息交给真实连接——MuduoConnection 与 ConnectionFactory
c++·rpc
骑着蜗牛撵大象3276 天前
飞牛fnOS部署Damselfly:Compose编排、照片目录与7100固定访问
java·docker·rpc·dubbo·fnos·照片管理·damselfly
syagain_zsx8 天前
BRpc 二次封装:Channel 管理、服务管理与回调工厂
c++·学习·程序人生·rpc·谷歌
mister_guo8 天前
HTTP 和 RPC:从一次干饭说清它们的区别
网络协议·http·rpc
zzu123zsw8 天前
面对多 RPC 扇入超时崩溃:深入 asyncTool 核心 DAG 编排与线程池隔离防护
java·网络·spring boot·网络协议·rpc·并发编程
syagain_zsx8 天前
BRpc 搭建简单 rpc 服务器
服务器·c++·网络协议·rpc·protobuf·库
SatanII10 天前
K8s 存储与配置管理|Volume、ConfigMap、Secret 核心笔记
笔记·rpc·kubernetes
斌糖雪梨10 天前
registerListeners(); 方法详解
网络·网络协议·rpc