大数据、分布式都用到了的Netty,这几大核心知识你必定要看看!

1. Netty 基础

Netty 是一个高性能、异步事件驱动的 NIO 框架,它提供了对 TCP、UDP 和文件传输的支持,做为一个异步 NIO 框架,Netty 的全部 IO 操做都是异步非阻塞的,经过 Future-Listener 机制,用户能够方便的主动获取或者经过通知机制得到 IO 操做结果。它是一个网路应用框架。react

2. Netty 高性能之道

2.1. RPC 调用的性能模型分析

2.1.1. 传统 RPC 调用性能差的三宗罪

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

下面,咱们经过 BIO 通讯模型图看下 BIO 通讯的弊端:算法

图2-1 BIO 通讯模型图编程

采用 BIO 通讯模型的服务端,一般由一个独立的 Acceptor 线程负责监听客户端的链接,接收到客户端链接以后为客户端链接建立一个新的线程处理请求消息,处理完成以后,返回应答消息给客户端,线程销毁,这就是典型的一请求一应答模型。该架构最大的问题就是不具有弹性伸缩能力,当并发访问量增长后,服务端的线程个数和并发访问数成线性正比,因为线程是 JAVA 虚拟机很是宝贵的系统资源,当线程数膨胀以后,系统的性能急剧降低,随着并发量的继续增长,可能会发生句柄溢出、线程堆栈溢出等问题,并致使服务器最终宕机。后端

序列化方式问题:Java 序列化存在以下几个典型问题:安全

  1. Java 序列化机制是 Java 内部的一种对象编解码技术,没法跨语言使用;例如对于异构系统之间的对接,Java 序列化后的码流须要可以经过其它语言反序列化成原始对象(副本),目前很难支持;服务器

  2. 相比于其它开源的序列化框架,Java 序列化后的码流太大,不管是网络传输仍是持久化到磁盘,都会致使额外的资源占用;网络

  3. 序列化性能差(CPU 资源占用高)。多线程

线程模型问题:因为采用同步阻塞 IO,这会致使每一个 TCP 链接都占用1个线程,因为线程资源是 JVM 虚拟机很是宝贵的资源,当 IO 读写阻塞致使线程没法及时释放时,会致使系统性能急剧降低,严重的甚至会致使虚拟机没法建立新的线程。架构

2.1.2. 高性能的三个主题

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

  2. 协议:采用什么样的通讯协议,HTTP 或者内部私有协议。协议的选择不一样,性能模型也不一样。相比于公有协议,内部私有协议的性能一般能够被设计的更优。

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

图2-2 RPC 调用性能三要素

2.2. Netty 高性能之道

2.2.1. 异步非阻塞通讯

在 IO 编程过程当中,当须要同时处理多个客户端接入请求时,能够利用多线程或者 IO 多路复用技术进行处理。IO 多路复用技术经过把多个 IO 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的状况下能够同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优点是系统开销小,系统不须要建立新的额外进程或者线程,也不须要维护这些进程和线程的运行,下降了系统的维护工做量,节省了系统资源。

JDK1.4 提供了对非阻塞 IO(NIO)的支持,JDK1.5_update10 版本使用 epoll 替代了传统的 select/poll,极大的提高了 NIO 通讯的性能。

JDK NIO 通讯模型以下所示:

图2-3 NIO 的多路复用模型图

与 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不一样的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用很是简单,可是性能和可靠性都很差,非阻塞模式正好相反。开发人员通常能够根据本身的须要来选择合适的模式,通常来讲,低负载、低并发的应用程序能够选择同步阻塞 IO 以下降编程复杂度。可是对于高负载、高并发的网络应用,须要使用 NIO 的非阻塞模式进行开发。

Netty 架构按照 Reactor 模式设计和实现,它的服务端通讯序列图以下:

图2-3 NIO 服务端通讯序列图

客户端通讯序列图以下:

图2-4 NIO 客户端通讯序列图

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

2.2.2. 零拷贝技术

不少用户都据说过 Netty 具备“零拷贝”功能,可是具体体如今哪里又说不清楚,本小节就详细对 Netty 的“零拷贝”功能进行讲解。

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 的建立:

