Netty权威指北

本文主要是对网络IO模型以及Netty原理总结。

Netty是由JBOSS提供的一个java开源框架,现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于NIO的客户、服务器端的编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

“快速”和“简单”并不用产生维护性或性能上的问题。Netty 是一个吸收了多种协议(包括FTP、SMTP、HTTP等各种二进制文本协议)的实现经验,并经过相当精心设计的项目。最终,Netty 成功的找到了一种方式,在保证易于开发的同时还保证了其应用的性能,稳定性和伸缩性。

在了解Netty之前,有必要先了解网络IO模型的基础知识。

网络IO模型

常见I/O模型

1.阻塞I/O(BIO + 线程池)
2.非阻塞I/O (NIO)

用户进程需要不断的主动询问kernel数据好了没有。因为需要不断地轮询,这消耗了大量的CPU的资源。 一般不采用

3) I/O复用(select,poll,epoll) (I/O multiplexing)

当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

select:
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

  1. 单个进程可监视的fd数量被限制,即能监听端口的大小有限。
    一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

  2. 每次select()遍历所有FD, IO效率会因为FD增加而下降:
    当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

  3. 需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll:
poll本质上和select没有区别,它没有最大连接数的限制,原因是它是基于链表来存储的。但是同样有以下缺点:

  1. 遍历所有fd, IO效率会因为FD增加而下降

  2. 大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

  3. poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

select和poll缺点在于当同时检查大量文件描述符时性能延展性不佳。

epoll:

1.FD数量不受限制(链表)

2.IO效率不会因为FD增加而下降(扫描活跃fd)

3.使用mmap与用户空间用同一块内存,减少复制

epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就需态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

Java从1.4开始提供了NIO工具包,支持用 I/O复用模型来进行网络编程。

img

4)信号驱动I/O (SIGIO)
5)异步I/O (AIO) JDK7加入的NIO2.0支持AIO,但性能在linux并没比epoll强,被抛弃。

应用进程与系统内核(等待数据准备、将数据拷贝到应用进程)

screenshot

Reactor模型是一种事件驱动设计模式,epoll是具体实现。

(一个主线程接收请求,多线程处理IO)

Proactor采用全异步AIO完成。

Netty:

为什么使用Netty

分布式系统中,各个节点之间需要远程服务调用,高性能的PRC框架必不可少,Netty作为异步高性能通信框架,往往作为基础通信组件被这些RPC框架使用。

为什么使用Netty而不直接使用Nio?

  • NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等
  • 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序
  • 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等
  • JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决

Netty的优点
Netty 对 JDK 自带的 NIO 的 API 进行了封装,解决了上述问题

  • 设计优雅:适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池
  • 使用方便:详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。
  • 高性能、吞吐量更高:延迟更低;减少资源消耗;最小化不必要的内存复制。
  • 安全:完整的 SSL/TLS 和 StartTLS 支持。
  • 社区活跃、不断更新:社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入

IO线程模型

1.Reactor单线程模型

所有IO操作都在一个NIO线程上完成。

Acceptor类接收连接,连接成后由Dispatcher分发到指定handler。

2.Reactor多线程模型

专门一个Acceptor线程处理链接。

网络IO由一个线程池完成。

3.主从reactor多线程模型:

负责接收客户端连接的Acceptor线程池。(BossGroup)

负责网络读写的IO操作线程池subReactor。(WorkerGroup)

1、Netty抽象出两组线程池,BossGroup专门负责接收客户端的链接,WorkerGroup专门负责网络的读写
2、BossGroup和WorkerGroup类型都是NioEventLooGroup
3、NioEventLoogGroup相当于事件循环组,这个组中含有多个事件循环,每个事件循环是NioEventLoop
4、NioEventLoopGroup可以有多个线程,即可以含有多个NioEventLoop
5、NioEventLoop表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个Selector,用于监听绑定在其上的socket的网络通讯
6、每个Boss NioEventLoop循环执行的步骤有三步

1)轮询accept事件
2)处理accept事件,与client建立链接,生成NioSocketChannel,并将其注册到某个worker NioEventLoop上的Selector
3)处理任务队列的任务,即runAllTasks

7、每个Worker NioEventLoop循环执行步骤

1)轮询read,write事件
2)处理I/O事件,即read/write事件,在对应NioSocketChannel处理
3)处理任务队列的任务,即runAllTasks

