Netty常见问题
什么是Netty
Netty是一个基于Java NIO (Non-blocking I/O)的网络通信框架,它提供了高性能、可扩展性和可靠性的网络编程解决方案,是一个广泛应用于分布式系统的网络通信库
Netty有哪些核心组件
Netty由三层结构构成:
- 网络通信层:有三个组件:Bootstrap、ServerBootstrap、Channel
- Bootstrap负责客户端启动,连接指定服务器
- ServerBootstrap负责服务器启动,监听指定端口
- Channel是网络通信的载体
- 事件调度层:有EventLoopGroup、EventLoop
- EventLoopGroup本质上是一个线程池,主要是负责接受IO请求,分配线程处理请求
- EventLoop是具体的一个线程
- 服务编排层:ChannelPipline、ChannelHandler、ChannelHandlerContext
- ChannelPipline负责将多个ChannelHandler组成一个链,可以看成一个流水线
- ChannelHandler是对数据进行处理,可以看作成一道道工序
- ChannelHandlerContext用来保存ChannelHandler的上下文信息
Netty有几种线程模型
一共三种Reactor模型
- 单线程单Reactor模型,有三个组件:
- Acceptor:处理客户端连接请求
- Reactor:监听和分发事件
- Handler:业务处理
- 缺点:
- 如果有一个Handler阻塞,会影响整个服务的吞吐量
- 无法充分利用多核CPU的性能

