Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

 

 

 

 

 

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?更多手艺分享可关注我

前言 

Netty以高性能著称,然则在实际使用中,不可制止会遇到耗时的营业逻辑,那么这些耗时操作应该写在哪儿呢,有什么注重的坑吗?本篇文章将逐一总结。原文:​Netty耗时的营业逻辑应该写在哪儿,有什么注重事项?

Netty线程调剂模子回首

这部分内容前面都有总结,很简朴,只要心中有一个图像就能hold住——对于Netty来说,它的每个NIO线程都对应一个转动起来的“轮盘”,即I/O事宜监听+I/O事宜分类处置+异步义务处置,三件事组成一个“轮盘”循环往复的转动,直到被优雅停机或者异常中断。。。也许结构如下:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

详细细节和源码的剖析参考:

Netty的线程调剂模子剖析(5)

Netty的线程调剂模子剖析(6)

Netty的线程调剂模子剖析(7)

Netty的线程调剂模子剖析(8)

Netty的线程调剂模子剖析(9)

本文不再赘述。

 

Netty的NIO线程常见的壅闭场景

知道一个大前提:Netty的ChannelHandler是营业代码和Netty框架交汇的地方(关于pipeline机制的细节后续专题剖析,先知道即可),ChannelHandler里的营业逻辑,正常来说是由NioEventLoop(NIO)线程串行执行,以Netty服务端举例,在服务端接收到新新闻后,第一步要做的往往是用解码的handler解码新闻的字节序列,字节序列解码后就变为了新闻工具,第二步将新闻工具丢给后续的营业handler处置,此时若是某个营业handler的流程异常耗时,好比需要查询数据库,那么为了制止I/O线程(也就是Netty的NIO线程)被长时间占用,需要使用分外的非I/O线程池来执行这些耗时的营业逻辑,这也是基本操作。

下面看下NIO线程常见的壅闭情形,一共两大类:

  • 无意识:在ChannelHandler中编写了可能导致NIO线程壅闭的代码,然则用户没有意识到,包罗但不限于查询种种数据存储器的操作、第三方服务的远程挪用、中间件服务的挪用、守候锁等

  • 有意识:用户知道有耗时逻辑需要分外处置,然则在处置历程中翻车了,好比自动切换耗时逻辑到营业线程池或者营业的新闻行列做处置时发生壅闭,最典型的有对方是壅闭行列,锁竞争猛烈导致耗时,或者投递异步义务给新闻行列时异机房的网络耗时,或者义务行列满了导致守候,等等

JDK的线程池照样Netty的非I/O线程池?

如上一节的剖析,不论是哪类缘故原由,都需要使用非I/O线程池处置耗时的营业逻辑,这个操作有两个注重的点,第一个点是需要确定使用什么样的营业线程池,第二个点是这个线程池应该用在哪儿?

好比下面这个Netty线程池使用的架构图,熟悉Netty线程调剂模子的人一看就懂,然则详细到非营业线程池的使用细节可能一部分人就不知道了:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

如上图,既然知道了应该将耗时的营业逻辑封装在分外的营业线程池中执行,那么是使用JDK的原生线程池,照样用其它的自定义线程池,好比Netty的线程池呢?

 

可以通过看Netty的ChannelPipeline源码来找到谜底,如下ChannelPipeline接口的注释写的很明了:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

即Netty建议使用它自身提供的营业线程池来驱动非I/O的耗时营业逻辑,若是营业逻辑执行时间很短或者是完全异步的,那么不需要使用分外的非I/O线程池。而且详细用法是Netty在添加handler时,在ChannelPipeline接口提供了一个重载的addLast方式,专用于为对应handler添加Netty营业线程池,如下:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

其最终的内部实现如下:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

[白话剖析] 通俗剖析集成学习之bagging,boosting & 随机森林

提交的营业线程池——group工具,会被包裹进Netty的pipeline的新节点中,最终会赋值给该handler节点的父类的线程池executor工具,这样后续该handler被执行时,会将执行的义务提交到指定的营业线程池——group执行。如下是pipeline的新节点的数据结构——AbstractChannelHandloerContext:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

关于Netty的handler添加机制以及pipeline机制后续剖析,暂时看不懂没关系,先简朴领会。

 

直接看demo,重点是两个红框的代码:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

I/O线程池是Nio开头的group,非I/O线程池使用DefaultEventExecutorGroup这个Netty默认实现的线程池。在看第二个红框处,ChannelInitializer内部类里BusinessHandler这个入站处置器使用DefaultEventExecutorGroup执行,该处置器在channelRead事宜方式里模拟一个耗时逻辑,如下休眠3s模拟查询大量数据:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

回到demo:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

EchoServerHandler也是入站处置器,且被添加在pipeline的最后,那么进入服务器的字节序列会先进入BusinessHandler,再进入EchoServerHandler,下面是测试效果:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

看红框发现BusinessHandler被非I/O线程驱动,EchoServerHandler被NIO线程驱动,它的执行不受耗时营业的影响。且BusinessHandler的channelRead方式内,会将该channelRead事宜继续流传出去,由于挪用了父类的channelRead方式,如下:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

