本文时间:2018-11-21,作者:krircc, 简介:天青色
欢迎向 Rust 中文社区投稿,投稿地址,好文将在以下地方直接展示
高性能服务器至少要满足如下几个需求:
而满足如上需求的一个基础就是高性能的 IO!
讲到高性能 IO 绕不开 Reactor 模式,它是大多数 IO 相关组件如 Netty、Redis 在使用的 IO 模式
几乎所有的网络连接都会经过读请求内容——》解码——》计算处理——》编码回复——》回复的过程
Socket 之间建立链接及通信的过程!实际上就是对 TCP/IP 连接与通信过程的抽象:
BIO 优点
BIO 缺点
缺点:主要瓶颈在线程上。每个连接都会建立一个线程。虽然线程消耗比进程小,但是一台机器实际上能建立的有效线程有限,且随着线程数量的增加,CPU 切换线程上下文的消耗也随之增加,在高过某个阀值后,继续增加线程,性能不增反降!而同样因为一个连接就新建一个线程,所以编码模型很简单!
就性能瓶颈这一点,就确定了 BIO 并不适合进行高性能服务器的开发!
NBIO:
优点
缺点
NBIO 的优缺点和 BIO 就完全相反了!性能高,不用一个连接就建一个线程,可以一个线程处理所有的连接!相应的,编码就复杂很多,从上面的代码就可以明显体会到了。还有一个问题,由于是非阻塞的,应用无法知道什么时候消息读完了,就存在了半包问题!需要自行进行处理!例如,以换行符作为判断依据,或者定长消息发生,或者自定义协议!
NBIO 虽然性能高,但是编码复杂,且需要处理半包问题!为了方便的进行 NIO 开发,就有了 Reactor 模型!
Proactor 和 Reactor 是两种经典的多路复用 I/O 模型,主要用于在高并发、高吞吐量的环境中进行 I/O 处理。
I/O 多路复用机制都依赖于一个事件分发器,事件分离器把接收到的客户事件分发到不同的事件处理器中,如下
在操作系统级别 select,poll,epoll 是 3 个常用的 I/O 多路复用机制,简单了解一下将有助于我们理解 Proactor 和 Reactor。
select 的原理如下:
用户程序发起读操作后,将阻塞查询读数据是否可用,直到内核准备好数据后,用户程序才会真正的读取数据。
poll 与 select 的原理相似,用户程序都要阻塞查询事件是否就绪,但 poll 没有最大文件描述符的限制。
epoll 是 select 和 poll 的改进,原理图如下:
epoll 使用“事件”的方式通知用户程序数据就绪,并且使用内存拷贝的方式使用户程序直接读取内核准备好的数据,不用再读取数据
Proactor 是一个异步 I/O 的多路复用模型,原理图如下:
Reactor 是一个同步的 I/O 多路复用模型,它没有 Proactor 模式那么复杂,原理图如下:
前面已经简单介绍了 Proactor 和 Reactor 模型,在实际中 Proactor 由于需要操作系统的支持,实现的案例不多,有兴趣的可以看一下 Boost Asio 的实现,我们主要说一下 Reactor 模型,Netty 也是使用 Reactor 实现的。
但单线程的 Reactor 模型每一个用户事件都在一个线程中执行:
使用线程池的技术来处理 I/O 操作,原理图如下:
在多线程 Reactor 中只有一个 Acceptor,如果出现登录、认证等耗性能的操作,这时就会有单点性能问题,因此产生了主从 Reactor 多线程模型,原理如下:
在解决了什么是 Reactor 模式后,我们来看看 Reactor 模式是由什么模块构成。图是一种比较简洁形象的表现方式,因而先上一张图来表达各个模块的名称和他们之间的关系:
Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer 等。由于 Reactor 模式一般使用在网络编程中,因而这里一般指 Socket Handle,即一个网络连接( Connection,在 Java NIO 中的 Channel )。这个 Channel 注册到 Synchronous Event Demultiplexer 中,以监听 Handle 中发生的事件,对 ServerSocketChannnel 可以是 CONNECT 事件,对 SocketChannel 可以是 READ、WRITE、CLOSE 事件等。
Synchronous Event Demultiplexer:阻塞等待一系列的 Handle 中的事件到来,如果阻塞等待返回,即表示在返回的 Handle 中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的 select 来实现。在 Java NIO 中用 Selector 来封装,当 Selector.select()返回时,可以调用 Selector 的 selectedKeys()方法获取Set<SelectionKey>
,一个 SelectionKey 表达一个有事件发生的 Channel 以及该 Channel 上的事件类型。上图的“ Synchronous Event Demultiplexer ---notifies--> Handle ”的流程如果是对的,那内部实现应该是 select()方法在事件到来后会先设置 Handle 的状态,然后返回。不了解内部实现机制,因而保留原图。
Initiation Dispatcher:用于管理 Event Handler,即 EventHandler 的容器,用以注册、移除 EventHandler 等;另外,它还作为 Reactor 模式的入口调用 Synchronous Event Demultiplexer 的 select 方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的 Handle 将其分发给对应的 Event Handler 处理,即回调 EventHandler 中的 handle_event()方法。
Event Handler:定义事件处理方法:handle_event(),以供 InitiationDispatcher 回调使用。
Concrete Event Handler:事件 EventHandler 接口,实现特定事件处理逻辑。
1 )响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
2 )编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程 /进程的切换开销;
3 )可扩展性,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
4 )可复用性,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
1 )相比传统的简单模型,Reactor 增加了一定的复杂性,因而有一定的门槛,并且不易于调试。
2 ) Reactor 模式需要底层的 Synchronous Event Demultiplexer 支持,比如 Java 中的 Selector 支持,操作系统的 select 系统调用支持,如果要自己实现 Synchronous Event Demultiplexer 可能不会有那么高效。
3 ) Reactor 模式在 IO 读写数据时还是在同一个线程中实现的,即使使用多个 Reactor 机制的情况下,那些共享一个 Reactor 的 Channel 如果出现一个长时间的数据读写,会影响这个 Reactor 中其他 Channel 的相应时间,比如在大文件传输时,IO 操作就会影响其他 Client 的相应时间,因而对这种操作,使用传统的 Thread-Per-Connection 或许是一个更好的选择,或则此时使用 Proactor 模式。
Rust 的高性能异步网络编程模式目前是基于mio
和futures
这两个库构建的生态。
Tokio则连接这 2 个库构建了一个异步非阻塞事件驱动编程平台。
mio
,futures
,tokio
Mio 是 Rust 的轻量级快速低级 IO 库,专注于非阻塞 API,事件通知以及用于构建高性能 IO 应用程序的其他有用实用程序.
Rust 中的零成本异步编程库,Futures 可在没有标准库的情况下工作,例如在裸机环境中。
提供了许多用于编写异步代码的核心抽象:
Future
是由异步计算产生的单一最终值。一些编程语言(例如 JavaScript )将此概念称为“ promise ”。Streams
表示异步生成的一系列值。Sinks
支持异步写入数据。Executors
负责运行异步任务。还包含异步 I/O 和跨任务通信的抽象。
所有这些是任务系统的基础,它是轻量级线程(协程)的一种形式。使用Future
,Streams
和Sinks
构建大型异步计算,然后将其生成作为独立完成的任务运行,但不阻塞运行它们的线程。
Tokio : Rust 编程语言的异步运行时,提供异步事件驱动平台,构建快速,可靠和轻量级网络应用。利用 Rust 的所有权和并发模型确保线程安全
这些组件提供构建异步应用程序所需的运行时组件。
Tokio 构建于 Rust 之上,提供极快的性能,使其成为高性能服务器应用程序的理想选择。
1:零成本抽象
与完全手工编写的等效系统相比,Tokio 的运行时模型不会增加任何开销。
使用 Tokio 构建的并发应用程序是开箱即用的。Tokio 提供了针对异步网络工作负载调整的多线程,工作窃取任务调度程序。
2:非阻塞 I/O
Tokio 由操作系统提供的非阻塞,事件 I/O 堆栈提供支持。
虽然 Tokio 无法阻止所有错误,但它的目的是最小化它们。Tokio 在运送关键任务应用程序时带来了安心。
1- 所有权和类型系统
Tokio 利用 Rust 的类型系统来提供难以滥用的 API。
2- Backpressure
Backpressure 开箱即用,无需使用任何复杂的 API。
3- 取消
Rust 的所有权模型允许 Tokio 自动检测何时不再需要计算。Tokio 将自动取消它而无需用户调用 cancel 函数。
Tokio 可以很好地扩展,而不会增加应用程序的开销,使其能够在资源受限的环境中茁壮成长。
1- 没有垃圾收集器
因为 Tokio 使用 Rust,所以不包括垃圾收集器或其他语言运行时。
2- 模块化
Tokio 是一个小组件的集合。用户可以选择最适合手头应用的部件,而无需支付未使用功能的成本。