中间件知识点(Netty)一

Netty

什么是 NIO Reactor 模式 ?
反应器模式(其实就是eventbus的原理,关注某个事件,当有该事件消息来的时候会被通知,类似观察者模式,但是比观察者赋予了更多的作用)
反应”即“倒置”,“控制逆转”,具体事件处理程序不调用反应器,而向反应器注册一个事件处理器,表示自己对某些事件感兴趣,有事件来了,反应器会将事件分发给对应类型事件处理器,然后事件处理器会通知事件处理程序事件来了,具体
事件处理程序对某个指定的事件发生做出反应;这种控制逆转又称为“好莱坞法则”(不要调用我(反应器),让我来调用你(时间处理程序))

NIO是单线程实现,也可以是多线程实现

NIO Reactor 模式分类 dt.
1.单线程 Reactor 模式流程
一个Reactor是一个线程对象
I/O 操作和非I/O操作都在Reactor线程执行。
2.单线程 Reactor,工作者线程池
I/O 操作在Reactor线程,非IO操作在工作者线程
非 I/O 操作,比如计算、编解码的。IO操作比如读写操作、网络通信等。
3.多 Reactor 线程模式
设计了Reactor线程池(不是工作者线程池),Reactor 线程池中的每一 Reactor 线程都会有自己的 Selector、线程和分发的事件循环逻辑。
mainReactor 可以只有一个,但 subReactor 一般会有多个。mainReactor 线程主要负责接收客户端的连接请求,然后将接收到的SocketChannel 传递给subReactor,由subReactor 来完成和客户端的通信。
即mainReactor负责连接操作,subReactor负责和客户端读写操作,工作线程还是一样负责非IO操作。

NIO Reactor模式使用场景
redis:
redis5.0使用的是单线程 Reactor模式
redis5.0虽说使用的是单线程 Reactor模式(单Reactor线程),但其实redis不是所有操作都使用的都是一个线程。
(具体看下面Redis6.0之前的版本真的是单线程吗?)

Redis6.0 之前的版本真的是单线程吗?
Redis 在处理客户端的请求时,包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。
但如果严格来讲从 Redis4.0 之后并不是单线程,除了主线程外,它也有后台线程在处理一些较为缓慢的操作,例如清理脏数据、无用连接的释放、大 key 的删除等等。

Redis6.0 之前为什么一直不使用多线程?
(多线程(注意这里的多线程是指多Reactor线程)性能方面虽说比较优异,但实现起来比较复杂,而且单线程模式也使用了IO多路复用,处理性能也不会太差)
1.使用 Redis 时,几乎不存在 CPU 成为瓶颈的情况, Redis 主要受限于内存和网络。
Redis 通过使用 pipelining 每秒可以处理 100 万个请求,所以如果应用程序主要执行的复杂度为 O(N)或O(log(N))的命令,它几乎不会占用太多 CPU。
2.多线程模型虽然在某些方面表现优异,但带来了并发读写的一系列问题,比如非命令执行的读写操作,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
3.单线程,可维护性高。
4.Redis通过 AE 事件模型以及 IO 多路复用等技术,处理性能非常高,因此没有必要使用多线程。
5.单线程机制使得 Redis 内部实现的复杂度大大降低。

那Redis6.0 为什么还要引入多线程呢?
(对于亿级流量,单线程可能不够用。)
redis 支持多线程主要就是两个原因:
• 可以充分利用服务器 CPU 资源,目前主线程(即单线程模式)只能利用一个核
• 多线程任务可以分摊 Redis 同步 IO 读写负荷
主线程主要负责连接请求,从线程主要负责网络数据的读取和发送。
redis单线程的瓶颈是卡在网络数据的读取和发送(亿级流量命令请求很多的情况下),通过多线程可以提高这个性能。

