前言
做为一个高性能的 NIO 通讯框架,Netty 被普遍应用于大数据处理、互联网消息中间件、游戏和金融行业等。大多数应用场景对底层的通讯框架都有很高的性能要求,做为综合性能最高的 NIO 框架 之一,Netty 能够彻底知足不一样领域对高性能通讯的需求。本文咱们将从架构层对 Netty 的高性能设计和关键代码实现进行剖析,看 Netty 是如何支撑高性能网络通讯的。react
RPC 调用性能模型分析
传统 RPC 调用性能差的缘由
1.网络传输方式问题编程
传统的 RPC 框架或者基于 RMI 等方式的远程过程调用采用了同步阻塞 I/O,当客户端的并发压力或者网络时延增大以后,同步阻塞 I/O 会因为频繁的 wait 致使 I/O 线程常常性的阻塞,因为线程没法高效的工做,I/O 处理能力天然降低。bootstrap
采用 BIO 通讯模型的服务端,一般由一个独立的 Acceptor 线程负责监听客户端的链接,接收到客户端链接以后,为其建立一个新的线程处理请求消息,处理完成以后,返回应答消息给客户端,线程销毁,这就是典型的 “ 一请求,一应答 ” 模型。该架构最大的问题就是不具有弹性伸缩能力,当并发访问量增长后,服务端的线程个数和并发访问数成线性正比,因为线程是 Java 虛拟机 很是宝贵的系统资源,当线程数膨胀以后,系统的性能急剧降低,随着并发量的继续增长,可能会发生句柄溢出、线程堆栈溢出等问题,并致使服务器最终宕机。安全
2.序列化性能差服务器
Java 序列化存在以下几个典型问题:网络
1.Java 序列化机制是 Java 内部的一 种对象编解码技术,没法跨语言使用。例如对于异构系统之间的对接,Java 序列化后的码流须要可以经过其余语言反序列化成原始对象,这很难支持。2.相比于其余开源的序列化框架,Java 序列化后的码流太大,不管是网络传输仍是持久化到磁盘,都会致使额外的资源占用。3.序列化性能差,资源占用率高 ( 主要是 CPU 资源占用高 )。多线程
3.线程模型问题架构
因为采用同步阻塞 I/O,这会致使每一个 TCP 链接 都占用 1 个线程,因为线程资源是 JVM 虚拟机 很是宝贵的资源,当 I/O 读写阻塞致使线程没法及时释放时,会致使系统性能急剧降低,严重的甚至会致使虚拟机没法建立新的线程。并发
4.IO 通讯性能三原则框架
尽管影响 I/O 通讯性能的因素很是多,可是从架构层面看主要有三个要素。
1.传输:用什么样的通道将数据发送给对方。能够选择 BIO、NIO 或者 AIO,I/O 模型 在很大程度上决定了通讯的性能;2.协议:采用什么样的通讯协议,HTTP 等公有协议或者内部私有协议。协议的选择不一样,性能也不一样。相比于公有协议,内部私有协议的性能一般能够被设计得更优;3.线程模型:数据报如何读取?读取以后的编解码在哪一个线程进行,编解码后的消息如何派发,Reactor 线程模型的不一样,对性能的影响也很是大。
5.异步非阻塞通讯
在 I/O 编程过程当中,当须要同时处理多个客户端接入请求时,能够利用多线程或者 I/O 多路复用技术进行处理。I/O 多路复用技术经过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的状况下能够同时处理多个客户端请求。 与传统的多线程 / 多进程模型比,I/O 多路复用的最大优点是系统开销小,系统不须要建立新的额外进程或者线程,也不须要维护这些进程和线程的运行,下降了系统的维护工做量,节省了系统资源。
JDK1.4 提供了对非阻塞 I/O 的支持,JDK1.5 使用 epoll 替代了传统的 select / poll,极大地提高了 NIO 通讯 的性能。
与 Socket 和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不一样的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。 阻塞模式使用很是简单,可是性能和可靠性都很差,非阻塞模式则正好相反。开发人员通常能够根据本身的须要来选择合适的模式,通常来讲,低负载、低并发的应用程序能够选择同步阻塞 I/O 以下降编程复杂度。可是对于高负载、高并发的网络应用,须要使用 NIO 的非阻塞模式进行开发。
Netty 的 I/O 线程 NioEventLoop 因为聚合了多路复用器 Selector,能够同时并发处理成百上千个客户端 SocketChannel。因为读写操做都是非阻塞的,这就能够充分提高 I/O 线程 的运行效率,避免由频繁的 I/O 阻塞 致使的线程挂起。另外,因为 Netty 采用了异步通讯模式,一个 I/O 线程 能够并发处理 N 个客户端链接和读写操做,这从根本上解决了传统 同步阻塞 I/O “ 一链接,一线程 ” 模型,架构的性能、弹性伸缩能力和可靠性都获得了极大的提高。
高效的 Reactor 线程模型
经常使用的 Reactor 线程模型有三种,分别以下:
1.Reactor 单线程模型;2.Reactor 多线程模型;3.主从 Reactor 多线程模型。
Reactor 单线程模型,指的是全部的 I/O 操做都在同一个 NIO 线程上面完成,NIO 线程的职责以下:
1.做为 NIO 服务端,接收客户端的 TCP 链接;2.做为 NIO 客户端,向服务端发起 TCP 链接;3.读取通讯对端的请求或者应答消息;4.向通讯对端发送消息请求或者应答消息。
因为 Reactor 模式使用的是异步非阻塞 I/O,全部的 I/O 操做 都不会致使阻塞,理论上一个线程能够独立处理全部 I/O 相关的操做。从架构层面看,一个 NIO 线程确实能够完成其承担的职责。例如,经过 Acceptor 接收客户端的 TCP 链接请求消息,链路创建成功以后,经过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户 Handler 能够经过 NIO 线程 将消息发送给客户端。
对于一些小容量应用场景,可使用单线程模型,可是对于高负载、大并发的应用却不合适,主要缘由以下。
1.一个 NIO 线程 同时处理成百上千的链路,性能上没法支撑。 即使 NIO 线程 的 CPU 负荷 达到 100%,也没法知足海量消息的编码,解码、读取和发送;2.当 NIO 线程 负载太重以后,处理速度将变慢,这会致使大量客户端链接超时,超时以后每每会进行重发,这更加剧了 NIO 线程 的负载,最终会致使大量消息积压和处理超时,NIO 线程会成为系统的性能瓶颈;3.可靠性问题。一旦 NIO 线程意外跑飞,或者进入死循环,会致使整个系统通讯模块不可用,不能接收和处理外部消息,形成节点故障。
为了解决这些问题,演进出了 Reactor 多线程模型,下面咱们看一下 Reactor 多线程模型。
Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程 处理 I/O 操做,它的特色以下。
1.有一个专门的 NIO 线程 —— Acceptor 线程 用于监听服务端口,接收客户端的 TCP 链接请求;2.网络 IO 操做 —— 读、写等由一个 NIO 线程池 负责,线程池能够采用标准的 JDK 线程池 实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程 负责消息的读取、解码、编码和发送;3.1 个 NIO 线程 能够同时处理 N 条链路,可是 1 个链路只对应 1 个 NIO 线程,以防止发生并发操做问题。
在绝大多数场景下,Reactor 多线程模型 均可以知足性能需求,可是,在极特殊应用场景中,一个 NIO 线程负责监听和处理全部的客户端链接可能会存在性能问题。例如百万客户端并发链接,或者服务端须要对客户端的握手消息进行安全认证,认证自己很是损耗性能。在这类场景下,单独一个 Acceptor 线程 可能会存在性能不足问题,为了解决性能问题,产生了第三种 Reactor 线程模型 —— 主从 Reactor 多线程模型。
主从 Reactor 线程模型的特色是,服务端用于接收客户端链接的再也不是个单线程的链接处理 Acceptor,而是一个独立的 Acceptor 线程池。Acceptor 接收到客户端 TCP 链接请求 处理完成后 ( 可能包含接入认证等 ),将新建立的 SocketChannel 注册到 I/O 处理线程池 的某个 I/O 线程 上,由它负责 SocketChannel 的读写和编解码工做。Acceptor 线程池 只用于客户端的登陆、握手和安全认证,一旦链路创建成功,就将链路注册到 I/O 处理线程池的 I/O 线程 上,每一个 I/O 线程 能够同时监听 N 个链路,对链路产生的 IO 事件 进行相应的 消息读取、解码、编码及消息发送等操做。
利用主从 Reactor 线程模型,能够解决 1 个 Acceptor 线程 没法有效处理全部客户端链接的性能问题。所以,Netty 官方也推荐使用该线程模型。
事实上,Netty 的线程模型并不是固定不变,经过在启动辅助类中建立不一样的 EventLoopGroup 实例 并进行适当的参数配置,就能够支持上述三种 Reactor 线程模型。能够根据业务场景的性能诉求,选择不一样的线程模型。
Netty 单线程模型服务端代码示例以下:
EventLoopGroup reactor = new NioEventLoopGroup(1);ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(reactor, reactor) .channel(NioServerSocketChannel.class) ......
Netty 多线程模型代码示例以下:
EventLoopGroup acceptor = new NioEventLoopGroup(1); EventLoopGroup ioGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(acceptor, ioGroup) .channel(NioServerSocketChannel.class) ......
Netty 主从多线程模型代码示例以下:
EventLoopGroup acceptorGroup = new NioEventLoopGroup(); EventLoopGroup ioGroup = new NioEventLoopGroup(); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(acceptorGroup, ioGroup) .channel(NioServerSocketChannel.class) ......
无锁化的串行设计
在大多数场景下,并行多线程处理能够提高系统的并发性能。可是,若是对于共享资源的并发访问处理不当,会带来严重的锁竞争,这最终会致使性能的降低。为了尽量地避免锁竞争带来的性能损耗,能够经过串行化设计,即消息的处理尽量在同一个线程内完成,期间不进行线程切换,这样就避免了多线程竞争和同步锁。
为了尽量提高性能,Netty 对消息的处理采用了串行无锁化设计,在 I/O 线程 内部进行串行操做,避免多线程竞争致使的性能降低。Netty 的串行化设计工做原理图以下图所示。
Netty 的 NioEventLoop 读取到消息以后,直接调用 ChannelPipeline 的 fireChannelRead(Object msg),只要用户不主动切换线程,一直会由 NioEventLoop 调用到 用户的 Handler,期间不进行线程切换。这种串行化处理方式避免了多线程操做致使的锁的竞争,从性能角度看是最优的。
零拷贝
Netty 的“ 零拷贝 ”主要体如今以下三个方面。
第一种状况。Netty 的接收和发送 ByteBuffer 采用堆外直接内存 (DIRECT BUFFERS) 进行 Socket 读写,不须要进行字节缓冲区的二次拷贝。若是使用传统的 堆内存(HEAP BUFFERS) 进行 Socket 读写,JVM 会将 堆内存 Buffer 拷贝一份到 直接内存 中,而后才写入 Socket。相比于堆外直接内存,消息在发送过程当中多了一次缓冲区的内存拷贝。
下面咱们继续看第二种“ 零拷贝 ” 的实现 CompositeByteBuf,它对外将多个 ByteBuf 封装成一个 ByteBuf,对外提供统一封装后的 ByteBuf 接口。CompositeByteBuf 实际就是个 ByteBuf 的装饰器,它将多个 ByteBuf 组合成一个集合,而后对外提供统一的 ByteBuf 接口,添加 ByteBuf,不须要作内存拷贝。
第三种 “ 零拷贝 ” 就是文件传输,Netty 文件传输类 DefaultFileRegion 经过 transferTo() 方法 将文件发送到目标 Channel 中。不少操做系统直接将文件缓冲区的内容发送到目标 Channel 中,而不须要经过循环拷贝的方式,这是一种更加高效的传输方式,提高了传输性能,下降了 CPU 和内存占用,实现了文件传输的 “ 零拷贝 ” 。
内存池
随着 JVM 虚拟机 和 JIT 即时编译技术 的发展,对象的分配和回收是个很是轻量级的工做。可是对于缓冲区 Buffer,状况却稍有不一样,特别是对于堆外直接内存的分配和回收,是一件耗时的操做。为了尽可能重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制。 ByteBuf 的子类中提供了多种 PooledByteBuf 的实现,基于这些实现 Netty 提供了多种内存管理策略,经过在启动辅助类中配置相关参数,能够实现差别化的定制。