Netty

一个异步事件驱动的网络应用框架,用于快速开发高性能、可拓展协议的服务器和客户端。

Reactor

Reactor模式基于事件驱动,适合处理海量IO事件。

反应器设计模式:为处理服务请求并发提交到一个或多个服务处理程序的事件设计模式。请求到达后,服务器处理程序使用多路分配策略,然后同步的派发这些请求到相关的请求处理程序。

单线程模型

所有的IO操作,都在同一个NIO(同步非阻塞IO模型)完成,NIO线程的职责如下:

  • 作为NIO服务端,接收客户端的TCP连接
  • 作为NIO客户端,像服务端发起TCP连接。
  • 读取通信对端的请求或应答信息。
  • 想通信对端发送消息请求或应答消息。

采用异步非阻塞IO,IO操作不会导致阻塞。通过Acceptor接受客户端的tcp请求,通过Dispatch将对应的byte分发到指定的handler进行消息解码。

但是对于高负载、高并发的场景不合适。

多线程模型

与单线程最大的区别就是有一组NIO来处理线程的IO操作。

  • 一个专门的NIO线程监听Acceptor线程用于监听服务端,接受客户端的TCP连接请求。
  • 网络IO操作-读-写,由一个NIOP线程池负责,线程池可以采用标准的JDK线程池实现,他包含一个任务队列和N个可用的线程,有这些NIO线程负责消息的读取,编码解码和发送。
  • 一个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止并发操作问题。

主从多线程模型

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

工作流程总结如下:

  • 从主线程池中随机选择一个 Reactor 线程作为 Acceptor 线程,用于绑定监听端口,接收客户端连接;
  • Acceptor 线程接收客户端连接请求之后创建新的 SocketChannel ,将其注册到主线程池的其它 Reactor 线程上,由其负责接入认证、IP 黑白名单过滤、握手等操作;
  • 步骤 2 完成之后,业务层的链路正式建立,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上摘除,重新注册到 Sub 线程池的线程上,用于处理 I/O 的读写操作。

优势

  • 多路复用,在NIO的基础上进行更高层次的抽象,
  • 事件机制
  • 功能强大,预置了多种解码功能,支持主流协议。
  • 定制功能强,可以通过channelhandler对通信框架进行灵活的拓展。

为什么性能好?

  • 纯异步,Reactor线程模型
  • IO多路复用
  • GC优化
  • 内存泄露检测
  • zero copy

Zero copy

  • 将多个 ByteBuf 合并为一个逻辑上的 ByteBuf , 避免了各个 ByteBuf 之间的拷贝.
  • 通过 wrap 操作, 我们可以将 byte[] 数组、ByteBufByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作.
  • ByteBuf 支持 slice 操作, 因此可以将 ByteBuf 分解为多个共享同一个存储区域的 ByteBuf, 避免了内存的拷贝.
  • 通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel , 避免了传统通过循环 write 方式导致的内存拷贝问题.

垃圾回收

框架里HeapByteBuffer底下的byte[]能够依赖JVM自然回收;而DirectByteBuffer底下是java堆外内存,最好主动回收。所以Netyy要有自己的计数器和回收过程。

原生的JVM GC很难回收掉DirectByteBuffer占用的Native Memory

Netty 中采用引用计数对 DirectByteBuffer 进行对象可达性检测,当 DirectByteBuffer 上的引用计数为 0 时将对象释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public boolean release() {
for (;;) {
int refCnt = this.refCnt;
if (refCnt == 0) {
throw new IllegalReferenceCountException(0, -1);
}
if (refCntUpdater.compareAndSet(this, refCnt, refCnt - 1)) {
if (refCnt == 1) {
deallocate();
return true;
}
return false;
}
}
}

Netty 内存泄漏,主要是针对池化的 ByteBuf 。 ByteBuf 对象被 JVM GC 掉之前,没有调用 release() 把底下的 DirectByteBufferbyte[] 归还,会导致池越来越大。而非池化的 ByteBuf ,即使像 DirectByteBuf 那样可能会用到 System.gc() ,但终归会被 release 掉的,不会出大事。因此 Netty 默认会从分配的 ByteBuf 里抽样出大约 1% 的来进行跟踪。

源码

ByteBuffer

DirectBuffer vs HeapBuffer

在执行网络IO或者文件IO时,如果是使用 DirectBuffer 就会少一次内存拷贝。如果是非 DirectBuffer ,JDK 会先创建一个 DirectBuffer ,再去执行真正的写操作。这是因为,当我们把一个地址通过 JNI 传递给底层的C库的时候,有一个基本的要求,就是这个地址上的内容不能失效。然而,在 GC 管理下的对象是会在 Java 堆中移动的。也就是说,有可能我把一个地址传给底层的 write ,但是这段内存却因为 GC 整理内存而失效了。所以我必须要把待发送的数据放到一个 GC 管不着的地方。这就是调用 native 方法之前,数据一定要在堆外内存的原因。

启动以及链接建立过程

image

Epoll触发

水平触发(LT)和边缘触发(ET)

在LT模式下,只要某个fd还有数据没读完,那么下次轮询还会被选出。而在ET模式下,只有fd状态发生改变后,该fd才会被再次选出。ET模式的特殊性,使在ET模式下的一次轮询必须处理完本次轮询出的fd的所有数据,否则该fd将不会在下次轮询中被选出。

  • NIOChannel:水平触发
  • EpollChannel:边缘触发

补充

java Guide

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

请我喝杯咖啡吧~

支付宝
微信