redis6.0使用的是类似多Reactor线程模式
Redis6.0 默认是否开启了多线程?
Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf 配置文件:io-threads-do-reads yes
开启多线程后,还需要设置线程数,否则是不生效的。
关于线程数的设置,官方有一个建议:4 核的机器建议设置为 2 或 3 个线程,8 核的建议设置为 6 个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了 8 个基本就没什么意义了。

开启多线程条件
如果开启多线程,至少要 4 核的机器,且 Redis 实例已经占用相当大的 CPU耗时的时候才建议采用,否则使用多线程没有意义。

Redis6.0 采用多线程后,性能的提升效果如何?
性能提升至少是一倍以上.

Redis6.0 多线程的实现机制?
流程简述如下:
1、主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列
2、主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程
3、主线程阻塞等待 IO 线程读取 socket 完毕
4、主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行回写 socket
5、主线程阻塞等待 IO 线程将数据回写 socket 完毕
6、解除绑定,清空等待队列
该设计有如下特点:
1、IO 线程要么同时在读 socket,要么同时在写,不会同时读或写
2、IO 线程只负责读写 socket 解析命令,不负责命令处理

开启多线程后,是否会存在线程并发安全问题?
(多线程是指多Reactor线程)
从上面的实现机制可以看出,Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。

netty
netty也使用了多Reactor线程模式。

BIO、NIO编程与直接内存、零拷贝

短连接是指 SOCKET 连接后发送接和收完数据后马上断开连接。
长连接指建立 SOCKET 连接后不管是否使用都保持连接。

在通信编程里,我们关注的其实也就是三个事情:连接(客户端连接服务器,服务器等待和接收连接)、读网络数据、写网络数据,所有模式的通信编程都是围绕着这三件事情进行的。
下面要说的BIO 和 NIO 其实都是处理上面三件事,只是处理的方式不一样。

原生 JDK 网络编程 BIO(连接和io操作都由一个线程处理)
BIO,意为 Blocking I/O,即阻塞的 I/O。
bio 的阻塞,主要体现在两个地方。
①若一个服务器启动就绪,那么主线程就一直在等待着客户端的连接,这个等待过程中主线程就一直在阻塞。
②在连接建立之后,在读取到 socket 信息之前,线程也是一直在等待,一直处于阻塞的状态下的。

BIO可以通过使用线程池来处理每个连接,防止阻塞,但是加了线程池虽说提高了服务端能处理的连接数,但是他会到受操作系线程数的限制。如果发生读取数据较慢时(比如数据量大、网络传输慢等),大量并发的情况下,其他客户端建立连接的请求,只能一直等待,这就是最大的弊端。要提高连接数,还得要用NIO(epoll原理),且NIO只需要一个线程。

原生 JDK 网络编程- NIO
什么是 NIO?
NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 BIO 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO 被称为 no-blocking io 或者 new io 都说得通。

和 BIO 的主要区别
1.面向流与面向缓冲:Java NIO 和 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。
IO它不能前后移动流中的数据,如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。所以NIO可以在缓冲区前后移动流中的数据。
2.阻塞与非阻塞 IO:Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。
Java NIO的非阻塞模式, 一个线程请求写 入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。

NIO 三大核心组件
NIO 有三大核心组件:Selector 选择器(相当于Reactor)、Channel 管道(和事件相关)、buffer 缓冲区。
Selector相当于reactor设计模式的反应器
Channel类似reactor设计模式的事件处理器,和事件有关。
Channel 可以设置对哪些 IO 事件感兴趣。
可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写。
通道中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。
buffer 缓冲区:用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。
Selector(管理Channel、注册事件到channel)

通道,被建立的一个应用程序和操作系统交互事件、传递内容的渠道(注意是连接到操作系统)。那么既然是和操作系统进行内容的传递,那么说明应用程序可以通过通道读取数据,也可以通过通道向操作系统写数据,而且可以同时进行读写(但是应用程序不是直接和通道直接打交道,而是和Buffer打交道)。

