vlambda博客
学习文章列表

Netty的异步模型分析(4)














目录

再看Netty优雅停机的用法和认识误区

  • Netty中单例模式的应用——全局的线程单例执行器

  • 如何正确使用Netty的优雅停机API



接上一篇,总结了Netty的优雅停机分两个部分:

1、shutdownGracefully方法设置NIO线程标志位

2、run方法以及它的外层调用里的资源清理和善后工作


通过Netty的优雅停机API:

1、技术上可以学到:

1、读写分离技术避免加锁

2、CAS+自旋的正确用法,比如需要用局部变量缓存全局属性,以及使用原3子更新器代替JDK封装的原子类,性能更佳

3、属性的状态标记类型为整型的好处,方便迅速判断当前状态的范围


2、业务上,可以学到:

1、Netty使用完毕,或者意外退出时,请尽快释放Netty线程池(当然也包括用户自定义的线程池)以及上面绑定的资源,比如MPSCQ,I/O多路复用器,Channel,发送缓冲区等,推荐使用异步监听回调的方法及时调用优雅停机API。


2、只要是涉及到缓存的消息处理,或者异步任务排队处理的框架,都应该对积压的消息(任务)设计优雅的停机策略,比如Netty就是把某个NIO线程的MPSCQ处理完后,才关闭该队列,并且需要有一个时间上限。


3、优秀的网络框架,在停机时需要对正在发生的读写逻辑设计优雅停机策略,比如可以将它们完整的执行完毕,比如也可以将来不及执行的,或者定时任务清理掉,并及时给出提示,可以打印日志,Netty就是这么做的。


4、应该允许用户注册一些停机的钩子,类比JDK的JVM关机钩子,Netty没有使用JDK自带的钩子方法,而是自己搞了一套,原因是JDK的钩子方法有一些缺陷,后续分析。


5、在实际项目中,Netty 作为高性能NIO框架,往往作为基础通信框架负责各种协议的接入、解析和调度等,例如在RPC和分布式服务框架中,往往会使用Netty作为内部私有协议的基础通信框架。当应用进程优雅退出时,作为通信框架的Netty也需要优雅退出

下面再看看优雅停机API的正确理解和使用




01

再看Netty优雅停机的用法和认识误区


Netty的所有的API都是异步的,即优雅停机API——shutdownGracefully也是异步的:

当然,这里说它是异步API其实不太准确,因为该方法内部仍然是串行执行的(事实上所有的Netty的异步的API都是如此),只不过一般调用优雅停机API的线程都是外部线程,这样的用法,Netty的异步API才是真正的异步。对于使用外部线程调用优雅停机API,这也是Netty的建议,否则容易在调用诸如异步转同步的方法时导致线程死锁。


再次强调:Netty的优雅停机将责任进行了分离,即优雅停机API只负责设置NIO线程的状态——关闭中,具体的停机逻辑由NIO线程的run方法以及外围的doStartThread方法处理,故可以这样理解这个异步API,因为它执行时间极快,几乎可以看成shutdownGracefully方法不会阻塞调用线程,其实后续会专题分析——Netty的I/O的API,底层都是NIO线程完成的,如果用户使用对应的NIO线程调用这些API,那么本质还是串行执行,不会实现真正的异步


Netty中单例模式的应用——全局的线程单例执行器


再看shutdownGracefully返回的future对象,如下:

Netty的异步模型分析(4)

该future对象内置了一个GlobalEventExecutor.INSTANCE——全局的线程单例执行器,它会自动开始执行MPSCQ里的异步任务,当在1秒内没有任务添加到该队列时,自动停止当前线程。注意:此执行器不适合调度大量的任务,调度大量任务可以用专用的Netty线程池执行器。也就是说,可以使用该单例执行器,执行一些微小的任务,Netty demo里有它的用法示范,它的单例实现很朴素:

Netty的异步模型分析(4)

如上也能看到,Netty对单例模式的实现没有那么细致的讲究,就是直接用的饿汉式加载,即没有懒加载。如下是它的私有构造器:

Netty的异步模型分析(4)

但是在实际的优雅停机API里,它几乎用不上,我找遍了全网资料以及Netty的demo,都发现没有使用shutdownGracefully的返回的future去注册监听器的用法,而且在我看来,它属于设计缺陷,因为它提供了addListener方法,但是却无法正常使用。


在这里之所以提到这个GlobalEventExecutor.INSTANCE,是因为可以学习Netty这种设计思路——单例线程如何自动终止,如何自动开始。至于它的用法,可以在Netty的线程池里塞入这个线程执行器,如下:

Netty的异步模型分析(4)

注意这个单例线程执行器是一个全局的处理微小的工作的工具,全局都会共用这个单例的线程执行器,和常规的NIO线程池不一样,常规线程池的每个NIO线程所共有的线程执行器是默认的ThreadPerTaskExecutor(回忆前面分析过的Netty默认的线程执行器,参考:),只不过Netty允许用户额外创建多个ThreadPerTaskExecutor,而GlobalEventExecutor全局有且仅有一个,因为它是饿汉式加载的单例工具类。下面看它的一个demo:

Netty的异步模型分析(4)


下面看它的实现原理和设计思路。先看它的私有的构造器的内部实现:

Netty的异步模型分析(4)

它在初始化时,默认给自己的定时任务队列添加了一个定时任务——即延时1s后,周期性(T=1S)执行的定时任务,如下:

Netty的异步模型分析(4)

再看它的线程执行的方法execute的实现,和普通的NIO线程默认自带的线程执行器——ThreadPerTaskExecutor没什么大的区别:

Netty的异步模型分析(4)

主要看它启动线程的方法——即execute下面的startThread方法,它内部实现逻辑很简单——启动线程驱动了一个taskRunner(本质是Runnable),下面看这个runnable的run方法:

具体细节不多说了,它和NioEventLoop的事件循环机制——run方法里的runAllTasks方法一样的套路:从定时任务队列里抓取即将到deadline的任务到MPSCQ缓存,同时依次执行MPSCQ里的异步任务,直到task == quietPeriodTask,这就说明1s的周期时间到了,马上执行for(;;)的后半段逻辑,代码就不贴了,很多但不复杂,就是判断该定时任务是不是之前塞入的那个周期1s执行的空的定时任务,如果是,且MPSCQ为空了,那么就自动结束该线程;否则继续正常运转,后续每个T=1s的周期都判断一次。。。直到符合要求后结束该线程,或者被外界强制关闭。


如何正确使用Netty的优雅停机API


1、最好是自己根据业务场景,指定shutdownGracefully的超时时间,尤其是对性能要求较高的场景,以防止因为一些积压的任务被长时间阻塞而无法快速的退出服务


2、最好不要完全依赖Netty的优雅停机API,因为它并不是真正的优雅,前面分析源码也看到了,因为不论怎么停机,都避免不了丢失定时任务,以及丢失Netty发送缓冲区里缓存的旧的消息。比如即使触发了Netty的优雅退出方法,在Netty优雅退出方法执行期间,应用线程仍然有可能继续调用Channel发送消息(用户可以随时调用Channel的write系列API发消息),这些新的消息将发送失败,如果是敏感数据,那么就丢失了。比如confirmShutdown方法最后结束时的注释也说明了:

而且Netty也一直在升级,其优雅退出的策略一直在调整,故不要单一的依赖它


3、用户提交给NIO线程的MPSCQ的普通异步任务,定时任务和注册在NIO线程的钩子方法,无法一定被完全执行完毕,Netty给不了这个承诺。因为超时时间是可被用户自定义的,或者默认情况下,堆积了大量异步任务,或者其它耗时操作,都可能导致停机过程超时而提前结束NIO线程


4、需要在应用层面自己做容错处理。例如,服务端在返回响应之前关闭了,导致响应没有发送给客户端,这可能会触发客户端的I/O异常,或者恰好发生了超时异常,客户端需要对I/O 或超时异常做容错处理,采用重试机制,重试集群中其他可用的Netty服务器,而不能寄希望于单个服务器永远正确,故实际生产还是需要多机器部署


5、尽量避免使用NIO线程调用优雅停机API,推荐用外部线程调用


6、Netty优雅停机API——shutdownGracefully可以和JDK的钩子函数——ShutdownHook配合使用,下一篇细说。