网关大JSON报文处理中因缓冲区未释放导致内存泄漏,关键在流式解析器、HTTP缓存及字节缓冲区未显式close/reset。
这个问题本质是网关在处理大 JSON 报文时,反复调用解析逻辑却未释放底层缓冲资源,造成内存持续增长。关键不在“解析失败”,而在“解析后残留”——尤其是流式解析器(如 Jackson 的
)、HTTP 请求体缓存、或自定义字节缓冲区未显式 close 或 reset。
确认是否为缓冲区未清除引发的泄漏
先排除其他干扰项,聚焦“缓冲区残留”特征:
内存增长与请求体大小强相关:发送 1MB JSON 和 10KB JSON,内存增幅呈线性比例
堆快照中高频出现
、
、
、
等对象,且其 retained heap 占比异常高
网关日志中无明显错误,但 GC 后老年代内存不回落,尤其在高并发下更明显
使用
查看,
或
类别内存持续上涨,提示 JVM 外部缓冲未释放
检查网关层 JSON 解析链路的关键释放点
微服务网关(如 Spring Cloud Gateway、自研 Dubbo HTTP 转换网关)通常在以下环节持有缓冲:
请求体预读取
:网关为做鉴权、限流、日志等,常调用
或
,若未消费完或未调用
(Netty 场景),则
永远不归还池
Jackson 流式解析器未关闭
:使用
后,必须显式调用
;否则内部
缓冲池无法复用,且部分版本会泄漏
缓冲
泛化调用中的序列化上下文复用
:Dubbo 泛化调用若使用
传入原始 JSON 字符串,网关内部可能用
解析后再转为
参数,若 ObjectMapper 配置了
等特性,会隐式增大缓冲需求
日志中间件自动 toString()
:开启全量请求日志(如
或自研审计日志)时,对大 JSON 调用
,触发 Netty
全量复制到堆内存,且不自动释放
定位具体泄漏代码的实操步骤
不依赖猜测,用数据说话:
Find JSON Path Online
Easily find JSON paths within JSON objects using our intuitive Json Path Finder
下载
启用 Netty 内存泄露检测:
启动网关,复现后控制台会打印泄漏点堆栈(指向某行
未 release)
用 Arthas trace 监控关键方法:
和
,观察调用频次与 buffer 分配是否失衡
对比两次堆快照(MAT):筛选出新增的
实例,用 “Merge Shortest Paths to GC Roots” 查看谁长期持有它们——大概率指向某个静态缓存 Map、未关闭的
实例,或被
持有的解析器
检查网关配置:确认是否启用了
等限制;若设得过大(如 100MB),而业务又未及时释放,等于主动制造泄漏温床
修复与加固建议
从编码习惯和架构设计两个层面堵住漏洞:
所有
、
必须放在 try-with-resources 中,禁止裸调用
Netty
操作后,统一用
,而非依赖 finalize
对大于 1MB 的请求体,禁用全量日志,改用采样日志(如只记录 method + uri + status + size)
引入流式校验前置:用
边解析边校验字段名/深度/字符串长度,发现超限时立即
并返回 400,避免解析全程完成才抛错
将大 JSON 解析逻辑下沉至独立线程池(非 I/O 线程),配合
缓存实例,降低单次请求的内存峰值
这类泄漏往往安静而顽固,不会立刻 OOM,但会让网关在流量高峰时雪崩。核心思路就一条:谁分配,谁释放;谁打开,谁关闭。不要假设框架会帮你兜底。
JsonParserbyte[]ByteBufferByteArrayInputStreamcom.fasterxml.jackson.core.json.UTF8StreamJsonParserjcmd VM.native_memory summary InternalOtherServerWebExchange#getRequestBody()exchange.getRequest().getBody()buffer.release()PooledByteBufJsonFactory.createParser(InputStream)parser.close()BufferRecyclerchar[]GenericService.$invoke()ObjectMapper.readValue(jsonStr, Map.class)Object[].enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)Logbookbody.toString()CompositeByteBuf-Dio.netty.leakDetection.level=paranoidbuffer.readBytes(...)trace com.fasterxml.jackson.core.json.UTF8StreamJsonParser nextTokentrace io.netty.buffer.PooledByteBufAllocator newHeapBufferbyte[]JsonParserThreadLocalspring.codec.max-in-memory-size=5MBJsonParserJsonGeneratorcreateParser()ByteBufReferenceCountUtil.safeRelease(buf)JsonParserparser.close()SoftReference