背景
公司我的项目存在一个服务,相似于爬虫,须要解析给定的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 公布