Netty学习总结二(Reactor介绍和Netty线程模型)

Netty学习总结一(Netty优点场景、IO模型、JavaNIO)

Netty学习总结二(Reactor介绍和Netty线程模型)

Netty学习总结三(Netty启动流程和重要组件介绍)

Reactor介绍和组成

Reactor反应器模式

先从设计模式开始介绍一下。
事件驱动模型主要包括 4 个基本组件:
事件队列(event queue):接收事件的入口,存储待处理事件。
分发器(event mediator):将不同的事件分发到不同的业务逻辑单元。
事件通道(event channel):分发器与处理器之间的联系渠道。
事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作。
可以看出,相对传统轮询模式,事件驱动有如下优点:

  • 可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑。
  • 高性能,基于队列暂存事件,能方便并行异步处理事件。

反应器模式比“事件驱动编程”更具体.它是在进行事件驱动编程时使用的特定实现技术.但是,这个术语在典型的对话中并没有得到很好的准确使用,所以你应该小心使用它并期望你的听众理解你,你在使用它时应该小心如何解释这个术语。
Reactor模型中定义的三种角色:
Reactor反应器线程:负责监听和响应IO事件,将I/O事件分派给对应的Handler。这里的IO事件,就是NIO中选择器监控的通道IO事件。新的事件包含连接建立就绪、读就绪、写就绪等。
Acceptor:处理客户端新连接,并分派请求到处理器链中。
Handlers处理器:非阻塞的执行业务处理逻辑。与IO事件(或者选择键)绑定,负责IO事件的处理。完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写出到通道等。

Reactor对IO事件的处理流程

第1步:通道注册。IO源于通道(Channel)。IO是和通道(对应于底层连接而言)强相关的。一个IO事件,一定属于某个通道。但是,如果要查询通道的事件,首先要将通道注册到选择器。只需通道提前注册到Selector选择器即可,IO事件会被选择器查询到。
第2步:查询选择。在反应器模式中,一个反应器(或者SubReactor子反应器)会负责一个线程;不断地轮询,查询选择器中的IO事件(选择键)。
第3步:事件分发。如果查询到IO事件,则分发给与IO事件有绑定关系的Handler业务处理器。
第4步:完成真正的IO操作和业务处理,这一步由Handler业务处理器负责。

单Reactor单线程模型

在这里插入图片描述
Reactor反应器和Handers处理器处于一个线程中执行。
单线程Reactor反应器模式,是基于Java的NIO实现的。相对于传统的多线程OIO,反应器模式不再需要启动成千上万条线程,效率自然是大大提升了。但在单线程反应器模式中,Reactor反应器和Handler处理器,都执行在同一条线程上。这样,带来了一个问题:当其中某个Handler阻塞时,会导致其他所有的Handler都得不到执行。
注意,Redis的请求处理也是单线程模型,为什么Redis的性能会如此之高呢?因为Redis的读写操作基本都是内存操作,并且Redis协议比较简洁,序列化/反序列化耗费性能更低

单Reactor多线程模型

该模型在事件处理器(Handler)部分采用了多线程(线程池)。
相对于第一种模型来说,在处理业务逻辑,也就是获取到IO的读写事件之后,交由线程池来处理,handler收到响应后通过send将响应结果返回给客户端。这样可以降低Reactor的性能开销,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:

  • 多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
  • Reactor承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能

主从Reactor多线程模型

是将Reactor分成两部分:

  • mainReactor负责监听server
    socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给subReactor。
  • subReactor主要做和建立起来的socket做数据交互和事件业务处理操作。通常,subReactor个数上可与CPU个数等同。

Nginx、Swoole、Memcached和Netty都是采用这种实现。
消息处理流程:

  1. 从主线程池中随机选择一个Reactor线程作为acceptor线程,用于绑定监听端口,接收客户端连接.
  2. acceptor线程接收客户端连接请求之后创建新的SocketChannel,将其注册到主线程池的其它Reactor线程上,由其负责接入认证、IP黑白名单过滤、握手等操作
  3. 步骤2完成之后,业务层的链路正式建立,将SocketChannel从主线程池的Reactor线程的多路复用器上摘除,重新注册到Sub线程池的线程上,并创建一个Handler用于处理各种连接事件
  4. 当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应
  5. Handler通过Read读取数据后,会分发给后面的Worker线程池进行业务处理
  6. Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理
  7. Handler收到响应结果后通过Send将响应结果返回给Client
    为了提升效率,建议SubReactor的数量和选择器的数量一致。避免多个线程负责一个选择器,导致需要进行线程同步,引起的效率降低。

Reactor的优缺点

Reactor模型具有如下的优点:

  1. 响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;
  2. 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  3. 可扩展性,可以方便地通过增加Reactor实例个数来充分利用CPU资源;
  4. 可复用性,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。

