首页 国际新闻正文

谨以此文献给李林锋行将重生的爱女。

1. 布景

1.1 直播渠道内存泄露问题

某直播渠道,一些网红的直播间在事务高峰期,会有 10W+ 的粉丝接入,假如瞬间发作许多客户端衔接掉线、或许一些客户端网络比较慢,发现依据 Netty 构建的效劳端内存会飙升,发作内存泄露(OOM),导致直播卡顿、或许客户端接纳不到效劳端推送的音讯,用户体会遭到很大影响。

1.2 问题剖析

首要对 GC 数据进行剖析,发现老时代已满,发作屡次 Full GC,耗时达 3 分多,体系现已无法正常运转(示例):

园崎美弥
叶子楣,Netty防止内存泄露方法,耽美小说引荐

Dump 内存仓库进行剖析,发现许多的发送使命堆积,导致内存邪煞缠身溢出(示例):

经过以上剖析能够看出,在直播高峰期,效劳端向上万客户端推送音讯时,发作了发送行列积压,引起内存泄露,终究导致效劳端频频 GC,无法正常处理事务。

1.3 处理战略

效劳端在进行音讯发送的时分做维护,详细战略如下:

效劳端依据上述战略优化了代码,内存泄露问题得到处理。

1.4. 总结

虽然 Netty 结构本身做了许多的可靠性规划,可是关于详细的事务场景,仍然需求用户做针对特定范畴和场景的可靠性规划,这样才干提高运用的可靠性。

除了音讯发送积压导致的内存泄露,Netty 还有其它常见的一些内存泄露点,本文将针对这些或许导致内存泄露的功用点进行剖析和总结。

2. 音讯收发防内存泄露战略

2.1. 音讯接纳

2.1.1 音讯读取

Netty 的音讯读取并不存在音讯行列,可是假如音讯解码战略不妥,则或许会发作内存泄露,首要有如下几点:

防止内存泄露的战略如下:

不管选用哪种解码器完结,都对音讯的最大长度做约束,当超越约束之后,抛出解码失利反常,用户能够挑选疏忽当时现已读取的音讯,或许直接封闭链接。

以 Netty 的 DelimiterBasedFrameDecoder 代码为例,创立 DelimiterBasedFrameDecoder 目标实例时,指定一个比较合理的音讯最大长度约束,防止内存溢出:

/**

* Creates a new instance.

*

* @parammaxFrameLength the maximum length of the decoded frame.

* A {@linkTooLongFrameException} is thrown if

* the length of the frame exceed缀满礼品的树s this value.

* @paramstripDelimiter whether the decoded frame should strip out the

* delimiter or not

* @paramdelimiter the delimiter

*/

publicDelimiterBasedFrameDecoder(

intmaxFrameLength, booleanstripDelimiter, ByteBuf delimiter){

this(maxFrameLength, stripDelimiter, true, delimiter);

}

需求依据单个 Netty 效劳端能够支撑的最大客户端并发衔接数、音讯的最大长度约束以及当时 JVM 装备的最大内存进行核算,并结合事务场景,合理设置 maxFrameLength 的取值。

2.1.2 ChannelHandler 的并发履行

Netty 的 ChannelHandler 支撑串行和异步并发履行两种战略,在将 ChannelHandler 加入到 ChannelPipeline 时,假如指定了 EventExecutorGroup,则 ChannelHandler 将叶子楣,Netty防止内存泄露方法,耽美小说引荐由 EventExecutorGroup 中的 EventExecutor 异步履行。这样的优点是能够完结 Netty I/O 线程与事务 ChannelHandler 逻辑履行的别离,防止 ChannelHandler 中耗时事务逻辑的履行堵塞 I/O 线程。

ChannelHandler 异步履行的流程如下所示:

假如事务 ChannelHandler 中履行的事务逻辑耗时较长,音讯的读取速度又比较快,很简单发作音讯在 EventExecutor 中积压的问题,假如创立 EventExecutor 时没有经过 io.netty.eventexecutor.maxPendingTasks 参数指定积压的最大音讯个数,则默许取值为 0x7fffffff,长期的积压将导致内存溢出,相关代码如下所示(异步履行 ChannelHandler,将音讯封装成 Task 加入到 taskQueue 中):

