netty框架2


深入了解netty的运行机制,掌握NIOEventLoop、Pipeline、ByteBuf的核心原理,及常见netty的调优方案。
内容定位:
1、深入了解netty源码的朋友。
2、未来可能参与中间件开发的人群。

Netty为何高性能

使用Netty(NIO框架)相比于传统基于Java序列化+BIO(同步阻塞IO)的通信框架,性能提升了8倍多。
传统的rpc调用性能差的3宗罪。

网络传输方式的问题
序列化方式的问题
线程模型的问题

网络传输方式的问题:
传统的RPC框架或者基于RMI等方式的远程服务(过程)调用采用了同步阻塞IO,当客户端的并发压力或者网络延时增大后,同步阻塞IO会由于频繁的wait导致IO线程经常性阻塞,由于线程无法高效的工作,IO处理能力自然下降。

在这里插入图片描述
采用BIO通信模型,通常有一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接之后为客户端连接创建一个新的线程处理请求信息,处理完成之后,返回应答消息给客户端,线程销毁。这是典型的一请求一应答模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加之后,服务端的线程个数和并发访问数成线性正比,由于线程是JAVA虚拟机非常宝贵的系统资源。当线程数膨胀后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出,并导致服务器最终宕机。

序列化方式的问题:
java序列化存在如下典型问题:
1)Java序列化是java内部的一种对象编解码技术,无法跨语言使用;例如:对于异构系统之间的对接,java序列化后的码流需要能够通过其它语言反序列化成原始对象(副本),目前很难支持。
2)相比其它开源的序列化框架,java序列化后的码流太大,无论网络传输还是持久化磁盘,都会导致额外的资源占用。
3)序列化性能差(CPU资源占用高)

线程模型问题:
由于采用同步阻塞IO,这会导致每个TCP连接都占用1个线程,由于线程资源是JVM虚拟机非常宝贵的资源,当IO读写阻塞导致线程无法及时释放,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。

高性能的3个主题

1)传输:
用什么样的通道将数据发送给对方,BIO、NIO或者AIO,IO模型很大程度决定了框架的性能。
2)协议:
采用什么样的通信协议,HTTP或者内部私有协议。协议的选择不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计优化。
3)线程:
数据报如何读取?读取后编解码在哪个线程进行,编解码后的消息如何派发。Reactor线程模型的不同,对性能的影响也非常大。

在这里插入图片描述

Netty高性能之道

高性能之道 --> 1 异步非阻塞通信

在IO编程中,当需要同时处理多个客户端请求时,可以利用多线程或者IO多路复用技术进行处理。IO多路复用技术通过把多个IO的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。
与传统的多线层模式比,IO多路复用的最大优势就是系统开销小,系统不需要创建新的额外线程,也不需要维护这些线程的运行,降低系统的维护工作量,节省了系统的资源。
JDK NIO通信模型如下:

在这里插入图片描述

与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性不好,非阻塞模式正好相反。开发人员一般可以根据自己的需要来选择合适的模式,一般来说,低负载、低并发的应用程序可以选择同步阻塞IO以降低编程的复杂度。但是对于高负载、高并发的网络应用,需要使用NIO的非阻塞模式进行开发。Netty架构按照Reactor模式设计和实现,它的服务端通信序列图如下:
在这里插入图片描述

客户端通信序列图如下:

在这里插入图片描述

Netty的IO线程NioEventLoop聚合了多路复用器Selector,可以同时并发处理成百上千个客户端Channel,由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致线程的挂起。另外,由于Netty采用了异步通信模式,一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞IO一连接一线程模型,架构性能、弹性伸缩能力和可靠性都得到了极大提升。

高性能之道 -->2 零拷贝

Netty的“零拷贝”主要体现在如下三个方面:
1)Netty的接收和发送ByteBuffer采用Direct Buffers,使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝。
如果使用传统的堆内存(Heap Buffers)进行Socket读写,JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2)Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个Buffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer。
3)Netty的文件传输采用了transferTo()方法,它可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环write()方式导致的内存拷贝问题。

下面,对以上三种‘“零拷贝”进行说明,先看Netty接收Buffer的创建。(异步消息读取“零拷贝”):
在这里插入图片描述
每循环读取一次消息,就通过ByteBufAllocator的ioBuffer方法获取ByteBuf对象,下面继续看它的接口定义。
ByteBufAllocator 通过ioBuffer分配堆外内存:

在这里插入图片描述
当进行Socket IO读写的时候,为了避免从堆内存拷贝一份副本到直接内存,Netty的ByteBuf分配器直接创建非堆内存避免缓冲区的二次拷贝,通过“零拷贝”来提升读写性能。

