Netty 是如何支撑高性能网络通信的?

Doocs开源社区

共 6188字,需浏览 13分钟

 ·

2020-08-13 13:05

本文选自 Doocs 开源社区旗下“源码猎人”项目,作者 AmyliaY。

项目将会持续更新,欢迎 Star 关注。

项目地址:https://github.com/doocs/source-code-hunter

作为一个高性能的 NIO 通信框架,Netty 被广泛应用于大数据处理、互联网消息中间件、游戏和金融行业等。大多数应用场景对底层的通信框架都有很高的性能要求,作为综合性能最高的 NIO 框架 之一,Netty 可以完全满足不同领域对高性能通信的需求。本文我们将从架构层对 Netty 的高性能设计和关键代码实现进行剖析,看 Netty 是如何支撑高性能网络通信的

RPC 调用性能模型分析

传统 RPC 调用性能差的原因

网络传输方式问题

传统的 RPC 框架或者基于 RMI 等方式的远程过程调用采用了同步阻塞 I/O,当客户端的并发压力或者网络时延增大之后,同步阻塞 I/O 会由于频繁的 wait 导致 I/O 线程经常性的阻塞,由于线程无法高效的工作,I/O 处理能力自然下降。

采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听客户端的连接,接收到客户端连接之后,为其创建一个新的线程处理请求消息,处理完成之后,返回应答消息给客户端,线程销毁,这就是典型的 “ 一请求,一应答 ” 模型。该架构最大的问题就是不具备弹性伸缩能力,当并发访问量增加后,服务端的线程个数和并发访问数成线性正比,由于线程是 Java 虛拟机 非常宝贵的系统资源,当线程数膨胀之后,系统的性能急剧下降,随着并发量的继续增加,可能会发生句柄溢出、线程堆栈溢出等问题,并导致服务器最终宕机。

序列化性能差

Java 序列化存在如下几个典型问题:

1.Java 序列化机制是 Java 内部的一 种对象编解码技术,无法跨语言使用。例如对于异构系统之间的对接,Java 序列化后的码流需要能够通过其他语言反序列化成原始对象,这很难支持。2.相比于其他开源的序列化框架,Java 序列化后的码流太大,无论是网络传输还是持久化到磁盘,都会导致额外的资源占用。3.序列化性能差,资源占用率高 ( 主要是 CPU 资源占用高 )。

线程模型问题

由于采用同步阻塞 I/O,这会导致每个 TCP 连接 都占用 1 个线程,由于线程资源是 JVM 虚拟机 非常宝贵的资源,当 I/O 读写阻塞导致线程无法及时释放时,会导致系统性能急剧下降,严重的甚至会导致虚拟机无法创建新的线程。

IO 通信性能三原则

尽管影响 I/O 通信性能的因素非常多,但是从架构层面看主要有三个要素

1.传输:用什么样的通道将数据发送给对方。可以选择 BIO、NIO 或者 AIO,I/O 模型 在很大程度上决定了通信的性能;2.协议:采用什么样的通信协议,HTTP 等公有协议或者内部私有协议。协议的选择不同,性能也不同。相比于公有协议,内部私有协议的性能通常可以被设计得更优;3.线程模型:数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,Reactor 线程模型的不同,对性能的影响也非常大。

异步非阻塞通信

在 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 提供了多种内存管理策略,通过在启动辅助类中配置相关参数,可以实现差异化的定制。

全文完!

希望本文对大家有所帮助。如果感觉本文有帮助,有劳转发或“点赞”、“在看”!让更多人收获知识!


长按识别下图二维码,关注公众号「Doocs 开源社区」,第一时间跟你们分享好玩、实用的技术文章与业内最新资讯。



浏览 31
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报