什么是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线程调用)

  1. 创建NioServerSocketChannel
  2. 初始化handler等待调用

注册

  1. 启动nio线程(main线程)
  2. 原生ServerSocketChannel注册至selector未关注事件(nio线程)
  3. 执行NioServerSocketChannel初始化handler(nio线程)

绑定监听端口(nio线程调用)

  1. 原生ServerSocketChannel绑定
  2. 触发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()调用,会让它提前返回

正常的流程应该是这样的:

  1. 注册 SocketChannel,关注 OP_READ(读事件)
  2. 内核通过 epoll 机制监听这个 fd 的读事件
  3. 当数据来了,Selector.select() 会返回
  4. Selector 内部会把这个 key 放进 selectedKeys 集合
  5. 你拿到 key 之后就可以处理读事件了

这时候,Selector 和底层 epoll 的状态是一致的

状态失衡:

  1. 取消某个 SelectionKey(调用了 key.cancel()
  2. 按理说,这个 Channel 不再需要监听,应从 epoll 中注销
  3. 但由于某些 bug 或 race condition,epoll 并没有真正移除它
  4. 下次 select() 调用时,epoll 返回“有事件”(因为它还在监听)
  5. 但 Java NIO 的 selectedKeys 集合中没有把它加入,因为 Selector 认为这个 Key 已经无效
  6. 于是,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流程

  1. selector.select()阻塞直到事件发生
  2. 遍历处理selectedKeys
  3. 拿到一个key,判断事件类型是否为accept
  4. 创建SocketChannel,设置非阻塞
  5. 将SocketChannel注册至selector
  6. 关注selectionKey的read事件

read流程

  1. selector.select()阻塞直到事件发生
  2. 遍历处理selectedKeys
  3. 拿到一个key,判断事件类型是否为read
  4. 读取操作