同步操作将从 doocs/source-code-hunter 强制同步,此操作会覆盖自 Fork 仓库以来所做的任何修改,且无法恢复!!!
确定后同步将在后台操作,完成时将刷新页面,请耐心等待。
作为一个高性能的 NIO通信框架,Netty 被广泛应用于大数据处理、互联网消息中间件、游戏和金融行业等。大多数应用场景对底层的通信框架都有很高的性能要求,作为综合性能最高的 NIO框架 之一,Netty 可以完全满足不同领域对高性能通信的需求。本章我们将从架构层对 Netty 的高性能设计和关键代码实现进行剖析,看 Netty 是如何支撑高性能网络通信的。
一、网络传输方式问题。
传统的 RPC框架 或者基于 RMI 等方式的 远程过程调用 采用了同步阻塞I/O,当客户端的并发压力或者网络时延增大之后,同步阻塞I/O 会由于频繁的 wait 导致 I/O线程 经常性的阻塞,由于线程无法高效的工作,I/O 处理能力自然下降。
采用 BIO通信模型 的服务端,通常由一个独立的 Acceptor线程 负责监听客户端的连接,接收到客户端连接之后,为其创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的 “ 一请求,一应答 ” 模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是 Java虛拟机 非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。
二、序列化性能差。
Java序列化 存在如下几个典型问题:
三、线程模型问题。
由于采用 同步阻塞I/O,这会导致每个 TCP连接 都占用 1 个线程,由于线程资源是 JVM虚拟机 非常宝贵的资源,当 I/O 读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。
尽管影响 I/O通信性能 的因素非常多,但是从架构层面看主要有三个要素。
在 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单线程模型,指的是所有的 I/O操作 都在同一个 NIO线程 上面完成,NIO线程 的职责如下:
由于 Reactor模式 使用的是 异步非阻塞I/O,所有的 I/O操作 都不会导致阻塞,理论上一个线程可以独立处理所有 I/O 相关的操作。从架构层面看,一个 NIO线程 确实可以完成其承担的职责。例如,通过 Acceptor 接收客户端的 TCP连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户Handler 可以通过 NIO线程 将消息发送给客户端。
对于一些小容量应用场景,可以使用单线程模型,但是对于高负载、大并发的应用却不合适,主要原因如下。
为了解决这些问题,演进出了 Reactor多线程模型,下面我们看一下 Reactor多线程模型。
Rector多线程模型 与单线程模型最大的区别就是有一组 NIO线程 处理 I/O操作,它的特点如下。
在绝大多数场景下,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 提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。
网络由下往上分为 物理层、数据链路层、网络层、传输层和应用层。IP协议 对应于网络层,TCP协议 对应于传输层,而 HTTP协议 对应于应用层,三者从本质上来说没有可比性,Socket 则是对 TCP/IP协议 的封装和应用 (程序员层面上)。也可以说,TPC/IP协议 是传输层协议,主要解决数据如何在网络中传输,而 HTTP 是应用层协议,主要解决如何包装数据。Socket 是对 TCP/IP协议 的封装,Socket 本身并不是协议,而是一个 调用接口(API)。 通过 Socket,我们才能使用 TCP/IP协议。
建立 Socket连接 至少需要一对套接字,其中一个运行于客户端,称为 clientSocket ,另一个运行于服务器端,称为 serverSocket 。套接字之间的连接过程分为三个步骤:服务器监听,客户端请求,连接确认。
HTTP协议 是 Web联网 的基础,也是手机联网常用的协议之一,HTTP协议 是建立在 TCP协议 之上的一种应用。HTTP连接 最显著的特点是客户端发送的每次请求 都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为 “一次连接”。
Socket、SocketChannel 二者的实质都是一样的,都是为了实现客户端与服务器端的连接而存在的,但是在使用上却有很大的区别。具体如下:
下面是 SocketChannel方式 需要用到的几个核心类:
ServerSocketChannel:ServerSocket 的替代类, 支持阻塞通信与非阻塞通信。
SocketChannel:Socket 的替代类, 支持阻塞通信与非阻塞通信。
Selector:为 ServerSocketChannel 监控接收客户端连接就绪事件, 为 SocketChannel 监控连接服务器读就绪和写就绪事件。
SelectionKey:代表 ServerSocketChannel 及 SocketChannel 向 Selector 注册事件的句柄。当一个 SelectionKey对象 位于 Selector对象 的 selected-keys集合 中时,就表示与这个 SelectionKey对象 相关的事件发生了。在 SelectionKey类 中有如下几个静态常量:
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。