publicvoidexecute(Runnable task){

if厕拍(task == null) {

thrownewNullPointerException("task");

}

booleaninEventLoop = inEventLoop();

if(inEventLoop) {

addTask(task);

} else{

startThread();

addTask(task);

if(isShutdown() && removeTask(task)) {

reject();

}

}

处理对策:对 EventExecutor 中使命行列的容量做约束,能够经过 io.netty.eventexecutor.maxPendingTasks 参数做大局设置,也能够经过结构方法传参设置。结合 EventExecutorGroup 中 EventExecutor 的个数来核算 taskQueue 的个数,依据 taskQueue * N * 使命行列均匀巨细 * maxPendingTasks < 系数K(0 < K < 1)* 总内存的公式来进行核算和评价。

2.2. 音讯发送

2.2.1 怎么防止发送行列积压

为了防止高并发场景下,因为对方处理慢导致本身音讯积压,除了效劳端做流控之外,客户端也需求做并发维护,防止本身发作音讯积压。

运用 Netty 供给的凹凸水位机制,能够完结客户端更精准的流控,它的作业原理如下:

当发送行列待发送的字节数组抵达高水位上限时,对应的 Channel 就变为不可写状况。因为高水位并不影响事务线程调用 write 方法并把音讯加入到待发送行列中,因而,有必要要在音讯发送时对 Channel 的状况进行判别:当抵达高水位时,Channel 的状况被设置为不可写,经过对 Channel 的可写状况进行判别来决议叶子楣,Netty防止内存泄露方法,耽美小说引荐是否发送音讯。

在音讯发送时设置凹凸水位并对 Channel 状况进行判别,相关代码示例如下:

publicvoidchanne无限国际之战役之王lActive(finalChannelHandlerContext ctx){

**ctx.channel().config().setWriteBufferHighWaterMark(10* 1024* 1024);**

loadRunner = newRunnable() {

@Override

publicvoidrun(){

try{

TimeUnit.SECONDS.sleep(30);

} catch(Interru叶子楣,Netty防止内存泄露方法,耽美小说引荐ptedException e) {

e.printStackTrace();

}

ByteBuf msg = null;

while(true) {

**if(ctx.channel().isWritable()) {**

msg = Unpooled.wrappedBuffer("Netty OOM Example".getBytes());

ctx.writeAndFlush(msg);

} else{

LOG.warning("The write queue 竹山天气预报is busy : "+ ctx.channel().unsafe().outboundBuffer().nioBufferSize());

}

}

}

};

newThread(loadRunner, "LoadRunner-Thread").start();

}

对上述代码做验证,客户端代码中打印行列积压相关日志,阐明依据高水位的流控机制收效,日志如下:

正告: The write queue is busy : 17

经过内存监控,发现内存占用平稳:

在实践项目中,依据事务 QPS 规划、客户端处理功能、网络带宽、链路数、音讯均匀码流巨细等归纳要素核算并设置高水位(WriteBufferHighWaterMark)阈值,运用高水位做音讯发送速率的流控,既能够维护本身,一起又能减轻效劳端的压力,防止效劳端被压挂。

2.2.2 其它或许导致发送行列积压的要素

需求指出的snh王璐是,并非只需高并发场景才会触发音讯积压,在一些反常场景下,虽然体系流量不大,但仍然或许会导致音讯积压,或许的场景包含:

当呈现许多排队时,很简单导致 Netty 的直接内存泄露,示例如下:

咱们在规划体系时,需求依据事务的场景、所在的网络环境等要素进行归纳规划,为潜在的各种毛病做容错和维护,防止因为外部要素导致本身发作内存泄露。

3. ByteBuf 的请求和开释战略

3.1 ByteBuf 请求和开释的了解误区

有一种说法以为 Netty 结构分配的 ByteBuf 结构会自动开释,事务不需求开释;事务创立的 ByteBuf 则需求自己开释,Netty 结构不会开释。

事实上,这种观念是过错的,即使 ByteBuf 是 Netty 创立的,假如运用不妥仍然会发作内存泄露。在实践项目中怎么更好的办理 ByteBuf,下面咱们分四种场景进行阐明。

3.2 ByteBuf 的开释战略

3.2.1 依据内存池的恳求 ByteBuf

这类 ByteBuf 首要包含 PooledDirectByteBuf 和 PooledHeapByteBuf,它由 Netty 的 NioEventLoop 线程在处理 Channel 的读操作时分配,需求在事务 ChannelInboundHandler 处理完恳求音讯之后开释(通常是解码之后),它的开释有 2 种战略:

@Override

publicvoidchannelRead(ChannelHandlerContext ctx, Object msg)throwsException {

booleanrelease = true;

try{

if(acceptInboundMessage(msg)) {陈滨陈爱莲

I imsg = (I) msg;

channelRead0(ctx, imsg);

} else{

release = false;

ctx.fireChannelRead(msg);

}

} finally{

**if(autoRelease && release) {**

**ReferenceCountUtil.release(msg);**

**}**

}

}

假如当时事务 ChannelInboundHandler 需求履行,则调用完 channelRead0 之后履行 ReferenceCountUtil.release(msg) 开释当时恳求音讯。假如没有匹配上需求持续履行后续的 ChannelInboundHandler,则不开释当时恳求音讯,调用 ctx.fireChannelRead(msg) 驱动 ChannelPipeline 持续执摘瓜歌行。

承继自 SimpleChannelInboundHa叶子楣,Netty防止内存泄露方法,耽美小说引荐ndler,即使事务不开释恳求 ByteBuf 目标,仍然不会发作内存泄露,相关示例代码如下所示:

publicclassRouterServerHandlerV2**extendsSimpleChannelInboundHandler** {

// 代码省掉...

@Override

publicvoidchannelRead0(ChannelHandlerContext ctx, ByteBuf msg){

byte[] body = newbyte[msg.readableBytes()];

executorService.execute(()->

{

// 解析恳求音讯,做路由转发,代码省掉...

// 转发成功,回来呼应给客户端

ByteBuf respMsg = allocator.heapBuffer(body.length);

respMsg.writeBytes(body);// 作为示例,简化处理,将恳求回来

ctx.writeAndFlush(respMsg);

});

}

对上述代码做功能测验,发现内存占用平稳,无内存泄露问题,验证了之前的剖析定论。

protectedvoidonUnhandledInboundMessage(Object msg) {

try{

logger.debug(

"Discarded inbound message {} that reached at the tail of thesjyp官网 pipeline. "+

"Please check your pipeline configuration.", msg);

**} finally{**

**ReferenceCountUtil.release(msg);**

**}**

}

3.2.2 依据非内存池的恳求 ByteBuf

假如事务运用非内存池形式掩盖 Netty 默许的内存池形式创立恳求 ByteBuf,例如经过如下代码修正内存请求战略为 Unpooled:

// 代码省掉...

.childHandler(newChann实在阅历elIni荷西我喜欢你tializer() {

@Override

publicvoidinitChannel(SocketChannel ch)throwsException {

ChannelPipeline p = ch.pipeline(); ch.config().setAllocator(UnpooledByteBufAllocator.DEFAULT);

p.addLast(newRouterServerHandler());

}

});

}

也需求依照内存池的方法去开释内存。

3.2.3 依据内存池的呼应 ByteBuf

只需调用了 writeAndFlush 或许 flush 方法,在音讯发送完结之后都会由 Netty 结构进行内存开释,事务不需求自动开释内存。

它的作业原理如下:

调用 ctx.writeAndFlush(respMsg) 方法,当音讯发送完结之后,Netty 结构会自动协助运用林式瓦来开释内存,内存的开释分为两种场景:

protectedfinalByteBuf newDirectBuffer(ByteBuf buf){

finalintreadableBytes = buf.readableBytes();

if(readableBytes == 0) {

**ReferenceCountUtil.safeRelease(buf);**

returnUnpooled.EMPTY_BUFFER;

}

finalByteBufAllocator all煮avoc = alloc();

if(alloc.isDirectBufferPooled()) {

ByteBuf directBuf = alloc.directBuffer(readableBytes);

directBuf.writeBytes(buf, buf.readerIndex(), readableBytes);

**ReferenceCountUtil.safeRelease(buf);**

returndirectBuf;

} }

// 后续代码省掉

}

