Java网络编程(6) - Netty高性能体现,Netty为什么快?高性能的三大要素?Netty的高能性在什么地方?Netty的线程模型?Netty的零拷贝技术是怎么样?Netty内存池是怎么样的?

Netty高性能体现

高性能体现三大要素

      1、传输:用什么样的通道传输数据给对方,BIO、NIO、AIO,IO模型在很大程度上决定了框架性能。

      2、协议:采用什么样的通信协议,Http或内部私有协议。协议选择的不同,性能模型也不同。相比于公有协议,内部私有协议的性能通常可以被设计的更优。

      3、线程:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,Reactor线程模型的不同,对性能的影响也很大。

 

Netty高性能体现-IO模型

      Netty的IO模型基于非阻塞IO的实现,底层依赖的是JDK NIO框架的Selector。

      Netty的非阻塞IO实现关键是基于IO复用模型,在IO编程过程中,但需要同时处理多个客户端接入请求时,可以利用多线程或IO多路复用技术进行处理。

      IO多路复用技术会把多个IO阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下,可以同时处理多个客户端的请求。

      与传统的多线程/多进程模型相比,IO多路复用的最大优势在于系统开销低,系统不需要创建新的额外进程或现场,也吧需要维护这些进程和线程,降低了系统的维护工作量,节省了系统资源。

      Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成千上百个客户端连接。当线程从某个客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

      线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。

      由于读写都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致线程挂起,一个IO线程可以并发处理N个客户端链接和读写操作,这从根本上解决了传统同步阻塞IO一链接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

      对应JDK的NIO API的Socket和ServerSocket类,Netty也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。

阻塞模式使用非常简单,但是性能和可靠性都不好。非阻塞模式正好相反。

      同步阻塞IO:降低编程复杂度,要求是低负载、低并发的应用。

      非阻塞IO:编程复杂度提高,要求是高负载、高并发的应用。

 

Netty高性能体现-线程模型

      数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能影响也非常大。

     

     通常一个事件处理模型的程序有两种设计思路:

      轮询方式:线程不断轮询访问相关事件发生源有没有发生事件,有就调用事件处理逻辑。

      事件驱动方式:在发生事件时,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,根据事件类型,调用事件对应的处理逻辑处理事件。事件驱动方式也称为消息通知方式,也是设计模式中的观察者思路。

      对比:相对于传统的轮询方式,事件驱动方式有这些优点:1、可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑;2、高性能,基于队列暂存事件,能方便并行异步处理事件。

 

     事件驱动方式主要把包括四个基本组件:

      事件队列(Event Queue):接收事件的入口存储待处理的事件。

      分发器(Event Mediator):将不同的事件分发到不同的业务逻辑单元。

      事件通道(Event Channel):分发器与处理器之间的联系通道。

      事件处理器(Event Processor):实现业务逻辑,处理完成后发出事件,触发下一步操作。

 

Reactor线程模型

      Reactor是反应堆的意思,这个模型是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。即服务端程序处理传入的多路请求,并将它们同步分派给请求对应的处理线程。

      Reactor模式也叫作Dispatcher模式,即IO多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的技术之一。

 

      Reactor模型有两个关键组成:

      Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应,类似公司的电话接线员,接听到客户的电话转发给适当的联系人。

      Handlers处理程序执行IO事件要完成的实际事件,类似于客户想要与交谈的公司中的实际人员。Reactor通过调度适当的处理程序响应IO事件,处理程序执行非阻塞操作。

      如上图:Reactor模式是事件驱动的,有一个或多个并发输入源,有一个ServiceHandler,有多个RequestHandlers。

      ServerHandler会同步的将输入的请求(Event)多路复用的分发给相应的RequestHandlers。

      从结构上,这有点类似生产者消费者模式,既有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll出这个事件来处理。而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到ServiceHandler之后,该ServiceHandler会立刻的根据不同的Event类型将其分发给对应的RequestHandler来处理。

      如下图:这样做的好处在于,可以将处理Event的RequestHandler实现一个单独的线程。

      这样ServiceHandler和RequestHandler实现了异步,加快了ServiceHandler处理Event的速度,那么每一个Reques同样也可以多线程的形式来处理自己的Event,即Thread1扩展成ThreadPool1。

 