- 多线程多Reactor模型(主从多线程Reactor模型)
- 把Reactor拆分为了:Main-Reactor和SubReactor
- Main-Reactor负责接受连接,然后随机分配给SubReactor
- 主从 Reactor 多线程 + 业务线程池(WorkerGroup + 自定义业务线程池)
- WorkerGroup 只做 IO(读写、解码、编码)
- 业务逻辑交由独立线程池执行(例如使用
DefaultEventExecutorGroup
) - 优点:避免耗时业务阻塞IO线程,更高的业务并发能力,线程资源更加分离
- 缺点:如果业务线程池设置不当,也可能成为瓶颈
Netty的优势
- 使用简单:封装了NIO的很多细节,使用更简单
- 功能强大:预置了多种编解码功能,支持多种主流协议
- 定制能力强:可以通过ChannelHandler对通信框架进行灵活地扩展
Netty高性能表现在哪些方面
- IO线程模型:同步非阻塞,用最少的资源做更多的事
- 内存零拷贝:尽量减少不必要的内存拷贝,实现了更高效率的传输
- 内存池设计:申请的内存可以重用,主要指直接内存
- 串形化处理读写:避免使用锁带来的性能开销
Netty的心跳机制
Netty的心跳机制是指通过定时发送心跳包来保持连接的机制。通常情况下,当连接空闲一段时间后,就会发送心跳包,如果对端没有响应,则判断连接已经失效,需要进行重连或其他操作。Netty的心跳机制可以通过IdleStateHandler
实现,它可以检测连接的空闲状态并触发相应事件
Netty的内存管理机制
Netty的内存管理机制采用了堆外内存池的方式,即通过ByteBuf实现内存的分配和回收。Netty提供了两种ByteBuf实现类:PooledByteBuf和UnpooledByteBuf,前者是基于内存池实现的,可以重复利用内存,提高了内存的利用率;后者是基于堆内存实现的,不需要进行内存的池化和池化管理
Netty的常用协议
- HTTP/HTTPS
- WebSocket
- TCP/UDP
- DNS
- STOMP
Netty的ChannelHandler有哪些类型
- ChannelInboundHandler:用于处理Channel的入站数据和事件
- ChannelOutboundHandler:用于处理Channel的出站数据和事件
- ChannelDuplexHandler:同时实现了ChannelInboundHandler和ChannelOutboundHandler接口,可以同时处理入站数据和出站数据
- ChannelHandlerContext:用于传递ChannelHandler之间的上下文信息
Netty的ByteBuf和Java NIO的ByteBuffer有什么区别
- 内存分配方式不同
- Netty的ByteBuf采用了池化的方式进行内存分配,可以重复利用内存,减少了内存的分配和回收开销
- Java NIO的ByteBuffer采用了直接或堆内存的方式进行内存分配,需要进行内存的分配和回收管理。
- 功能不同
- Netty的ByteBuf提供了更加丰富的API,支持对缓冲区进行动态扩容、切片、读写标记等操作
- Java NIO的ByteBuffer只提供了基本的读写方法
Netty的启动流程
- 创建EventLoopGroup对象,用于管理Channel的EventLoop
- 创建ServerBootstrap对象,用于配置ServerChannel和Channel的参数和属性
- 绑定ServerChannel监听端口,并调用bind()方法启动服务
- 创建ChannelInitializer对象,用于初始化Channel的处理器链
- 在ChannelPipeline中添加ChannelHandler处理器,用于处理Channel上的I/O事件和数据
初始化(main线程调用)
- 创建NioServerSocketChannel
- 初始化handler等待调用
注册
- 启动nio线程(main线程)
- 原生ServerSocketChannel注册至selector未关注事件(nio线程)
- 执行NioServerSocketChannel初始化handler(nio线程)
绑定监听端口(nio线程调用)
- 原生ServerSocketChannel绑定
- 触发NioServerSocketChannel active事件
NioEventLoop
重要组成部分:selector、线程、任务队列
NioEventLoop即会处理io事件,也会处理普通事件
selector何时创建
在构造方法调用时创建
EventLoop为什么有两个selector成员(selector和unwarppedSelector)
为了增强灵活性、性能优化和对JDK selector的bug做规避处理
unwarppedSelector:Java NIO 的原始 Selector,Netty保留它为了保底、调试和 fallback
selector:Netty包装过的Selector,使用自定义的 SelectedSelectionKeySet
替换掉了 JDK 默认的 HashSet
,避免频繁的内存分配和迭代开销
EventLoop的nio线程什么时候启动
在首次调用execute方法时启动,通过state
状态位保证线程只会启动一次
netty的weakup方法如何理解
只有其他线程提交任务时,才会调用selector的weakup方法,并且方法使用weakenup变量,使用CAS来保证并发,如果有多个线程提交任务时,可以避免weakup方法被频繁调用
每次循环时,什么时候进入SelectStrategy.SELECT分支
没有任务时进入,有任务时会调用selectNow方法,顺便拿到io事件
何时会select阻塞,阻塞多久
没有定时任务时,selectDeadLineNanos(截止时间)= 当前时间 + 1s
timeoutMillis(超时时间)= 1s + 0.5ms
nio的空轮询bug在哪里体现的,netty是如何解决的
空轮询bug的表现:调用 Selector.select()
(或 select(timeout)
)无故立即返回0,且连续调用总是如此,即使没有任何事件准备好
问题现象:CPU占用飙升(100%),但程序没有实际处理任何I/O事件,处于所谓的“空轮询死循环”状态
发生原因
这个bug的本质在于Selector与其内部的selectedKey集合状态不一致,通常由以下原因触发:
Channel被取消注册但未从epoll数据结构中清理:例如调用了
SelectionKey.cancel()
,但实际底层并没有完全从 epoll 中注销该事件并发取消key(多线程中对selector操作)
Signal中断(signal中断了selector的阻塞):某些native信号中断了
select()
调用,会让它提前返回
正常的流程应该是这样的:
- 注册 SocketChannel,关注 OP_READ(读事件)
- 内核通过 epoll 机制监听这个 fd 的读事件
- 当数据来了,
Selector.select()
会返回 - Selector 内部会把这个 key 放进
selectedKeys
集合 - 你拿到 key 之后就可以处理读事件了
这时候,Selector 和底层 epoll 的状态是一致的
状态失衡:
- 取消某个 SelectionKey(调用了
key.cancel()
) - 按理说,这个 Channel 不再需要监听,应从 epoll 中注销
- 但由于某些 bug 或 race condition,epoll 并没有真正移除它
- 下次
select()
调用时,epoll 返回“有事件”(因为它还在监听) - 但 Java NIO 的
selectedKeys
集合中没有把它加入,因为 Selector 认为这个 Key 已经无效 - 于是,Selector
select()
调用返回了,但你没有事件可处理
这就造成了 Selector(Java 层)认为没事件,epoll(内核层)认为有事件,二者状态不一致
再进一步,连续多次这样的状态不一致:
- Selector 每次调用
select()
,都会立刻返回 0 selectedKeys
是空的,没有任何事件- 但是底层仍然返回一个假的触发(因为 epoll 没删干净)
- 于是主线程进入空转,形成了所谓的 空轮询 bug
解决办法:netty内部维护了一个selectCnt,记录循环次数,当连续select()
返回0的次数超过阈值,Netty会判断可能遇到空轮询 bug,主动重建一个新的Selector,将旧selector中所有注册的channel重新注册到新selector,再关闭旧selector
ioRadio控制什么,设置100有什么作用
ioRadio控制io事件所占用的时间比例,一般是50,如果设置成100,不会把所有时间都分给io事件,而是先把所有io事件处理完之后,在finally块中运行所有任务
selectedKeys优化
默认对selectedKeys遍历采用的是set形式,netty中会尝试使用数组的形式对selectedKeys进行管理,优化遍历时间
accept流程
- selector.select()阻塞直到事件发生
- 遍历处理selectedKeys
- 拿到一个key,判断事件类型是否为accept
- 创建SocketChannel,设置非阻塞
- 将SocketChannel注册至selector
- 关注selectionKey的read事件
read流程
- selector.select()阻塞直到事件发生
- 遍历处理selectedKeys
- 拿到一个key,判断事件类型是否为read
- 读取操作