chapter18 EventLoop和EventLoopGroup

来源:互联网 发布:淘宝界面 购物车代码 编辑:程序博客网 时间:2024/06/04 20:02

  • chapter18 EventLoop和EventLoopGroup
    • Reactor单线程模型
    • Reactor多线程模型
    • 主从Reactor多线程模型
    • 4Netty的线程模型
    • 5NioEventLoop源码分析

chapter18 EventLoop和EventLoopGroup

1. Reactor单线程模型:

  • 异步非阻塞,理论上一个线程可以独立处理所有IO相关操作。
  • 不适合高负载、高并发场景原因:
    1. 一个NIO线程同时处理成百上千的链路,性能无法支撑,即便NIO线程的cpu负荷达到100%,也无法满足海量消息的编码、解码、读取与发送。
    2. 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时重发导致消息积压和处理超时。
    3. 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,造成节点故障。

2. Reactor多线程模型:

  • 最大的区别就是有一组NIO线程来处理IO操作。
  • 特点:
    1. 有专门一个NIO线程——Acceptor线程用于监听服务端,接收客户端的TCP连接请求。
    2. 网络IO操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送。
    3. 一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
    4. 问题:
      • 一个NIO线程负责监听和处理所有客户端连接,并发百万客户端连接或者服务端需要对客户端握手进行安全认证,此类场景下,单独一个Acceptor可能会存在性能问题。

3. 主从Reactor多线程模型

  • 特点:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。
  • Acceptor线程池仅仅用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责处理后续的IO操作。

4、Netty的线程模型

  • 通过调整线程池个数、是否共享线程池等方式,Netty的Reactor线程模型可以在单线程、多线程和主从线程间切换。
  • 服务启动的时候,创建了两个独立的Reactor线程池,一个用于接收客户端的TCP链接,一个用于处理IO相关的读写操作,或者执行系统Task、定时任务Task等。
  • Netty用于接收客户端请求的线程池职责:

    1. 接收客户端TCP链接,初始化Channel参数;
    2. 将链路状态变更事件通知给CHannelPipeline
  • Netty处理IO操作的Reactor线程池职责:

    1. 异步读取通信对端的数据报,发送读事件到ChannelPipeline
    2. 异步发送消息到通信对端,调用ChannelPipeline的消息发送接口
    3. 执行系统调用Task;
    4. 执行定时任务Task,例如链路空闲状态监测定时任务。
  • 采用无锁化设计,如在IO线程内部进行串行操作,避免多线程竞争导致性能下降,相比一个队列多个工作线程的模型性能更优。

  • 最佳实践:[总的来说Handler尽早释放,完成IO线程内的串行]

    1. 创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO IO线程;
    2. 尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外);
    3. 解码要放在NIO线程调用的解码Handler中机型,不要切换到用户线程中完成消息的解码;
    4. 如果业务逻辑操作非常简单,没有复杂业务逻辑计算,没有可能会导致线程被阻塞的磁盘操作等,可以直接在NIO线程上完成业务逻辑编排,不需要切换用户线程。
    5. 如果业务逻辑复杂,不要在NIO线程完成业务逻辑计算,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快释放,处理其他IO操作。
  • 推荐线程数量计算公式:

    1. 线程数量 =(线程总时间/瓶颈资源时间)* 瓶颈资源的线程并行数
    2. QPS=1000/线程总时间*线程数

5、NioEventLoop源码分析

  1. NioEventLoop设计原理

    • 除了负责IO的读写外,还处理以下两类任务:
      1. 系统Task:通过调用NioEventLoop的execute(Runnable task)方法实现,Netty有很多系统Task,解决IO线程和用户线程同时操作网络资源时,防止并发操作导致资源竞争,将用户封装成Task放入消息队列中,由IO线程负责执行,实现局部无锁化。
      2. 定时任务:通过调用NioEventLoop的schedule(Runnable command,long dalay,TimeUnit unit)方法实现。
  2. NioEventLoop

    • 由于需要处理网络IO读写事件,因此必须聚合一个多路复用器对象(Selector)。Netty对Selector的selectedKeys进行了优化,用户可以通过io.netty.noKeySetOptimization开关决定是否启用该优化项。默认不打开。优化的好处,在于每次在轮询到nio事件的时候,netty只需要O(1)的时间复杂度就能将 SelectionKey 塞到 set中去,而jdk底层使用的hashSet需要O(lgn)的时间复杂度
    • 所有的逻辑都在for循环体内运行。只有当NioEventLoop接收到退出指令的时候才退出循环体。
    • Seletor空轮询导致的epoll bug使IO线程一直处于100%状态,Netty解决方案:
      1. 对Selector的select操作周期进行统计;
      2. 每完成一次空的select操作进行一次计数;
      3. 在某个周期(例如100ms)内如果连续发生N次空轮询,说明触发了epoll()死循环bug
      4. 监听到Selector处于死循环后,需要通过重建Selector的方式让系统恢复正常。