Reactor线程模型 – 单线程

      所有的IO操作都在同一个NIO线程上面完成,即多路复用、事件分发和处理都在其上完成。既要接收客户端的链接请求,向服务端发起连接,又要发送、响应消息。

即一个线程(单线程)来处理Connect事件(Acceptor),一个线程池(多线程)来处理read,一个线程(线程池)来处理write,那么从Reactor Thread到Handler都是异步的,从而IO操作也多线程化。

一个NIO县城同时处理成百上千的链路,性能上无法支撑,速度慢,若薪酬进入死循环,整个程序就不可用了,对于高负载,大并发的应用场景不合适。但对于一些小容量应用场景,可以使用单独线程模型。

      这里跟BIO对比以及提升了很大的性能,但是还可以提升的,由于TeactorThread依然为单线程,从性能上考虑依然有所限制。

Reactor线程模型 – 多线程

      有一个NIO线程(Acceptor)只负责监听服务端,接收客户端的TCP连接请求。NIO线程池负责网络IO操作,即消息的读、解码、编码、发送。一个NIO线程可以同时处理N条链路,但是一条链路只对应一个NIO线程,这是为了防止发生并发操作的问题,但在并发百万客户端链接或需要安全链接时,一个Acceptor线程可能会存在性能不足的问题。

      多线程与单线程模型最大的区别就是有一组NIO线程处理IO操作,即线程池提高了Event的分发能力。主要是用于高并发、大业务场景。

Reactor线程模型 – 主从

      主从Reactor线程模型的特点是服务端用于接收客户端链接不再是单个单独的NIO线程,而是一个独立的IO线程池。利用NIO线程模型可以解决1个服务端监听线程无法有效处理所有客户端链接的性能不足问题。

      Acceptor线程用于绑定监听端口,接收客户端链接,将SocketChannel从主线程池的Reactor线程的多路复用器上移除,重新注册到Sub线程池的线程上,用于处理IO的读写等操作,从而保证mainReactor只负责接入认证、握手等操作。

Netty所属的线程模型

      Netty是基于主从Reactors多线程模型做了一些修改,其中的Reactor多线程模型有多个Reactor:MainReactor和SubReactor。

      MainReactor负责客户端的链接请求,并将请求转发给SubReactor。

      SubReactor负责相应通道的IO读写请求。

      非IO请求(具体业务逻辑处理):这种任务则会直接写入队列,等待Worker Threads进行处理。

      注:虽然Netty的线程模型基于主从Reactor多线程模型,借用了MainReactor和SubReactor的结构,但是实际上,SubReactor和Worker线程在同一个线程池。

//这个是服务端用来接收客户端的链接的

         EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup();

        

         //这个是用来与客户端进行网络通信的(网络读写)

         EventLoopGroup workerEventLoopGroup = new NioEventLoopGroup();

        

         //启动服务辅助工具类,用于服务器的启动一系列性相关配置参数

         ServerBootstrap serverBootstrap = new ServerBootstrap();

         serverBootstrap.group(connectEventLoopGroup, dataEventLoopGroup) //绑定两个线程组

 

      bossEventLoopGroup和workerEventLoopGroup是Bootstrap构造方法传入的两个对象,这连个group都是线程池。

      connectEventLoopGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,每个端口对应一个Boss线程

      workerGroup线程池会被各个SubReactor和Worker线程充分利用。

 

Netty高性能体现-基于Buffer

      缓冲区就是内存的一块区域,把数据先存储到内存,然后一次性写入,类似数据库的批量操作,这样大大的提高了数据的读写速度。

传统的IO是面向字节流或字符流的,以流的方式顺序的从一个Stream中读取一个或多个字节,因此不能随意改变读取指针的位置。

      在NIO中,抛弃了传统的IO流,而是引入了Channel和Buffer的概念,只能从Channel中读取数据到Buffer中火将数据Buffer中写入到Channel。

      基于Buffer操作不像传统IO的顺序操作,NIO可以随意读取任意位置的数据。

 

Netty高性能体现-零拷贝