网络数据的流向是:网络-》通道-》缓冲区》应用程序

Reactor 模式在NIO的体现
在这里插入图片描述
buffer 缓冲区
我们前面说过 JDK NIO 是面向缓冲的。Buffer 就是这个缓冲,用于和 NIO 通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中的。以写为例,应用程序都是将数据写入缓冲,再通过通道把缓冲的数据发送出去,读也是一样,数据总是先从通道读到缓冲,应用程序再读缓冲的数据。


private void doConnect() throws IOException{
/非阻塞的连接/
if(socketChannel.connect(new InetSocketAddress(host,port))){
socketChannel.register(selector,SelectionKey.OP_READ);
}else{
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
上面是客户端nio连接服务端的代码,为什么调用了connect后还需要关注OP_CONNECT事件,因为nio是非阻塞的,且连接是需要3次握手的,调用了connect后不一定马上返回true即代表的连接成功,可能返回false(false不表示连接失败,只是3次握手还未完成)。所以需要关注OP_CONNECT事件,等待连接成功通知。


//连接事件
if(key.isConnectable()){
if(sc.finishConnect()){
socketChannel.register(selector,
SelectionKey.OP_READ);
}
else System.exit(1);
}
isConnectable表示收到连接操作已经完成事件,结果可能是连接成功/连接失败。(相当于接口请求完了,可能返回success/error)
finishConnect(),这个方法是进一步确认连接是否建立成功,可以安全使用这个连接发送或者接收数据。
这是和BIO的区别,BIO,socket.connect()还没连接完成,是会一直阻塞的。而socketChannel.connect不会阻塞。且NIO只需要一个线程,不需要用到线程池。
上面是客户端nio连接服务端的代码。

reactor模式和线程没有关系,nio reactor模式使用的是单线程,而redis6.0以前的reactor模式也是单线程,6.0开始使用多线程。nio是reactor的一种实现。netty也是reactor的一种实现,也用到了reactor模式,但不是单线程。
所以netty可以说是对nio的一种改进。即nio和netty都是reactor的一种实现。

多路复用是指多个连接使用一个线程。

直接内存
直接内存(即Native内存,也叫堆外内存)
HeapByteBuffer 与 DirectByteBuffer,在原理上,前者可以看出分配的 buffer是在heap区域的,其实真正 flush 到远程的时候会先拷贝到直接内存,再做下一步操作;在 NIO 的框架下,很多框架会采用 DirectByteBuffer 来操作,这样分配的内存不再是在 java heap上,经过性能测试,可以得到非常快速的网络交互,在大量的网络交互下,一般速度会比HeapByteBuffer 要快速好几倍。

NIO 可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。

直接内存(堆外内存)与堆内存比较
直接内存申请空间耗费更高的性能,当频繁申请到一定量时尤为明显
直接内存 IO 读写的性能要优于普通的堆内存,在多次读写操作的情况下差异明显

应用进程缓冲区即buffer。它可以放在直接内存,也可以放在堆。建议直接放直接内存,而不是放堆。如果放堆的话,比如放jvm的堆。jdk会先在jvm外创建一个存放在直接内存的缓冲区(应用进程缓冲区),传输数据的时候会从堆先传输到存放在直接内存的缓冲,然后再发送到socket缓冲区。也就是说怎样都要先拷贝到直接内存,也就是上面说的。那么如果我们直接将buffer分配在直接内存,就少了一次从堆到直接内存的拷贝,避免了二次拷贝。另外为什么将buffer定义在堆的时候需要,发送数据时先要拷贝到直接内存,因为堆可能会发生gc,堆中的数据位置就会移动,这样写或读的时候就会发生错乱。
在这里插入图片描述
上面说的Socket缓冲区、应用进程缓冲区(nio /Buffer)的区别?
nio buffer即应用进程缓存区,它可以在堆分配也可以在堆外即直接内存上分配。
nio buffer不是socket缓冲区。
nio buffer数据flush 到远程的时候会先拷贝到直接内存,如果nio buffer是在直接内存上分配的,即可以减少一次复制的操作,这样应用进程缓冲区的数据就可以直接通过write写到socket缓冲区中,然后再发送到远程。

nio Buffer是Java程序内部的概念,Socket缓冲区是在操作系统层面的概念。当程序通过 SocketChannel 将nio buffer数据写入到操作系统的网络连接时,数据实际上会被写入到操作系统管理的 Socket 缓冲区中的输出缓冲区部分。

零拷贝
什么是零拷贝?
是指计算机执行操作时,CPU 不需要先将数据从某处内存复制到另一个特定区域。这种技术通常用于通过网络传输文件时节省 CPU 周期和内存带宽,从而提升数据传输效率。
不是说不需要拷贝,只是说减少冗余[不必要]的拷贝。
下面这些组件、框架中均使用了零拷贝技术:Kafka、Netty、Rocketmq、Nginx、Apache。

直接内存访问DMA(Direct Memory Access)的出现,为什么要使用到DMA?
DMA的主要作用是减轻CPU的负担,提高系统的整体性能。如果不使用DMA,那么下面的DMA拷贝数据到系统内核缓冲区(文件读取缓存区)的操作就得由CPU来处理(DMA 控制器也是一种芯片,没有cpu价格高)。 在这里插入图片描述
直接内存和DMA是一样的东西吗?
直接内存是存储数据的地方,而 DMA 是一种数据传输技术,用于优化数据在设备和内存之间的传输过程。它们虽然有关联,但是代表了不同的概念。所以不要和上面的混淆了。
上面的应用进程缓冲区即buffer。它可以放在直接内存,也可以放在堆。如果是放在堆,右边的应用进程缓冲区在拷贝数据到套接字发送缓冲区前,得先拷到直接内存。

传统数据传送需要经过4次上下文切换,先从用户态切换到内核态,再从内核态切换到用户态,再从用户态切换到内核态,再从内核态切换到用户态。

Linux 支持的(常见)零拷贝方式
1.mmap 内存映射(Android binder用到就是这种零拷贝方式)
2.sendfile
3.splice

Java 生态圈中的零拷贝
Linux 提供的零拷贝技术 Java 并不是全支持,支持 2 种(内存映射 mmap、sendfile)
1.mmap:NIO 提供的内存映射MappedByteBuffer
NIO 中的 FileChannel.map()方法其实就是采用了操作系统中的内存映射方式,底层就是调用 Linux mmap()实现的。
将内核缓冲区的内存和用户缓冲区的内存做了一个地址映射。这种方式适合读取大文件,同时也能对文件内容进行更改(因为将数据读取到应用程序缓冲区,可以进行修改),但是如果其后要通过 SocketChannel 发送,还是需要 CPU 进行数据的拷贝。
2:sendfile:NIO 提供的 sendfile
通过该方法传输数据并不需要将源数据从内核态拷贝到用户态,再从用户态拷贝到目标通道的内核态,同时也避免了两次用户态和内核态间的上下文切换,也即使用了“零拷贝”(也就是上面说的真正操作系统 意义上的零拷贝),所以其性能一般高于 Java IO 中提供的方法。
在这里插入图片描述Kafka使用的就是这种零拷贝,这也是为啥kafka高性能。
Netty 的零拷贝(Netty 的零拷贝主要包含三个方面)
在网络通信上,Netty 的接收和发送 ByteBuffer 采用 DIRECT BUFFERS,使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝。
在缓存操作上,Netty 提供了CompositeByteBuf 类,它可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf,避免了各个ByteBuf 之间的拷贝。
在文件传输上,Netty 的通过 FileRegion 包装的 FileChannel.tranferTo 实现文件传输,它可以直接将文件缓冲区的数据发送到目标 Channel,避免了传统通过循环 write 方式导致的内存拷贝问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值