这样会执行到下一个入站handler——EchoServerHandler的channelRead方式。

异步的执行效果怎么回到Netty的NIO线程?

我看不少初学者会搞不明了这里,即将异步义务丢给了服务端外部的非I/O线程池执行,那外一客户端需要异步义务的盘算效果,这个盘算的效果怎么回到Netty的NIO线程呢,即他们嫌疑或者说不理解这个异步效果是怎么被Netty发出去给客户端的呢?

可以继续看第3节的demo的执行效果:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

发现BusinessHandler确实是被非I/O线程驱动的,即日志打印的线程名是default开头的,而EchoServerHandler又确实是被NIO线程驱动的,即日志打印的线程是nio开头的,它的执行不受耗时营业的影响。这底层流转到底是怎么回事呢,且看剖析。如下是demo里,BusinessHandler的channelRead方式:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

该方式是一个回调的用户事宜,当Channel里有数据可读时,Netty会自动挪用它,这种机制后续专题总结,这里知道结论即可。注重红线处前面也提到了——会将该channelRead事宜继续流传给下一个handler节点,即执行到下一个入站处置器——EchoServerHandler的channelRead方式。而在pipeline上流传这个事宜时,Netty会对其驱动的流传历程做一个判断。看如下的invokeChannelRead方式源码:其中参数next是入站节点EchoServerHandler,其executor是NIO线程,焦点代码如下红框处——会做一个判断:

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

若是当前执行的线程是Netty的NIO线程(就是该Channel绑定的谁人NIO线程,即executor,暂时不理解也没关系,知道结论,后续专题剖析),那么就直接驱动,若是不是NIO线程,那么会将该流程封装成一个新的task扔到NIO线程的MPSCQ,排队守候被NIO线程处置,这里关于MPSCQ可以参考:Netty的线程调剂模子剖析(9)。因此将耗时的营业逻辑放到非NIO线程池处置,也不会影响Netty的I/O调剂,仍然能通过NIO线程向客户端返回效果。

JDK的线程池拒绝计谋的坑可能导致壅闭

使用过线程池的都知道,若是营业逻辑处置慢,那么会导致线程池的壅闭行列积压义务,当积压义务到达容量上限,JDK有对应的处置计谋,一样平常有如下几个已经提供的拒绝计谋:

ThreadPoolExecutor.AbortPolicy:抛弃义务并抛出RejectedExecutionException异常。
ThreadPoolExecutor.DiscardPolicy:也是抛弃义务,然则不抛出异常。
ThreadPoolExecutor.DiscardOldestPolicy:抛弃行列最前面的义务,然后重新实验执行义务(重复此历程)
ThreadPoolExecutor.CallerRunsPolicy:由挪用线程处置该义务

JDK线程池默认是AbortPolicy,即自动抛出RejectedExecutionException

回到Netty,若是使用其非I/O线程池欠妥,可能造成NIO线程壅闭,好比营业上有人会设置线程池满的拒绝计谋为CallerRunsPolicy 计谋,这导致会由挪用方的线程——NioEventLoop线程执行营业逻辑,最终导致NioEventLoop线程可能被长时间壅闭,在服务端就是无法实时的读取新的请求新闻。

实际使用Netty时,一定注重这个坑。即当提交的义务的壅闭行列满时,再向行列加入新的义务,万万不能壅闭NIO线程,要么抛弃当前义务,或者使用流控并向营业方和运维职员报警的方式规避这个问题,好比实时的动态扩容,或者提高算法能力,提升机械性能等。

使用Netty非I/O线程池的准确姿势

前面实在剖析过Netty的EventExecutorGroup线程池,可以参考:Netty的线程调剂模子剖析(10)——《Netty有几类线程池,它们的区别,以及和JDK线程池区别?》,它也是类似NIO的线程池机制,只不过它没有绑定I/O多路复用器,它和Channel的绑定关系和NIO线程池一样,也是来一个新毗邻,就用线程选择器选择一个线程与之绑定,后续该毗邻上的所有非I/O义务,都在这一个线程中串行执行,此时并不能施展EventExecutorGroup的作用,纵然初始值设置100个线程也无济于事。

两句话:

1、若是所有客户端的并发毗邻数小于营业线程数,那么建议将请求新闻封装成义务投递到后端通俗营业线程池执行即可,ChannelHandler不需要处置庞大营业逻辑,也不需要再绑定EventExecutorGroup

2、若是所有客户端的并发毗邻数大于即是营业需要设置的线程数,那么可以为营业ChannelHandler绑定EventExecutorGroup——使用addLast的方式

后记

dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习条记的分享和一样平常吐槽,包罗但不限于互联网行业,附带分享一些PDF电子书,资料,协助内推,迎接拍砖!

 

Netty耗时的营业逻辑应该写在哪儿,有什么注意事项?

原创文章,作者:28qn新闻网,如若转载,请注明出处:https://www.28qn.com/archives/4235.html