深入探索Netty:高级用法与性能优化

前言

这边深入探索Netty一些特性和介绍一些Netty优化

ByteBuf(字节容器)

ByteBuf 是 Netty 中用于表示字节序列的缓冲区类。它提供了一种灵活而高效的方式来处理字节数据,支持各种操作,包括读、写、切片、合并等。
我们看ByteBuf 类介绍

提供两个指针变量来支持顺序
   读取和写入操作 
   - {@link #readerIndex() readerIndex} 用于读取
  - {@link #writerIndex() writerIndex}用于写入。 下图显示了缓冲区是如何分段的
   两个指针的三个区域:
       +-------------------+------------------+------------------+
       | discardable bytes |  readable bytes  |  writable bytes  |
       |                   |     (CONTENT)    |                  |
       +-------------------+------------------+------------------+
       |                   |                  |                  |
       0      <=      readerIndex   <=   writerIndex    <=    capacity


三种数据组成:discardable bytes,readable bytes,writable bytes
	Discardable Bytes(可丢弃字节):
		定义: discardable bytes 指的是已经被读取过的字节,可以被忽略或丢弃。
		用途: 通常在读取数据后,可以通过 discardReadBytes() 方法将已读取的字节从缓冲区中丢弃,释放空间。这有助于优化内存使用。
	Readable Bytes(可读字节):
		定义: readable bytes 表示当前缓冲区中可以被读取的字节数。
		用途: 通过读取操作,可以从 ByteBuf 中获取这些字节,进行进一步的处理。例如,使用 readByte()readBytes() 等方法来读取数据。
	Writable Bytes(可写字节):
		定义: writable bytes 表示当前缓冲区中可以被写入的字节数。
		用途: 通过写入操作,可以向 ByteBuf 中写入数据。例如,使用 writeByte()writeBytes() 等方法来往缓冲区写入字节数据。

接下来我们再看看ByteBuffer 和ByteBuf 区别

ByteBufferByteBuf 区别

API 的设计:
	ByteBufferJava NIO 包中提供的原生缓冲区,其 API 设计较为基础,需要手动跟踪读写位置,使用时需要小心管理读写索引。
	ByteBufNetty 提供的高级缓冲区,其 API 设计更加友好,提供了一系列的方法来简化读写操作,而且具备读写索引的分离。
	
内存管理:
	ByteBuffer 采用 JVM 堆上的内存分配,而且在进行 Socket I/O 操作时需要进行额外的内存拷贝。
	ByteBuf 支持池化的内存分配,可以选择在堆上分配,也可以选择在直接内存上分配,能够减少内存拷贝的次数,提高性能。
	
可扩展性:
	ByteBuffer 在容量不足时,需要手动扩展容量,并可能导致数据拷贝。同时,ByteBuffer 不支持自动收缩,需要手动调整缓冲区的大小。
	ByteBuf 允许动态扩展和收缩,支持自动扩容,当容量不足时,会自动进行扩展而不需要额外的操作。
	
零拷贝:
	ByteBuffer 在进行网络传输时,需要进行多次内存拷贝,例如将数据从缓冲区拷贝到 Socket 缓冲区,再从 Socket 缓冲区拷贝到 ByteBufferByteBuf 支持零拷贝,通过直接内存或复合缓冲区的方式,可以避免中间拷贝,提高数据传输的效率
	
引用计数:
	ByteBuf 引入了引用计数机制,用于跟踪对缓冲区的引用,当引用计数降为零时,相关资源得以释放,有助于避免内存泄漏。
	ByteBuffer 没有引用计数的概念,需要开发者手动管理内存的释放。

零拷贝

零拷贝(Zero Copy)是一种技术,其目标是在数据传输中尽量减少数据在内存之间的拷贝操作。这有助于提高性能、减少资源占用,特别是在网络数据传输和文件 I/O 操作中。
同概念上看就是减少复制次数,还有一种提高I/O性能是写时复制(copy-on-write,COW)

写时复制(copy-on-write,COW):复制一个文件,只是增加一个文件引用,并标记这两个地址空间为只读。当文件有写操作时才真正复制文件,对这个文件副本修改。

在介绍零拷贝之前我们先介绍下在传统的拷贝操作中,数据从一个缓冲区(如内核空间)被拷贝到用户空间,然后再从用户空间拷贝到另一个缓冲区(如网络协议栈的缓冲区)。

在Linux操作系统中,用户空间(User Space)和内核空间(Kernel Space)是两个关键的内存空间,它们分别用于存储用户应用程序和操作系统内核的代码和数据。
用户空间(User Space)大部分应用程序的代码和数据都存在于用户空间,包括用户的应用逻辑、库函数等。
内核空间(Kernel Space)内核空间包含了操作系统的核心组件,如调度器、设备驱动程序、文件系统等。

用户空间和内核空间之间的交互:
系统调用(System Call): 用户空间的应用程序通过系统调用接口请求内核空间执行一些特权操作,例如文件读写、网络通信等。

中断和异常: 用户空间的应用程序可以通过触发中断或异常,引起操作系统内核的响应,执行一些特殊的操作。

共享内存: 通过一些机制如共享内存,用户空间的应用程序可以和内核空间进行数据交换。

I/O 操作: 用户空间的应用程序进行输入输出操作时,通常需要调用内核空间提供的相关函数。

Netty 在处理数据时使用了零拷贝的技术,主要体现在接收和发送阶段,以及采用了直接内存(Direct Buffers)来进行 Socket 读写。这样的设计确实避免了字节缓冲区的二次拷贝,提高了性能。
Netty 使用堆外直接内存(Direct Buffers)而不是传统的堆内存来存储数据。直接内存的优势在于它可以绕过 JVM 的垃圾回收机制,同时在进行 Socket 传输时,避免了数据在用户空间和内核空间之间的拷贝。

Heap Buffer(堆缓冲区):Heap BufferJava 堆内存中进行分配。这意味着它是由 JVM 管理的常规对象,受到 Java 堆管理和垃圾回收的影响。由于在堆内存中分配,Heap Buffer 对象的访问速度可能相对较慢,因为它需要经过额外的间接层(在 Java 堆和底层内存之间)。

Direct Buffer(直接缓冲区):Direct Buffer 则在堆外直接内存中进行分配。这意味着它不受 Java 堆管理和垃圾回收的影响,也不会占用 Java 堆的空间。由于直接分配在堆外内存,Direct Buffer 对象的访问速度相对较快,因为它直接映射到物理内存。

使用 Heap Buffer 适用于一般的数据处理场景,对于小型数据量和不频繁进行 I/O 操作的情况。
使用 Direct Buffer 适用于需要频繁进行 I/O 操作、大量数据传输或对性能要求较高的场景,例如在网络编程中。
Netty零拷贝和Linux零拷贝
Netty 利用了操作系统提供的零拷贝机制,通过调用相关的 API 来实现在用户空间和内核空间之间的零拷贝。
Linux 系统的零拷贝是一种更底层的系统级别的优化,通过系统调用和硬件支持实现。通过sendfile 系统调用,mmap 系统调用,DMADirect Memory Access)一些特性

在 Netty 中使用零拷贝技术通常是隐式的,由 Netty 框架自动处理。

NIO

NIO 是 Java 中的 New I/O(非阻塞 I/O)的缩写,它是 Java 提供的一组支持非阻塞 I/O 操作的新 API。

BIOBlocking I/O),NIONon-blocking I/O),和AIOAsynchronous I/O)是 Java 中用于处理 I/O 操作的三种不同的编程模型
    BIO (Blocking I/O):
        同步阻塞模型。当应用程序发起 I/O 操作(如读取或写入数据),线程会被阻塞,直到操作完成。
        缺点:对大量并发连接的支持能力较差,每个连接都需要一个独立的线程,容易导致资源浪费
    NIO (Non-blocking I/O):
        同步非阻塞模型。使用通道(Channel)和缓冲区(Buffer)进行数据传输,支持非阻塞的 select 操作。
        优点:支持大量并发连接,线程较少。缺点:编程模型相对复杂,需要处理事件循环和状态管理。
    AIO (Asynchronous I/O):
        异步非阻塞模型。引入了异步 CompletionHandler 机制,不需要通过轮询(polling)来等待 I/O 完成。
        优点:异步操作,可以更高效地处理大量并发连接,不需要阻塞等待。缺点:编程模型相对复杂,不太适用于短连接和快速响应的场景。
    在网络编程中,NIO 是一个比较常见的选择,而 AIO 通常在需要处理大量连接且每个连接的 I/O 操作较耗时的场景中使用。

