源码分析 -Netty: 高性能之道
系列文章:
一 摘要
在源码分析-Netty: 架构剖析中,我们介绍了 Netty 的逻辑架构,本篇将继续深入,从架构层面对 Netty 的高性能设计和关键代码进行分析,看 Netty 如何支撑高性能网络通信。
二 RPC 调用模型分析
2.1 RPC 调用-关键性能瓶颈
涉及到 RPC 框架的三大核心部分:网络传输方式、序列化、线程模型。接下来我们逐个分析:
2.1.1 网络传输方式
RPC 采用的 I/O 模型。古老的 RPC 框架,或基于 RMI 等方式实现的远程过程调用,使用的是同步阻塞 I/O(BIO),根据前面对几种 I/O 模型及演化过程的介绍,可知这通常是效率最低的一种。当客户端的并发压力或网络时延增大以后,同步阻塞 I/O 会由于频繁 wait 导致 I/O 线程多次阻塞,从而 I/O 处理能力下降。
2.1.2 序列化
Java 序列化普遍存在着性能差的问题,列举如下:
1、Java 序列化机制是语言专属(内部)的一种对象编解码技术,不能跨语言;
2、与其他开源序列化框架相比,Java 序列化后的码流过大,网络传输或持久化磁盘时都会导致过多额外的资源占用;
3、序列化性能差,资源占用高(主要是 CPU 资源)
2.1.3 线程模型
基于 Java 实现的服务端,如果采用的是 BIO 通信模型,通常由一个独立的 Acceptor 线程来监听客户端链接;当接收到客户端连接请求时,会为其创建一个新的线程处理请求信息,并在处理完成并返回应答(response)消息后,线程销毁。这种一请求一应答的架构模型不具备弹性伸缩能力,当访问量增加时,服务端线程个数和并发请求数成线性增长,同时由于在 Java 虚拟机中,线程是非常宝贵的系统资源(创建线程、线程间切换时会造成大量开销),当线程数膨胀时系统性能会急剧下降,可能会导致句柄溢出甚至线程堆栈溢出问题,最终导致服务崩溃。
2.2 I/O 通信要素
影响 I/O 性能的因素很多,主要有以下三个:
1、传输
就是 I/O 模型,BIO、NIO 或 AIO。
2、协议
通信协议,HTTP 等公共协议还是私有协议。协议也直接影响性能。相对公有协议来说,内部私有协议通常可以被设计得性能更优。
3、线程模型
数据报文如何读取?读取后编码和解码怎样分配到线程?编解码后的消息怎样继续下发?
三 Netty 的高性能之道
3.1 非阻塞 I/O 模型
Netty 提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,并且这两种都支持阻塞和非阻塞模式。
NioEventLoop 由于聚合了 Selector(多路复用器),可以同时并发处理上千个 SocketChannel,这可以充分提升 I/O 线程的运行效率。避免频繁 I/O 阻塞导致的线程挂起。
3.2 高效的 Reactor 线程模型
常用的 Reactor 线程模型有 Reactor 单线程、Reactor 多线程、主从 Reactor 多线程三种。几种模型的区别分别在于是否有一组 NIO 线程专门处理 I/O 操作,以及服务器用于接收客户端连接的是单线程还是线程池。相关内容可查看之前的文章及参考资料,这里暂时不做赘述。
Netty 中可以通过在启动辅助类中创建不同的 EventLoopGroup 并设置参数,支持上述三种 Reactor 线程模型,以满足不同业务场景的性能诉求。
3.3 无锁化的串行设计
高并发场景,为了正确同步,锁成为常用的解决方案。但锁的使用不当时,会带来不必要的锁竞争,这会导致性能的急剧下降。串行化设计就是一种解决方案,尽可能避免/降低锁竞争带来的性能损耗。所谓串行化,就是消息的处理尽可能在同一个线程内完成,期间不做线程切换,这样就避免了多线程竞争和锁同步。Netty 就在 I/O 线程内进行了串行设计。
通常的理解,串行化会带来 CPU 利用不高、并行度不够的问题,但实际上,Netty 支持通过调整 NIO 线程池的参数,同时启动多个串行化的线程并运行,这种局部的无锁化串行线程设计在性能上可以优于多个工作线程模型。
Netty 串行化设计工作原理如上图所示,涉及 NioEventLoop、ChannelPipeline。NioEventLoop 读取到消息之后,调用 ChannelPipeline 的 fireChannelRead(Object msg)方法,期间如果用户不主动切换线程,那么就会一直由 NioEventLoop 调用 Handler,不做线程切换。
3.4 高效的并发编程
源码分析-Netty: 并发编程的实践(二)中做过介绍,主要包括以下几点:
1)volatile 的大量且正确使用
2)CAS 和原子操作类的广泛使用
3)线程安全容器的使用
4)读写锁
3.5 高性能的序列化框架
Netty 默认提供了对 Google Protobuf 的支持,通过扩展编解码接口,用户可以实现其他高性能序列化框架,例如 Thrift。
序列化的框架除了 Protobuf 和 Thrift 之外还有很多,都旨在空间、性能等消耗上达到最优。影响序列化性能的主要因素有以下几个:
1)序列化后的码流大小——即网络带宽的占用
2)序列化 &反序列化的性能——CPU 资源占用
3)是否支持跨语言——异构系统对接和开发语言切换
3.6 零拷贝
Netty 的零拷贝体现在三个方面:
1、接收和发送 ByteBuffer 采用 DIRECT BUFFERS,也就是使用堆外内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝;
2、CompositeByteBuf,对外部将多个 ByteBuf 封装成一个 ByteBuf,提供统一封装后的 ByteBuf 接口。CompositeByteBuf 实际上就是 ByteBuf 的装饰器。
3、文件传输,Netty 中的文件传输类 DefaultFileRegion 通过 transferTo 方法将文件发送到目标 Channel 中。
3.7 内存池
与线程池、连接池类似,都属于池化技术,只不过管理对象是内存。随着 JVM 虚拟机和 JIT 即时编译技术的发展,对象的分配和回收是比较轻量级的工作。但对 Buffer(缓冲区),特别是堆外直接内存的分配和回收,是比较耗时的操作。为了尽量重用缓冲区,Netty 提供了基于内存池的缓冲区重用机制,即 ByteBuf。
在 Netty 权威指南第 2 版中,对使用内存池的 ByteBuf 与不使用内存池的 ByteBuf 做了一个性能对比,性能提高达 23 倍之多。
内存池分配器创建直接内存缓冲,使用的是 PooledByteBufAllocator,内存分配的关键代码:
this.newDirectBuffer 是一个抽象方法,定义在 AbstractByteBufAllocator 中。两个子类 PooledByteBufAllocator 和 UnpooledByteBufAllocator 中分别实现了这个方法。
3.7.1 PooledByteBufAllocator
PooledByteBufAllocator 的 newDirectBuffer()方法:
可见,是从 PoolThreadCache 中获取内存区域 PoolArena,并调用它的 allocate()方法执行内存分配。
3.7.2 UnpooledByteBufAllocator
UnpooledByteBufAllocator 中的方法实现:
3.8 灵活的 TCP 参数配置能力
Netty 中可以配置 TCP 参数以满足不同的业务场景。相关配置类:ChannelOption。
从上图可见,ChannelOption 中定义了多个 TCP 参数配置,几个常用且对性能影响较大的参数如下:
1)SO_RCVBUF 和 SO_SNDBUF,TCP 接收缓冲区的容量上限 和 TCP 发送缓冲区的容量上限,通常建议设置为 128KB 或 256KB;
2)SO_TCPNODELAY:NAGLE 算法通过将缓冲区内的小封包自动相连,组成较大的包,阻止大量小包发送阻塞网络,以达到提高网络应用效率的目的。但实际应用时,由于对时延敏感,所以经常需要关闭 NAGLE 算法;
版权声明: 本文为 InfoQ 作者【程序员架构进阶】的原创文章。
原文链接:【http://xie.infoq.cn/article/394fbd47de1ce181f848b9a06】。文章转载请联系作者。
评论