下面我们继续看第二种“零拷贝”的实现CompositeByteBuf,它对外将多个ByteBuf封装成一个ByteBuf,对外提供统一封装后的ByteBuf接口,它的类定义如下(CompositeByteBuf类继承关系)
在这里插入图片描述
通过继承关系我们可以看出CompositeByteBuf实际就是个ByteBuf的包装器,它将多个ByteBuf组合成一个集合,然后对外提供统一的ByteBuf接口,相关定义如下(CompositeByteBuf类定义):
在这里插入图片描述
添加ByteBuf,不需要做内存拷贝,相关代码如下(新增ByteBuf的“零拷贝”):

在这里插入图片描述
最后,我们看下文件传输的“零拷贝”:
在这里插入图片描述
Netty文件传输DefaultFileRegion通过transferTo方法将文件发送到目标Channel中,下面重点看FileChannel的transferTo方法,它的API DOC说明如下:
在这里插入图片描述
对于很多操作系统它直接将文件缓冲区的内容发送到目标Channel中,而不需要通过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。

高性能之道 -->3 内存池

随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作。但是对于缓冲区Buffer,情况稍有不同,特别是对于堆外直接内存的分配和回收,是一件耗时的操作。为了尽量重用缓冲区,Netty提供了基于内存池的缓冲区重用机制。下面看一下Netty ByteBuf的实现:
在这里插入图片描述

Netty提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。
下面通过性能测试,看一下基于内存池循环利用的ByteBuf和普通ByteBuf的性能差异。
eg1:使用内存池分配器创建直接内存缓冲区

在这里插入图片描述
eg2:使用非堆内存分配器创建的直接内存缓冲区

在这里插入图片描述
各执行300万次,性能对比结果如下所示:

在这里插入图片描述
性能测试经验表明,采用内存池的ByteBuf相比于相比于朝生夕死的ByteBuf,性能高23倍左右。
下面分析下netty内存池的内存分配:
在这里插入图片描述
继续看newDirectBuffer方法,我们发现它是一个抽象方法,由AbstractByteBufAllocator的子类负责具体实现,代码如下:
在这里插入图片描述
代码跳转到PooledByteBufAllocator的newDirectBuffer方法,从Cache中获取内存区域PoolArena,调用它的allocate方法进行内存分配:
在这里插入图片描述
PoolArena的allocate方法如下:
在这里插入图片描述
重点分析newByteBuf的实现,它同样是个抽象方法,由子类DirectArena和HeapArena来实现不同类型的缓冲区分配,由于测试用例使用的是堆外内存:
在这里插入图片描述
因此重点分析DirectArena的实现:如果没有开启使用sun的unsafe,则:

在这里插入图片描述
执行PooledDirectByteBuf的newInstance方法,代码如下:

在这里插入图片描述
通过RECYCLER的get方法循环使用ByteBuf对象,如果是非内存池实现,则直接创建一个新的ByteBuf对象。从缓冲池中获取ByteBuf之后,调用AbstractReferenceCountedByteBuf的setRefCnt方法设置引用计数器,用于对象的引用计数和内存回收(类似JVM垃圾回收机制)。

高性能之道 -->4 高效的Reactor线程模型

常用的Reactor线程模型有3种,分别如下:

1)Reactor单线程模型
2)Reactor多线程模型
3)主从Reactor多线程模型

Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上完成,NIO线程的职责如下:
1)作为NIO的客户端,向服务端发起TCP连接;
2)作为NIO的服务端,接收客户端的TCP连接;
3)读取通信对端的请求或者应答消息;
4)向通信对端发送消息或者应答消息;
Reactor单线程模型如下:

在这里插入图片描述

由于Reactor使用的是异步非阻塞IO,所有的IO操作都不会阻塞,理论上一个线程可以处理所有IO相关的操作。从架构层面看,一个NIO线程确实可以完成其承担的职责。例如,通过Acceptor接收客户端TCP连接请求消息,链路建立成功后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上进行消息解码。用户Handler可以通过NIO线程将消息发送给客户端。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下:
1)一个NIO线程同时处理成百上千的链路,性能无法支撑,即便是NIO的线程CPU负荷达到100%,也无法满足海量消息的编码、解码。读取和发送。
2)当NIO线程的负载过重之后,处理速度将变慢,这会导致大量的客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会有大量消息积压和处理超时,NIO线程会成为系统的瓶颈。
3)可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,衍生了Reactor多线程模型。
Reactor多线程模型与单线程模型最大的区别就是一组NIO线程处理IO操作,它的原理图如下:

在这里插入图片描述