图2-5 异步消息读取“零拷贝”

每循环读取一次消息,就经过 ByteBufAllocator的ioBuffer 方法获取 ByteBuf 对象,下面继续看它的接口定义:

图2-6 ByteBufAllocator 经过 ioBuffer 分配堆外内存

当进行 Socket IO 读写的时候,为了不从堆内存拷贝一份副本到直接内存,Netty 的 ByteBuf 分配器直接建立非堆内存避免缓冲区的二次拷贝,经过“零拷贝”来提高读写性能。

下面咱们继续看第二种“零拷贝”的实现 CompositeByteBuf,它对外将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口,它的类定义以下:

图2-7 CompositeByteBuf 类继承关系

经过继承关系咱们能够看出 CompositeByteBuf 实际就是个 ByteBuf 的包装器,它将多个 ByteBuf 组合成一个集合,而后对外提供统一的 ByteBuf 接口,相关定义以下:

图2-8 CompositeByteBuf 类定义

添加 ByteBuf,不须要作内存拷贝,相关代码以下:

图2-9 新增 ByteBuf 的“零拷贝”

最后,咱们看下文件传输的“零拷贝”:

图2-10 文件传输“零拷贝”

Netty 文件传输 DefaultFileRegion 经过 transferTo 方法将文件发送到目标 Channel 中,下面重点看 FileChannel 的 transferTo 方法,它的 API DOC 说明以下:

图2-11 文件传输 “零拷贝”

对于不少操做系统它直接将文件缓冲区的内容发送到目标 Channel 中,而不须要经过拷贝的方式,这是一种更加高效的传输方式,它实现了文件传输的“零拷贝”。

2.2.3. 内存池

随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是个很是轻量级的工做。可是对于缓冲区 Buffer,状况却稍有不一样,特别是对于堆外直接内存的分配和回收,是一件耗时的操做。为了尽可能重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。下面咱们一块儿看下 Netty ByteBuf 的实现:

图2-12 内存池 ByteBuf

Netty 提供了多种内存管理策略,经过在启动辅助类中配置相关参数,能够实现差别化的定制。

下面经过性能测试,咱们看下基于内存池循环利用的 ByteBuf 和普通 ByteBuf 的性能差别。

用例一,使用内存池分配器建立直接内存缓冲区:

图2-13 基于内存池的非堆内存缓冲区测试用例

用例二,使用非堆内存分配器建立的直接内存缓冲区:

图2-14 基于非内存池建立的非堆内存缓冲区测试用例

各执行300万次,性能对比结果以下所示:

图2-15 内存池和非内存池缓冲区写入性能对比

性能测试代表,采用内存池的 ByteBuf 相比于朝生夕灭的 ByteBuf,性能高23倍左右(性能数据与使用场景强相关)。

下面咱们一块儿简单分析下 Netty 内存池的内存分配:

图2-16 AbstractByteBufAllocator 的缓冲区分配

继续看 newDirectBuffer 方法,咱们发现它是一个抽象方法,由 AbstractByteBufAllocator 的子类负责具体实现,代码以下:

图2-17 newDirectBuffer 的不一样实现

代码跳转到 PooledByteBufAllocator 的 newDirectBuffer 方法,从 Cache 中获取内存区域 PoolArena,调用它的 allocate 方法进行内存分配:

图2-18 PooledByteBufAllocator 的内存分配

PoolArena 的 allocate 方法以下:

图2-18 PoolArena 的缓冲区分配

咱们重点分析 newByteBuf 的实现,它一样是个抽象方法,由子类 DirectArena 和 HeapArena 来实现不一样类型的缓冲区分配,因为测试用例使用的是堆外内存,

图2-19 PoolArena 的 newByteBuf 抽象方法

所以重点分析 DirectArena 的实现:若是没有开启使用 sun 的 unsafe,则

图2-20 DirectArena 的 newByteBuf 方法实现

执行 PooledDirectByteBuf 的 newInstance 方法,代码以下:

图2-21 PooledDirectByteBuf 的 newInstance 方法实现

