【.com原稿】本文旨在提供一个有用的网络服务模型概述和比较,以揭开高性能网络架构设计和实现的神秘面纱。服务端处理网络请求首先我们来看一下服务端处理网络请求的典型流程:从上图可以看出,主要处理步骤包括:获取请求数据,客户端与服务端建立连接发送请求,服务器接受请求(1-3)。构造响应,当服务器收到请求后,在用户空间处理客户端的请求,直到响应的构造完成(4)。服务端返回数据后,将构造好的响应通过内核空间的网络I/O发送回客户端(5-7)。在设计服务器端并发模型时,有两个关键点:服务器如何管理连接和获取输入数据。服务器如何处理请求。以上两个关键点,归根结底都关系到操作系统的I/O模型和线程(进程)模型。下面详细介绍这两种模型。I/O模型在介绍操作系统的I/O模型之前,我们先了解几个概念:阻塞调用和非阻塞调用。阻塞调用意味着当前线程将在调用结果返回之前被挂起。调用线程只有在得到结果后才会返回。非阻塞调用是指调用不会阻塞当前线程,直到不能立即得到结果。两者最大的区别在于调用者在被调用者收到请求到返回结果之间是否一直在等待。阻塞意味着调用者一直在等待,什么都不做;非阻塞意味着调用者先忙于其他事情。同步处理和异步处理同步处理是指被叫方得到最终结果后返回给主叫方;异步处理是指被调用方先返回响应,然后计算调用结果,计算出最终结果平方后通知并返回给调用方。阻塞、非阻塞与同步、异步的区别阻塞、非阻塞与同步、异步的对象其实是不同的:阻塞、非阻塞的讨论对象是调用者。同步和异步的讨论对象是被调用者。recvfrom函数recvfrom函数(通过Socket接收数据),这里算是系统调用。输入操作通常由两个不同的阶段组成:等待数据就绪将数据从内核复制到进程对于套接字上的输入操作,第一步通常涉及等待数据从网络到达。当等待的数据包到达时,它被复制到内核中的缓冲区。第二步是将数据从内核缓冲区复制到应用程序进程缓冲区。实际应用程序通过系统调用完成上述2步操作时,调用方式有阻塞和非阻塞之分,而操作系统在处理应用请求时,同步处理和异步处理的处理方式不同,可以分为5种I/O模型。(参考《UNIX网络编程卷1》)阻塞I/O模型(blockingI/O)在阻塞I/O模型中,应用程序阻塞调用recvfrom到当它返回一个datagramready时,recvfrom返回成功后,应用程序进程启动处理数据报。比喻:一个人在钓鱼,没有鱼上钩,他就坐在岸边等待。优点:程序简单,进程/线程在阻塞等待数据时挂起,基本不占用CPU资源。缺点:每个连接都需要独立的进程/线程处理。当并发请求数很大时,为了维护程序,内存和线程切换的开销比较大。这种模式在实际生产中很少使用。非阻塞I/O模型(non-blockingI/O)在非阻塞I/O模型中,应用程序将一个socket设置为非阻塞的,它告诉内核,当请求的I/O操作无法完成时,不要让进程休眠。而是返回错误,应用程序会根据I/O操作函数不断轮询数据是否就绪。如果没有,继续轮询直到数据准备好。打个比方:边钓鱼边玩手机,过一会看有没有鱼上钩,有鱼就赶紧拉竿。优点:不会阻塞内核等待数据的过程,每次发起的I/O请求都可以立即返回,无需阻塞等待,实时性更好。缺点:轮询会不断询问内核,占用大量CPU时间,系统资源利用率低,所以一般web服务器不采用这种I/O模型。I/O多路复用模型(I/Omultiplexing)在I/O多路复用模型中,会用到Select或Poll函数或Epoll函数(Linux2.6后内核支持),这两个函数也会阻塞进程,但不同于阻塞I/O。这两个函数可以同时阻塞多个I/O操作,可以同时检测多个读操作和多个写操作的I/O函数,直到有数据可读或有数据才真正调用I/O可写的。O操作功能。打个比方:放一堆鱼竿,一直守在岸边的那堆鱼竿,没鱼上钩就玩手机。优点:可以基于一个阻塞对象同时在多个描述符上等待就绪,而不用使用多线程(每个文件描述符一个线程),可以大大节省系统资源。缺点:当连接数较少时,效率低于多线程+阻塞I/O模型,延迟可能会更大,因为单次连接处理需要2次系统调用,耗时会增加。信号驱动I/O模型(signal-drivenI/O)在信号驱动I/O模型中,应用程序使用socket接口进行信号驱动I/O,并安装一个信号处理函数,而进程继续运行而不会阻塞。当数据准备好后,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数来处理数据。打个比方:钓竿上系着一个铃铛。铃声一响,就知道鱼上钩了,可以专心玩手机了。优点:线程在等待数据时不会阻塞,可以提高资源利用率。缺点:大量IO操作时,可能会因为信号队列溢出导致信号I/O得不到通知。信号驱动的I/O在处理UDP套接字时很有用,其中此类信号意味着数据报已到达,或者已返回异步错误。但是,对于TCP来说,信号驱动的I/O方式几乎没什么用,因为导致这种通知的条件有很多,每一个判断都会消耗大量的资源,相比起来就失去了优势以前的方法。异步I/O模型(asynchronousI/O)由POSIX规范定义。应用程序告诉内核开始一个操作,并让内核在整个操作(包括将数据从内核复制到应用程序的缓冲区)完成后通知应用程序。.该模型与信号驱动模型的主要区别在于信号驱动I/O是内核通知应用程序何时开始I/O操作,而异步I/O模型是内核通知应用程序什么时候开始I/O操作I/O操作完成。优点:异步I/O可以充分利用DMA的特性,允许I/O操作与计算重叠。缺点:要实现真正的异步I/O,操作系统需要做很多工作。目前真正的异步I/O是通过Windows下的IOCP来实现的。在Linux系统下,引入了Linux2.6。目前,AIO并不完美。因此,在Linux下实现高并发网络编程时,IO多路复用模型是主要模式。5种I/O模型总结从上图可以看出,越往后阻塞越少,理论上效率最高。五种I/O模型中,前四种是同步I/O,因为真正的I/O操作(recvfrom)会阻塞进程/线程,只有异步I/O模型兼容异步I/O由POSIX定义。匹配。在线程模型介绍了服务器如何基于I/O模型管理连接和获取输入数据之后,下面介绍服务器如何基于进程/线程模型处理请求。值得注意的是,具体选择线程还是进程更多的是与平台和编程语言相关。比如C语言可以使用线程和进程(比如Nginx使用进程,Memcached使用线程),Java语言一般使用线程(比如Netty)。为了描述方便,下面使用线程进行描述。传统阻塞I/O服务模型的特点:使用阻塞I/O模型获取输入数据。每个连接都需要一个独立的线程来完成数据输入、业务处理、数据返回的完整操作。存在问题:并发量大时,需要创建大量线程处理连接,系统资源占用大。连接建立后,如果当前线程暂时没有数据可读,线程会阻塞在Read操作上,造成线程资源浪费。Reactor模式针对的是传统阻塞I/O服务模型的两个缺点。比较常见的解决方案如下:基于I/O多路复用模型,多个连接共享一个阻塞对象,应用只需要等待一个阻塞对象。无需阻塞等待所有连接。当某个连接有新数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始业务处理。基于线程池的多路复用线程资源,无需为每个连接创建一个线程。连接完成后,将业务处理任务分配给线程处理。一个线程可以处理多个连接的业务。I/O多路复用结合线程池是Reactor模式的基本设计思想,如下图所示:更多的投入。服务器程序处理传入的多通道请求,并将它们同步分派给与请求对应的处理线程。Reactor模式也称为Dispatcher模式。即I/O多路复用统一监听事件,收到事件后进行分发(Dispatchtoaprocess),是编写高性能网络服务器的必备技术之一。Reactor模式中有2个关键组件:Reactor,Reactor运行在一个单独的线程中,负责监听和派发事件,派发给合适的handler来响应IO事件。它就像公司的电话接线员一样,接听客户的电话并将线路转接到适当的联系人。处理程序,处理程序执行I/O事件以完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,这些处理程序执行非阻塞操作。根据Reactor的个数和处理资源池线程的个数,典型的实现有三种:SingleReactorSingleThreadSingleReactorMultithreadingMaster-SlaveReactorMultithreading下面分别详细介绍三种实现。SingleReactorSingleThread其中,Select是在之前的I/O多路复用模型中引入的标准网络编程API,可以让应用程序通过一个阻塞对象监听多个连接请求。其他方案原理图类似。方案说明:Reactor对象通过Select监听客户端请求事件,收到事件后通过Dispatch进行分发。如果是连接建立请求事件,Acceptor通过Accept处理连接请求,连接完成后,再创建一个Handler对象来处理后续的业务处理。如果不是连接建立事件,Reactor会调度调用连接对应的Handler进行响应。Handler会完成读取→业务处理→发送的完整业务流程。优点:模型简单,不存在多线程、进程通信、竞争等问题,都在一个线程中完成。缺点:性能问题,只有一个线程,无法充分发挥多核CPU的性能。当Handler在一个连接上处理业务时,整个流程无法处理其他连接事件,容易造成性能瓶颈。可靠性问题,线程意外跑路,或者进入死循环,都会导致整个系统的通信模块不可用,无法接收和处理外部消息,导致节点故障。使用场景:客户端数量有限,业务处理速度非常快,比如Redis,业务处理的时间复杂度为O(1)。单Reactor多线程方案说明:Reactor对象通过Select监听客户端请求事件,收到事件后通过Dispatch进行分发。如果是连接建立请求事件,Acceptor通过Accept来处理连接请求,连接完成后,再创建一个Handler对象来处理后续的各种事件。如果不是连接建立事件,Reactor会调度调用连接对应的Handler进行响应。Handler只负责响应事件,不做具体的业务处理。通过Read读取数据后,会分发到后续的Worker线程池进行业务处理。Worker线程池会分配独立的线程来完成真正的业务处理,以及如何将响应结果发送给Handler进行处理。Handler收到响应结果后,通过Send将响应结果返回给Client。优点:可以充分利用多核CPU的处理能力。缺点:多线程数据共享和访问比较复杂;Reactor负责对所有事件进行监听和响应,单线程运行,在高并发场景下很可能成为性能瓶颈。主从Reactor多线程针对的是单Reactor多线程模型。Reactor在单线程中运行。在高并发场景下,很容易成为性能瓶颈。Reactor可以在多线程中运行。方案说明:Reactor主线程MainReactor对象通过Select监听连接建立事件,通过Acceptor接收事件,处理连接建立事件。Acceptor处理完连接建立事件后,MainReactor将连接Reactor子线程分配给SubReactor处理。SubReactor将连接添加到连接队列中进行监听,并创建一个Handler来处理各种连接事件。当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应。Handler通过Read读取数据后,会分发到后续的Worker线程池进行业务处理。Worker线程池会分配独立的线程来完成真正的业务处理,以及如何将响应结果发送给Handler进行处理。Handler收到响应结果后,通过Send将响应结果返回给Client。优点:父线程与子线程的数据交互简单,职责明确。父线程只需要接收新的连接,子线程完成后续的业务处理。父线程和子线程之间的数据交互很简单。Reactor主线程只需要将新连接传递给子线程,子线程不需要返回数据。这种模型在很多项目中被广泛使用,包括Nginx主从Reactor多进程模型、Memcached主从多线程、Netty主从多线程模型支持。这三种模式可以打个比方来理解:餐厅往往会聘请接待员来迎接顾客,当顾客落座后,服务员专门为这一桌服务。SingleReactor单线程,前台和服务员是同一个人,全程为客户服务。单Reactor多线程,1个接待员,多个服务员,接待员只负责接待。主从Reactor多线程,多个接待员,多个服务员。Reactor模式有以下优点:响应速度快,不会被单个同步事件阻塞,虽然Reactor本身还是同步的。编程比较简单,可以最大程度避免复杂的多线程和同步问题,避免多线程/进程的切换开销。可扩展性,您可以通过增加Reactor实例的数量轻松充分利用CPU资源。复用性,Reactor模型本身与具体的事件处理逻辑无关,复用性强。Proactor模型在Reactor模式下,Reactor等待事件或适用或操作状态的发生(例如,可以读取和写入文件描述符,或者可以读取和写入Socket)。然后将这个事件传递给预先注册的Handler(事件处理函数或回调函数),由后者进行实际的读写操作。所有的读写操作都需要应用同步,所以Reactor是一个非阻塞的同步网络模型。如果把I/O操作改成异步的,也就是交给操作系统来完成,性能还能进一步提升。这就是异步网络模型Proactor。Proactor与异步I/O有关。详细方案如下:ProactorInitiator创建Proactor和Handler对象,通过AsyOptProcessor(AsynchronousOperationProcessor)将Proactor和Handler都注册到内核中。AsyOptProcessor处理注册请求并处理I/O操作。当I/O操作完成时,AsyOptProcessor通知Proactor。Proactor根据不同的事件类型,回调不同的Handler进行业务处理。Handler完成业务处理。可以看到Proactor和Reactor的区别:Reactor在事件发生时通知预先注册的事件(读写在应用线程中处理)。Proactor在事件发生时(由内核完成)完成基于异步I/O的读写操作,I/O操作完成后回调给应用程序的处理器进行业务处理。从理论上讲,Proactor比Reactor更高效,异步I/O可以充分利用DMA(DirectMemoryAccess,直接内存访问),但有以下缺点:编程复杂,由于事件的初始化和完成在异步中操作流程两者在时间和空间上是分离的,因此开发异步应用程序更加复杂。由于反向流控制,应用程序也可能变得更难调试。内存使用,buffer必须在读或写操作的时间段内保持,这可能会造成连续的不确定性,并且每个并发操作都需要一个独立的cache。与Reactor模式相比,Socket在准备好读写之前,不需要开启缓存。在操作系统的支持下,Windows下通过IOCP实现了真正的异步I/O,但是在Linux系统下,引入了Linux2.6,目前异步I/O并不完善。所以Linux下高并发网络编程的实现都是基于Reactor模型。参考文章:从0开始学习架构——阿里巴巴技术专家李云华技术:Linux网络IO模型多线程网络服务模型IO阻塞、非阻塞、同步、异步UNIX网络编程第1卷:Socket网络API(第3版)陈菜花(caison),异步网络模型,主要从事服务器开发、需求分析、系统设计、优化重构,主要开发语言为Java。微信号:hua1881375。【原创稿件,合作网站转载请注明原作者和出处为.com】