为什么要零拷贝技术?

      当数据写操作的系统调用指令发出,操作系统通常会将数据从应用程序地址空间的缓冲区拷贝到操作系统内核的缓冲区去。这样做的好处接口简单,但是却在很大程度上损失了系统性能,这种数据拷贝不但需要占用CPU时间片,同时也需要占用额外的内存带宽。

      对于Liunx在于这个数据拷贝操作过程中的数据损耗,主要在于基于数据排序或者校验等各方面因素的考虑,操作系统内核会在处理传输的过程中进行多次拷贝操作,某些情况下这些数据拷贝操作会极大的降低数据传输性能。

      毕竟在传统的数据传输过程中,即便使用了DMA来进行与硬件通讯,CPU仍然需要访问数据两次;在读数据的过程中数据不是直接来自硬盘的,而是必须先经过操作系统的文件系统层;在写过程中,为了和要传输的数据包大小吻合,数据必须要先被分割成块,而且还要预先考虑包头,并且要进行数据校验和操作。

 

什么是零拷贝技术?

      零拷贝技术就是一种避免CPU将数据从一块存储拷贝到另外一块存储的技术,在数据考虑的同时,允许CPU执行其他任务来实现。即减少数据拷贝和共享总线操作的次数,消除传输数据在存储器之间不必要的中间拷贝次数,从而有效的提高数据传输效率。

      说白了就是在传输文件时,不需要将文件内容拷贝到用户空间,而是直接在内核空间中传输到网络的方式,避免了用户空间和内存空间之间的拷贝,从而提升了系统的整体性能。

      避开:

避免操作系统内核缓冲区之间的数据拷贝。

      避免数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间进行拷贝。

      避开不需要的系统调用和上下文切换。

 

      需要:

      需要拷贝的数据可以先被缓存起来。

      需要尽量对数据进行处理的操作给硬件做。

      需要尽量把数据传输交给DMA(Direct Memory Access,直接存储器访问)来组。

 

Netty中的零拷贝技术

      1、Netty接收和发送ByteBuffer采用Direct Buffers(直接缓冲),使用对外直接内存进行Socket读写(JVM篇),不需要进行字节缓冲区的二次拷贝,如果使用传统的堆内存(Heap Buffers)进行Soket读写,JVM会将对内存Buffer拷贝一份到直接内存中,然后才写入Socket中。相比堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。

      2、Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象,用户可以像操作一个BUffer那样方便的对组合Buffer进行操作,避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大Buffer。

      3、Netty的文件传输采用了transferTo方法,可以直接将文件缓冲区的数据发送到目标Channel,避免了传统通过循环wirte方式导致的内存拷贝问题。

 

Netty高性能-内存池

为什么要使用内存池?

      随着JVM虚拟机和JIT即时编译技术的发展,对象的分配和回收是个非常轻量级的工作,但是对于缓冲区Buffer,情况稍微有点不同,特别是对于对外直接内存的分配和回收是一件非常耗时的操作。

      而且这些实例随着消息的处理朝生夕灭,这会给服务器带来沉重的GC压力,同时消耗大量的内存。

      为了避免尽量重用缓冲区,Netty提供了急于内存池的缓冲区重用机制。公开的测试数据中,采用内存池的ByteBuf相比于朝生夕灭的ByteBuf,性能高23倍左右。

 

Netty中的内存池

      在Netty4或Netty5中实现了一个新的ByteBuf内存池,这是一个纯Jva版本的jemalloc(Facebook也在用)。现在Netty不会在因为用零填充缓冲区而浪费内存带宽。

      不过由于这个是不依赖于GC,开发人员需要非常小心内存泄露的问题,如果忘记在处理程序中释放缓冲区,那么内存使用率会无限的增长。

      Netty默认是不使用内存池的,需要在创建客户端或者服务端的时候,在引导辅助类中配置。

 

Netty内存池分类

      Netty的ByteBuf缓冲区的种类分为支持堆缓冲区和堆外直接缓冲区,一般来说,底层IO处理线程的缓冲区使用对外直接缓冲区,减少一次IO复制。业务消息的编解码使用堆缓冲区,分配效率更高,而且不设计到内核缓冲区的复制问题。

      ByteBuf的堆缓冲区又分为内存池缓冲区PooledByteBuf和普通内存缓冲区UnpooledHeapByteBuf。

PooledByteBuf采用二叉树来实现一个内存池,集中管理内存分配和释放,不用每次都新建一个缓冲区对象。

UnpooledHeapByteBuf可以节约内存的分配,在性能上能够保证的情况下,可以使用UnpooledHeapByteBuf,实现比较简单。

如果只是为了将数据写入ByteBuf中并发送出去,那么应该直接使用对外直接缓冲区DirectBuffer。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值