非阻塞 I/O(NIO,Non-blocking I/O)原理:

通道和缓冲区 (Channels and Buffers): NIO 引入了两个主要的概念:通道(Channel)和缓冲区(Buffer)。通道是数据的源头或目的地,而缓冲区是在通道和应用程序之间传输数据的容器。通道和缓冲区提供了更灵活的数据操作方式。

选择器 (Selector): 选择器是非阻塞 I/O 的核心组件。它允许一个线程管理多个通道,实现了在单个线程上监视多个通道的能力。通过选择器,一个线程可以等待多个通道中的任何一个通道准备好进行读取或写入操作。

非阻塞模式 (Non-blocking Mode): 通道可以以非阻塞模式打开,这意味着当调用 read()write() 方法时,如果没有数据可用或者无法立即写入数据,这些方法不会阻塞线程,而是立即返回。这使得一个线程能够处理多个通道而不被阻塞。

事件驱动 (Event-Driven): NIO 是事件驱动的。当一个通道上发生某个事件(如数据准备好读取或可以写入数据)时,选择器会通知相应的线程处理这个事件。这使得程序可以异步地响应 I/O 事件。


简单的非阻塞 I/O 流程如下:
	打开一个通道,将其设置为非阻塞模式。
	将通道注册到选择器上,以便监视特定的事件。
	在循环中,选择器等待通道上的事件。
	一旦通道上发生事件,线程被唤醒,可以执行相应的操作。

