我以通俗易懂的语言
记录了,优雅退出,流量控制,流量整形,内存池,读写队列积压,内存泄漏 等重要点
如果你看到了这文章,对你有一点帮助,可以点个赞
假设客户端每秒给服务器发送1G的数据
而服务器每秒一定要读取1G吗?服务端会在高峰时期导致读队列积压然后宕机吗
假设服务端每秒给客户端发送1G的数据
而客户端接收的过来吗? 高峰期服务端会造成发送队列积压导致雪崩吗
netty的bytebuf中的堆外内存,堆内存,池化和未池化,如何正确使用?
netty的work线程组如何保证高效执行.
netty的优雅退出之类的.
很多这样问题都很有意思.这是入坑netty最常见的疑问.
需要先记得比较重要的一件事.
当我们调用了channel的write或writeandflush方法,并不代表数据真的发送过去了
这个时候对发送字节做统计是不准确的,netty的网络io操作都是异步的,并不是同步执行的.
异步和同步的思维是不同的,不是说调用了这个方法后就立刻执行.
如果想监听是否发送的成功,应该在write后返回的channelfuture对象中进行判断.
当write发送成功后,write就会回调channelfuturelisten的operationcomplete方法.
但是异步的回调方法的执行是由Work的NioEventloop线程调用的,
也要保证该线程不能阻塞,或者在回调内开启一个业务线程执行.并且也要考虑多线程的并发安全问题.
ByteBuf bt1 = Unpooled.buffer();
bt1.writeBytes("我爱netty".getBytes("UTF-8"));
ChannelFuture cf = ctx.writeAndFlush(bt1);
cf.addListener((f)->
{
System.out.println("数据发送成功的回调");
});
Main运行后,JVM管理的多个线程中(main主线程,GC守护线程,部分子线程)
只有非守护线程都运行完毕后,Jvm就会自动推出并且关闭守护线程,
守护线程常见的有GC垃圾回收,也可以自己设定线程为守护线程
Thread.setDaemon(true);可以用来进行一些无关紧要的日志输出为守护线程.
Netty中通过boostrap.bind(port)绑定端口这个方法,
并非在调用方线程执行,通俗的说,虽然是main线程调用的它,但它会用自己的线程执行这个操作
(也就是底层通过NioEventLoop线程执行)(调用方main,执行方nioEventLoop)
这个时候异步就产生了,main线程进行接下来的代码执行,NioEventLoop线程进行端口绑定.
而我们的多数工作都需要等端口绑定完毕后执行,所以需要在.bind(port)后面添加.sync()方法
它能保证让我们的main线程,在这个异步方法执行完毕后,再往后执行,会让main阻塞下来等待.
根据上面的特性来说
绑定完毕线程后,如果设置端口的时候,没设置sync(),
这个时候main线程没有被阻塞住,且main后面没有任何代码需要执行的时候
那么当NioEventLoop执行完毕绑定端口后,main线程代码也早已执行完毕,已经不存在了.
如果main中写了finally,就代表运行即退出
(一个线程绑定端口,一个线程运行完毕执行了finally,异步就混乱了)这是非常需要注意的.
注意的就是NioEventLoop是非守护线程,运行之后,不会主动退出,只有调用shutdown才会退出.
如果你的netty服务端意外退出了,那么很大概率是被调用了shutdown.
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//代码省略
}
});
ChannelFuture cf = sb.bind(9999).sync();
cf.channel().closeFuture().sync();//A标记
} finally {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
一般shutdownGracefully会写在finally中比如上面的操作:
这个代码的运行也是没有任何问题的.但是它不优雅.
这样会有一个main线程在A标记处无限的阻塞
此时main线程最终作用,就是等待A标记处的sync的返回,这样也会浪费一个main线程…
它有点违背了netty的异步操作的概念了.
可以把shutdownGracefully函数写在cf.channel().closeFuture().addListener中,
这样main线程会主动释放无需在阻塞等待了.并且管道关闭的时候,会自动的进行优雅关闭.
优雅式:初始化netty服务端,绑定监听端口,向colosefuture注册监听器,在监听器中释放资源.调用方线程返回
非优雅:初始化netty服务端,绑定监听端口,同步阻塞等待端口关闭,释放资源,调用方线程释放.
优雅的方式:
//优雅方式
public static void main(String[] args) throws InterruptedException {
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap sb = new ServerBootstrap();
sb.group(boosGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//代码省略
}
});
ChannelFuture cf = sb.bind(9999).sync();
cf.channel().closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
boosGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
System.out.println("我退出了");
}
});
}
这样在main线程不需要无限的阻塞,这样mian它只是作为了一个启动线程,把netty启动后主动就释放了,并不会无限阻塞的等待.
要知道netty的优雅关闭需要先知道java的
Java的优雅关闭是通过注册JDK的ShutdownHook函数来实现的,当系统接收到退出指令,首先标记系统退出,不再接收新的信息,也就是先对外告知关闭,然后将积压的信息处理完毕后,最后调用资源回收接口将资源销毁,各种线程退出和销毁执行.也就是System.exit(0),通过JDK的ShutdownHook可实现关闭回调.
Netty的优雅退出
1.尽快释放NIO线程和句柄资源
2.使用flush批量发送,需要将积压队列的尽快发送完毕
3.读写消息需要继续处理,把最后一次的处理完毕
4.设置在nioeventloop线程调度器的定时任务,需要执行完和清理掉
总结下来为:把NIO线程状态设置为ST_SHUTTING_DOWN不再处理新消息,不许再对外发送消息
把队列的内容发送完毕,正在发送的或马上发送的,把已经到期或退出超时到期的定时任务执行完毕
把用户注册到NIO线程的退出HOOK任务执行完成,释放channel,多路复用器的注册和关闭
所有队列的和定时任务的清空取消,最后是Eventloop线程的退出,这些步骤都是调用showtdownGraceFully就可以自动实现的,非常方便
Netty优雅退出涉及到:线程组,NIO线程,Channel和定时任务等
待发送的消息:调用优雅退出方法之后,不会立即关闭链路,ChannelOutBoundBuffer中的消息可以继续发送(也就是队列积压还没发送出去的信息,会继续的发送),本轮发送操作完毕之后,无论是否还有消息,在下一轮的selector轮询中,链路都会被关闭,没有发送的会被丢弃和释放
另外,需要发送的新消息:我们可以随时随地的调用channel的write,即便某种情况下触发了netty的优雅退出,
在优雅退出执行期间,这些消息是无法发送出去的.
对此之间我们只需要记得最重要的,当优雅关闭触发后,netty把最后一次的正在执行的写或读执行完毕后,之后管道就会彻底关闭,在此之外存储的channel是无法继续发送的.最后一次的fireChannelRead也是可以执行完毕的
但是这并不是一定都会发送出去的,可能会有许多意外,具体的处理建议服务端关闭之前,对所有客户端发送"关闭封包"的提示,让客户端主动断开连接.服务端手动不对外接受连接,一律屏蔽,之后调用showtdownGraceFully来进行最后的处理是比较妥当的.否则服务端正常关闭了,客户端还浑然不知是服务端宕机了还是什么情况,客户端无法分辨…这并不太正确.
当netty的1个业务线程多执行0.1秒钟,在发生堵塞的情况下,100个线程就多执行了10秒钟.
任何涉及服务端的处理我们都要尽量保证高效,所以池化的概念就出现了
池化的理念:
和客户端不同,客户端创建出一个对象,是不确定的,什么时候使用,什么时候在创建也是可以的.(创建对象是需要时间的)
比如:客户端要使用一个string对象,那么使用的时候创建就可以了.这也是未池化的意思.
服务端则可能每分每秒都需要非常多的对象,如果我们将对象提前创建好1000个,需要的时候直接使用,这就节省了创建的时间.
不需要的时候把对象复原返还到原本的地方去,这个地方就称之为池,这样就节省了GC回收的时间.
这就是池的理念,对象池,线程池,内存池,等等之类.它可以帮助我们非常快速高效的复用已创建好的对象.
但这样很方便的同时暴露出一个问题,内存泄漏的问题.
你必须手动的获取池中的内容,不使用的时候手动释放这个对象让它重新返回池中,以可复用
如果你没有手动释放这个对象返回内存池中,那么内存就泄漏了…
有利有弊. 不过利大于弊.
Netty4 的bytebuf对象
PooledByteBufAllocator pb = new PooledByteBufAllocator(false);//池化变量 理解为一个内存池子就行
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf b1 = pb.heapBuffer(30); //池化堆内存 数据越少性能最快
ByteBuf b2 = pb.directBuffer(30); //池化堆外内存 数据越多性能最快
ByteBuf b3 = Unpooled.buffer(30);//未池化堆内存 数据越多性能最慢
ByteBuf b4 = Unpooled.directBuffer(30); //未池化堆外内存 数据越多性能较慢
Netty4很贴心的为我们提供了bytebuf用于存放字节的对象,它就分别有池化,未池化的,堆外内存存和堆内存的
使用netty提供的bytebuf池化对象,因为它的申请和释放采用了池化技术,你可以假设netty先给你创建了1000个bytebuf对象
通过PooledByteBufAllocator可以直接从netty创建的1000个对象中,拿到其中一个bytebuf对象.这样就避免了消息读写都要申请和释放bytebuf.
这个时候分出了一个 堆内存和直接堆内存的概念
堆内存:JVM管理的内存,也就是垃圾回收可以自动帮你回收的地方
堆外内存:这是JVM管理外的内存,也就是JVM虚拟机之外的内存,它不会管理这内存的生死,你需要手动的释放.(注意内存泄漏)
java的JVM的GC垃圾回收它管理堆内存时,需要先将堆内存转换为堆外内存.,才能回收.,毕竟回收内存是系统做的事,必须先转到堆外才行.
两者性能
无论是基于内存池还是非内存池分配的bytebuf,如果是堆内存,需要将堆内存转换成堆外内存再释放,如果是堆外内存可以直接释放,所以可以看得出来,堆外内存减少了一次转换步骤,大量操作的时候效率是很高的,尤其是池化是堆外内存.从netty 4开始,netty加入了内存池管理,采用内存池管理比普通的ByteBuf性能提高了数十倍
对性能要求苛刻或想搭建出高质量的neety服务端,可以善用池化的直接内存和堆内存
但不要以为池化的堆外内存效率就是最高的.这是错的,较大数据用池化堆外内存,较少数据存储用池化堆内存
池化和未池化对比
当程序高并发同时,执行1亿次非池化buffer创建释放需要126秒,而池化只需要12秒.并且这是不算上CG时间的.是在堆内存里面创建的,并没有使用直接内存,直接堆内存会更快.在高并发的场景下单个线程优化一毫秒,一亿个并发就是节省了一亿个1毫秒.
netty的内存池原理:略过,我不打算追究这个.浪费脑子,只需要知道,netty内存池实现是非常复杂的就行.
关于bytebuf内存泄漏和释放问题请看下面的代码,我会写出来
public class MyHandler2 extends ChannelInboundHandlerAdapter{
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
}
}
假设整个程序只有一个handler,上面的代码出错了吗?很显然,这是出错的代码,即使什么也没写,这代码内存也泄漏了.
Object msg参数得到的是一个池化堆外内存的ByteBuf, netty为了性能,所以会给你传入池化堆外内存的buf
如果你添加了一句代码:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.writeAndFlush(msg);
或者这句代码:
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf bb = (ByteBuf)msg;
bb.release();
这样程序就不会内存泄漏了.
当你调用ctx.writeAndFlush,他会自动的把你需要发送的msg发送完毕后自动释放. 这是隐式的释放
当你调用bb.release();这是显式的释放.
所以如果你创建了堆外内存的对象,一定要记得释放.
如果使用堆外内存,请一定要手动释放,如果是池化对象,无论堆外和堆内存都要手动释放. 因为池化的要返回到内存池中
如果你将要把数据write出去,那么你可以不用手动显式的释放,write会自动帮你做.
PooledByteBufAllocator pb = new PooledByteBufAllocator(false);//池化变量 理解为一个内存池子就行
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf b1 = pb.heapBuffer(30); //需要手动释放 调用release即可
ByteBuf b2 = pb.directBuffer(30); //需要手动释放 调用release即可
ByteBuf b3 = Unpooled.buffer(30);//可手动 可等GC自动回收
ByteBuf b4 = Unpooled.directBuffer(30); //需要手动释放 调用release即可
如果用的channelRead0函数,则netty会自动帮我们释放这个msg参数的内存.即使没手动释放或write,因为netty写了个finally
对于bytebuf额外的说一句:
当在readerindex开始读,读到了writeindex后或之前,可以使用disardreadbytes来重用这个空间,以节约内存,防止bytebuf不断扩张
另外netty池化的概念其实挺广泛的,EventLoopGroup也是池化线程,避免每次来一个连接就创建一个线程,创建的时间是很浪费的.
目的:
假设客户端向服务器请求1000兆的数据下载.
而客户端的网络下载带宽每秒只有1兆左右
服务器需要立刻准备好1000兆的数据放入发送队列的内存中吗?
相比肯定不需要吧,否则的话并发100个1000兆的下载,服务器就直接内存不足宕机了.
所以我们需要对此加以处理,我们只需要按照客户端最大带宽能力处理就可以了
这样也能避免一定的假下载封包的流量攻击.
高低水位
利用netty提供的高低水位机制(setwriteBufferHighWaterMake(int)),可以精准实现客户端流控
高低水位听起来比较抽象,但它只是对发送队列的大小计算的阈值,具体看下面的代码 只是一种思路
public class NettyHandler extends ChannelInboundHandlerAdapter{
static EventExecutorGroup group = new DefaultEventExecutorGroup(3);//业务线程组
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.channel().config().setWriteBufferHighWaterMark(1024*1024*10);//对此管道设置10兆高水位
ctx.channel().config().setWriteBufferLowWaterMark(1024*1024*3);//对此管道设置3兆低水位
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
System.out.println("触发了高低水位改变,回调函数");
if(ctx.channel().isWritable()==true) {//6.恢复至低水位了
//7.把记录下来还没发送的数据 继续发送给客户端.
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
group.submit(new Callable<Object>(){
@Override
public Object call() throws Exception {
while(true) {
if(ctx.channel().isWritable()) {
ctx.writeAndFlush("假设这是11兆数据");//1.假设一直无限写11兆的数据发送
//2.那么在第二次执行此方法时,ctx.channel().isWritable()会返回false,就无法进入了
//客户端读取速度假设是每秒1兆,我们的高水位设置了10兆,发送的是11兆
//那么要恢复到低水位,需要8秒的时间.在此我们无法发送数据,都会执行else的输出语句
//3.channelWritabilityChanged回调事件.
//水位一旦改变就会执行channelWritabilityChanged
//4.while不能再nio线程中使用,不然回调的函数执行线程就在nio,会导致低水位回调无法执行.
//所以我们在group中执行. 毕竟把回调线程阻塞了肯定就不行
}else {
System.out.println("触发了高水位");
//5.记录下来还有什么数据没发送,在channelWritabilityChanged变为低水位的时候继续发送.
}
}
}
});
}
}
当发送队列待发送的字节数组达到高水位,(服务端发送的快,客户端下载的慢,发送队列的内容,就达到了水位限制)
对应的channel就会变为不可写状态
我们就需要手动记录下来,还要发送的数据封包,待水位恢复后继续发送.
(也就是客户端下载了8兆,还有3兆没下载的时候,到了我们的低水位限制,触发了低水位,可以正常发送了.)
但是它并不影响业务线程调用write方法,因此必须在消息发送时进行判断是否channel可写,channel.iswriteable()
(如果我们不进行iswriteable判定,write仍然可以发送,只不过超了水位,没有流量限制管理了.)
对于代码中注释5的解释:
高水位满的时候的else中,应该记录下来还有什么数据没发送,大数据不要直接放入数组之后发,这样仍然占用着服务器的内存
如果正在传输文件,应该记录下来文件流传输到哪里了,
水位变低后,接着这文件流继续发.而不是把文件流的数据存入到数组中,之后读取这个数组的内容.
对于小数据的存储可以暂时放入数组内.水位变低后读取剩余数组内容,发送给客户端.
高低水位是针对客户端的写数据速度 ,请一定记清楚.否则很容易和流量整形理解混
高低水位是针对客户端的写数据速度 ,请一定记清楚.否则很容易和流量整形理解混
高低水位是针对客户端的写数据速度 ,请一定记清楚.否则很容易和流量整形理解混
目的
如果客户端以每秒10G的速度向服务器传输数据,(即使服务器的宽带也有10G速率)
但可能不过5分钟,服务器就已经内存溢出或者宕机了.
这就是读积压,因为服务器每秒最大处理1G的内容,剩下9G的数据要存入内存中等待读取.
应该对读客户端进行一个限制,限制读取客户端每秒最多发送的数据量,以保证服务器稳定运行.
当然流量整形还有一个 服务端对客户端的写速度限制,这个写并不是太好.并且不做处理仍然会内存泄漏,
建议用高低水位自己做写处理可以更自由一点.
流量整形的使用:
在解码前,编码后添加一个netty内置的handler
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new ChannelTrafficShapingHandler(1024 * 1024,1024 * 1024,1000));//流量整形
sc.pipeline().addLast(new MyHandler());
}
});
对单个channel每秒发送的流量限制为1兆大小
对单个channel每秒读取的流量限制了1兆大小
第三个参数:1000,单位是毫秒,表示每1秒统计一次发送和读取的字节总和.
如果需要对流量进行分析,可适当设置第三参数进行处理.
第三个参数的回调函数为:doAccounting,可以继承ChannelTrafficShapingHandler类进行重写doAccounting方法来统计
public class c extends ChannelTrafficShapingHandler{
public c(long writeLimit, long readLimit, long checkInterval) {
super(writeLimit, readLimit, checkInterval);
}
@Override
protected void doAccounting(TrafficCounter counter) {
super.doAccounting(counter);
System.out.println("周期内总共读取字节数::"+counter.lastReadBytes());
System.out.println("周期内总共发送字节数::"+counter.lastWrittenBytes());
}
}
上面继承ChannelTrafficShapingHandler,重写doAccounting方法来实现,第三个参数的具体实现.
误区
千万不要觉得 设置了流量整形服务端就一定是安全的.
流量整形可以让客户端的发送数据积压数据都存在于客户端,
之后服务端根据流量整形限制的读取速度,慢慢的消化客户端的积压数据.
但是将要发送的数据!依然会积压于服务端
但是将要发送的数据!依然会积压于服务端
但是将要发送的数据!依然会积压于服务端
当你设置了流量整形每秒最多发送1兆流量给客户端,假设服务器直接发送10兆给客户端,
那么因为流量整形,所以每秒最多给客户端发送1兆数据,剩下的9兆数据都要缓存在服务器内存中.
等待每秒的继续发送.
这样在高峰时期,一旦积压的发送数据内存压力较大,服务端就可能会宕机或崩溃.
为了稳定的服务端,给发送值设最大不限制,对发送的操作应该用流控来完成.
流量整形和流控的目的尽可能将积压的发送和读取队列积压放在客户端处理
能不放在服务器尽可能不放在服务器… 以保证高效稳定的运行
@Override
protected void initChannel(SocketChannel sc) throws Exception {
sc.pipeline().addLast(new c(1024,1024,1000));//设置了流量整形
sc.pipeline().addLast(new MyHandler());
}
假设客户端发送了一次102420的封包大小给服务端
服务端设置了流量整形 按照1024个字节,每秒进行的读取.
服务端一定是按照每秒1024字节读取吗?
并不是这样的,netty为了效率,它只会在20秒内读完这102420的大小字节包
可能前19次读0字节,最后一次读1024*20个字节. 所以数据获取是有延时性的
这样的好处就是,可以减少缓存数组的创建和释放.
并且也不一定是在20秒整完成,可能在第19秒,18秒就完成了.
如果设置的流量整形读取大于等于65536字节(handler中的read参数最大分配的buf大小65536)
那么它就不会出现上面的情况.毕竟已经达到最大了无需再分配了,也不需要优化读取了.
1.Netty根据上次实际读取的码流大小可以对下次接受buffer缓冲区进行预测和调整
2.ChannelHandler.Sharable可设为共享Handler,但是需要注意多线程安全问题,尽量少用.如果要用尽可能使用原子锁来保证,原子锁不会发生上下文切换,比普通的锁速度约快3倍.channelhandler本身是安全的,但channelpipeline是安全的,可以动态的向里面添加.它有一把锁保证.
3.使用Netty提供的DefaultEventExecutorGroup来执行业务代码,但需要记得,这个业务线程池有100个,它也无法同一个tcp并发2次的请求处理,因为每个tcp单独对应的handler最大只能一个.它不会并行的处理.需要在业务类的内部额外的提交任务以达到效果DefaultEventExecutor是JDK线程池Executorservice的一种优化实现,针对通信领域,在实际业务中业务线程是大于IO线程的
4.当在readerindex开始读,读到了writeindex后或之前,可以使用disardreadbytes来重用这个空间,以节约内存,防止bytebuf不断扩张
服务端的编写是至关重要的,netty提供了如此方便的高性能框架值得我们认真学习
还有更多的协议和意想不到的问题也没写出来
如果你需要,可以看netty进阶之路这本书
这篇的总结也是基于这本书的总结