Netty
1. Netty 介绍
Netty 是一个异步的、基于事件驱动的网络应用框架,用于快速开发可维护、高性能的网络服务器和客户端。Netty 在 Java 网络应用框架中的地位就好比:Spring 框架在 JavaEE 开发中的地位。
RocketMQ、ES、Dubbo、ZK
- Kafka 自己实现了网络模型做 RPC。底层基于 Java NIO,采用和 Netty 一样的 Reactor 线程模型。
- Redis 也是自己的 IO 多路复用和事件分发器,原理类似
1.1. IO
[[IO多路复用]]
1.2. 原生 NIO 存在的问题
- NIO 的类库和 API 繁杂,使用麻烦:需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等。
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
- 开发工作量和难度都非常大:例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常流的处理等等。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU100%。直到 JDK1.7 版本该问题仍旧存在,没有被根本解决。
1.3. Netty 的优势
- Netty vs NIO,工作量大,bug 多
- 需要自己构建协议
- 解决 TCP 传输问题,如粘包、半包
- epoll 空轮询导致 CPU 100%
- 对 API 进行增强,使之更易用,如 FastThreadLocal => ThreadLocal,ByteBuf => ByteBuffer
- Netty vs 其它网络应用框架
- Mina 由 apache 维护,将来 3.x 版本可能会有较大重构,破坏 API 向下兼容性,Netty 的开发迭代更迅速,API 更简洁、文档更优秀
- 久经考验,16 年,Netty 版本
2. Epoll
[[框架源码专题-Redis-4、原理进阶-数据类型-单线程多路复用#^tmrkhl]]
3. 三种 Reactor 网络模型⭐️🔴
https://juejin.cn/post/6892687008552976398#heading-20
https://www.cnblogs.com/alimayun/p/12509771.html
3.1. 传统阻塞 I/O 服务模型->每客户端 1 线程
3.1.1. 工作原理图
黄色的框表示对象,蓝色的框表示线程
白色的框表示方法(API)
3.1.2. 模型特点
采用阻塞 IO 模式获取输入的数据
每个连接都需要独立的线程完成数据的输入,业务处理,数据返回
3.1.3. 问题分析
- 当并发数很大,就会创建大量的线程,占用很大系统资源
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler 对象中的 read 操作,导致上面的处理线程资源浪费
3.2. 单 Reactor 单线程->连接应答和业务处理使用同一个线程
3.2.1. 方案说明
- Select 是前面 I/O 复用模型介绍的标准网络编程 API,可以实现应用程序通过一个阻塞对象监听多路连接请求
- Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
- 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
- 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
- Handler 会完成 Read → 业务处理 → Send 的完整业务流程
结合实例:服务器端用一个线程通过多路复用搞定所有的 IO 操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,前面的 NIO 案例就属于这种模型。
3.2.2. 优缺点分析
- 优点:模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
- 缺点:性能问题,只有一个线程,无法完全发挥多核 CPU 的性能。Handler 在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈
- 缺点:可靠性问题,线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
- 使用场景:客户端的数量有限,业务处理非常快速,比如 Redis 6 之前,所有任务都是单线程完成,在业务处理的时间复杂度 O(1) 的情况。但 Redis 中具体的实现逻辑不一样,Redis 分为 3 个处理器:连接应答处理器、命令回复处理器、命令请求处理器。
3.3. 单 Reactor 多线程
3.3.1. 方案说明
- Reactor 对象通过 Select 监控客户端请求事件,收到事件后,通过 Dispatch 进行分发
- 如果是建立连接请求,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理完成连接后的各种事件
- 如果不是连接请求,则由 Reactor 分发调用连接对应的 handler 来处理(也就是说连接已经建立,后续客户端再来请求,那基本就是数据请求了,直接调用之前为这个连接创建好的 handler 来处理)
- handler 只负责响应事件,不做具体的业务处理(这样不会使 handler 阻塞太久),通过 read 读取数据后,会分发给后面的 worker 线程池的某个线程处理业务。【业务处理是最费时的,所以将业务处理交给线程池去执行】
- worker 线程池会分配独立线程完成真正的业务,并将结果返回给 handler
- handler 收到响应后,通过 send 将结果返回给 client
3.3.2. 优缺点分析
优点:可以充分利用多核 CPU 的处理能力。
缺点:多线程数据共享和访问比较复杂;Reactor 承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈。
3.4. 主从 Reactor 多线程⭐️🔴
针对单 Reactor 多线程模型中,Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 Reactor 在多线程中运行。
3.4.1. 方案说明
SubReactor 是可以有多个的,如果只有一个 SubReactor 的话那和单 Reactor 多线程就没什么区别了。
- Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
- 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
- subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
- 当有新事件发生时,subreactor 就会调用对应的 handler 处理
- handler 通过 read 读取数据,分发给后面的 worker 线程处理
- worker 线程池分配独立的 worker 线程进行业务处理,并返回结果
- handler 收到响应的结果后,再通过 send 将结果返回给 client
- Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor
3.4.2. 优缺点分析
优点:父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。
这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。
3.5. Reactor 模式小结
3.5.1. 3 种模式用生活案例来理解
单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服务
单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
主从 Reactor 多线程,多个前台接待员,多个服务生
3.5.2. Reactor 模式具有如下的优点
响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的(比如你第一个 SubReactor 阻塞了,我可以调下一个 SubReactor 为客户端服务)
可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
扩展性好,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源
复用性好,Reactor 模型本身与具体事件处理逻辑无关,具有很高的复用性
3.5.3. 注意点
注意:前面介绍的四种 reactor 模式在具体实现时遵循的原则是:每个文件描述符只由一个线程操作。这样可以轻轻松松解决消息收发的顺序性问题,也避免了关闭文件描述符的各种 race condition。一个线程可以操作多个文件描述符,但是一个线程不能操作别的线程拥有的文件描述符。这一点不难做到。epoll 也遵循了相同的原则。Linux 文档中并没有说明,当一个线程证阻塞在 epoll_wait 时,另一个线程往 epoll fd 添加一个新的监控 fd 会发生什么。新 fd 上的事件会不会在此次 epoll_wait 调用中返回?为了稳妥起见,我们应该吧对同一个 epoll fd 的操作 (添加、删除、修改等等) 都放到同一个线程中执行。
3.5.4. 与 Epoll 关系
reactor 就是对 epoll 进行封装,进行网络 IO 与业务的解耦,将 epoll 管理 IO 变成管理事件,整个程序由事件进行驱动执行。就像下图一样,有就绪事件返回,reactor:由事件驱动执行对应的回调函数;epoll:需要自己判断。
4. Netty 的网络模型⭐️🔴
❕ ^9ze94j
- Netty 抽象出两组线程池,BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写
- BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
- NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个 NioEventLoop 都有一个 Selector,用于监听绑定在它上面的 socket 的网络通讯
- NioEventLoopGroup 可以有多个线程,即可以含有多个 NioEventLoop
- 每个 BossGroup 下面的 NioEventLoop 循环执行的步骤有 3 步
- 轮询 accept 事件
- 处理 accept 事件,与 client 建立连接,生成 NioScocketChannel,并将其注册到 workerGroup 的某个 NioEventLoop 上的 Selector 上面
- 继续处理任务队列的任务,即 runAllTasks
- 每个 WorkerGroup NioEventLoop 循环执行的步骤
- 轮询 read,write 事件
- 处理 I/O 事件,即 read,write 事件,在对应 NioScocketChannel 处理
- 处理任务队列的任务,即 runAllTasks
- 每个 Worker NioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器。
https://blog.csdn.net/Youth_lql/article/details/115734142
5. Netty 的逻辑架构⭐️🔴
Netty 的逻辑处理架构是典型的网络分层架构设计的,分别为网络通信层、事件调度层和服务编排层。每一层各司其职,如下图所示:
5.1. 网络通信层
网络通信层的职责就是执行网络 I/O 的操作。它支持多种网络协议和 I/O 模型的连接操作。当网络数据读取到内核缓冲区后,会触发各种网络事件,这些网络事件会分发给事件调度层来处理。网络通信层有三个核心组件:Bootstrap、ServerBootstrap 和 Channel。
5.1.1. Bootstrap
意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,用于连接服务端的,一般用于 Client 的开发上
5.1.2. ServerBootstrap
是服务端启动引导类,用于服务端启动绑定本地端口,一般绑定两个: EventLoopGroup(主从多线程 Reactor),一个称为 boss,另外一个成为 worker。
Bootstrap/ServerBootstrap 组件更加方便我们配置和启动 Netty 程序,它是整个 Netty 程序的入口,串联了 Netty 所有核心组件的初始化工作。
5.1.3. channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。它是 Netty 网络通信的载体。Channel 提供了基本的 API 用于网络 I/O 操作,例如 register 注册事件、bind 绑定事件、connect 连接事件、read 读事件、write 写事件、flush 数据冲刷等。
Netty 中的 Channel 是基于 JDK NIO 的 Channel 上做了更高层次的抽象,同时屏蔽了底层 Socket 的复杂性,增强了 Channel 的功能,在使用 Netty 时不需要再直接跟 JDK Socket 打交道。
5.2. 事件调度层
事件调度层通过 Reactor 线程模型对各类事件进行聚合处理,通过 Selector 主循环线程集成多种事件 (I/O 事件、信号事件、定时事件等),实际的业务处理逻辑交给服务编排层的 ChannelHandler 来处理。
事件调度层最核心的组件就是 EventLoop 和 EventLoopGroup:EventLoopGroup 实质上是 Netty 基于 JDK 线程池的抽象,本质就是线程池,主要负责接收 I/O 请求,并分配线程执行处理。而 EventLoop 可以理解成一个线程,EventLoop 创建出来之后会跟一个线程绑定并且处理 Channel 中的事件。如下图所示:
5.3. 服务编排层
服务编排的职责是组装各类服务,它是 Netty 的核心处理链,用以实现网络事件的动态编排和有序传播。
服务编排的核心组件有:ChannelPipeline、ChannelHandler、ChannelHandlerContext。
5.3.1. ChannelPipeline
ChannelPipeline 是 Netty 的核心编排组件,负责组装各类的 ChannelHandler
ChannelPipeline 本质是一个双向链表,通过责任链模式将不同的 ChannelHandler 链接在一起。
每个 Channel 有且仅有一个 ChannelPipeline 与之对应。
当 I/O 读写事件触发时,ChannelPipeline 会一次调用 ChannelHandler 列表对 Channel 的数据进行拦截和处理。
5.3.2. ChannelHandler
实际数据的编解码以及加工处理的操作是由 ChannelHandler 来完成的
5.3.3. ChannelHandlerContext
每创建一个 Channel 都会绑定一个新的 ChannelPipeline,ChannelPipeline 中每加入一个 ChannelHandler 都会绑定一个 ChannelHandlerContext。由此可见,ChannelPipeline、ChannelHandlerContext、ChannelHandler 三个组件的关系是密切相关的。
ChannelHandlerContext 用于保存 ChannelHandler 上下文,通过 ChannelHandlerContext 我们可以知道 ChannelPipeline 和 ChannelHandler 的关联关系。
6. Netty 特性
6.1. 零拷贝
并发进阶-1、零拷贝6.2. bytebuf
6.2.1. ByteBuf 优势
- 池化 - 可以重用池中 ByteBuf 实例,更节约内存,减少内存溢出的可能
- 读写指针分离,不需要像 ByteBuffer 一样切换读写模式
- 可以自动扩容
- 支持链式调用,使用更流畅
- 很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf
6.3. 高性能无锁队列 (Mpsc)
队列相关:并发编程专题-基础-19、Queue
Mpsc 的全称是 Multi Producer Single Consumer,多生产者单消费者。Mpsc Queue 可以保证多个生产者同时访问队列是线程安全的,而且同一时刻只允许一个消费者从队列中读取数据。Netty Reactor 线程中任务队列 taskQueue 必须满足多个生产者可以同时提交任务,所以 JCTools 提供的 Mpsc Queue 非常适合 Netty Reactor 线程模型。
MpscArrayQueue 还只是 Jctools 中的冰山一角,其中蕴藏着丰富的技术细节,我们对 MpscArrayQueue 的知识点做一个简单的总结。
- 通过大量填充 long 类型变量解决伪共享问题。
- 环形数组的容量设置为 2 的次幂,可以通过位运算快速定位到数组对应下标。
- 入队 offer() 操作中 producerLimit 的巧妙设计,大幅度减少了主动获取消费者索引 consumerIndex 的次数,性能提升显著。
- 入队和出队操作中都大量使用了 UNSAFE 系列方法,针对生产者和消费者的场景不同,使用的 UNSAFE 方法也是不一样的。Jctools 在底层操作的运用上也是有的放矢,把性能发挥到极致。
6.4. 内存池
13 举一反三:Netty 高性能内存管理设计(上)
https://miaowenting.site/2020/02/09/Netty%E5%86%85%E5%AD%98%E6%B1%A0%E5%8C%96%E7%AE%A1%E7%90%86/
6.5. 解决黏包 (粘包) 半包
6.5.1. 黏包 (粘包)
现象,发送 abc def,接收 abcdef
原因
1. 应用层:接收方 ByteBuf 设置太大(Netty 默认 1024)
2. 滑动窗口:假设发送方 256 bytes 表示一个完整报文,但由于接收方处理不及时且窗口大小足够大,这 256 bytes 字节就会缓冲在接收方的滑动窗口中,当滑动窗口中缓冲了多个报文就会粘包
3. Nagle 算法:会造成粘包
6.5.2. 半包
- 现象,发送 abcdef,接收 abc def
- 原因
- 应用层:接收方 ByteBuf 小于实际发送数据量
- 滑动窗口:假设接收方的窗口只剩了 128 bytes,发送方的报文大小是 256 bytes,这时放不下了,只能先发送前 128 bytes,等待 ack 后才能发送剩余部分,这就造成了半包
- MSS 限制:当发送的数据超过 MSS 限制后,会将数据切分发送,就会造成半包
本质是因为 TCP 是流式协议,消息无边界
7. 面试题
请说一下Netty中Reactor模式的理解
[[BIO,NIO,AIO,Netty面试题 35道.pdf]]
[[3.358道.pdf]]
8. 实战经验
9. 参考与感谢
❕ ^hefe8a
9.1. 黑马
9.1.1. 视频
9.1.2. 资料
1 |
|
9.2. 尚硅谷 Netty 视频教程(B 站超火,好评如潮)
https://www.bilibili.com/video/BV1DJ411m7NR?p=4&vd_source=c5b2d0d7bc377c0c35dbc251d95cf204
9.3. 网络笔记
Netty 架构: https://www.maishuren.top/archives/netty-zheng-ti-jia-gou-jie-xi
尚硅谷学习笔记: https://blog.csdn.net/Youth_lql/article/details/115734142