skynet性能优化学习

常见问题

消息堆积 / 服务延迟飙高

现象

某个服务 mq长度持续增长,skynet.stat看到 mqlen很大,同节点其他服务也跟着抖。

根因

  • 单条 callback 太重:在 skynet.dispatch里做了同步重计算(协议编解码、JSON 序列化、大表遍历),阻塞了 Worker 线程,这条 mq 里的后续消息只能排队。

  • skynet.call链式 / 广播滥用:A→B→C→D 一长串 call,或者一广播就是几十个服务,发送方被全程挂起,接收方 mq 瞬间打满。

  • 跨节点 call 没设 timeout:对方节点抖动,skynet.call的 coroutine 一直挂,session 映射表和 mq 都堆。

优化

  • 重计算拆出去:CPU 密集型活丢给专用的"计算服"做 skynet.send+ 异步回传,或切 C 模块用 skynet_worker(如果有)甚至外部进程。

  • 分帧 / fork:Lua 层大循环 for i=1,1e6 do ... end改成每帧处理一段,skynet.fork让出。

  • call 必带 timeout:skynet.call(addr, "lua", ..., 100)(0.6+ 语法)或外层用 pcall + timeout兜底。

  • 广播用 multicast或 clusterd 批量,别自己 for 循环 send。

Lua GC 压力大

现象

  • 服务 cpu占用里 GC 占比 > 30%

  • 每隔几秒一次"微卡顿",时延 P99 跳

  • 内存缓慢涨,不降

根因

  • 每服务一个 LuaVM:Skynet 的 Lua 服务本质是 snlua,每个都独立 VM、独立 GC。服务多了(几千个 agent)GC 总开销线性放大。

  • 临时对象疯狂造:skynet.call每次生成新 session 表、closure;协议 unpack 每消息 new 一个 table;字符串拼接用 ...。

  • 热服务消息 QPS 高:网关、聊天、排行榜这类每秒上千消息的服,GC 被反复触发。

优化

  • 对象复用:

    • 协议层用 protobuf/sproto的 decode复用 table,或手写 C 解码。

    • session/coroutine 映射表定期清理,避免闭包捕获大对象。

  • LuaJIT 开起来:确认编译开了 LUAJIT_ENABLE,JIT 能把热循环编成机器码,GC 压力也间接降。

  • 合批处理:网关类服务把多条消息攒一波再 dispatch,减少 per-message 的 Lua 调用次数。

  • 超大服拆小:单服 agent 过多时,按 uid 区间拆多个 snlua 进程,GC 并行度反而上来了。

全局消息队列(global_queue)争抢

现象

  • Worker 线程数开得多(≥ 8)时,吞吐量不再涨,甚至略降

  • perf top看到 spinlock/ atomic相关指令占比高

根因

global_queue 是无锁循环数组 + CAS​ 实现的(3.x 后改成了更轻量的版本,但高并发下仍有争抢)。当:

  • Worker 数 ≈ 核数 × 2

  • 单节点 QPS 极高(> 50w msg/s)

  • 大量服务 mq 频繁入队(Timer/Socket 两个生产者狂 push)

多个 Worker 同时 pop+ Timer/Socket 同时 push,CAS 失败重试变多。

优化

  • Worker 数 = CPU 物理核数(别超,超了反而自旋空转)。

  • 热点服务拆分到不同节点,让 global_queue 的入队源少一个 Socket/Timer 集中点。

  • 3.x 之后确认用的是新版本,老版 2.x 的 global_queue 锁更重。

Socket 层瓶颈

现象

  • 连接数上了 1w+,CPU 花在 socket_server上多

  • 收发包延迟涨,但业务服 mq 并不满

根因

  • 每一帧 Socket 线程把所有 fd 事件转成消息 push 到 snlua-gate的 mq,gate 服瞬间被冲。

  • socket.write大包阻塞:单个包 > 64KB 或 send buffer 满时,Socket 线程会自旋等。

  • TCP_NODELAY / 缓冲区没调:小包频发时 Nagle + Lua 层频繁 write 来回折腾。

优化

  • gate 服分片:按连接 ID % N 拆多个 gate 服务,每个管一部分 fd。

  • 合包 / 限流:Lua 层协议加长度前缀,避免半包反复 callback;上行消息频率做简单 throttle。

  • socket 参数调优:socket.limit控制 send buffer;内核侧 somaxconn、tcp_mem一起调。

Timer 模块抖动

现象

  • 大量短周期 timer(比如每 100ms 一个 agent 心跳),某帧突然卡 10ms+

  • skynet.timeout精度漂移

根因

Skynet 定时器是四级时间轮(近似 Linux 实现),单线程(Timer 线程)扫。当:

  • 同一时刻到期 timer 太多(比如 1w 个 agent 同时加的 1s 超时)

  • timer 回调里又 skynet.call或做重活

Timer 线程这一帧就拖长,间接影响全局消息生产节奏。

优化

  • timer 回调只做轻量事:发一条消息给 Worker 服去干活,别在 timer 线程里 call。

  • 同类 timer 合并:1w 个 agent 各自 1s 心跳 → 改成一个"心跳服" 1s 触发一次,再 send通知所有 agent(agent 只注册一次,用个计数器就行)。

  • 长周期用 cron思路,别用大量 100ms 短 timer 轮询。

跨节点(Cluster)性能问题

现象

  • 跨节点 call延迟比本地高一个数量级

  • 集群间带宽打满

根因

  • 序列化开销:cluster 默认用 Lua Table 序列化,跨机每消息编解码一次。

  • connection 单 TCP 连接:默认两个节点间只有一条 TCP,所有服务共享,头部争抢 + 串行化。

  • 没做 batch:高频小消息一条一条发,TCP 小包多。

优化

  • 换 sproto / protobuf​ 做 cluster 序列化,比原生 table 序列化省 CPU 也省带宽。

  • 高频通道拆独立 connection(3.x 支持多 harbor 或多 TCP 通道)。

  • 批量 call:多个小请求攒一波,一次 send+ 一次回包。

排查工具链

  • skynet.stat()------ 看 mqlen、cpu、gc 时间

  • skynet.mem_limit()------ 看内存

  • profile模块 ------ Lua 层函数级耗时

  • debug控制台:list、stat、mem、netstat

  • perf top/ perf record------ C 层热点(global_queue 自旋、socket_server 等)

  • jeprof(LuaJIT) ------ 内存泄漏定位