Netty高性能的奥秘-Reactor线程模型
前言
对于超大规模并发的网络应用程序来说,可扩展性(Scalability)是支撑高并发的必要条件。 一方面需要保证基本功能的正确性,另一方面还需要保证程序的高性能。而不同的IO模型,对程序的性能影响是非常明显的。
为什么要使用Reactor
传统阻塞IO模型的不足
每个连接都需要独立线程处理,当并发数大时,创建线程数多,占用资源
采用阻塞IO模型,连接建立后,若当前线程没有数据可读,线程会阻塞在读操作上,造成资源浪费
针对传统阻塞IO模型的两个问题,可以采用如下的方案
基于池化思想,避免为每个连接创建线程,连接完成后将业务处理交给线程池处理
基于IO复用模型,多个连接共用同一个阻塞对象,不用等待所有的连接。遍历到有新数据可以处理时,操作系统会通知程序,线程跳出阻塞状态,进行业务逻辑处理
Reactor线程模型的思想就是基于IO复用和线程池的结合。其高效的原因在于网络I/O是耗时操作,将等待I/O数据就绪的过程抽象成为事件监听并用单独的线程处理,而具体的处理逻辑可以由其他业务线程处理,这样就不会阻塞业务线程从而增大了系统的吞吐量。缺点是提升了编码的复杂度。
Netty 中三种 Reactor 线程模型
单线程模型
该图描述了 Reactor 的单线程模型结构,在 Reactor 单线程模型中,所有 I/O 操作(包括连接建立、数据读写、事件分发等),都是由一个线程完成的。单线程模型逻辑简单,缺陷也十分明显:
一个线程支持处理的连接数非常有限,CPU 很容易打满,性能方面有明显瓶颈,对于多核资源机器来说是有点浪费的
当多个事件被同时触发时,只要有一个事件没有处理完,其他后面的事件就无法执行,这就会造成消息积压及请求超时
线程在处理 I/O 事件时,Select 无法同时处理连接建立、事件分发等操作
如果 I/O 线程一直处于满负荷状态,很可能造成服务端节点不可用
多线程模型
由于单线程模型有性能方面的瓶颈,多线程模型作为解决方案就应运而生了。Reactor 多线程模型将业务逻辑交给多个线程进行处理。除此之外,多线程模型其他的操作与单线程模型是类似的,例如读取数据依然保留了串行化的设计。当客户端有数据发送至服务端时,Select 会监听到可读事件,数据读取完毕后提交到业务线程池中并发处理,充分利用多核机器的资源、提高性能并且增加可靠性
该线程模型的不足,Reactor线程承担所有的事件,例如监听和响应,高并发场景下单线程存在性能问题
主从多线程模型
这种模型下和第二种模型相比是把Reactor线程拆分了mainReactor和subReactor两个部分,每个 Reactor 线程都有独立的Selector对象。MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。再由 SubReactor 分配线程池中的 I/O 线程与其连接绑定,它将负责连接生命周期内所有的 I/O 事件
Reactor三种模式形象比喻
饭店一般有接待员和服务员,接待员负责在门口接待顾客,服务员负责全程服务顾客,Reactor的三种线程模型可以用接待员和服务员类比
单Reactor单线程模型:接待员和服务员是同一个人,一直为顾客服务。客流量较少适合
单Reactor多线程模型:一个接待员,多个服务员。客流量大,一个人忙不过来,由专门的接待员在门口接待顾客,然后安排好桌子后,由一个服务员一直服务,一般每个服务员负责一片中的几张桌子
多Reactor多线程模型:多个接待员,多个服务员。这种就是客流量太大了,一个接待员忙不过来了
Netty 推荐使用主从多线程模型,这样就可以轻松达到成千上万规模的客户端连接。在海量客户端并发请求的场景下,主从多线程模式甚至可以适当增加 SubReactor 线程的数量,从而利用多核能力提升系统的吞吐量。