共计 4000 个字符,预计需要花费 10 分钟才能阅读完成。
背景
公司我的项目存在一个服务,相似于爬虫,须要解析给定的 URL,从返回的 HTML 中提取页面的题目、封面图、摘要、icon 等信息。因为这是一个无 DB 拜访的纯内存服务,且上游服务(需解析的 URL 地址)并非外部服务,无需思考并发压力,在服务搭建时选用 WebFlux
作为 web 层框架,选用 spring 的 WebClient
作为申请上游服务的 HTTP 客户端。
服务部署于 k8s 容器内,JDK 版本为 OpenJDK11,Pod 配置 4C4G,Java 服务配置最大堆内存 2G。
问题形容
服务上线后申请压力不大,但长时间运行后,服务堆内存占用达到 99%,日志监控呈现大量 OOM 报错,继而容器 Pod 重启。重启后可失常工作一段时间,之后再次堆内存占用 99%,呈现 OOM 报错。
解决过程
初步剖析
通过容器监控,查看 Pod 重启前一段时间的机器内存占用图,发现图出现持续上升趋势,且达到堆内存调配下限后,Pod 产生重启。初步揣测是产生了内存透露。
应用 jmap -histo:live 1
查看存活对象散布,发现 byte 数组占用内存较多,且 PoolSubpage
对象数量也较多,狐疑是 netty 产生了内存透露。
排查 ELK 中的 ERROR 日志,除 OOM 报错外,另发现大量 netty 的报错信息,异样堆栈如下:
LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
Created at:
io.netty.buffer.PooledByteBufAllocator.newHeapBuffer(PooledByteBufAllocator.java:332)
io.netty.buffer.AbstractByteBufAllocator.heapBuffer(AbstractByteBufAllocator.java:168)
io.netty.buffer.AbstractByteBufAllocator.heapBuffer(AbstractByteBufAllocator.java:159)
io.netty.handler.codec.compression.JdkZlibDecoder.decode(JdkZlibDecoder.java:180)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:493)
io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:432)
...
从异样提示信息可见,netty 的堆内存 ByteBuf
在未被开释的状况下被 GC 回收,而 netty 应用内存池进行堆内存治理,如 ByteBuff
未通过 release()
办法调用即被 GC 回收,将导致内存池中大量内存块的援用计数无奈归零,导致内存无奈回收。且 ByteBuf
被 GC 回收后,应用程序曾经无奈再调用 release()
办法,即导致了内存透露。
定位问题呈现地位
我的项目中应用 netty 的中央有:Redisson
、WebFlux
、WebClient
。思考到第三方库很成熟,通过很多商业我的项目利用,问题不太可能呈现在库代码中,可能是本人的应用形式有误。应用程序中本人编码应用的次要是WebClient
,用于申请第三方页面 HTML。
业务应用场景中,须要读取 ResponseHeader 和 ResponseBody 两局部内容。Header 用于从 Content-Type 中解析编码;Body 用于间接读取二进制数据,确定页面真正的编码格局。
之所以须要确定页面真正编码格局,是因为有些第三方页面,response header 中通过 Content-Type 申明编码格局为 UTF-8,但真正的编码格局却是 GBK 或 GB2312,导致解析中文摘要时乱码。因而须要读取二进制流后,依据流内容判断实在编码格局。写过爬虫的兄弟应该了解。
WebClient
提供了如下多个获取 Response 的办法:
- WebClient.RequestHeadersSpec#retrieve
能够将 body 间接解决为指定类型的对象,然而无奈间接操作 response; - WebClient.RequestHeadersSpec#exchange
能够间接操作 response,但 body 的读取操作须要自行处理;
为满足需要,我的项目中应用了 WebClient.RequestHeadersSpec#exchange
办法,这也是我的项目中惟一一处能够间接操作 ByteBuf 数据的中央。在应用此办法时,仅进行了数据读取操作,并没有开释 body。而在办法的正文上,刚好有这么一段:
NOTE 局部翻译过去的大抵意思是:
与 retrieve() 不同,在应用 exchange() 时,不管在任何状况下(胜利、异样、无奈解决的数据等),应用程序都该当生产掉响应内容。不这样做可能会导致内存透露。请参阅 ClientResponse 以获取可用于生产 body 的形式。通常应该应用 retrieve(),除非您有充沛的理由应用 exchange(),它容许您查看响应状态和题目,并在之后用于决定是否生产 body、如何生产 body。
而刚好在一些业务校验失败的状况下,如 Content-Type 中标识返回的数据不是 HTML 内容时,利用代码间接进行了 return,而没有生产 body,导致了内存透露。
// 申请代码示例
WebClient.builder().build()
.get()
.uri(ctx.getUri())
.headers(headers -> {headers.set(HttpHeaders.USER_AGENT, CHROME_AGENT);
headers.set(HttpHeaders.HOST, ctx.getUri().getHost());
})
.cookies(cookies -> ctx.getCookies().forEach(cookies::add))
.exchange()
.flatMap(response -> {
// 再次检测是否超时
// 留神,这里间接返回了 Mono.error,而没有开释 response
if (ctx.isParseTimeout(PARSE_TIMEOUT)) {return Mono.error(ReadTimeoutException.INSTANCE);
}
// 先解析重定向,不存在重定向则解析 body
return judgeRedirect(response, ctx)
.flatMap(redirectTo -> followRedirect(ctx, redirectTo))
.switchIfEmpty(Mono.defer(() -> Mono.just(parser.parse(ctx))))
.map(LinkParseResult::detectParseFail);
})
解决问题
曾经定位到问题产生的起因,且官网文档已给出了解决办法 参阅 ClientResponse 以获取可用于生产 body 的形式
。在ClientResponse
接口的正文上,列出来所有用于生产 Response 的办法:
具体每个办法的作用就不赘述,依据业务场景,该当在不须要生产 body 时调用 releaseBody()
办法进行开释。批改后的代码如下:
// 申请代码示例
WebClient.builder().build()
.get()
.uri(ctx.getUri())
.headers(headers -> {headers.set(HttpHeaders.USER_AGENT, CHROME_AGENT);
headers.set(HttpHeaders.HOST, ctx.getUri().getHost());
})
.cookies(cookies -> ctx.getCookies().forEach(cookies::add))
.exchange()
.flatMap(response -> {
// 再次检测是否超时,并开释 response
if (ctx.isParseTimeout(PARSE_TIMEOUT)) {return response.releaseBody()
.then(Mono.error(ReadTimeoutException.INSTANCE));
}
// 先解析重定向,不存在重定向则解析 body
return judgeRedirect(response, ctx)
.flatMap(redirectTo -> followRedirect(ctx, redirectTo))
.switchIfEmpty(Mono.defer(() -> Mono.just(parser.parse(ctx))))
.map(LinkParseResult::detectParseFail);
})
总结
在应用响应式 HTTP 客户端 WebClient
时,承受响应数据应用了 exchange()
办法,但又在一些流程分支中没有调用 ClientResponse#releaseBody()
办法,导致大量数据得不到开释,netty 内存池占满,后续的申请在申请内存时报 OOM 异样。
失去经验教训:应用不相熟的三方库时,肯定要浏览办法正文、类正文。
参考文档:
- Netty 内存透露排查
-
Web on Reactive Stack
本文由博客群发一文多发等经营工具平台 OpenWrite 公布