经过 RECYCLER 的 get 方法循环使用 ByteBuf 对象,若是是非内存池实现,则直接建立一个新的 ByteBuf 对象。从缓冲池中获取 ByteBuf 以后,调用 AbstractReferenceCountedByteBuf的setRefCnt 方法设置引用计数器,用于对象的引用计数和内存回收(相似 JVM 垃圾回收机制)。

2.2.4. 高效的 Reactor 线程模型(重点)

经常使用的 Reactor 线程模型有三种,分别以下:

  1. Reactor 单线程模型;

  2. Reactor 多线程模型;

  3. 主从 Reactor 多线程模型(很重点)

Reactor 单线程模型,指的是全部的 IO 操做都在同一个 NIO 线程上面完成,NIO 线程的职责以下:

  1. 做为 NIO 服务端,接收客户端的 TCP 链接;

  2. 做为 NIO 客户端,向服务端发起 TCP 链接;

  3. 读取通讯对端的请求或者应答消息;

  4. 向通讯对端发送消息请求或者应答消息。

Reactor 单线程模型示意图以下所示:

图2-22 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 多线程模型。

Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操做,它的原理图以下:

图2-23 Reactor 多线程模型

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 操做。

它的线程模型以下图所示:

图2-24 Reactor 主从多线程模型

利用主从 NIO 线程模型,能够解决1个服务端监听线程没法有效处理全部客户端链接的性能不足问题。所以,在 Netty 的官方 demo 中,推荐使用该线程模型。

事实上,Netty 的线程模型并不是固定不变,经过在启动辅助类中建立不一样的 EventLoopGroup 实例并经过适当的参数配置,就能够支持上述三种 Reactor 线程模型。正是由于 Netty 对 Reactor 线程模型的支持提供了灵活的定制能力,因此能够知足不一样业务场景的性能诉求。

2.2.5. 无锁化的串行设计理念

在大多数场景下,并行多线程处理能够提高系统的并发性能。可是,若是对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会致使性能的降低。为了尽量的避免锁竞争带来的性能损耗,能够经过串行化设计,即消息的处理尽量在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。

为了尽量提高性能,Netty 采用了串行无锁化设计,在 IO 线程内部进行串行操做,避免多线程竞争致使的性能降低。表面上看,串行化设计彷佛 CPU 利用率不高,并发程度不够。可是,经过调整 NIO 线程池的线程参数,能够同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列-多个工做线程模型性能更优。

Netty 的串行化设计工做原理图以下:

图2-25 Netty 串行化工做原理图

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

2.2.6 灵活的 TCP 参数配置能力

合理设置 TCP 参数在某些场景下对于性能的提高能够起到显著的效果,例如 SO_RCVBUF 和 SO_SNDBUF。若是设置不当,对性能的影响是很是大的。下面咱们总结下对性能影响比较大的几个配置项:

  1. SO_RCVBUF 和 SO_SNDBUF:一般建议值为 128 K 或者 256 K;

  2. SO_TCPNODELAY:NAGLE 算法经过将缓冲区内的小封包自动相连,组成较大的封包,阻止大量小封包的发送阻塞网络,从而提升网络应用效率。可是对于时延敏感的应用场景须要关闭该优化算法;

  3. 软中断:若是 Linux 内核版本支持 RPS(2.6.35以上版本),开启 RPS 后能够实现软中断,提高网络吞吐量。RPS 根据数据包的源地址,目的地址以及目的和源端口,计算出一个 hash值,而后根据这个 hash 值来选择软中断运行的 cpu,从上层来看,也就是说将每一个链接和 cpu 绑定,并经过这个 hash 值,来均衡软中断在多个 cpu 上,提高网络并行处理性能。

Netty 在启动辅助类中能够灵活的配置 TCP 参数,知足不一样的用户场景。相关配置接口定义以下:

图2-27 Netty 的 TCP 参数配置定义

2.3. 总结

经过对 Netty 的架构和性能模型进行分析,咱们发现 Netty 架构的高性能是被精心设计和实现的,得益于高质量的架构和代码,Netty 支持 10W TPS 的跨节点服务调用并非件十分困难的事情。

欢迎关注:《老男孩的成长之路》,后台私信“资料”领取《Java面试宝典Plus》版

相关文章
相关标签/搜索