假如音讯完好的被写到 SocketChannel 中,则开释 DirectByteBuffer,代码如下(ChannelOutboundBuffer)所示:

publicbooleanremove(){

Entry e = flushedEntry;

if(e == null) {

clearNioBuffers();

returnfalse;

}

Object msg = e.msg;

ChannelPromise promise = e.promise;

intsize = e.pendingSize;

removeEntry(e);

if(!e.cancelled) {

**ReferenceCountUtil.safeRelease(msg);**

safeSuccess(promise);

decrementPendingOutboundBytes(size, false, true);

}

// 后续代码省掉

}

对 Netty 源码进行断点调试,验证上述剖析:

断点 1:在呼应音讯发送处打印断点,获取到 PooledUnsafeHeapByteBuf 实例 ID 为 1506。

断点 2:在 HeapByteBuffer 转化成 DirectByteBuffer 处打断点,发现实例 ID 为 1506 的 PooledUnsafeHeapByteBuf 被开释。

断点 3:转化之后待发送的呼应音讯 PooledUnsafeDirec叶子楣,Netty防止内存泄露方法,耽美小说引荐tByteBuf 实例 ID 为 1527。

断点 4:呼应音讯发送完结之后,实例 ID 为 1527 的 PooledUnsafeDirectByteBuf 被开释到内存池。

假如是 DirectByteBuffer,则不需求转化,当音讯发送完结之后,由 ChannelOutboundBuffer 的 remove() 担任开释。

