高性能网络模型
高性能网络模型
一、Reactor模型
reactor
设计模式是event-driven architectur
e的一种实现方式,处理多个客户端并发的向服务端请求服务的场景。每种服务在服务端可能由多个方法组成。reactor
会解耦并发请求的服务并分发给对应的事件处理器来处理。目前,许多流行的开源框架都用到了reactor
模式,如:netty、node.js
等
这里说明多Reactor多线程的模式
1.多Reactor多线程的模式
方案详细说明:
- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程
- 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件
- 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应
- Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程
多 Reactor 多线程的方案虽然看起来复杂的,但是实际实现时比单 Reactor 多线程的方案要简单的多,原因如下:
- 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责完成后续的业务处理
- 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端
上图各部分分析:
- 主Reactor (MainReactor):
- select:这表示主Reactor正在使用某种形式的I/O多路复用技术(如select、poll或epoll)来监听多个网络事件,主要是新的客户端连接请求。
- dispatch:当主Reactor检测到新的连接请求时,它将分派(或分配)这些连接到合适的子Reactor。
- Acceptor:这是一个特定的处理组件,负责接受新的连接请求。在accept操作之后,新的客户端连接被创建。
- 子Reactor (SubReactor):
- 每个子Reactor都有自己的select和dispatch过程,用于管理它负责的客户端连接。
- Handler:处理从子Reactor收到的事件。通常,一个Handler会负责读取数据(read)、进行业务处理,并将响应发送(send)回客户端。
工作流程:
- 客户端发起连接请求到服务器。
- 主Reactor监听到新的连接请求,并通过Acceptor接受这个连接,然后创建一个用于通信的套接字文件描述符
- 连接被分配到一个子Reactor,子Reactor将这个新的套接字文件描述符注册到自己的事件监听系统中
- 当有数据到达时,子Reactor的select检测到事件,并通过dispatch将事件交给对应的Handler处理
- Handler处理完业务逻辑后,读取数据,处理数据,并发送回客户端
工作原理:
- 主Reactor:负责处理客户端的连接请求。主Reactor接收客户端的连接,并将新的连接分配给一个子Reactor。
- 子Reactor:每个子Reactor运行在自己的线程中,负责处理已经建立连接的客户端的IO事件,如读取数据、写入数据等。每个子Reactor都有自己的事件循环,可以独立处理事件。
- 工作线程池:当需要进行更复杂或耗时的业务处理时,子Reactor将任务提交给工作线程池中的线程进行处理,处理完成后通常由工作线程回写结果到客户端。
- 事件分发:主Reactor接收到新的连接请求后,根据某种策略(如轮询、最少连接数等)将连接分配给某个子Reactor处理,以平衡负载
(1)每一个Reactor均有一个自己的epoll实例
每一个子Reactor通常都会拥有属于自己的epoll
实例。在多Reactor多线程模式中,这种设计允许每个子Reactor独立地管理和监控其负责的客户端连接的I/O事件。通过为每个子Reactor配备一个独立的epoll
实例,可以实现并行处理,提升整个系统的效率和响应速度
子Reactor拥有独立的epoll实例的好处:
- 并行性:不同的子Reactor可以在不同的线程中运行,每个线程管理自己的
epoll
实例,这样可以充分利用多核CPU的并行处理能力。 - 效率提高:每个
epoll
实例只管理一部分客户端连接,减少了单个epoll
实例需要处理的事件数量,这有助于减轻单个线程的负载,提高事件处理的效率。 - 隔离性:由于每个子Reactor管理自己的客户端连接,因此连接之间的干扰被最小化。一旦一个子Reactor遇到问题或者崩溃,它不会影响到其他子Reactor。
- 可扩展性:根据服务器的负载情况,可以动态地增加或减少子Reactor的数量。每个新的子Reactor可以简单地通过创建新的
epoll
实例来初始化,使得系统的扩展变得更加灵活
子Reactor使用epoll的工作流程:
- 初始化:当子Reactor被创建时,它会初始化自己的
epoll
实例。这涉及调用epoll_create
函数来创建一个epoll
文件描述符,用于后续的事件监听和处理。 - 事件注册:子Reactor会将其管理的各个连接的文件描述符注册到它的
epoll
实例中。这通常通过epoll_ctl
函数实现,指定要监听的事件类型(如读就绪、写就绪等)。 - 事件循环:子Reactor进入事件循环,调用
epoll_wait
函数等待事件的发生。一旦有事件发生,epoll
实例会返回那些有事件发生的文件描述符,子Reactor随后根据事件类型进行相应的处理,如读取数据、发送数据等。 - 事件处理:子Reactor根据返回的事件进行具体的处理操作。如果操作涉及复杂或耗时的处理任务,可以将这些任务交给工作线程池,不阻塞事件循环
(2)主Reactor具体是怎样将用于通信的套接字文件描述符,分配给子Reactor
在多Reactor多线程模式中,主Reactor将新建立的客户端连接(即用于通信的套接字文件描述符)分配给子Reactor的过程是关键的一步,它影响到系统的负载均衡和整体性能。以下是几种常见的方法和策略,用于实现这一分配过程:
轮询方法
轮询是最简单也是最常用的分配策略之一。主Reactor维护一个子Reactor的列表,并按顺序将新的连接逐一分配给列表中的每个子Reactor。这种方法尽量保证了每个子Reactor获得相同数量的连接,但不考虑各个子Reactor的当前负载情况
最少连接方法
在这种策略中,主Reactor将新的连接分配给当前拥有最少活跃连接的子Reactor。这需要主Reactor跟踪每个子Reactor的连接数。这种方法试图更公平地分配负载,以优化资源利用和响应时间
随机分配
随机分配是另一种简单的策略,主Reactor随机选择一个子Reactor来处理新的连接。这种方法的实现简单,但可能导致某些子Reactor承担更重的负载。
基于性能分配
这种策略考虑到每个子Reactor的性能指标(如CPU使用率、响应时间等),将新连接分配给性能最佳的子Reactor。这种方法更为复杂,需要实时监控和评估子Reactor的性能状态
具体实现步骤:
- 连接接受:主Reactor监听到新的客户端连接请求后,使用
accept
函数接受连接,从而获取一个新的套接字文件描述符。 - 选择子Reactor:根据上述某种策略,主Reactor决定将这个新连接分配给哪一个子Reactor。
- 传递文件描述符:主Reactor可以通过多种方式将套接字文件描述符传递给选定的子Reactor,常见的方法包括使用共享内存、线程安全队列或通过管道发送文件描述符。
- 注册事件监听:子Reactor接收到新的套接字文件描述符后,将其注册到自己的
epoll
实例中,开始监听和处理该连接的事件
(3)多Reactor多线程的模式下,有多少个子Reactor呢
子Reactor的数量没有一个固定的规定,它取决于应用程序的具体需求、系统的硬件资源(如CPU核心数)、以及预期的负载和性能目标。根据不同的场景和需求,开发者可以选择不同的策略来配置子Reactor的数量
确定子Reactor数量的几个关键因素:
- 硬件资源:最基本的,子Reactor的数量通常与服务器的CPU核心数相关。通常情况下,为了充分利用多核性能,子Reactor的数量可以设置为与CPU核心数相等或略多。这样可以确保每个核心都能得到充分利用,同时避免过度的线程竞争和上下文切换。
- 客户端连接数:如果预期的客户端并发连接数非常高,可能需要更多的子Reactor来分散这些连接,以维持系统的响应性和稳定性。然而,过多的子Reactor可能导致管理复杂性增加和资源分配不均。
- 应用程序的I/O模式:如果应用程序涉及大量的短连接,可能需要更多的子Reactor来快速处理连接的建立和释放。相反,如果主要处理长连接,较少的子Reactor可能已足够,因为每个子Reactor可以维护较长时间的连接状态。
- 性能与可伸缩性测试:实际应用中,确定最优的子Reactor数量通常需要通过性能测试和可伸缩性分析来进行。通过模拟不同的负载情况,可以观察不同子Reactor配置下的系统表现,从而找到最佳平衡点
实践中的常见配置:
- 少量子Reactor:在不是非常高并发的应用中,可能只需要几个子Reactor(例如,与CPU核心数相等)。这种配置简单且易于管理。
- 动态调整:在一些高级应用中,子Reactor的数量可能根据实时负载动态调整,以最优化资源使用和性能
二、Proactor模型
….