什么是 Netty ?
什么是Netty
Netty是一款提供异步的, 事件驱动的网络应用程序框架和工具, 用以快速开发高性能, 高可靠性的网络服务器和客户端程序.
也就是说, Netty是一个基于NIO的客户,服务器端编程框架. 使用Netty可以确保你快速和简单地开发出一个网络应用, 例如实现了某种协议的客户, 服务端应用. Netty相当简化和流线化了网络应用的编程开发过程, 例如: TCP和UDP的socket服务开发
| 分类 | Netty的特性 |
|---|---|
| 设计 | 1. 统一的 API ,支持多种传输类型( 阻塞和非阻塞的 ) 2. 简单而强大的线程模型 3. 真正的无连接数据报套接字( UDP )支持 4. 连接逻辑组件( ChannelHander 中顺序处理消息 )以及组件复用( 一个 ChannelHandel 可以被多个ChannelPipeLine 复用 ) |
| 易于使用 | 1. 详实的 Javadoc 和大量的示例集 2. 不需要超过 JDK 1.6+ 的依赖 |
| 性能 | 拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟( 得益于池化和复用 ),更低的资源消耗以及最少的内存复制 |
| 健壮性 | 1. 不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError 2. 消除在高速网络中 NIO 应用程序常见的不公平读 / 写比率 |
| 安全性 | 完整的 SSL/TLS 以及 StartTLs 支持,可用于受限环境下,如 Applet 和 OSGI |
| 社区驱动 | 发布快速而且频繁 |
为什么选择 Netty ?
- 使用简单:API 使用简单,开发门槛低。
- 功能强大:预置了多种编解码功能,支持多种主流协议。
- 定制能力强:可以通过 ChannelHandler 对通信框架进行灵活的扩展。
- 性能高:通过与其它业界主流的 NIO 框架对比,Netty 的综合性能最优。
- 成熟稳定:Netty 修复了已经发现的所有 JDK NIO BUG,业务开发人员不需要再为 NIO 的 BUG 而烦恼。
- 社区活跃:版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入。
- 案例丰富:经历了大规模的商业应用考验,质量已经得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等众多行业得到成功商用,证明了它可以完全满足不同行业的商业应用。
实际上,这个也是我们做技术选型的一些参考点,不仅仅适用于 Netty ,也同样适用于其他技术栈。当然,😈 面试都可以酱紫回答,显得很高端。
为什么说 Netty 使用简单?
🦅 我们假设要搭建一个 Server 服务器,使用 Java NIO 的步骤如下:
- 创建 ServerSocketChannel 。
- 绑定监听端口,并配置为非阻塞模式。
- 创建 Selector,将之前创建的 ServerSocketChannel 注册到 Selector 上,监听 SelectionKey.OP_ACCEPT 。
- 循环执行 Selector#select() 方法,轮询就绪的 Channel。
- 轮询就绪的 Channel 时,如果是处于 OP_ACCEPT 状态,说明是新的客户端接入,调用 ServerSocketChannel#accept() 方法,接收新的客户端。
- 设置新接入的 SocketChannel 为非阻塞模式,并注册到 Selector 上,监听 OP_READ 。
- 如果轮询的 Channel 状态是 OP_READ ,说明有新的就绪数据包需要读取,则构造 ByteBuffer 对象,读取数据。
- 这里,解码数据包的过程,需要我们自己编写。
🦅 使用 Netty 的步骤如下:
- 创建 NIO 线程组 EventLoopGroup 和 ServerBootstrap。
- 设置 ServerBootstrap 的属性:线程组、SO_BACKLOG 选项,设置 NioServerSocketChannel 为 Channel
- 设置业务处理 Handler 和 编解码器 Codec 。
- 绑定端口,启动服务器程序。
- 在业务处理 Handler 中,处理客户端发送的数据,并给出响应。
🦅 那么相比 Java NIO,使用 Netty 开发程序,都简化了哪些步骤呢?
- 无需关心 OP_ACCEPT、OP_READ、OP_WRITE 等等 IO 操作,Netty 已经封装,对我们在使用是透明无感的。
- 使用 boss 和 worker EventLoopGroup ,Netty 直接提供多 Reactor 多线程模型。
- 在 Netty 中,我们看到有使用一个解码器 FixedLengthFrameDecoder,可以用于处理定长消息的问题,能够解决 TCP 粘包拆包问题,十分方便。如果使用 Java NIO ,需要我们自行实现解码器。
说说业务中 Netty 的使用场景?
- 构建高性能、低时延的各种 Java 中间件,Netty 主要作为基础通信框架提供高性能、低时延的通信服务。例如:
- RocketMQ ,分布式消息队列。
- Dubbo ,服务调用框架。
- Spring WebFlux ,基于响应式的 Web 框架。
- HDFS ,分布式文件系统。
- 公有或者私有协议栈的基础通信框架,例如可以基于 Netty 构建异步、高性能的 WebSocket、Protobuf 等协议的支持。
- 各领域应用,例如大数据、游戏等,Netty 作为高性能的通信框架用于内部各模块的数据分发、传输和汇总等,实现模块之间高性能通信。
说说 Netty 如何实现高性能?
- 线程模型 :更加优雅的 Reactor 模式实现、灵活的线程模型、利用 EventLoop 等创新性的机制,可以非常高效地管理成百上千的 Channel 。
- 内存池设计 :使用池化的 Direct Buffer 等技术,在提高 IO 性能的同时,减少了对象的创建和销毁。并且,内吃吃的内部实现是用一颗二叉查找树,更好的管理内存分配情况。
- 内存零拷贝 :使用 Direct Buffer ,可以使用 Zero-Copy 机制。
- 协议支持 :提供对 Protobuf 等高性能序列化协议支持。
- 使用更多本地代码。例如:
- 直接利用 JNI 调用 Open SSL 等方式,获得比 Java 内建 SSL 引擎更好的性能。
- 利用 JNI 提供了 Native Socket Transport ,在使用 Epoll edge-triggered 的情况下,可以有一定的性能提升。
- 其它:
- 利用反射等技术直接操纵 SelectionKey ,使用数组而不是 Java 容器等。
- 实现 FastThreadLocal 类,当请求频繁时,带来更好的性能。
Zero-Copy ,在操作数据时,不需要将数据 Buffer 从一个内存区域拷贝到另一个内存区域。因为少了一次内存的拷贝,因此 CPU 的效率就得到的提升。
Netty 的高性能如何体现?
性能是设计出来的,而不是测试出来的。那么,Netty 的架构设计是如何实现高性能的呢?
- 线程模型 :采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同步阻塞 I/O 模式下服务端无法平滑处理客户端线性增长的问题。
- 堆外内存 :TCP 接收和发送缓冲区采用直接内存代替堆内存,避免了内存复制,提升了 I/O 读取和写入性能。
- 内存池设计 :支持通过内存池的方式循环利用 ByteBuf,避免了频繁创建和销毁 ByteBuf 带来的性能消耗。
- 参数配置 :可配置的 I/O 线程数目和 TCP 参数等,为不同用户提供定制化的调优参数,满足不同的性能场景。
- 队列优化 :采用环形数组缓冲区,实现无锁化并发编程,代替传统的线程安全容器或锁。
- 并发能力 :合理使用线程安全容器、原子类等,提升系统的并发能力。
- 降低锁竞争 :关键资源的使用采用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的 CPU 资源消耗问题。
- 内存泄露检测 :通过引用计数器及时地释放不再被引用的对象,细粒度的内存管理降低了 GC 的频率,减少频繁 GC 带来的时延增大和 CPU 损耗。
Netty 的高可靠如何体现?
- 链路有效性检测:由于长连接不需要每次发送消息都创建链路,也不需要在消息完成交互时关闭链路,因此相对于短连接性能更高。为了保证长连接的链路有效性,往往需要通过心跳机制周期性地进行链路检测。使用心跳机制的原因是,避免在系统空闲时因网络闪断而断开连接,之后又遇到海量业务冲击导致消息积压无法处理。为了解决这个问题,需要周期性地对链路进行有效性检测,一旦发现问题,可以及时关闭链路,重建 TCP 连接。为了支持心跳,Netty 提供了两种链路空闲检测机制:
- 读空闲超时机制:连续 T 周期没有消息可读时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取到心跳消息,可以主动关闭链路,重建连接。
- 写空闲超时机制:连续 T 周期没有消息需要发送时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取对方发回的心跳消息,可以主动关闭链路,重建连接。
- 《精尽 Netty 源码解析 —— ChannelHandler(五)之 IdleStateHandler》
- 内存保护机制:Netty 提供多种机制对内存进行保护,包括以下几个方面:
- 通过对象引用计数器对 ByteBuf 进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护。
- 可设置的内存容量上限,包括 ByteBuf、线程池线程数等,避免异常请求耗光内存。
- 优雅停机:优雅停机功能指的是当系统推出时,JVM 通过注册的 Shutdown Hook 拦截到退出信号量,然后执行推出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘和数据库中,等到资源回收和缓冲区消息处理完成之后,再退出。
Netty 的可扩展如何体现?
可定制、易扩展。
- 责任链模式 :ChannelPipeline 基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
- 基于接口的开发 :关键的类库都提供了接口或抽象类,便于用户自定义实现。
- 提供大量的工厂类 :通过重载这些工厂类,可以按需创建出用户需要的对象。
- 提供大量系统参数 :供用户按需设置,增强系统的场景定制性。
并入整理
以下内容由同主题重复文章合并而来,便于集中复习。
Netty 为什么要实现内存管理?
Netty 为什么要实现内存管理?
🦅 老艿艿的理解
在 Netty 中,IO 读写必定是非常频繁的操作,而考虑到更高效的网络传输性能,Direct ByteBuffer 必然是最合适的选择。但是 Direct ByteBuffer 的申请和释放是高成本的操作,那么进行池化管理,多次重用是比较有效的方式。但是,不同于一般于我们常见的对象池、连接池等池化的案例,ByteBuffer 是有大小一说。又但是,申请多大的 Direct ByteBuffer 进行池化又会是一个大问题,太大会浪费内存,太小又会出现频繁的扩容和内存复制!!!所以呢,就需要有一个合适的内存管理算法,解决高效分配内存的同时又解决内存碎片化的问题。
🦅 官方的说法
FROM 《Netty 学习笔记 —— Pooled buffer》 Netty 4.x 增加了 Pooled Buffer,实现了高性能的 buffer 池,分配策略则是结合了 buddy allocation 和 slab allocation 的 jemalloc 变种,代码在io.netty.buffer.PoolArena 中。
官方说提供了以下优势:
频繁分配、释放 buffer 时减少了 GC 压力。 在初始化新 buffer 时减少内存带宽消耗( 初始化时不可避免的要给buffer数组赋初始值 )。 及时的释放 direct buffer 。
🦅 hushi55 大佬的理解
C/C++ 和 java 中有个围城,城里的想出来,城外的想进去! 这个围城就是自动内存管理!
Netty 4 buffer 介绍
Netty4 带来一个与众不同的特点是其 ByteBuf 的实现,相比之下,通过维护两个独立的读写指针, 要比 io.netty.buffer.ByteBuf 简单不少,也会更高效一些。不过,Netty 的 ByteBuf 带给我们的最大不同,就是他不再基于传统 JVM 的 GC 模式,相反,它采用了类似于 C++ 中的 malloc/free 的机制,需要开发人员来手动管理回收与释放。从手动内存管理上升到GC,是一个历史的巨大进步, 不过,在20年后,居然有曲线的回归到了手动内存管理模式,正印证了马克思哲学观: 社会总是在螺旋式前进的,没有永远的最好。
① GC 内存管理分析
的确,就内存管理而言,GC带给我们的价值是不言而喻的,不仅大大的降低了程序员的心智包袱, 而且,也极大的减少了内存管理带来的 Crash 困扰,为函数式编程(大量的临时对象)、脚本语言编程带来了春天。 并且,高效的GC算法也让大部分情况下程序可以有更高的执行效率。 不过,也有很多的情况,可能是手工内存管理更为合适的。譬如: 对于类似于业务逻辑相对简单,譬如网络路由转发型应用(很多erlang应用其实是这种类型), 但是 QPS 非常高,比如1M级,在这种情况下,在每次处理中即便产生1K的垃圾,都会导致频繁的GC产生。 在这种模式下,erlang 的按进程回收模式,或者是 C/C++ 的手工回收机制,效率更高。
Cache 型应用,由于对象的存在周期太长,GC 基本上就变得没有价值。
所以,理论上,尴尬的GC实际上比较适合于处理介于这 2 者之间的情况: 对象分配的频繁程度相比数据处理的时间要少得多的,但又是相对短暂的, 典型的,对于OLTP型的服务,处理能力在 1K QPS 量级,每个请求的对象分配在 10K-50K 量级, 能够在 5-10s 的时间内进行一 次younger GC ,每次GC的时间可以控制在 10ms 水平上, 这类的应用,实在是太适合 GC 行的模式了,而且结合 Java 高效的分代 GC ,简直就是一个理想搭配。
② 影响 Netty 4 引入了手工内存的模式,我觉得这是一大创新,这种模式甚至于会延展, 应用到 Cache 应用中。实际上,结合 JVM 的诸多优秀特性,如果用 Java 来实现一个 Redis 型 Cache、 或者 In-memory SQL Engine,或者是一个 Mongo DB,我觉得相比 C/C++ 而言,都要更简单很多。 实际上,JVM 也已经提供了打通这种技术的机制,就是 Direct Memory 和 Unsafe 对象。 基于这个基础,我们可以像 C 语言一样直接操作内存。实际上,Netty4 的 ByteBuf 也是基于这个基础的。
Netty 如何实现内存管理?
Netty 如何实现内存管理?
这个题目,简单了解即可,如果深入,就要去看 《精尽 Netty 源码解析 —— Buffer》 相关的源码。而且,看完就忘记,比较难和复杂。
当然,看懂那一刻,乐趣无穷,哈哈哈哈。
Netty 内存管理机制,基于 Jemalloc 算法。
- 首先会预申请一大块内存 Arena ,Arena 由许多 Chunk 组成,而每个 Chunk 默认由2048个page组成。
- Chunk 通过 AVL 树的形式组织 Page ,每个叶子节点表示一个 Page ,而中间节点表示内存区域,节点自己记录它在整个 Arena 中的偏移地址。当区域被分配出去后,中间节点上的标记位会被标记,这样就表示这个中间节点以下的所有节点都已被分配了。大于 8k 的内存分配在 PoolChunkList 中,而 PoolSubpage 用于分配小于 8k 的内存,它会把一个 page 分割成多段,进行内存分配。
Netty 如何实现重连?
Netty 如何实现重连?
- 客户端,通过 IdleStateHandler 实现定时检测是否空闲,例如说 15 秒。
- 如果空闲,则向服务端发起心跳。
- 如果多次心跳失败,则关闭和服务端的连接,然后重新发起连接。
- 服务端,通过 IdleStateHandler 实现定时检测客户端是否空闲,例如说 90 秒。
- 如果检测到空闲,则关闭客户端。
- 注意,如果接收到客户端的心跳请求,要反馈一个心跳响应给客户端。通过这样的方式,使客户端知道自己心跳成功。
如下艿艿在自己的 TaroRPC 中提供的一个示例:
- NettyClient.java 中,设置 IdleStateHandler 和 ClientHeartbeatHandler。核心代码如下:
1
2
3
// NettyHandler.java
.addLast("idleState", new IdleStateHandler(TaroConstants.TRANSPORT_CLIENT_IDLE, TaroConstants.TRANSPORT_CLIENT_IDLE, 0, TimeUnit.MILLISECONDS))
.addLast("heartbeat", new ClientHeartbeatHandler())
NettyServer.java中,设置 IdleStateHandler 和 ServerHeartbeatHandler。核心代码如下:
1
2
3
// NettyServer.java
.addLast("idleState", new IdleStateHandler(0, 0, TaroConstants.TRANSPORT_SERVER_IDLE, TimeUnit.MILLISECONDS))
.addLast("heartbeat", new ServerHeartbeatHandler())
- ClientHeartbeatHandler.java 中,碰到空闲,则发起心跳。不过,如何重连,暂时没有实现。需要考虑,重新发起连接可能会失败的情况。具体的,可以看看 《一起学Netty(十四)之 Netty生产级的心跳和重连机制》 文章中的,ConnectionWatchdog 的代码。
- ServerHeartbeatHandler.java 中,检测到客户端空闲,则直接关闭连接。
Netty 的零拷贝实现?
Netty 的零拷贝实现?
Netty 的零拷贝实现,是体现在多方面的,主要如下:
- 【重点】Netty 的接收和发送 ByteBuffer 采用堆外直接内存 Direct Buffer 。
- 使用堆外直接内存进行 Socket 读写,不需要进行字节缓冲区的二次拷贝;使用堆内内存会多了一次内存拷贝,JVM 会将堆内存 Buffer 拷贝一份到直接内存中,然后才写入 Socket 中。
- Netty 创建的 ByteBuffer 类型,由 ChannelConfig 配置。而 ChannelConfig 配置的 ByteBufAllocator 默认创建 Direct Buffer 类型。
- CompositeByteBuf 类,可以将多个 ByteBuf 合并为一个逻辑上的 ByteBuf ,避免了传统通过内存拷贝的方式将几个小 Buffer 合并成一个大的 Buffer 。
- #addComponents(…) 方法,可将 header 与 body 合并为一个逻辑上的 ByteBuf 。这两个 ByteBuf 在CompositeByteBuf 内部都是单独存在的,即 CompositeByteBuf 只是逻辑上是一个整体。
- 通过 FileRegion 包装的 FileChannel 。
- #tranferTo(…) 方法,实现文件传输, 可以直接将文件缓冲区的数据发送到目标 Channel ,避免了传统通过循环 write 方式,导致的内存拷贝问题。
- 通过 wrap 方法, 我们可以将 byte[] 数组、ByteBuf、ByteBuffer 等包装成一个 Netty ByteBuf 对象, 进而避免了拷贝操作。
Netty 自己实现的 ByteBuf 有什么优点?
Netty 自己实现的 ByteBuf 有什么优点?
- A01. 它可以被用户自定义的缓冲区类型扩展
- A02. 通过内置的符合缓冲区类型实现了透明的零拷贝
- A03. 容量可以按需增长
- A04. 在读和写这两种模式之间切换不需要调用 #flip() 方法
- A05. 读和写使用了不同的索引
- A06. 支持方法的链式调用
- A07. 支持引用计数
- A08. 支持池化
- 特别是第 A04 这点,相信很多胖友都被 NIO ByteBuffer 反人类的读模式和写模式给坑哭了。在 《精尽 Netty 源码分析 —— NIO 基础(三)之 Buffer》 中,我们也吐槽过了。😈
什么是 Netty 的内存泄露检测是如何进行实现的?
(原文内容为空,已合并为占位说明)
什么是 Netty 空闲检测?
什么是 Netty 空闲检测?
在 Netty 中,提供了 IdleStateHandler 类,正如其名,空闲状态处理器,用于检测连接的读写是否处于空闲状态。如果是,则会触发 IdleStateEvent 。
IdleStateHandler 目前提供三种类型的心跳检测,通过构造方法来设置。代码如下:
1
2
3
4
5
// IdleStateHandler.javapublic IdleStateHandler(
int readerIdleTimeSeconds,
int writerIdleTimeSeconds,
int allIdleTimeSeconds){
this(readerIdleTimeSeconds, writerIdleTimeSeconds, allIdleTimeSeconds,TimeUnit.SECONDS);}
- readerIdleTimeSeconds 参数:为读超时时间,即测试端一定时间内未接受到被测试端消息。
- writerIdleTimeSeconds 参数:为写超时时间,即测试端一定时间内向被测试端发送消息。
- allIdleTimeSeconds 参数:为读或写超时时间。
另外,我们会在网络上看到类似《IdleStateHandler 心跳机制》这样标题的文章,实际上空闲检测和心跳机制是两件事。
- 只是说,因为我们使用 IdleStateHandler 的目的,就是检测到连接处于空闲,通过心跳判断其是否还是有效的连接。
- 虽然说,TCP 协议层提供了 Keeplive 机制,但是该机制默认的心跳时间是 2 小时,依赖操作系统实现不够灵活。因而,我们才在应用层上,自己实现心跳机制。
原生的 NIO 存在 Epoll Bug 是什么?Netty 是怎么解决的?
🦅 Java NIO Epoll BUG
Java NIO Epoll 会导致 Selector 空轮询,最终导致 CPU 100% 。
官方声称在 JDK 1.6 版本的 update18 修复了该问题,但是直到 JDK 1.7 版本该问题仍旧存在,只不过该 BUG 发生概率降低了一些而已,它并没有得到根本性解决。
🦅 Netty 解决方案
对 Selector 的 select 操作周期进行统计,每完成一次空的 select 操作进行一次计数,若在某个周期内连续发生 N 次空轮询,则判断触发了 Epoll 死循环 Bug 。
艿艿:此处空的 select 操作的定义是,select 操作执行了 0 毫秒。
此时,Netty 重建 Selector 来解决。判断是否是其他线程发起的重建请求,若不是则将原 SocketChannel 从旧的 Selector 上取消注册,然后重新注册到新的 Selector 上,最后将原来的 Selector 关闭。
简单介绍 Netty 的核心组件?
简单介绍 Netty 的核心组件?
Netty 有如下六个核心组件:
- Bootstrap & ServerBootstrap
- Channel
- ChannelFuture
- EventLoop & EventLoopGroup
- ChannelHandler
- ChannelPipeline
2.1 Bootstrap & ServerBootstrap
这 2 个类都继承了AbstractBootstrap,因此它们有很多相同的方法和职责。它们都是启动器,能够帮助 Netty 使用者更加方便地组装和配置 Netty ,也可以更方便地启动 Netty 应用程序。相比使用者自己从头去将 Netty 的各部分组装起来要方便得多,降低了使用者的学习和使用成本。它们是我们使用 Netty 的入口和最重要的 API ,可以通过它来连接到一个主机和端口上,也可以通过它来绑定到一个本地的端口上。总的来说,它们两者之间相同之处要大于不同。
老艿艿:Bootstrap & ServerBootstrap 对于 Netty ,就相当于 Spring Boot 是 Spring 的启动器。
它们和其它组件之间的关系是它们将 Netty 的其它组件进行组装和配置,所以它们会组合和直接或间接依赖其它的类。
Bootstrap 用于启动一个 Netty TCP 客户端,或者 UDP 的一端。
- 通常使用 #connet(…) 方法连接到远程的主机和端口,作为一个 Netty TCP 客户端。
- 也可以通过 #bind(…) 方法绑定本地的一个端口,作为 UDP 的一端。
- 仅仅需要使用一个 EventLoopGroup 。
ServerBootstrap 往往是用于启动一个 Netty 服务端。
- 通常使用 #bind(…) 方法绑定本地的端口上,然后等待客户端的连接。
- 使用两个 EventLoopGroup 对象( 当然这个对象可以引用同一个对象 ):第一个用于处理它本地 Socket 连接的 IO 事件处理,而第二个责负责处理远程客户端的 IO 事件处理。
2.2 Channel
Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel 的 EventLoop 。
在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而 Netty 的 Channel 则提供的一系列的 API ,它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势( 摘自《Netty权威指南( 第二版 )》) :
- 在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
- Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
- 具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,由 Channel 统一负责和调度,功能实现更加灵活。
2.2 EventLoop && EventLoopGroup
Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。
Channel 为Netty 网络操作抽象类,EventLoop 负责处理注册到其上的 Channel 处理 I/O 操作,两者配合参与 I/O 操作。
EventLoopGroup 是一个 EventLoop 的分组,它可以获取到一个或者多个 EventLoop 对象,因此它提供了迭代出 EventLoop 对象的方法。
下图是 Channel、EventLoop、Thread、EventLoopGroup 之间的关系( 摘自《Netty In Action》) :
说说 Netty 的逻辑架构?
Netty 采用了典型的三层网络架构进行设计和开发,其逻辑架构如下图所示:
- Reactor 通信调度层:由一系列辅助类组成,包括 Reactor 线程 NioEventLoop 及其父类,NioSocketChannel 和 NioServerSocketChannel 等等。该层的职责就是监听网络的读写和连接操作,负责将网络层的数据读到内存缓冲区,然后触发各自网络事件,例如连接创建、连接激活、读事件、写事件等。将这些事件触发到 pipeline 中,由 pipeline 管理的职责链来进行后续的处理。
- 职责链 ChannelPipeline:负责事件在职责链中的有序传播,以及负责动态地编排职责链。职责链可以选择监听和处理自己关心的事件,拦截处理和向后传播事件。
- 业务逻辑编排层:业务逻辑编排层通常有两类,一类是纯粹的业务逻辑编排,一类是应用层协议插件,用于特定协议相关的会话和链路管理。由于应用层协议栈往往是开发一次到处运行,并且变动较小,故而将应用协议到 POJO 的转变和上层业务放到不同的 ChannelHandler 中,就可以实现协议层和业务逻辑层的隔离,实现架构层面的分层隔离。
说说 Netty 如何实现高性能?
- 线程模型 :更加优雅的 Reactor 模式实现、灵活的线程模型、利用 EventLoop 等创新性的机制,可以非常高效地管理成百上千的 Channel 。
- 内存池设计 :使用池化的 Direct Buffer 等技术,在提高 IO 性能的同时,减少了对象的创建和销毁。并且,内吃吃的内部实现是用一颗二叉查找树,更好的管理内存分配情况。
- 内存零拷贝 :使用 Direct Buffer ,可以使用 Zero-Copy 机制。
Zero-Copy ,在操作数据时,不需要将数据 Buffer 从一个内存区域拷贝到另一个内存区域。因为少了一次内存的拷贝,因此 CPU 的效率就得到的提升。
- 协议支持 :提供对 Protobuf 等高性能序列化协议支持。
- 使用更多本地代码。例如:
- 直接利用 JNI 调用 Open SSL 等方式,获得比 Java 内建 SSL 引擎更好的性能。
- 利用 JNI 提供了 Native Socket Transport ,在使用 Epoll edge-triggered 的情况下,可以有一定的性能提升。
- 其它:
- 利用反射等技术直接操纵 SelectionKey ,使用数组而不是 Java 容器等。
- 实现 FastThreadLocal 类,当请求频繁时,带来更好的性能。
Netty 的高性能如何体现?
性能是设计出来的,而不是测试出来的。那么,Netty 的架构设计是如何实现高性能的呢?
- 线程模型 :采用异步非阻塞的 I/O 类库,基于 Reactor 模式实现,解决了传统同步阻塞 I/O 模式下服务端无法平滑处理客户端线性增长的问题。
- 堆外内存 :TCP 接收和发送缓冲区采用直接内存代替堆内存,避免了内存复制,提升了 I/O 读取和写入性能。
- 内存池设计 :支持通过内存池的方式循环利用 ByteBuf,避免了频繁创建和销毁 ByteBuf 带来的性能消耗。
- 参数配置 :可配置的 I/O 线程数目和 TCP 参数等,为不同用户提供定制化的调优参数,满足不同的性能场景。
- 队列优化 :采用环形数组缓冲区,实现无锁化并发编程,代替传统的线程安全容器或锁。
- 并发能力 :合理使用线程安全容器、原子类等,提升系统的并发能力。
- 降低锁竞争 :关键资源的使用采用单线程串行化的方式,避免多线程并发访问带来的锁竞争和额外的 CPU 资源消耗问题。
- 内存泄露检测 :通过引用计数器及时地释放不再被引用的对象,细粒度的内存管理降低了 GC 的频率,减少频繁 GC 带来的时延增大和 CPU 损耗。
Netty 的高可靠如何体现?
- 链路有效性检测:由于长连接不需要每次发送消息都创建链路,也不需要在消息完成交互时关闭链路,因此相对于短连接性能更高。为了保证长连接的链路有效性,往往需要通过心跳机制周期性地进行链路检测。使用心跳机制的原因是,避免在系统空闲时因网络闪断而断开连接,之后又遇到海量业务冲击导致消息积压无法处理。为了解决这个问题,需要周期性地对链路进行有效性检测,一旦发现问题,可以及时关闭链路,重建 TCP 连接。为了支持心跳,Netty 提供了两种链路空闲检测机制:
- 读空闲超时机制:连续 T 周期没有消息可读时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取到心跳消息,可以主动关闭链路,重建连接。
- 写空闲超时机制:连续 T 周期没有消息需要发送时,发送心跳消息,进行链路检测。如果连续 N 个周期没有读取对方发回的心跳消息,可以主动关闭链路,重建连接。
- 《精尽 Netty 源码解析 —— ChannelHandler(五)之 IdleStateHandler》
- 内存保护机制:Netty 提供多种机制对内存进行保护,包括以下几个方面:
- 通过对象引用计数器对 ByteBuf 进行细粒度的内存申请和释放,对非法的对象引用进行检测和保护。
- 可设置的内存容量上限,包括 ByteBuf、线程池线程数等,避免异常请求耗光内存。
- 优雅停机:优雅停机功能指的是当系统推出时,JVM 通过注册的 Shutdown Hook 拦截到退出信号量,然后执行推出操作,释放相关模块的资源占用,将缓冲区的消息处理完成或清空,将待刷新的数据持久化到磁盘和数据库中,等到资源回收和缓冲区消息处理完成之后,再退出。
- 《精尽 Netty 源码解析 —— EventLoop(八)之 EventLoop 优雅关闭》
Netty 的可扩展如何体现?
可定制、易扩展。
- 责任链模式 :ChannelPipeline 基于责任链模式开发,便于业务逻辑的拦截、定制和扩展。
- 基于接口的开发 :关键的类库都提供了接口或抽象类,便于用户自定义实现。
- 提供大量的工厂类 :通过重载这些工厂类,可以按需创建出用户需要的对象。
- 提供大量系统参数 :供用户按需设置,增强系统的场景定制性。