8、每个Worker NioEventLoop处理业务时,会使用pipeline(管道),pipeline中包含了channel

img

TCP粘包/拆包问题:

1.定长解码器 FixedLengthFrameDecoder

2.分隔符解码器 LineBasedFrameDecoder

3.消息头长度字段标识 DelimiterBasedFrameDecoder

序列化协议:

1.java serialize(无法跨语言、码流大、性能低)

2.json

3.protobuf

4.messagePack

4.hession

核心组件

包括了Selector,EventLoopGroup/EventLoop、ChannelPipeline,当然除了和模式相关的这三个组件,还有一个Buffer,到这里由和我们之前说的NIO实现的三大构成Selector,Channel,Buffer对上了,以下我们分别来看下这几样东西。

EventLoopGroup/EventLoop:

实际就是reactor线程池,负责调度执行客户端的接入、网络读写、定时任务.

EventLoop内部聚合多路复用器selector。

Netty采用了串行化设计理念,从消息的读取、编码以及后续Handler的执行,始终都由IO线程EventLoop负责,这就意外着整个流程不会进行线程上下文的切换,数据也不会面临被并发修改的风险。

Selector:

Selector即为NIO中提供的SelectableChannel多路复用器,充当着demultiplexer的角色,这里Netty基本使用了NIO中的Selector一套,封装了一个实现类SelectedSelectionKeySetSelector。不断轮询注册在其上的channel,channel就绪时会被轮询出来,通过selectionKey拿到就绪的channel集合。

(epoll bug处理: 检测周期内连续发生N次空轮询,则重建Selector恢复)

Channel:

网络数据通过channel读取和写入,双向,提供网络IO读写相关接口。 实际IO读写操作由UnSafe完成。

channel需要注册到多路复用器eventloop上。

ChannelPipeline:

本质是处理网络事件的职责链,负责管理执行channelhandler。担任着Reactor模式中的请求处理器这个角色。ChannelPipeline的默认实现是DefaultChannelPipeline,DefaultChannelPipeline本身维护着一个用户不可见的tail和head的ChannelHandler,他们分别位于链表队列的头部和尾部。tail在更上层的部分,而head在靠近网络层的方向。

在Netty中关于ChannelHandler有两个重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler。inbound可以理解为网络数据从外部流向系统内部,而outbound可以理解为网络数据从系统内部流向系统外部。用户实现的ChannelHandler可以根据需要实现其中一个或多个接口,将其放入Pipeline中的链表队列中,ChannelPipeline会根据不同的IO事件类型来找到相应的Handler来处理,同时链表队列是责任链模式的一种变种,自上而下或自下而上所有满足事件关联的Handler都会对事件进行处理。

ChannelInBoundHandler对从客户端发往服务器的报文进行处理,一般用来执行半包/粘包,解码,读取数据,业务处理等;ChannelOutBoundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码,发送报文到客户端。

ByteBuf:

Netty提供的经过扩展的Buffer相对NIO中的有个许多优势,作为数据存取非常重要的一块,我们来看看Netty中的Buffer有什么特点。

1)ByteBuf读写指针

在ByteBuffer中,读写指针都是position,而在ByteBuf中,读写指针分别为readerIndex和writerIndex,直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量,但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法,而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法。每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤,而且内容没有读完是不能写的,这样非常不灵活。相比之下我们看看ByteBuf,读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针,不需每次读写之前调用对应的方法,而且没有必须一次读完的限制。

2)零拷贝

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

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

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

3)引用计数与池化技术

在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源,因此为了获得对于内存的申请与回收更多的控制权,Netty自己根据引用计数法去实现了内存的管理。Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的,大大提高I/O操作的效率,然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点,即对于DirectBuffer的申请相比HeapBuffer效率更低,因此Netty结合引用计数实现了PolledBuffer,即池化的用法,当引用计数等于0的时候,Netty将Buffer回收致池中,在下一次申请Buffer的没某个时刻会被复用。

可靠性:

1.长连接。链路有效性检测,心跳检测

2.超时链路关闭

高性能:

1.IO模型。非阻塞IO

2.序列化协议

3.线程模型。 主从reactor,多路复用

坚持原创技术分享,您的支持将鼓励我继续创作!
分享到: