更多技术分享可关注我

前言

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

Netty线程调度模型回顾

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

具体细节和源码的分析参考:

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线程调度模型的人一看就懂,但是具体到非业务线程池的使用细节可能一部分人就不知道了:

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

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

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

其最终的内部实现如下:

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

关于Netty的handler添加机制以及pipeline机制后续分析,暂时看不懂没关系,先简单了解。

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

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

回到demo:

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

看红框发现BusinessHandler被非I/O线程驱动,EchoServerHandler被NIO线程驱动,它的执行不受耗时业务的影响。且BusinessHandler的channelRead方法内,会将该channelRead事件继续传播出去,因为调用了父类的channelRead方法,如下:

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

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

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

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

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

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

如果当前执行的线程是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耗时的业务逻辑应该写在哪儿,有什么注意事项?的更多相关文章

  1. 分享一个UI与业务逻辑分层的框架(一)

    序言 .NET(C#)的WinForm如何简单易行地进行UI与业务逻辑分层?本系列文章介绍一个WinForm分层框架,该框架针对WinForm中的TextBox,CheckBox,RadioButto ...

  2. SpringBoot切面控制业务逻辑

    业务逻辑:写一个公共拦截类,过滤传进Controller的参数 为了调用接口安全起见,每个需要调用的接口有一个参数accessToken,用于安全验证 注:先进入过滤器Filter,再进入aop,最后 ...

  3. 专人写接口+模型,专人写业务逻辑---interface_model -- business logical

    专人写接口+模型,专人写业务逻辑---interface_model -- business logical 0-控制台脚本重构为“面向接口编程”:1-仓库类通过__constru方法,来实现一处实例 ...

  4. Python写业务逻辑的几个编码原则

    作为一个写业务逻辑的boy,我需要专注的就是把业务逻辑写好.写业务逻辑并不复杂,就是把编程最基础的东西使用好,有变量.循环.流程控制.函数.数据库等. 但是写出的逻辑要通俗易懂.易于理解,避免炫技.晦 ...

  5. 9.1.3 .net framework通过业务逻辑层自动生成WebApi的做法

    首先需要说明的是这是.net framework的一个组件,而不是针对.net core的.目前工作比较忙,因此.net core的转换正在编写过程中,有了实现会第一时间贴出来. 接下来进入正题.对于 ...

  6. ASP.NET MVC5 网站开发实践(一) - 框架(续) 模型、数据存储、业务逻辑

    上次搭建好了项目框架,但还是觉得不太对劲,后来才想起来没有对开发目标进行定位,这个小demo虽然不用做需求分析,但是要实现什么效果还得明确.后来想了一下就做个最简单的网站,目标定为小公司进行展示用的网 ...

  7. 分享一个UI与业务逻辑分层的框架(三)

    序言 前两篇讲解了UIMediator框架的使用及具体原理代码.本篇讲述MediatorManager的实现代码及展望. MediatorManager MediatorManager的作用有两点: ...

  8. 从零开始,搭建博客系统MVC5+EF6搭建框架(1),EF Code frist、实现泛型数据仓储以及业务逻辑

    前言      从上篇30岁找份程序员的工作(伪程序员的独白),文章开始,我说过我要用我自学的技术,来搭建一个博客系统,也希望大家给点意见,另外我很感谢博客园的各位朋友们,对我那篇算是自我阶段总结文章 ...

  9. MapReduce实现手机上网流量分析(业务逻辑)

    一.问题背景 现在的移动刚一通话就可以在网站上看自己的通话记录,以前是本月只能看上一个月.不过流量仍然是只能看上一月的. 目的就是找到用户在一段时间内的上网流量. 本文并没有对时间分组.下一节进行分区 ...

随机推荐

  1. 对HTML语义化的一些理解和记录

    什么是HTML语义化 说HTML语义化就要先说说HTML到底负责的什么?下面摘自维基百科: 超文本标记语言(英语:HyperText Markup Language,简称:HTML)是一种用于创建网页 ...

  2. HTTP协议详解(深入理解)

    版权声明:本文为CSDN博主「有抱负的小狮子」的原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明.原文链接:https://blog.csdn.net/weixin_ ...

  3. java多线程之间的通信

    目的 如何让两个线程依次执行? 那如何让 两个线程按照指定方式有序交叉运行呢? 四个线程 A B C D,其中 D 要等到 A B C 全执行完毕后才执行,而且 A B C 是同步运行的 三个运动员各 ...

  4. vue中的插槽(slot)

    vue中的插槽,指的是子组件中提供给父组件使用的一个占位符,用<slot></slot>标签表示,父组件可以在这个占位符中填充任何模板代码,比如HTML.组件等,填充的内容会替 ...

  5. 第一个Hystrix程序 Hystrix 一

    1.导入jar包 <dependencies> <dependency> <groupId>com.netflix.hystrix</groupId> ...

  6. 最简单的???ubuntu 通过crontab定时执行一个程序

    crontab在liunx系统中下载,我默认是认为下载安装了的.. crontab貌似只能在liunx系统中存在,如果是windows系统我不知道 创建一个名为jiaoben的文件夹存储sh文件,进入 ...

  7. ASP.NET Core 中jwt授权认证的流程原理

    目录 1,快速实现授权验证 1.1 添加 JWT 服务配置 1.2 颁发 Token 1.3 添加 API访问 2,探究授权认证中间件 2.1 实现 Token 解析 2.2 实现校验认证 1,快速实 ...

  8. Mac Maven安装与配置

    下载 官网地址:http://maven.apache.org/download.cgi 配置环境变量 总步骤 编辑.bash_profile文件 vim ~/.bash_profile 配置mave ...

  9. MVC超链接调用控制器内的方法

    <a href="hello/Layout?name=Tom"><h1><span>Hello</span>World</h1 ...

  10. 【Python】2.19学习笔记 成员运算符,身份运算符,运算符优先级

    成员运算符 暂时不会用,等学链表时再补充 \(in\) 与 \(not in\) \(in\):如果在指定序列中找到指定值,则返回\(true\) \(not in\):如果在指定序列中找到指定值,则 ...