通过这种方式,一个线程可以有效地管理多个通道,而无需为每个通道创建一个单独的线程,从而提高了系统的性能和资源利用率。

I/O事件通知机制

I/O事件通知机制是一种编程模型,用于异步处理输入/输出操作

轮询(Polling:
	程序周期性地检查事件是否发生,这称为轮询。
	轮询的缺点是可能会导致资源浪费,因为在没有事件发生时,程序会一直执行轮询。
回调(Callback:	
	通过回调机制,应用程序注册一个回调函数或方法,当事件发生时,系统调用该回调来处理事件。
	常见于事件驱动编程,如图形用户界面 (GUI) 框架和异步编程。
信号(Signal:
	在类Unix系统中,信号可以用于通知进程发生了某个事件。例如,SIGIO 信号用于通知套接字可以进行读写操作。
	在信号处理程序中,可以执行相应的操作。
选择器(Selector:
	选择器是一种高效的事件通知机制,通常与非阻塞 I/O 一起使用。
	在Java中,java.nio.channels.Selector 提供了选择器的实现,可以用于监视多个通道上的事件。
	当通道上发生事件时,选择器会通知应用程序,应用程序可以采取相应的措施。
异步 I/O (AIO):
	异步 I/O 是一种先进的事件通知机制,允许应用程序在 I/O 操作完成时得到通知,而不需要轮询或回调。
	在一些操作系统中,AIO 提供了一些系统调用,允许程序异步地发起 I/O 操作,并在完成时得到通知。

I/O多路复用是一种在单个线程中同时监视多个I/O操作的机制。这样可以通过一个单独的系统调用同时等待多个I/O事件。

select:
	select 是UnixWindows平台上最古老的I/O多路复用机制之一。
	它通过一个fd_set数组来监听多个文件描述符(sockets)的I/O事件。
	缺点是它对文件描述符数目有限制(默认1024),并且每次调用都需要线性遍历整个文件描述符集合,效率可能受到影响。
poll:
	poll 是对select 的改进,解决了文件描述符数目的限制问题。
	与select 类似,poll 使用一个数组来管理多个文件描述符,但它没有了文件描述符数目的限制。
	在处理大量文件描述符时,poll 的性能可能优于 select,但在某些场景下仍然可能有性能瓶颈。
epoll:
	epoll 是Linux特有的I/O多路复用机制,相比于 select 和 poll,epoll 在性能和扩展性上都有显著的提升。
	epoll 使用一个文件描述符来管理多个文件描述符,通过一个事件列表返回就绪的文件描述符。
	epoll 使用了"事件通知"的方式,只通知发生变化的文件描述符,避免了遍历整个文件描述符集合的开销。

netty性能优化

netty性能优化

使用池化: 通过使用对象池(ByteBuf池、Channel池等)来减少内存分配和垃圾回收的开销。NettyPooledByteBufAllocatorPooledChannelAllocator等类提供了内存池功能。

调整工作线程数量: 根据应用程序的性质和硬件配置,调整Netty的工作线程数量。可以通过EventLoopGroup的构造函数参数来设置。

使用零拷贝: 尽可能使用FileRegion等零拷贝机制,避免不必要的数据复制。Netty提供了零拷贝的支持。

使用高性能序列化: 选择性能较好的序列化框架,例如GoogleProtocol BuffersApache Thrift等,以减少网络传输时的开销。

启用TCP Quick Ack: 在长连接的情况下,启用TCP Quick Ack可以减少小数据包的延迟。可以通过setTcpNoDelay(true)启用TCP Quick Ack。

使用更高版本的Netty: 使用最新版本的Netty,因为新版本通常包含了性能改进和bug修复。

精简Pipeline: 仔细设计和配置ChannelPipeline,只保留必要的处理器,避免不必要的处理器链。

合理设置TCP参数: 针对具体应用场景,合理设置TCP参数,如TCP连接超时、TCP心跳等。

使用CompositeByteBuf: 在需要聚合多个ByteBuf时,使用CompositeByteBuf,避免数据拷贝。

异步编程模式: 利用Netty的异步编程模式,合理使用回调机制,避免阻塞操作。

压缩: 在网络传输中启用压缩,减小数据传输量。Netty提供了HttpContentCompressor等压缩相关的支持。

优化GC: 避免频繁的内存分配和垃圾回收。可以通过使用内存池、减少对象的创建等方式。

合理配置EventLoop: 根据应用场景和硬件配置,合理配置EventLoop的参数,例如IO密集型和计算密集型的场景可能需要不同的配置。

复用channel,可以选择可共享的channel(@sharable,减少堆内存开销)

总结

以上是对Netty一些特性介绍,同时介绍了操作系统相关的I/O。

  • 40
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值