大家好,我是小风哥!今天我们来聊聊reactor模式。在设计高并发、高性能的服务器时,需要重点考虑的是I/O。I/O是个问题可能有的同学会有疑问,为什么I/O会出问题呢?假设有一个Web服务器每分钟有数百万个请求。服务器在处理请求时需要访问数据库。同时,服务器还可能请求其他服务。一个典型的后端服务器的一种可能的架构如图所示:一个用户请求到来后,服务器可能需要访问数据库,然后请求其他几台服务器获取用户请求的处理结果,然后将响应返回给客户端。从这张图,算算涉及到哪些IO?其实主要有两种:数据库操作的磁盘IO,文件IO和网络IO。据说我们通常用手机APP或者PC浏览器打开一个页面,点击一个按钮,直到完全响应。大部分时间花在了这两个IO上,实际用于处理数据的CPU时间并不多。这告诉我们一个道理,高效的IO处理对于高并发、高性能的服务器来说是至关重要的。两种经典设计模式处理网络请求的经典模式有两种:Thread(Process)-per-connection基于threads(processes),即一个线程(process)perrequest,以及事件驱动的Reactor模式,即响应设备模式。第一种模式在上一篇文章中已经解释过多次。这种模式会为每个请求创建一个线程或进程:但是这种模式的一个问题是在并发量大的时候需要创建很多。如果创建太多线程,就会出现性能问题。第二种基于事件的模式我们在上一篇文章中也讲解过,在这种模式下我们只需要一个线程同时处理多个用户请求:在基于事件的并发编程中,有一种模式叫做Reactor,非常流行,Node.js和Nginx使用Reactor。本篇我们详细讲解高性能高并发服务器中的Reactor模式。当然,在了解Reactor之前,我们先来看看cafe是如何工作的。咖啡店如何运作假设您有一家咖啡店。作为老板,你在前台为喝咖啡的顾客服务。你的生意很好,人们来这里喝咖啡。有时候,有些人点的是简单的东西,比如一杯咖啡或者牛奶,但是也有一些顾客点的是复杂的东西,比如意大利面等等,作为前台,如果你停止接待客户,做意大利面,都后续客户将不得不等待。好在身为老板的你还有几个厨师帮忙,只需要简单的吩咐做意大利面,“张三煮面,李四做酱汁,做好了通知我”.这样即使前台只有你一个人,也能快速接到客户的订单。实际上,Reactor模式本质上就是在这背后。Reactor模式其实可以把咖啡馆例子中的每个顾客理解为服务端收到的一个请求,前台的服务员理解为一个单线程的while循环。这个while循环有一个很形象的名字,eventloop,这个eventloop要做的事情很简单,就是接收用户的请求,然后让handler或者回调函数去处理。这里的handler或者回调函数就像大厨张三和李四。处理程序或回调函数可以与事件循环在同一个线程中运行,或者在与事件循环不同的线程中运行。既然模式是事件驱动的,那么事件是什么?我们需要关心的典型事件有:网络请求的到来,即socket编程中accept到客户端连接文件、可读文件和可写文件。看,这几类事件都是和IO相关的,涉及网络和文件。可能有同学会问,这个事件循环是怎么知道这些事件要来的呢?这涉及到IO多路复用技术,典型的如Linux中的select、poll、epoll。通过IO多路复用技术,我们可以一次监控一堆文件描述符。当这些文件描述符对应的IO事件发生时,我们就会收到操作系统的通知。这时候我们拿到事件,交给对应的handler或者回调函数去处理。总结一下,Reactor的核心组件就是事件循环+IO多路复用+回调函数。单线程还是多线程正如我们上面提到的,事件处理程序可以与事件循环运行在同一个线程中,也可以运行在不同的线程中。如果是运行在同一个线程,那么我们就不需要面对复杂的多线程问题,但是在现在的多核时代,单线程是无法充分利用多核资源的。另外,如果一个请求比较复杂,需要更多的CPU资源,那么在单线程下,所有其他的用户请求都得等待。基于以上考虑,我们可以使用线程池(多线程)技术。事件循环接收到事件后,将事件和处理事件的handler(回调函数)打包发送给线程池,线程池中的线程调用handler(回调函数)处理相应的事件收到打包任务后。这样我们的组合就变成了事件循环+IO多路复用+回调函数+线程池。在回调函数中加入协程的一个缺点是,如果处理用户请求的逻辑比较复杂,可能会导致回调地狱。你可以参考这个回调地狱。协程技术在一定程度上解决了这个问题。让我们以同步方式进行异步编程。关于协程,你可以参考这里和这里。最终我们的组合变成了事件循环+IO多路复用+协程+线程池。接下来,我们用Node.js来解释一下Reactor模式。Node.js和Reactor模式让我们看一下Node.js的架构图:这个架构图已经清楚地展示了Reactor模式是如何工作的。1.当一个用户请求到达时,需要将其放入一个队列中,因为事件循环是单线程运行的。2、接下来,事件循环不断检测事件队列中是否有事件。如果队列中有请求,那么根据队列的“先来先服务”的原则,事件循环取出相应的事件交给线程池。3、线程池不断检测是否有任务到来。这里的任务是通过封装事件和对应的回调函数形成的。4、线程接到任务后,线程池中的线程开始工作,比如查询数据库,读取文件等。5.当线程处理完一个请求后,调用任务对应的回调函数,并将处理结果响应发送给事件循环。6、事件循环收到处理结果后发送给客户端。怎么,这和上面的咖啡馆还有这里的核反应堆很像啊。这就是反应堆模式。另外,Node.js中的协程叫做Fiber,以同步的方式用于异步编程,这里就不详细解释了。ReactorvsProactorReactor模式使用的IO是同步IO,什么是同步IO?也就是说调用者会被阻塞等待IO完成。这种IO更具体的叫做同步阻塞IO。但是我们知道事件循环是在一个线程中运行的。如果在事件循环中调用了同步阻塞IO,整个线程就会被挂起。由于事件循环就像咖啡店的前台,所以非常关键。如果事件循环所在的线程被阻塞,则所有用户请求都必须等待。因此,事件循环中的IO不能阻塞。有同步阻塞IO,就有同步非阻塞IO。什么是同步非阻塞IO?意思是当我们调用一个同步非阻塞IO相关的函数时,该函数会立即返回并告诉我们文件是可读还是可写。如果可读或者可写,我们就真正的读写这个文件。这是同步非阻塞阻塞I/O。Reactor模式使用同步非阻塞IO。与同步IO对应的是异步IO。在异步IO下,我们需要告诉操作系统接收或写入数据的地址。操作系统会将进程地址空间中的数据写入文件或将文件内容写入进程地址空间。操作系统会在完成IO后通知我们。这就是异步IO。执行异步IO也不会阻塞调用线程。同步和异步的概念可以参考这里。使用异步IO的事件驱动编程称为Proactor。也就是说,Reactor和Proactor的区别在于一个使用同步IO,一个使用异步IO。下面我们以一个读取文件的例子来说明两者的区别。在Reactor中读取:告诉事件循环我们对一个文件可读事件感兴趣。事件循环等待事件到达。事件循环被唤醒并调用相应的处理程序。handler开始读取文件,处理数据,完成后返回到eventloop,Proactor的读取是这样的:我们对某个文件发起异步读取操作,告诉eventloop我们不关心这个文件是否存在文件是否可读,我们只关心文件是否被读取过。事件循环开始等待事件。与此同时,操作系统开始执行实际的文件读取。读取完成后,通知事件循环读取完成。事件循环被唤醒。此时文件已经读取完毕,调用相应的handlerHandler开始处理数据,完成后返回事件循环。现在你应该明白Reactor和Proactor的区别了。总结在这篇文章中,我们详细讲解了目前流行的高性能、高并发的Reactor模式。其实它的本质和咖啡店没什么两样。如果你善于观察和思考,你会发现很多技术问题在现实生活中是可以解决的。寻找相似的场景。希望本文能帮助大家理解Reactor模式。