3.2.4 依据非内存池的呼应 ByteBuf

不管是依据内存池还对错内存池分配的 ByteBuf,假如是堆内存,则将堆内存转化成堆外内存,然后开释 HeapByteBuffer,待音讯发送完结之后,再开释转化后的 DirectByteBuf;假如是 DirectByteBuffer,则无需转化,待音讯发送完结之后开释。因而关于需求发送的呼应 ByteBuf,由事务创立,可是不需求事务来开释。

4. Netty 效劳端高并发维护

4.1 高并发场景下的叶子楣,Netty防止内存泄露方法,耽美小说引荐 OOM 问题

在 RPC 调用时,假如客户端并发衔接数过多,效劳端又没有针对并发衔接数的流控机制,一旦服阴模务端处理慢,就很简单民国之战役贩子发作批量超时和断连重连问题。

以 Netty HTTPS 效劳端为例,典型的事务组网示例如下所示:

客户端选用 HTTP 衔接池罗富杨的方法与效劳端进行 RPC 调用,单个客户端衔接池上限为 200,客户端布置了 30 个实例,而效劳端只布置了 3有你的城市下雨也美丽 个实例。在事务高峰期,每个效劳端需求处理 6000 个 HTTP 衔接,当效劳端时延增大之后,会导致客户端批量超时,超时之后客户端会封闭衔接从头主张 connect 操作,在某个瞬间,几千个 HTTPS 衔接一起主张 SSL 握手操作,因为效劳端此刻也处于高负荷运转状况,就会导致部分衔接 SSL 握手失利或许超时,超时之后客户端会持续重连,进一步加剧效劳端的处理压力,终究导致效劳端来不及开释客户端 close 的衔接,引起 NioSocketChannel 许多积压,终究 OOM。

经过客户端的运转日志能够看到一些 SSL 握手发作了超时,示例如下:

效劳端并没有对客户端的衔接数做约束,这会导致虽然 ESTABLISHED 状况的衔接数并不会超越 6000 上限,可是因为一些 SSL 衔接握手失利,再加上积压在效劳端的衔接并没有及时开释,终究引起了 NioSocketChannel 的许多积压。

4.2.Netty HTTS 并发衔接数流控

在效劳端增加对客户端并发衔接数的操控,原理如下所示:

依据 Netty 的 Pipeline 机制,能够对 SSL 握手成功、SSL 衔接封闭做切面阻拦(类似于 Spring 的 AOP 机制,可是没选用反射机制,功能更高),经过流控切面接口,对 HTTPS 衔接做计数,依据计数器做流控,效劳端的流控算法如下:

在完结效劳端流控时,需求留意如下几点:

5. 总结

5.1. 其它的防内存泄露办法

5.1.1 NioEventLoop

履行它的 execute(Runnable task) 以及守时使命相关接口时,假如使命履行耗时过长、使命履行频度过高,或许会导致使命行列积压,从而引起 OOM:

主张事务在运用时,对 NioEventLoop 行列的积压状况进行收集和告警。

5.1.2 客户端衔接池

事务在初始化衔接池时,假如选用每个客户端衔接对应一个 EventLoopGroup 实例的方法,即每创立一个客户端衔接,就会一起创立一个 NioEventLoop 线程来处理客户端衔接以及后续的网络读写操作,选用的战略是典型的 1 个 TCP 衔接对应一个 NIO 线程的形式。当体系的衔接数许多、堆内存又缺乏时,就会发作内存泄露或许线程创立失利反常。问题暗示如下:

优化战略:客户端创立衔接池时,EventLoopGroup 能够重用,优化之后的衔接池线程模型如下所示:

5.2 内存泄露问题定位

5.2.1 堆内存泄露

经过 jmap -dump:format=b,file=xx pid 指令 Dump 内存仓库,然后运用王郡楠 MemoryAnalyzer 东西对内存占用进行剖析,查找内存泄露点,然后结合代码进行剖析,定位内存泄露的详细原因,示例如下所示:

5.2.2 堆外内存泄露

主张战略如下:

6. 作者简介

李林锋,10 年 Java NIO、渠道中间件规划和开发经历,通晓 Netty、Mina、分布式效劳结构、API Gateway、PaaS 等,《Netty 进阶之路》、《分布式效劳结构原理与实践》作者。现在在华为终端运用商场担任事务微效劳化、云化、全球化等相关规划和开发作业。

联系方法:新浪微博 Nettying 微信:Nettying

Email:neu_lilinfeng@sina.com

line 开发 客户端
声明:该文观念仅代表作者自己,搜狐号系信息发布渠道,搜狐仅供给信息存储空间效劳。
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。