开放问答
堆内 GC 全绿但 P99 周期性涨 50ms:DirectMemory 与分配器锁竞争,如何从数据上二选一定位?
该问题的正文、回答与评论。
提问者 程望舒-937 · 状态 open
JVMDirectMemoryNettyP99性能调优
【背景】
我负责一个实时行情推送服务:Spring Boot 3.1 + JDK 17.0.10(G1,-Xmx6g -XX:MaxGCPauseMillis=50 -XX:MaxDirectMemorySize=2g),K8s 1.28 / cgroup v2(cpu.limit=8),QPS 约 8k,主链路是 WebSocket 推送 + Netty 编解码。P50 长期稳定在 12ms,但近两个月 P99 出现「每天 14:00~15:30 行情高峰时段,每 20~40 分钟抖一次,单次 50~120ms」的规律性毛刺,其余时段 P99 稳定在 25ms 以内。
【现象】
- 抖动期间
jstat -gcutil显示 YGC 频率没变(约 1 次/30s),单次 Young GC 停顿 < 5ms,Old 区水位 48% 纹丝不动; - 但
jcmd <pid> VM.native_memory summary对比抖动前后,Internal与Thread段有 30~80MB 的增量,怀疑 DirectByteBuffer 池在反复扩容(Netty 的PooledByteBufAllocator默认 chunk=16MB,怀疑高峰期 arena 不够用触发了 rebind / 新 arena 分配); perf record -g -p <pid>火焰图里pthread_mutex_lock和__lll_lock_wait占比在抖动窗口从 1.2% 飙到 9%+,怀疑是 DirectMemory 的Unsafe.allocateMemory或 Netty 的 arena 锁争抢;- 但同样 9% 的锁等待也可能来自
G1BarrierSet::block(JIT 屏障)或os::allocate_commit(mmap 竞争),单看火焰图分不干净。
【已尝试】
- 把
MaxDirectMemorySize从 1g 提到 4g:毛刺频率没变,只是幅度从 120ms 降到 70ms(说明不是单纯容量不够,是分配路径本身在争抢); - 把 Netty
PooledByteBufAllocator的numDirectArenas从默认(2×CPU=16)调到 32:毛刺幅度降到 40ms,但 14:30 那次还是 85ms,说明锁竞争源不止 arena 一个; - 开了
-XX:+UnlockDiagnosticVMOptions -XX:+PrintGCApplicationStoppedTime -XX:+TraceG1HUMONGOUS_ALLOC:日志里 HUMONGOUS 分配记录很稀疏(每天 < 10 次),基本排除大对象触发 Full GC; numactl --interleave=all绑核 + 关 THP:毛刺几乎没变化,排除 NUMA 和 page fault 因素。
【期望得到的答案形态】
- 判定方法:有没有一套「三件套」命令/工具,能在抖动窗口内把「DirectMemory 分配路径锁竞争」和「JIT / GC 屏障竞争」在数据上切开?我目前想到的是
perf lock record+perf c2c record+jcmd VM.native_memory baseline/summary的差分,但不确定粒度够不够。 - 量化阈值:
pthread_mutex_lock在火焰图里占比超过多少,才能比较有信心地说「主因是用户态锁」而不是「内核态 mmap / page fault」?有没有经验值或判定公式? - 可落地的修复路径:如果最终确认是 DirectMemory 分配路径,除了调
numDirectArenas,还有什么「少分配 / 复用 / 换分配器」的手段?比如 Netty 的ThreadLocalFastPath是否真的把 arena 锁竞争降到了可忽略,还是只是把锁挪到了DirectByteBuffer的引用计数上? - 反例警示:有没有人调过
numDirectArenas调到 64 或 128 反而更糟的案例?(我直觉上 arena 越多,每个 arena 的热度越低、cache line 命中率越差,但没数据。)
我倾向的答案是:先给判定方法,再给阈值,最后给修复路径——这样我能按步骤复现,而不是直接抄一个调参方案。
已采纳
回答者
回答正文
评论
暂无评论。