跳到正文

Netty 生产检查清单 ​

适用 Netty 4.x。每条都可勾选,括号内是默认值或判断依据;版本差异以官方文档为准。 只列「不做会出事」的动作,机制解释见各篇正文。

上线前 ​

  • [ ] 堆外内存上限显式设置:-XX:MaxDirectMemorySize 必须配(默认等于 -Xmx,对长连接服务通常不够),并同时配 -Dio.netty.leakDetection.level=SIMPLE(默认);预发用 PARANOID(性能开销随负载变化,不按固定比例估算)。
  • [ ] Pipeline 顺序已验证:LengthFieldBasedFrameDecoder 在最前,业务解码器其次,Encoder 在业务 Handler 之后;用一个「半包 + 粘包 + 连续三帧」的单元测试确认业务 Handler 收到的是完整消息。
  • [ ] 帧边界与协议头已定:maxFrameLength 按业务设 64KB–1MB(RPC 常见区间),不用 Integer.MAX_VALUE;协议头带 Magic 与 Version 并约定未知版本处理方式;文件传输走 Chunked,不复用 FrameDecoder。
  • [ ] 心跳超时按 RTT 定:IdleStateHandler(readerIdleTime, writerIdleTime, 0) 中 readerIdleTime 取心跳间隔 × 3,客户端 writerIdleTime 取心跳间隔;30–60s 心跳对应 60–180s 判死,不要用 10s。
  • [ ] 重连有指数退避与抖动:退避 min(60, 2^attempt) 秒封顶,叠加 0–1000ms jitter;只做退避不加 jitter,客户端仍会在封顶时刻同步重连。
  • [ ] 背压与连接上限已配置:WriteBufferWaterMark(32KB, 64KB),并在 accept 后按 channelGroup.size() 拒绝超限连接;WorkerGroup 线程数取 CPU 核数 × 2,Boss 线程通常每个监听端口 1 个(多 ServerChannel 才需多个,多配不能给同一端口容错)。

运行中 ​

  • [ ] 堆外内存:io.netty.allocator.usedDirectMemory 超过 MaxDirectMemorySize 的 80% 告警;同时看进程 RSS 是否只涨不降。
  • [ ] 堆内与 GC:io.netty.allocator.usedHeapMemory 超过 -Xmx 的 80%,或 Full GC 频率持续上升,先怀疑消息积压或 ByteBuf 未释放。
  • [ ] EventLoop 积压:io.netty.eventLoop.pendingTasks 持续 5 分钟 > 1000,说明有任务在 IO 线程上阻塞。
  • [ ] 连接与流量:活跃连接数突降 30% 或突增 200%、写字节速率突然掉零,分别对应客户端批量掉线或下游写不出去;指标名以实际注册的为准,正文里的 io.netty.* 需先确认已导出。

故障时 ​

  • [ ] 堆外内存持续增长直至 OutOfDirectMemoryError → 看 RSS 与 usedDirectMemory 曲线 → 预发开 -Dio.netty.leakDetection.level=PARANOID 复现,按 LEAK: 分配栈定位忘记 release() 或异步队列漏 retain() 的 Handler。
  • [ ] P99 飙升但 CPU 只有 30% 左右 → 看 pendingTasks 与单 EventLoop 的 Channel 数 → 判定某个 Handler 做了同步 DB/RPC 调用,把耗时逻辑移出 IO 线程,处理完再切回 ctx.executor() 写回。
  • [ ] 客户端频繁断连重连但网络正常 → 对比 IdleStateHandler 阈值与实测 RTT P99 → 阈值低于 RTT × 3 就调大,并确认 allIdleTime 没有误开。
  • [ ] 服务端重启后瞬时 CPU/内存打满、大量 connect 超时 → 看客户端重连时间分布 → 没有 jitter 就补随机抖动,并把退避封顶从固定值改为指数退避。
  • [ ] 业务侧收到半条消息或解码异常 → 查 FrameDecoder 的 lengthFieldOffset/lengthFieldLength/lengthAdjustment 与实际协议是否一致 → 用抓包字节逐帧核对,不要先怀疑业务序列化。

把一门技术讲透到既懂也会用