cpu暴涨类
在匹配服务中新增一个死循环。
-- 玩家请求开始匹配functionCMD.start_match(uid,elo_val,name,agent_addr)-- ====== FAULT INJECTION (INJECTED): CPU 死循环 ======local_fault_counter=0whiletruedo_fault_counter=_fault_counter+1end-- ====== END FAULT INJECTION ======-- 防重复检查ifuid_bucket_map[uid]thenreturn{ok=false,errmsg="already in matching queue"}end-- 找到对应的段位桶localbucket_name=elo.get_bucket(elo_val)localbucket_players_a=get_bucket_players(bucket_name)table.insert(bucket_players_a,{uid=uid,elo=elo_val,name=name,agent_addr=agent_addr,enter_time=skynet.now()/100,})uid_bucket_map[uid]=bucket_name rpc.send("chat_mgr","update_player_state",uid,"matching")skynet.error(string.format("[match_mgr] player enter queue: uid=%d name=%s elo=%d bucket=%s total=%d",uid,name,elo_val,bucket_name,get_total_queue_size()))return{ok=true}end步骤
一、先查看正常情况下的skynet的正常状态
服务列表
消息队列和cpu
内存占用情况
查看cpu使用率
# 2. 看 Battle 节点的 CPUtop-p$(pgrep-f"chatroom.*battle"|head-1)-bn1|head-5解析:
第一行:系统概览
top - 21:24:02 up 1:13, 0 users, load average: 4.99, 4.75, 4.65| 字段 | 含义 | 你的值 |
|---|---|---|
21:24:02 | 当前时间 | 晚上 9:24 |
up 1:13 | 开机到现在 1 小时 13 分 | — |
0 users | 登录用户数 | 0(你可能是 ssh 进来的不算) |
load average | 面试高频考点 | ⚠️ 4.99 / 4.75 / 4.65 |
load average 三个数字指过去 1 分钟、5 分钟、15 分钟的平均负载。含义是:等待 CPU + 等待 IO 的进程数。你的值 4.99 意味着平均有 5 个进程排队等资源。如果机器是 4 核,那已经饱和了。
💡 面试技巧:load > CPU 核心数 → CPU 瓶颈;load 高但 CPU us 低 → IO 瓶颈。
第二行:进程统计
任务: 1 total, 0 running, 1 sleeping, 0 stopped, 0 zombie| 字段 | 含义 |
|---|---|
total | 总计进程/线程 |
running | 正在 CPU 上跑 |
sleeping | 在等事件(网络 IO、定时器等) |
stopped | 被信号暂停 |
zombie | 僵尸进程,不为 0 就要查 |
第三行:CPU 分解 ⭐ 最重要
%Cpu(s): 0.0 us, 1.6 sy, 0.0 ni, 98.4 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st这是瓶颈区分的核心依据:
| 缩写 | 全称 | 含义 | 告警阈值 |
|---|---|---|---|
us | user | 用户态 CPU(你的业务代码) | > 80% → 代码死循环/计算密集 |
sy | system | 内核态 CPU(系统调用) | > 30% → 频繁 IO/锁竞争 |
ni | nice | 低优先级进程的 CPU | 通常忽略 |
id | idle | 空闲 | 接近 100% = 没事 |
wa | iowait | 等磁盘 IO 的 CPU 时间 | > 10% → 磁盘瓶颈 |
hi | hardware irq | 硬件中断 | 通常 < 1% |
si | software irq | 软中断(网络包处理) | > 5% → 网络包太多 |
st | steal | 虚拟机被宿主机偷走的 CPU | 云主机关注,> 5% 不正常 |
你的当前状态:us=0.0, id=98.4→ CPU 几乎空闲,还没触发故障。
故障触发后你会看到:us=99.0+, id=0.0→ 用户态代码吃满 CPU,典型死循环。
记忆口诀:
us 高 = 代码有 bug(死循环、复杂计算) sy 高 = 系统调用太多(频繁 IO、锁) wa 高 = 磁盘慢(数据库、日志写盘) si 高 = 网络包太多(DDoS、广播风暴)第四、五行:内存
MiB Mem : 3876.0 total, 112.2 free, 2192.3 used, 1571.5 buff/cache MiB Swap: 2048.0 total, 2045.5 free, 2.5 used. 1408.1 avail Mem| 字段 | 含义 | 你的值 |
|---|---|---|
total | 总内存 | 3.8 GB |
free | 完全空闲 | 112 MB(看着少但正常) |
used | 已用 | 2.2 GB |
buff/cache | Linux 用空闲内存做文件缓存,可随时释放 | 1.5 GB |
avail Mem | 实际可用 = free + 可释放的 cache | 1.4 GB ✅ |
Swap used | 用了多少交换分区 | 2.5 MB(几乎没用,好事) |
💡
free低别紧张,看available。Linux 会尽可能把空闲内存用作文件缓存,这是正常的。只有available持续下降才说明内存不够。
🎯 排查时你只需要看这三个
排查 CPU 问题 → 看 %Cpu 那行:us 高 = 代码问题,sy 高 = 系统调用 排查 IO 问题 → 看 %Cpu 那行:wa 高 = 等磁盘 排查内存问题 → 看 Mem 那行:available 持续下降 = 内存泄漏 排查负载 → 看 load average:超过 CPU 核数 = 系统过载二、查看哪个进程cpu占用
使用top
看出是skynet的一个进程
接着查看skynet是谁
cat /proc/{PID}/cmdline | tr '\0' ' 'PID填写top查询到的异常进程id
确定是battle进程有问题
# 对 battle 进程采样 10 秒,看 CPU 时间花在哪里sudoperftop-p8999看收集结果,cpu全在跑lua解释器
由于我的linux无法使用pstack,为了不折腾环境,就是用gdb
# gdb 是 pstack 的底层,直接用它看 Lua 调用栈sudogdb-p$(pgrep-f"battle"|head-1)-batch-ex"thread apply all bt"2>/dev/null|grep-A5"lua"输出中找包含luaD_precall或luaV_execute的栈帧,附近会有关键线索。
但实际还是看不出来
于是就必须开启debug_console来查证问题
dbgcmd10TASK但遇到问题,使用dbgcmd指令会在cpu暴涨时卡住无法使用,我试了下再正常情况,是能正常输出.
dbgcmd 10 TASK没输出意味着:match_mgr 已经彻底卡死,拒绝处理任何消息——包括调试命令。
💡 核心学习点:Skynet 的死循环服务 = 彻底失联。
dbgcmd、inject、TASK都走skynet.call,服务必须在消息循环中才能响应。死循环 = 消息循环被卡住 = 所有 debug 命令超时。
我后续的解决方案就是引入metrics模块,监控rpc组件哪个操作耗时非常高,就直接定位。暂时没有找到其他查证手段。
内存泄漏
环境搭建
- 运维监控机器内存
- 业务支持内存监控组件,目前我是开发了一个子服务,需要监控的服务就在启动时创建memory_monitor,然后定时调用该服务中的gc接口用于监控
memory_monitor 核心逻辑
架构
每个节点一个实例(battle / logic),独立轮询本节点服务 │ ├─ timer.every(10s) → 回调 │ │ │ ├─ ① 遍历 watch_list,skynet.call 每个服务的 "diag_mem" │ ├─ ② 汇总 total,检查两档告警 │ ├─ ③ 上报 Prometheus(gauge_set) │ └─ ④ 输出日志(基线 / 常规 / 告警 / 恢复) │ └─ 被监控服务只需暴露 CMD.diag_mem() 接口两档告警
| 告警 | 触发器 | 防抖 |
|---|---|---|
| 单服务超阈值 | kb >= ALERT.per_service | alert_state[svc]标记,只告一次,降回阈值以下发[RECOVER] |
| 10 分钟增长 | recent - oldest >= ALERT.delta_10min | 无防抖(每次超都告),滑动窗口 60 个采样点 |
滑动窗口实现
history = { {tick, total}, {tick, total}, ... } ↓ 每次 tick: push 新采样点 → 移除 tick < now-6000 的旧点 ↓ delta_10min = history[#history].total - history[1].total关键设计点
skynet.call会永久阻塞:被监控服务如果不响应(无CMD.diag_mem或 dispatch 报错),定时器链断裂,之后所有轮询停止。用pcall+ 失败日志兜底。service 名称必须用小写:
skynet.queryservice查的是.service本地注册表(来自skynet.uniqueservice),不是harbor.globalname的集群注册表——大小写不匹配会永久阻塞。collectgarbage("count")返回浮点数:所有string.format用%.0f,不能用%d。detail字段是诊断内存泄漏的关键:被监控服务在diag_mem中返回各数据结构的条目数,日志输出match_mgr=68KB (match_history=3, queue_players=0),哪个字段独涨就定位到泄漏源。
步骤
第一层:运维告警(机器级)
Prometheus / Grafana 监控节点 RES/RSS → 单节点内存持续上涨,不回落 → 触发运维告警,通知研发介入这一层只告诉你哪个节点出了问题,不涉及具体服务。
第二层:memory_monitor 定位服务(服务级)
查看 memory_monitor 日志 → 常规行格式: battle total=92KB ⬆ delta=+12KB d10m=+12KB | match_mgr=92KB (match_history=2, queue_players=0, uid_mappings=0)判定方法:
| 观察点 | 结论 |
|---|---|
total持续单调上涨 | 确实有泄漏 |
delta每轮都为正 | 泄漏速率稳定 |
[ALERT] per_service触发 | 单服务越过阈值 |
[ALERT] 10min delta触发 | 长期趋势确认泄漏 |
某服务显示OFFLINE | 服务挂了或被监控的服务没注册 |
第三层:detail 字段定位数据结构(结构级)
match_mgr=92KB (match_history=2, queue_players=0, uid_mappings=0) ───────────┬────────── ──────┬────── ───────┬─────── ↑ 独涨 ↑ 归零 ↑ 归零 → 泄漏源就是 match_history判定规则:在所有detail字段中,正常字段随业务波动归零,泄漏字段只增不减。把业务跑一轮再看第二轮的 detail:
text
第一轮 match 后: match_history=1, queue_players=0 第二轮 match 后: match_history=2, queue_players=0 ← 只有 match_history 在涨第四层:代码 diff 定位提交(代码级)
git log -- chatroom/service/match_mgr.lua → 找到近期改动的 commit → git diff HEAD~3..HEAD -- chatroom/service/match_mgr.lua → 重点关注新增的全局表、table.insert 没有对应 remove、闭包引用等常见泄漏模式速查:
| 模式 | 特征 | detail 表现 |
|---|---|---|
| 全局表只增不减 | table.insert无对应的table.remove | 某个 field 单调递增 |
| 事件监听未解绑 | skynet.call或timer.every不 cancel | 内存缓慢增长,detail 无明显特征 |
| 闭包持有大对象 | 匿名函数捕获了 table 引用不释放 | detail 看不出,需用collectgarbage("count")手动对比 |
| 协程泄漏 | skynet.fork创建的协程永不退出 | 服务总内存涨但 detail 各字段正常 |