Reactor多线程模型的特点:
1)有专门的一个NIO线程(Acceptor线程)用于监听服务端,接收客户端的TCP连接请求;
2)网络IO操作-读、写等由一个NIO线程池负责,线程池采用标准的JDK线程池实现,它包含一个任务队列和N个可用线程,由这些NIO线程负责消息的读取、解码、编码和发送;
3)1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor多线程模型可以满足性能要求;但是在特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这种场景下,单独一个Acceptor线程可能会存在性能不足的问题,为了解决性能问题,产生了第三种Reactor线程模型—>主从Reactor多线程模型。

主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端的TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub Reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。
它的线程模型如下图所示:

在这里插入图片描述

利用主从NIO线程模型,可以解决1个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在netty的官方demo中,推荐使用该线程模型。
事实上,Netty的线程模型并非固定不变,通过在启动辅助类中不同的EventLoopGroup实例并通过适当的参数配置,就可以支持上述3种Reactor线程模型。

高性能之道 -->5 无锁化的串行设计理念

大多数场景下,并行多线程处理可以提升系统的并发性能。但是,如果对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会导致性能的下降。为尽可能的避免锁竞争带来的性能损耗,可以通过串行化设计,即消息的处理尽可能在同一个县城内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
为尽可能提升性能,Netty采用了串行无锁化设计,在IO线程内部进行串行操作,避免多线程竞争导致的性能下降。表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并运行,这种局部无锁化的串行线程设计相比一个队列-多个工作线程模型性能更优。
Netty的串行化设计工作原理如下:

在这里插入图片描述
Netty的NioEventLoop读取到消息后,直接调用ChannelPipeline的fireChannelRead(Object msg),只要用户不主动切换线程,一直会由NioEventLoop调用到用户的Handler,期间不进行线程切换,这种串行化处理方式避免了多线程操作导致的锁的竞争,从性能角度看最优。

高性能之道 -->6 高效的并发编程

Netty的高效并发编程,主要体现如下几点:
1)volatile的大量、正确使用;
2)CAS和原子类的广泛使用;
3)线程安全容器的使用;
4)通过读写锁提升并发性能;

高性能之道 -->7 高性能的序列化框架

影响序列化性能的关键因素总结如下:
1)序列化后的码流大小(网络带宽的占用)
2)序列化&反序列化的性能(CPU资源的占用)
3)是否支持跨语言(异构系统的对接和开发语言切换)
Netty默认使用Google Protobuf的支持,通过扩展Netty的编解码接口,用户可以实现其它高性能序列化框架。例如Thrift的压缩二进制编解码框架。下面我们看一下不同序列化&反序列化后的字节数组对比:

在这里插入图片描述
从上图可以看出,Protobuf序列化之后的码流只有java序列化的1/4左右。正是由于Java原生序列化性能表现太差,才催生出各种高性能的开源序列化技术和框架。

高性能之道 -->8 灵活的TCP参数配置能力

合理设置TCP参数在某些场景下对于性能的提升可以起到显著效果。例如SO_RECBUF和 SO_SNDBUF。如果设置不当,对性能的影响是非常大的。我们总结以下对性能影响比较大的几个配置项;
1)SO_RECBUF和 SO_SNDBUF:通常建议值为128k或者256k;
2)SO_TCPNODELAY:NAGLE算法通过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提高网络的应用效率。但是对于时延敏感的应用场景需要关闭该优化算法;
3)软中断:如果Linux内核版本支持RPS,开启RPS后可以实现软中断,提升网络吞吐量。RPS根据数据包的源地址、目的地址以及目的和源端口,计算出一个hash值,然后根据这个hash值选择软中断运行的CPU,从上层来看,也就是将每个连接和CPU绑定,并通过这个hash值,来均衡软中断在多个CPU上,提升网络并行处理性能。

Netty在启动类中可以灵活的配置TCP参数,满足不同的用户场景。相关配置接口定义如下:

在这里插入图片描述
Netty之ChannelOption的各种参数:

ChannelOption.SO_BACKLOG, 1024

BACKLOG用于构造服务端套接字ServerSocket对象,标识当服务器请求处理线程全满时,用于临时存放已完成三次握手的请求的队列的最大长度。如果未设置或所设置的值小于1,Java将使用默认值50。  

ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小

ChannelOption.SO_KEEPALIVE, true

  是否启用心跳保活机制。在双方TCP套接字建立连接后(即都进入ESTABLISHED状态)并且在两个小时左右上层没有任何数据传输的情况下,这套机制才会被激活。

ChannelOption.TCP_NODELAY, true

在TCP/IP协议中,无论发送多少数据,总是要在数据前面加上协议头,同时,对方接收到数据,也需要发送ACK表示确认。为了尽可能的利用网络带宽,TCP总是希望尽可能的发送足够大的数据。这里就涉及到一个名为Nagle的算法,该算法的目的就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。

