前奏
https://tech.meituan.com/2016/11/04/nio.html
综述
netty通过Reactor模型基于多路复用器接收并处理用户请求(能讲就多讲一点),内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件(通过口述加画图的方式,把请求的执行过程大概描述了一遍,时间有限,也不可能把所有的细节都说完,挑重点讲,挑记忆深刻的讲)
框架
-
一种理解方式
另一种理解方式
netty线程模型采用“服务端监听线程”和“IO线程”分离的方式,与多线程Reactor模型类似。
抽象出NioEventLoop来表示一个不断循环执行处理任务的线程,每个NioEventLoop有一个selector,用于监听绑定在其上的socket链路
- 功能架构
逻辑架构
Reactor 通信调度层:它由一系列辅助类完成,包括 Reactor 线程 NioEventLoop
及其父类,NioSocketChannel/ NioServerSocketChannel 及其父类,
ByteBuffer 以及由其衍生出来的各种 Buffer,Unsafe 以及其衍生出的各种内部
类等。该层的主要职责就是监听网络的读写和连接操作,负责将网络层的数据读
取到内存缓冲区中,然后触发各种网络事件,例如连接创建、连接激活、读事件、
写事件等,将这些事件触发到 PipeLine 中,由 PipeLine 管理的职责链来进行后
续的处理。职责链 ChannelPipeline:它负责事件在职责链中的有序传播,同时负责动
态地编排职责链。职责链可以选择监听和处理自己关心的事件,它可以拦截处理
和向后 / 向前传播事件。不同应用的 Handler 节点的功能也不同,通常情况下,
往往会开发编解码 Hanlder 用于消息的编解码,它可以将外部的协议消息转换成
内部的 POJO 对象,这样上层业务则只需要关心处理业务逻辑即可,不需要感知
底层的协议差异和线程模型差异,实现了架构层面的分层隔离。业务逻辑编排层(Service ChannelHandler):业务逻辑编排层通常有两类:
一类是纯粹的业务逻辑编排,还有一类是其他的应用层协议插件,用于特定协议
相关的会话和链路管理。例如 CMPP 协议,用于管理和中国移动短信系统的对接。架构的不同层面,需要关心和处理的对象都不同,通常情况下,对于业务开
发者,只需要关心职责链的拦截和业务 Handler 的编排。因为应用层协议栈往往
是开发一次,到处运行,所以实际上对于业务开发者来说,只需要关心服务层的
业务逻辑开发即可。各种应用协议以插件的形式提供,只有协议开发人员需要关
注协议插件,对于其他业务开发人员来说,只需关心业务逻辑定制。这种分层的
架构设计理念实现了 NIO 框架各层之间的解耦,便于上层业务协议栈的开发和业
务逻辑的定制
Netty的特点
- 一个高性能、异步事件驱动的NIO框架
- 支持TCP、UDP和文件传输
- 使用更高效的socket底层,对epoll空轮询引起的cpu占用飙升在内部进行了处理
- 避免了直接使用NIO的陷阱,简化了NIO的处理方式。
- 采用多种decoder/encoder 支持,对TCP粘包/分包进行自动化处理
- 可使用接受/处理线程池,提高连接效率,对重连、心跳检测的简单支持
- 可配置IO线程数、TCP参数, TCP接收和发送缓冲区使用直接内存代替堆内存,通过内存池的方式循环利用ByteBuf
- 通过引用计数器及时申请释放不再引用的对象,降低了GC频率
- 使用单线程串行化的方式,高效的Reactor线程模型
- 大量使用了volitale、使用了CAS和原子类、线程安全类的使用、读写锁的使用
Netty的高性能表现在哪些方面?
(1)采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同
步阻塞 I/O 模式下一个服务端无法平滑地处理线性增长的客户端的问题。
(2)TCP 接收和发送缓冲区使用直接内存代替堆内存,避免了内存复制,
提升了 I/O 读取和写入的性能。
(3)支持通过内存池的方式循环利用 ByteBuf,避免了频繁创建和销毁
ByteBuf 带来的性能损耗。
(4)可配置的 I/O 线程数、TCP 参数等,为不同的用户场景提供定制化的
调优参数,满足不同的性能场景。
(5)采用环形数组缓冲区实现无锁化并发编程,代替传统的线程安全容器
或者锁。
(6)合理地使用线程安全容器、原子类等,提升系统的并发处理能力。
(7)关键资源的处理使用单线程串行化的方式,避免多线程并发访问带来
的锁竞争和额外的 CPU 资源消耗问题。
(8)通过引用计数器及时地申请释放不再被引用的对象,细粒度的内存管
理降低了 GC 的频率,减少了频繁 GC 带来的时延增大和 CPU 损耗。
- 心跳检测空闲连接
为了支持心跳,Netty 提供了如下两种链路空闲检测机制: - 读空闲超时机制:当连续周期T没有消息可读时,触发超时Handler,用户
可以基于读空闲超时发送心跳消息,进行链路检测;如果连续N个周期仍
然没有读取到心跳消息,可以主动关闭链路 - 写空闲超时机制:当连续周期T没有消息要发送时,触发超时Handler,用
户可以基于写空闲超时发送心跳消息,进行链路检测;如果连续N个周期
仍然没有接收到对方的心跳消息,可以主动关闭链路。
对服务端:会定时清除闲置会话inactive(netty5)
对客户端:用来检测会话是否断开,是否重来,检测网络延迟,其中idleStateHandler类 用来检测会话状态
分析:
• IdleStateHandler 将通过 IdleStateEvent 调用 userEventTriggered
• 如果连接没有接收或发送数据超过60秒钟,则心跳发送到远端
• 发送的心跳并添加一个侦听器,如果发送操作失败将关闭连接
• 若事件不是 IdleStateEvent ,就将它传递给下一个处理程序内存保护机制
Netty 提供多种机制对内存进行保护,包括以下几个方面:
•通过对象引用计数器对Netty的ByteBuf等内置对象进行细粒度的内存申请
和释放,对非法的对象引用进行检测和保护。
• 通过内存池来重用ByteBuf,节省内存。
• 可设置的内存容量上限,包括ByteBuf、线程池线程数等。
如果长度解码器没有单个消息最大报文长度限制,当解码错位或者读取到畸
形码流时,长度值可能是个超大整数值,例如 4294967296,这很容易导致内存
溢出。如果有上限保护,例如单条消息最大不允许超过 10MB,当读取到非法消
息长度 4294967296 后,直接抛出解码异常,这样就避免了大内存的分配。优雅停机
优雅停机功能指的是当系统退出时,JVM 通过注册的 Shutdown Hook 拦截到退出
信号量,然后执行退出操作
释放相关模块的资源占用,将缓冲区的消息处理完成或者清空,将待刷新的数据持久化到磁盘或者数据库中,
等到资源回收和缓冲区消息处理完成之后,再退出。
优雅停机往往需要设置个最大超时时间 T,如果达到 T 后系统仍然没有退出,
则通过 Kill - 9 pid 强杀当前的进程。串行无锁化设计
即消息的处理尽可能在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
串行执行Handler链
分析:NioEventLoop读取到消息之后,直接调用ChannelPipeline的fireChannelRead(Object msg)方法,只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换。
一个NioEventLoop聚合了一个多路复用器Selector,因此可以处理成百上千的客户端连接,Netty的处理策略是每当有一个新的客户端接入,则从NioEventLoop线程组中顺序获取一个可用的NioEventLoop,当到达数组上限之后,重新返回到0,通过这种方式,可以基本保证各个NioEventLoop的负载均衡。一个客户端连接只注册到一个NioEventLoop上,这样就避免了多个IO线程去并发操作它。
- 可靠性
链路有效性检测:链路空闲检测机制,读/写空闲超时机制;
内存保护机制:通过内存池重用ByteBuf;ByteBuf的解码保护;
优雅停机:不再接收新消息、退出前的预处理操作、资源的释放操作。
-
Netty安全性
支持的安全协议:SSL V2和V3,TLS,SSL单向认证、双向认证和第三方CA认证。- SSL的三种认证方式
单向认证:客户端只验证服务端的合法性,服务端不验证客户端。
双向认证:与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。
CA认证:基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些CA_ROOT签名过的。就可以通过验证了。
- SSL的三种认证方式
高效并发编程的体现:
volatile的大量、正确使用;
CAS和原子类的广泛使用;
线程安全容器的使用;
通过读写锁提升并发性能。
IO通信性能三原则:传输(AIO)、协议(Http)、线程(主从多线程)
- 流量整型的作用(变压器)
防止由于上下游网元性能不均衡导致下游网元被压垮,业务流中断;
防止由于通信模块接受消息过快,后端业务线程处理不及时导致撑死问题。
- TCP参数配置
SO_RCVBUF和SO_SNDBUF:通常建议值为128K或者256K;SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
软中断:如果Linux内核版本支持RPS(2.6.35以上版本),开启RPS后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址,目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值来选择软中断运行的cpu,从上层来看,也就是说将每个连接和cpu绑定,并通过这个hash值,来均衡软中断在多个cpu上,提升网络并行处理性能。
Netty的高效并发编程主要体现在如下几点:
- volatile的大量、正确使用;
- CAS和原子类的广泛使用;
- 线程安全容器的使用;
- 通过读写锁提升并发性能。
Netty除了使用reactor来提升性能,当然还有
1、零拷贝,IO性能优化
2、通信上的粘包拆包
3、同步的设计
4、高性能的序列
Netty的线程模型
Netty 的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。
通过设置不同的启动参数,Netty 可以同时支持 Reactor 单线程模型、多线程模
型和主从 Reactor 多线层模型。
- Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应的Handler处理。
服务端启动的时候,创建了两个 NioEventLoopGroup,它们实际是两个独立
的 Reactor 线程池。一个用于接收客户端的 TCP 连接,另一个用于处理 I/O 相关
的读写操作,或者执行系统 Task、定时任务 Task 等。
- Netty 用于接收客户端请求的线程池职责如下。
(1)接收客户端 TCP 连接,初始化 Channel 参数;
(2)将链路状态变更事件通知给 ChannelPipeline。 - Netty 处理 I/O 操作的 Reactor 线程池职责如下。
(1)异步读取通信对端的数据报,发送读事件到 ChannelPipeline;
(2)异步发送消息到通信对端,调用 ChannelPipeline 的消息发送接口;
(3)执行系统调用 Task;
(4)执行定时任务 Task,例如链路空闲状态监测定时任务。
NioEventLoop设计原理
TCP 粘包/拆包的原因及解决方法
- TCP是以流的方式来处理数据,一个完整的包可能会被TCP拆分成多个包进行发送,也可能把小的封装成一个大的数据包发送。
- TCP粘包/分包的原因:
- 应用程序写入的字节大小大于套接字发送缓冲区的大小,会发生拆包现象,而应用程序写入数据小于套接字缓冲区大小,网卡将应用多次写入的数据发送到网络上,这将会发生粘包现象;
- 进行MSS大小的TCP分段,当TCP报文长度-TCP头部长度>MSS的时候将发生拆包
- 以太网帧的payload(净荷)大于MTU(1500字节)进行ip分片。
- 解决方法
- 消息定长:FixedLengthFrameDecoder类
- 包尾增加特殊字符分割:行分隔符类:LineBasedFrameDecoder或自定义分隔符类 :DelimiterBasedFrameDecoder
- 将消息分为消息头和消息体:LengthFieldBasedFrameDecoder类。分为有头部的拆包与粘包、长度字段在前且有头部的拆包与粘包、多扩展头部的拆包与粘包。
了解哪几种序列化协议
序列化(编码)是将对象序列化为二进制形式(字节数组),主要用于网络传输、数据持久化等;而反序列化(解码)则是将从网络、磁盘等读取的字节数组还原成原始对象,主要用于网络传输对象的解码,以便完成远程调用。
影响序列化性能的关键因素:
序列化后的码流大小(网络带宽的占用);
序列化的性能(CPU资源占用);
是否支持跨语言(异构系统的对接和开发语言切换)。Java默认提供的序列化:无法跨语言、序列化后的码流太大、序列化的性能差
XML
优点:人机可读性好,可指定元素或特性的名称。
缺点:序列化数据只包含数据本身以及类的结构,不包括类型标识和程序集信息;只能序列化公共属性和字段;不能序列化方法;文件庞大,文件格式复杂,传输占带宽。
适用场景:当做配置文件存储数据,实时数据转换。JSON,是一种轻量级的数据交换格式,
优点:兼容性高、数据格式比较简单,易于读写、序列化后数据较小,可扩展性好,兼容性好、与XML相比,其协议比较简单,解析速度比较快。
缺点:数据的描述性比XML差、不适合性能要求为ms级别的情况、额外空间开销比较大。
适用场景(可替代XML):跨防火墙访问、可调式性要求高、基于Web browser的Ajax请求、传输数据量相对小,实时性要求相对低(例如秒级别)的服务。Fastjson,采用一种“假定有序快速匹配”的算法。
优点:接口简单易用、目前java语言中最快的json库。
缺点:过于注重快,而偏离了“标准”及功能性、代码质量不高,文档不全。
适用场景:协议交互、Web输出、Android客户端Thrift,不仅是序列化协议,还是一个RPC框架。
优点:序列化后的体积小, 速度快、支持多种语言和丰富的数据类型、对于数据字段的增删具有较强的兼容性、支持二进制压缩编码。
缺点:使用者较少、跨防火墙访问时,不安全、不具有可读性,调试代码时相对困难、不能与其他传输层协议共同使用(例如HTTP)、无法支持向持久层直接读写数据,即不适合做数据持久化序列化协议。
适用场景:分布式系统的RPC解决方案Avro,Hadoop的一个子项目,解决了JSON的冗长和没有IDL的问题。
优点:支持丰富的数据类型、简单的动态语言结合功能、具有自我描述属性、提高了数据解析速度、快速可压缩的二进制数据形式、可以实现远程过程调用RPC、支持跨编程语言实现。
缺点:对于习惯于静态类型语言的用户不直观。
适用场景:在Hadoop中做Hive、Pig和MapReduce的持久化数据格式。Protobuf,将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
优点:序列化后码流小,性能高、结构化数据存储格式(XML JSON等)、通过标识字段的顺序,可以实现协议的前向兼容、结构化的文档更容易管理和维护。
缺点:需要依赖于工具生成代码、支持的语言相对较少,官方只支持Java 、C++ 、python。
适用场景:对性能要求高的RPC调用、具有良好的跨防火墙的访问属性、适合应用层对象的持久化
如何选择序列化协议
-
具体场景
- 对于公司间的系统调用,如果性能要求在100ms以上的服务,基于XML的SOAP协议是一个值得考虑的方案。
- 基于Web browser的Ajax,以及Mobile app与服务端之间的通讯,JSON协议是首选。对于性能要求不太高,或者以动态类型语言为主,或者传输数据载荷很小的的运用场景,JSON也是非常不错的选择。
- 对于调试环境比较恶劣的场景,采用JSON或XML能够极大的提高调试效率,降低系统开发成本。
- 当对性能和简洁性有极高要求的场景,Protobuf,Thrift,Avro之间具有一定的竞争关系。
- 对于T级别的数据的持久化应用场景,Protobuf和Avro是首要选择。如果持久化后的数据存储在hadoop子项目里,Avro会是更好的选择。
- 对于持久层非Hadoop项目,以静态类型语言为主的应用场景,Protobuf会更符合静态类型语言工程师的开发习惯。由于Avro的设计理念偏向于动态类型语言,对于动态语言为主的应用场景,Avro是更好的选择。
- 如果需要提供一个完整的RPC解决方案,Thrift是一个好的选择。
- 如果序列化之后需要支持不同的传输层协议,或者需要跨防火墙访问的高性能场景,Protobuf可以优先考虑。
protobuf的数据类型有多种:bool、double、float、int32、int64、string、bytes、enum、message。protobuf的限定符:required: 必须赋值,不能为空、optional:字段可以赋值,也可以不赋值、repeated: 该字段可以重复任意次数(包括0次)、枚举;只能用指定的常量集中的一个值作为其值;
protobuf的基本规则:每个消息中必须至少留有一个required类型的字段、包含0个或多个optional类型的字段;repeated表示的字段可以包含0个或多个数据;[1,15]之内的标识号在编码的时候会占用一个字节(常用),[16,2047]之内的标识号则占用2个字节,标识号一定不能重复、使用消息类型,也可以将消息嵌套任意多层,可用嵌套消息类型来代替组。
protobuf的消息升级原则:不要更改任何已有的字段的数值标识;不能移除已经存在的required字段,optional和repeated类型的字段可以被移除,但要保留标号不能被重用。新添加的字段必须是optional或repeated。因为旧版本程序无法读取或写入新增的required限定符的字段。
编译器为每一个消息类型生成了一个.java文件,以及一个特殊的Builder类(该类是用来创建消息类接口的)。如:UserProto.User.Builder builder = UserProto.User.newBuilder();builder.build();
Netty中的使用:ProtobufVarint32FrameDecoder 是用于处理半包消息的解码类;ProtobufDecoder(UserProto.User.getDefaultInstance())这是创建的UserProto.java文件中的解码类;ProtobufVarint32LengthFieldPrepender 对protobuf协议的消息头上加上一个长度为32的整形字段,用于标志这个消息的长度的类;ProtobufEncoder 是编码类
将StringBuilder转换为ByteBuf类型:copiedBuffer()方法
Netty的零拷贝实现
Netty的接收和发送ByteBuffer采用DIRECT BUFFERS,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。堆内存多了一次内存拷贝,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。ByteBuffer由ChannelConfig分配,而ChannelConfig创建ByteBufAllocator默认使用Direct Buffer
CompositeByteBuf 类可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf, 避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。addComponents方法将 header 与 body 合并为一个逻辑上的 ByteBuf, 这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的, CompositeByteBuf 只是逻辑上是一个整体
通过 FileRegion 包装的FileChannel.tranferTo方法 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环write方式导致的内存拷贝问题。
通过 wrap方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
Selector BUG:若Selector的轮询结果为空,也没有wakeup或新消息处理,则发生空轮询,CPU使用率100%;
Netty的解决办法:对Selector的select操作周期进行统计,每完成一次空的select操作进行一次计数,若在某个周期内连续发生N次空轮询,则触发了epoll死循环bug。重建Selector,判断是否是其他线程发起的重建请求,若不是则将原SocketChannel从旧的Selector上去除注册,重新注册到新的Selector上,并将原来的Selector关闭
串行化设计避免线程竞争
- netty采用串行化设计理念,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责。整个流程不会进行线程上下文切换,数据无并发修改风险。
- 一个NioEventLoop聚合一个多路复用器selector,因此可以处理多个客户端连接。
- netty只负责提供和管理“IO线程”,其他的业务线程模型由用户自己集成。
- 时间可控的简单业务建议直接在“IO线程”上处理,复杂和时间不可控的业务建议投递到后端业务线程池中处理。
定时任务与时间轮算法
在Netty中,有很多功能依赖定时任务,比较典型的有两种:
- 客户端连接超时控制;
- 链路空闲检测。
NioEventLoop中的Thread线程按照时间轮中的步骤不断循环执行:
a)在时间片Tirck内执行selector.select()轮询监听IO事件;
b)处理监听到的就绪IO事件;
c)执行任务队列taskQueue/delayTaskQueue中的非IO任务。
一种比较常用的设计理念是在NioEventLoop中聚合JDK的定时任务线程池ScheduledExecutorService,通过它来执行定时任务。这样做单纯从性能角度看不是最优,原因有如下三点:
- 在IO线程中聚合了一个独立的定时任务线程池,这样在处理过程中会存在线程上下文切换问题,这就打破了Netty的串行化设计理念;
- 存在多线程并发操作问题,因为定时任务Task和IO线程NioEventLoop可能同时访问并修改同一份数据;
- JDK的ScheduledExecutorService从性能角度看,存在性能优化空间。
最早面临上述问题的是操作系统和协议栈,例如TCP协议栈,其可靠传输依赖超时重传机制,因此每个通过TCP传输的 packet 都需要一个 timer来调度 timeout 事件。这类超时可能是海量的,如果为每个超时都创建一个定时器,从性能和资源消耗角度看都是不合理的。
根据George Varghese和Tony Lauck 1996年的论文《Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility》提出了一种定时轮的方式来管理和维护大量的timer调度。
Netty的定时任务调度就是基于时间轮算法调度,下面我们一起来看下Netty的实现。
定时轮是一种数据结构,其主体是一个循环列表,每个列表中包含一个称之为slot的结构,它的原理图如下:
定时轮的工作原理可以类比于时钟,如上图箭头(指针)按某一个方向按固定频率轮动,每一次跳动称为一个tick。这样可以看出定时轮由个3个重要的属性参数:ticksPerWheel(一轮的tick数),tickDuration(一个tick的持续时间)以及 timeUnit(时间单位),例如当ticksPerWheel=60,tickDuration=1,timeUnit=秒,这就和时钟的秒针走动完全类似了。
下面我们具体分析下Netty的实现:时间轮的执行由NioEventLoop来复杂检测,首先看任务队列中是否有超时的定时任务和普通任务,如果有则按照比例循环执行这些任务,代码如下:
如果没有需要立即执行的任务,则调用Selector的select方法进行等待,等待的时间为定时任务队列中第一个超时的定时任务时延,代码如下:
从定时任务Task队列中弹出delay最小的Task,计算超时时间;
定时任务的执行:经过周期tick之后,扫描定时任务列表,将超时的定时任务移除到普通任务队列中,等待执行,相关代码如下:
检测和拷贝任务完成之后,就执行超时的定时任务,代码如下:
为了保证定时任务的执行不会因为过度挤占IO事件的处理,Netty提供了IO执行比例供用户设置,用户可以设置分配给IO的执行比例,防止因为海量定时任务的执行导致IO处理超时或者积压。
因为获取系统的纳秒时间是件耗时的操作,所以Netty每执行64个定时任务检测一次是否达到执行的上限时间,达到则退出。如果没有执行完,放到下次Selector轮询时再处理,给IO事件的处理提供机会,代码如下:
聚焦而不是膨胀
Netty是个异步高性能的NIO框架,它并不是个业务运行容器,因此它不需要也不应该提供业务容器和业务线程。合理的设计模式是Netty只负责提供和管理NIO线程,其它的业务层线程模型由用户自己集成,Netty不应该提供此类功能,只要将分层划分清楚,就会更有利于用户集成和扩展。
令人遗憾的是在Netty 3系列版本中,Netty提供了类似Mina异步Filter的ExecutionHandler,它聚合了JDK的线程池java.util.concurrent.Executor,用户异步执行后续的Handler。
ExecutionHandler是为了解决部分用户Handler可能存在执行时间不确定而导致IO线程被意外阻塞或者挂住,从需求合理性角度分析这类需求本身是合理的,但是Netty提供该功能却并不合适。原因总结如下:
- 它打破了Netty坚持的串行化设计理念,在消息的接收和处理过程中发生了线程切换并引入新的线程池,打破了自身架构坚守的设计原则,实际是一种架构妥协;
- 潜在的线程并发安全问题,如果异步Handler也操作它前面的用户Handler,而用户Handler又没有进行线程安全保护,这就会导致隐蔽和致命的线程安全问题;
- 用户开发的复杂性,引入ExecutionHandler,打破了原来的ChannelPipeline串行执行模式,用户需要理解Netty底层的实现细节,关心线程安全等问题,这会导致得不偿失。
鉴于上述原因,Netty的后续版本彻底删除了ExecutionHandler,而且也没有提供类似的相关功能类,把精力聚焦在Netty的IO线程NioEventLoop上,这无疑是一种巨大的进步,Netty重新开始聚焦在IO线程本身,而不是提供用户相关的业务线程模型。
Netty线程开发最佳实践
时间可控的简单业务直接在IO线程上处理
如果业务非常简单,执行时间非常短,不需要与外部网元交互、访问数据库和磁盘,不需要等待其它资源,则建议直接在业务ChannelHandler中执行,不需要再启业务的线程或者线程池。避免线程上下文切换,也不存在线程并发问题。复杂和时间不可控业务建议投递到后端业务线程池统一处理
对于此类业务,不建议直接在业务ChannelHandler中启动线程或者线程池处理,建议将不同的业务统一封装成Task,统一投递到后端的业务线程池中进行处理。
过多的业务ChannelHandler会带来开发效率和可维护性问题,不要把Netty当作业务容器,对于大多数复杂的业务产品,仍然需要集成或者开发自己的业务容器,做好和Netty的架构分层。业务线程避免直接操作ChannelHandler
对于ChannelHandler,IO线程和业务线程都可能会操作,因为业务通常是多线程模型,这样就会存在多线程操作ChannelHandler。为了尽量避免多线程并发问题,建议按照Netty自身的做法,通过将操作封装成独立的Task由NioEventLoop统一执行,而不是业务线程直接操作,相关代码如下所示:
Ref:
http://www.jianshu.com/p/03bb8a945b37
http://www.voidcn.com/article/p-flamborw-bhh.html
http://www.voidcn.com/article/p-onnrusud-mz.html
http://blog.csdn.net/qq924862077/article/details/53316490