反应器模式的缺点如下:

  • 反应器模式增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
  • 反应器模式需要操作系统底层的IO多路复用的支持,如Linux中的epoll。如果操作系统的底层不支持IO多路复用,反应器模式不会有那么高效。
  • 同一个Handler业务线程中,如果出现一个长时间的数据读写,会影响这个反应器中其他通道的IO处理。例如在大文件传输时,IO操作就会影响其他客户端(Client)的响应时间。因而对于这种操作,还需要进一步对反应器模式进行改进。

Netty 线程模型

Nginx、Redis、Netty都是基于反应器模式的。
Netty 主要基于主从 Reactors 多线程模型做了一定的修改,其中主从 Reactor 多线程模型有多个 Reactor。
这里引用 Doug Lee 大神的 Reactor 介绍:Scalable IO in Java 里面关于主从 Reactor 多线程模型的图:

在这里插入图片描述

  • MainReactor 负责客户端的连接请求,并将请求转交给 SubReactor。
  • SubReactor 负责相应通道的 IO读写请求。
  • 非 IO 请求(具体逻辑处理)的任务则会直接写入队列,等待 worker threads 进行处理。
//创建反应器线程组
EventLoopGroup bossGroup = newNioEventLoopGroup();
EventLoopGroup workerGroup = newNioEventLoopGroup();

ServerBootstrap server= newServerBootstrap();
//设置反应器线程组
server.group(bossGroup, workerGroup)
//设置nio类型的通道
.channel(NioServerSocketChannel. class)

虽然 Netty 的线程模型基于主从 Reactor 多线程,借用了 MainReactor 和 SubReactor 的结构。但是实际实现上 SubReactor 和 Worker 线程在同一个线程池中

  • bossGroup 线程池则只是在 Bind 某个端口后,获得其中一个线程作为 MainReactor,专门处理端口的 Accept
    事件,每个端口对应一个 Boss 线程。
  • workerGroup 线程池会被各个 SubReactor 和 Worker 线程充分利用。

注意问题

  1. boss 线程池是处理 accept事件的,不管线程池多大,只会使用一个线程,既然只使用一个线程为什么要用线程池呢?
    主要是异常的情况下,线程die了,可以再创建一个新线程。

  2. 那什么情况下boss线程池可以使用多个线程呢?
    那就是当ServerBootstrap bind多个端口时。每个端口都有一个线程eventloop accept事件。

  3. 难道不能使用多线程来监听同一个对外端口么,即多线程epoll_wait到同一个epoll实例上?
    epoll相关的主要两个方法是epoll_wait和epoll_ctl,多线程同时操作同一个epoll实例,那么首先需要确认epoll相关方法是否线程安全:epoll是通过锁来保证线程安全的, epoll中粒度最小的自旋锁ep->lock(spinlock)用来保护就绪的队列, 互斥锁ep->mtx用来保护epoll的重要数据结构红黑树。
    java中多线程来监听同一个对外端口,epoll方法是线程安全的,这样就可以使用使用多线程监听epoll_wait了么,当然是不建议这样干的,除了epoll的惊群问题之外,还有一个就是,一般开发中我们使用epoll设置的是LT模式,

水平触发方式,与之相对的是ET默认,前者只要连接事件未被处理就会在epoll_wait时始终触发,后者只会在真正有事件来时在epoll_wait触发一次

这样的话,多线程epoll_wait时就会导致第一个线程epoll_wait之后还未处理完毕已发生的事件时,第二个线程也会epoll_wait返回,显然这不是我们想要的。
看到这里,可能有的小伙伴想到了Nginx多进程针对监听端口的处理策略,Nginx是通过accept_mutex机制来保证的。accept_mutex是nginx的(新建连接)负载均衡锁,让多个worker进程轮流处理与client的新连接。当某个worker进程的连接数达到worker_connections配置(单个worker进程的最大处理连接数)的最大连接数的7/8时,会大大减小获取该worker获取accept锁的概率,以此实现各worker进程间的连接数的负载均衡。accept锁默认打开,关闭它时nginx处理新建连接耗时会更短,但是worker进程之间可能连接不均衡,并且存在“惊群”问题。

惊群现象(thundering herd)
就是当多个进程和线程在同时阻塞等待同一个事件时,如果这个事件发生,会唤醒所有的进程,但最终只可能有一个进程/线程对该事件进行处理,其他进程/线程会在失败后重新休眠,这种性能浪费就是惊群。

只有在使能accept_mutex并且当前系统不支持原子锁时,才会用文件实现accept锁。注意,accept_mutex加锁失败时不会阻塞当前线程,类似tryLock。
现代linux中,多个socker同时监听同一个端口也是可行的,nginx 1.9.1也支持这一行为。linux 3.9以上内核支持SO_REUSEPORT选项,允许多个socker bind/listen在同一端口上。这样,多个进程可以各自申请socker监听同一端口,当连接事件来临时,内核做负载均衡,唤醒监听的其中一个进程来处理,reuseport机制有效的解决了epoll惊群问题。

参考文章:
彻底搞懂Reactor模型和Proactor模型
这可能是目前最透彻的Netty原理架构解析
彻底搞懂 netty 线程模型
《Netty、Redis、Zookeeper高并发实战》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值