TCP_NODELAY就是用于启用或关于Nagle算法。如果要求高实时性,有数据发送时就马上发送,就将该选项设置为true关闭Nagle算法;如果要减少发送次数减少网络交互,就设置为false等累积一定大小后再发送。默认为false。

ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。

Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到了,组装成大的数据包进行发送,虽然该方式有效提高网络的有效

负载,但是却造成了延时,而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输,于TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送

数据,适用于文件传输。

ChannelOption.SO_REUSEADDR, true

SO_REUSEADDR允许启动一个监听服务器并捆绑其众所周知端口,即使以前建立的将此端口用做他们的本地端口的连接仍存在。这通常是重启监听服务器时出现,若不设置此选项,则bind时将出错。
SO_REUSEADDR允许在同一端口上启动同一服务器的多个实例,只要每个实例捆绑一个不同的本地IP地址即可。对于TCP,我们根本不可能启动捆绑相同IP地址和相同端口号的多个服务器。
SO_REUSEADDR允许单个进程捆绑同一端口到多个套接口上,只要每个捆绑指定不同的本地IP地址即可。这一般不用于TCP服务器。
SO_REUSEADDR允许完全重复的捆绑:当一个IP地址和端口绑定到某个套接口上时,还允许此IP地址和端口捆绑到另一个套接口上。一般来说,这个特性仅在支持多播的系统上才有,而且只对UDP套接口而言(TCP不支持多播)

ChannelOption.SO_RCVBUF && ChannelOption.SO_SNDBUF
定义接收或者传输的系统缓冲区buf的大小,

ChannelOption.ALLOCATOR

Netty4使用对象池,重用缓冲区
bootstrap.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
bootstrap.childOption(ChannelOption.ALLOCATOR,PooledByteBufAllocator.DEFAULT);

EpollChannelOption.SO_REUSEPORT, true

插曲

前几天一个技术群里的小伙伴,

** 问netty中.childOption(ChannelOption.SO_RCVBUF, XX) 设置了没有效果, 抓包显示TCP的滑动窗口依然我行我素,**
我把我当时的回答再稍加修饰润色一下:

第一个误区就是: 

TCP在三次握手建立连接期间就会通过ACK分组通告自己的初始接收窗口(通告窗口)大小,

而上边的代码是netty在连接建立成功后才设置的, 所以必然是无效的设置,

正确的方法是设置到ServerSocket上, 也就是Option(Option.SO_RCVBUF, XX),

一个连接被ServerSocket accept后会clone一个此连接对应的socket, 这个值会继承过来.

还有一个误区就是: 

其实并不是rcv_buf设置多大, 通告窗口就多大的, 他们之间有非比寻常的关系,

但绝对不是一一对应的关系, TCP是一种慢启动的协议, linux2.6.39版本之前, 

在以太网环境中初始通告窗口是的3个MSS(MSS即最大的segment size, 以太网环境中是1460个字 节) 
然后根据拥塞避免算法一点一点增加,

 3.x内核初始通告窗口是直接在代码中写死的10个MSS (是google一篇论文的建议). 

第三点是,

 我担心有人会这么想, 强调下recv_buf并不是个数组啥的(内核buf的数据结构大致是 

一个segment queue), 也不会预先分配内存, 只是个接收缓冲区size的最大限制, 对端不给 
你发数据, 内核不会自作多情分配内存给你, 要不然现在动辄单机上百万个长连接就是痴人说梦 
了 

总结: 通常情况下, 我个人经验是不建议设置rcv_buf, linux内核会对每一个连接做动态的 
调整, 一般情况下足够智能, 如果设置死了, 就失去了这个特性, 尤其是大量长连接的应用, 
我觉得这个设置就忘记吧, 要调优, 也最好到linux内核里面去配置对应参数. 

2.6.32内核代码(/net/ipv4/tcp_input.c):

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Netty框架的基本构造是ChannelHandler和Future。ChannelHandler是Netty提供的抽象,用于处理入站和出站事件。它是实现高性能网络应用程序的关键要素,Netty框架提供了大量开箱即用的ChannelHandler,如Http、SSL/TLS等。 同时,Future也是Netty框架的关键构件之一,它与ChannelHandler相互补充。Future用于异步执行操作,并返回操作的结果或状态。通过Future,我们可以实现非阻塞的网络通信,提高系统的并发性能和响应速度。 总之,Netty框架基于ChannelHandler和Future这两个关键构件,提供了高性能的网络应用程序框架,同时实现了分布式应用程序之间的通信。在实际使用中,可以通过使用Netty的各种ChannelHandler和Future来构建和扩展自己的网络应用程序。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Netty框架](https://blog.csdn.net/xing_jian1/article/details/128319462)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [dubbo协议、netty框架总结](https://download.csdn.net/download/qq_40585384/87996544)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值