skynet线上问题查证练习记录(更新中)

发布时间:2026/10/30 14:06:05
skynet线上问题查证练习记录(更新中)

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

这是瓶颈区分的核心依据:

缩写全称含义告警阈值
ususer用户态 CPU(你的业务代码)> 80% → 代码死循环/计算密集
sysystem内核态 CPU(系统调用)> 30% → 频繁 IO/锁竞争
ninice低优先级进程的 CPU通常忽略
ididle空闲接近 100% = 没事
waiowait等磁盘 IO 的 CPU 时间> 10% → 磁盘瓶颈
hihardware irq硬件中断通常 < 1%
sisoftware irq软中断(网络包处理)> 5% → 网络包太多
ststeal虚拟机被宿主机偷走的 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/cacheLinux 用空闲内存做文件缓存,可随时释放1.5 GB
avail Mem实际可用 = free + 可释放的 cache1.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_servicealert_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
关键设计点
  1. skynet.call会永久阻塞:被监控服务如果不响应(无CMD.diag_mem或 dispatch 报错),定时器链断裂,之后所有轮询停止。用pcall+ 失败日志兜底。

  2. service 名称必须用小写:skynet.queryservice查的是.service本地注册表(来自skynet.uniqueservice),不是harbor.globalname的集群注册表——大小写不匹配会永久阻塞。

  3. collectgarbage("count")返回浮点数:所有string.format用%.0f,不能用%d。

  4. 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 各字段正常

总结:四步闭环