聊聊高性能I/O设计模式与快餐店的点餐机制

最近接触了NIO,类名涉及不少术语,查了下原来这些术语均出自Reactor/Proactor两种经典的IO设计模式。读了《两种高性能I/O设计模式的比较》(附于文末)之后,中午在麦当劳点餐的时候突然意识到I/O过程和点餐这回事儿可以很好地类比:


=====================

场景1、餐厅闲时点餐的情景:

1、客户向接待员请求下单,点套餐(可乐+薯条+汉堡)。

2、客户在柜台前等待。

3、接待员从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。

4、客户拿东西走人。

5、轮到下一个客户向接待员发起新一轮点餐请求。

注意到,只要步骤3准备套餐所含食物的过程足够快(且架子上往往提前有储备),这样效率是很高的。

=====================

场景2、餐厅忙时点餐的情景:

1、客户们在几个柜台前排长队,每个柜台前都有一个接待员。

2、此时每个接待员只负责下单,由于下单后食物不能马上就绪,接待员会对客户说,“请您在旁边稍等”,此时客户等在柜台前,一边盯住自己的餐盘;而接待员立即给队列中下一位客户提供下单服务。

3、有几名专职的配餐员负责从身后的食品储物架上逐一选取套餐所含食物,完成后通知客户,“套餐齐了”。

4、收到通知的客户拿东西走人,没收到通知的继续等在柜台前。

5、有时候,某种食物如薯条恰好卖完了,这就需要等较长时间。配餐员会对客户说,“请您去座位上等,一会儿有薯条了,我们会通知您”。或者有些餐厅的做法是,给客户发一个号码牌。

6、客户回到座位上,把号码牌摆在显眼的地方。

7、薯条就绪后,配餐员根据号码牌分发到客户的座位上。

=====================

接下来回到程序世界:点餐的客户就是需要读取数据进行处理的handler,而食物就是待读取的数据。以柜台作为分水岭,场景1说明了,如果待读取的数据能瞬间到达、就绪,用同步读取的方式效率是最高的,用户在柜台前几乎感受不到等待。然而,互联网场景中,径由网络发送给服务器的数据往往是逐块、无序、缓慢地抵达(可乐、汉堡和薯条被逐个放在餐盘这个缓冲区里),且处理线程不得不等数据读完整了才能开始处理(用户也往往等套餐齐了才离开柜台)。那么:

同步阻塞模式即大量客户挤在柜台前等待,且什么事儿也不做。

同步非阻塞模式即客户们下单后到座位上等待,并不时去柜台看看套餐有否就绪。

异步模式即客户们回到座位上等待,套餐就绪后由接待员负责通知客户来拿。

如果不能预知等待数据就绪需要多久,始终选择上述任一一种模式,肯定会是低效的。

那么在客户众多的情况下(典型的互联网场景下,一台服务器伺候众多用户),有如下折衷方案:

多个柜台同时开放,每个柜台有一名接待员(事件分离器/事件选择器,监听事件到达)负责招待一群客户。由于人太多,不能全挤在柜台前,下单后客户都回到座位上等。当某个客户套餐中的部分食物就绪,如只有薯条就绪时,接待员可以:

立即通知客户来取走(标准Reactor模式)。客户往往要来来回回跑多次才能取得完整的套餐。

帮客户把套餐配齐,再通知客户来取走(模拟的Proactor模式,即模拟异步)。配餐的事通常由配餐员这个角色来处理,如果接待员很清闲,客户比较少,他完全可以胜任配餐员的角色;如果客户非常多,帮一大堆客户监听食物就绪事件就会忙得够呛,配餐的事可以交给专职人员。(注意场景1和场景2中谁在负责配餐工作的微妙变化

进一步地,如果有这样一家餐厅,拥有全自动配餐设备——意味着接待员帮用户下单后,配餐过程由机器代劳,OK,我们称之为Proactor模式


附:两种高性能I/O设计模式的比较 按自己的理解改动了多处有问题请大家指出


这是05年的老文章,网上应该有人早就翻译过了,我翻译它仅仅为了学习Reactor/Proactor两种IO设计模式,顺便作翻译练习。

标题:

两种高性能I/O设计模式的比较

作者:

Alexander Libman Vladimir Gilbourd

原文:

http://www.artima.com/articles/io_design_patterns.html

时间:

November 25, 2005

译者:

潘孙友 2010-01-26 于深圳

综述

这篇文章探讨并比较两种高性能IO设计模式除了介绍现有的解决方案还提出了一种更具伸缩性,只需要维护一份代码并且跨平台的解决方案(含代码示例), 以及其在不同平台上的微调此文还比较了java,c#,c++对各自现有以及提到的解决方案的实现性能.

系统I/O 可分为阻塞型非阻塞同步型以及非阻塞异步型[12]. 阻塞型I/O意味着控制权只到调用操作结束了才会回到调用者手里结果调用者被阻塞了这段时间做不了任何其它事情更郁闷的是,在等待IO结果的时间里,调用者所在线程此时无法腾出手来去响应其它的请求,这真是太浪费资源了。拿read()操作来说吧调用此函数的代码会一直僵在此处,直至它所读的socket缓存中有数据到来.

相比之下,非阻塞同步是会立即返回控制权给调用者的。调用者不需要等等,它从调用的函数获取两种结果:要么此次调用成功进行了;要么系统返回错误标识告诉调用者当前资源不可用,你再等等或者再试试看吧。比如read()操作如果当前socket无数据可读,则立即返回EWOULBLOCK/EAGAIN,告诉调用者:"数据还没准备好,你稍后再试".

在非阻塞异步调用中,稍有不同。调用函数在立即返回时,还告诉调用者,这次请求已经开始了。系统会使用另外的资源或者线程来完成这次调用操作,并在完成的时候知会调用者(如通过回调函数)。拿WindowsReadFile()或者POSIXaio_read()来说,调用它之后,函数立即返回,操作系统在后台同时开始读操作。

在以上三种IO形式中,非阻塞异步是性能最高、伸缩性最好的。

这篇文章探讨不同的I/O利用机制并提供一种跨平台的设计模式(解决方案). 希望此文可以给于TCP高性能服务器开发者一些帮助,选择最佳的设计方案。下面我们会比较 Java, c#, C++各自对探讨方案的实现以及性能我们在文章的后面就不再提及阻塞式的方案了,因为阻塞式I/O实在是缺少可伸缩性,性能也达不到高性能服务器的要求。

两种IO多路复用方案:Reactor and Proactor

一般情况下,I/O 复用机制需要事件分离器(event demultiplexor [13]). 事件分离器的作用,即将那些读写事件源分发给各读写事件的处理者,就像送快递的在楼下喊谁的什么东西送了快来拿吧!”开发人员在开始的时候需要在分离器那里注册感兴趣的事件,并提供相应的处理者(event handlers)或者是回调函数事件分离器在适当的时候会将请求的事件分发给这些handler或者回调函数.

涉及到事件分离器的两种模式称为:Reactor and Proactor [1]. Reactor模式基于同步I/O,而Proactor模式则基于异步I/O. Reactor模式中,事件分离器会等待一个就绪事件,例如当文件描述符或socket已对读写操作准备好,事件分离器就把这个事件传给事先注册的事件处理函数或回调函数,由后者来做实际的读写操作。

而在Proactor模式中,事件处理者(或者代由事件分离器发起)直接发起一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。提供给操作系统的参数包括用于存放数据的用户自定义区,以及这个请求完后的回调函数等信息。事件分离器得知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事件处理者或者回调函数。举例来说,在Windows事件处理者可以提交一个异步IO操作(微软称为overlapped技术)事件分离器则等待IOCompletion事件[1]表示操作完成这种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之为系统级别的或者真正意义上的异步,因为具体的读写是由操作系统代劳的。

举另外个例子来更好地理解ReactorProactor两种模式的区别。这里我们只关注read操作,因为write操作也是差不多的。下面是Reactor的做法:

· 某个事件处理者宣称它对某个socket上的读就绪事件很感兴趣(表示socket缓冲区可读);

· 事件分离器等着这个事件的发生;

· 事件发生时会唤醒事件分离器,由分离器通知先前那个事件处理者;

· 事件处理者收到消息,于是去目标socket的缓冲区上读数据了,这一次读完后,会把控制权还给调度器(dispatcher如果数据没完(缓冲区内可能只来了部分数据),可以再次宣称对这个socket上的读事件感兴趣,一直重复上面的步骤直到把数据读完;

下面再来看看真正意义的异步模式Proactor是如何做的:

· 事件处理者提交一个异步读操作(当然,操作系统必须支持异步I/O). 这个时候,事件处理者根本不关心I/O就绪事件,它只管提交这么个请求,然后等着接收这个操作的完成事件

· 事件分离器等着这个读事件的完成;

· 事件分离器默默等待完成事件到来的同时,操作系统会在一个内核线程上执行这个读操作,它分几次从目标socket的缓冲区上读取数据,再统一转储到用户自定义区中,最后通知事件分离器读操作完成;

· 事件分离器通知之前的事件处理者

· 事件处理者这时会发现想要读的数据已经完整地放在用户自定义区中,如有需要(目标可以无限大,需要读一点处理一点),事件处理者还可以继续提交一个新的异步读操作,再将控制权交还给事件分离器。

现行做法

开源C++开发框架 ACE[13](Douglas Schmidt, et al.开发提供了大量平台独立的底层并发支持类(线程、互斥量等). 同时在更高一层它也提供了独立的几组C++类,用于实现ReactorProactor模式。 尽管它们都是平台独立的单元,但他们都提供了不同的接口.

ACE ProactorMS-Windows上无论是性能还在健壮性都更胜一筹,这主要是由于Windows提供了一系列高效的底层异步API. [45].

(这段可能过时了点吧不幸的是,并不是所有操作系统都为底层异步提供健壮的支持。举例来说许多Unix系统就有麻烦.因此, ACE Reactor可能是Unix系统上更合适的解决方案正因为系统底层的支持力度不一,为了在各系统上有更好的性能,开发者不得不维护独立的好几份代码Windows准备的ACE Proactor以及为Unix系列提供的ACE Reactor.

就像我们提到过的,真正的异步模式需要操作系统级别的支持。由于事件处理者及操作系统交互的差异,为ReactorProactor设计一种通用统一的外部接口是非常困难的。这也是设计通行开发框架的难点所在。

更好的解决方案

在文章这一段时,我们将尝试提供一种融合了ProactorReactor两种模式的解决方案为了演示这个方案,我们将Reactor稍做调整,模拟成异步的Proactor(主要是在事件分离器里完成本该事件处理者做的分多次socket缓冲区的工作,我们称这种方法为"模拟异步")。 下面的示例可以看看read操作是如何完成的:

· 事件处理者宣称对IO事件就绪)感兴趣,并提供给事件分离器用于存储结果的用户自定义地址和数据长度等参数;

· 调度器(dispatcher等待事件(比如通过select()方法);

· 当有事件到来(即可读)调度器被唤醒它负责执行一个非阻塞的读操作(前面事件处理者已经给了足够的信息了)。读完后,它去通知事件处理者。

· 事件处理者这时被知会读操作已完成,它拥有完整的原先想要获取的数据了.

我们看到,通过给分离器的I/O模块(也就上面的调度器)加一些功能,可以让Reactor模式转换为Proactor模式。且转换后的这些操作,总工作量上其实是和Reactor模式完全一致的。我们只是把任务重新分配给不同的角色去完成而已。这样并不会有额外的开销,也不会有性能上的的损失,我们可以再仔细看看下面的两个过程,他们实际上完成了一样的事情:

标准/经典的 Reactor模式:

· 步骤 1) 等待事件 (Reactor 的工作)

· 步骤 2) "已经可读"事件发给事先注册的事件处理者或者回调 ( Reactor 的工作)

· 步骤 3) 读数据 (用户定义的处理者的工作)

· 步骤 4) 处理数据 (用户定义的处理者的工作)

模拟的Proactor模式:

· 步骤 1) 等待事件 (Proactor 的工作)

· 步骤 2) 读数据(现在是Proactor 的工作)

· 步骤 3) 把“读取完毕”事件调度(dispatch)给处理者(Proactor 的工作)

· 步骤 4) 处理数据 (用户定义的处理者的工作)

在没有底层异步I/O API支持的操作系统,这种方法可以帮我们隐藏掉socket接口的差异提供一个完全可用的统一"异步接口"。这样我们就可以开发真正平台独立的通用接口了。

TProactor

我们提出的TProactor方案已经由TerabitP/L [6]公司实现了它有两种实现: C++的和Java.C++版本使用了ACE平台独立的底层元件,最终在所有操作系统上提供了统一的异步接口。

TProactor中最重要的组件要数EngineWaitStrategy. Engine用于维护异步操作的生命周期;而WaitStrategy用于管理并发策略. WaitStrategyEngine一般是成对出现的两者间提供了良好的匹配接口.

Engines和等待策略被设计成高度可组合的(完整的实现列表请参照附录1)TProactor是高度可配置的方案,通过使用异步内核API和同步Unix API(select()poll(), /dev/poll (Solaris 5.8+), port_get (Solaris 5.10),RealTime (RT) signals (Linux 2.4+), epoll (Linux 2.6), k-queue (FreeBSD) ),它内部实现了三种引擎(POSIX AIO, SUN AIO and Emulated AIO)并隐藏了六类等待策略。TProactor实现了和标准的 ACE Proactor一样的接口。这样一来,为不同平台提供通用统一的只有一份代码的跨平台解决方案成为可能。

EnginesWaitStrategies可以像乐高积木一样自由地组合,开发者可以在运行时通过配置参数来选择合适的内部机制(引擎和等待策略)。可以根据需求设定配置,比如连接数,系统伸缩性,以及运行的操作系统等。如果系统支持相应的异步底层API,开发人员可以选择真正的异步策略,否则用户也可以选择使用模拟出来的异步模式。所有这一切策略上的实现细节都不太需要关注,我们看到的是一个可用的异步模型。

举例来说,对于运行在Sun Solaris上的HTTP服务器,如果需要支持大量的连接数,/dev/poll或者port_get()之类的引擎是比较合适的选择;如果需要高吞吐量,那使用基本select()的引擎会更好。由于不同选择策略内在算法的问题,像这样的弹性选择是标准ACE Reactor/Proactor模式所无法提供的(见附录2)。

在性能方面,我们的测试显示,模拟异步模式并未造成任何开销,没有变慢,反倒是性能有所提升。根据我们的测试结果,TProactor相较标签的ACE ReactorUnix/Linux系统上有大约10-35%性能提升,而在Windows上差不多(测试了吞吐量及响应时间)

性能比较 (JAVA / C++ / C#).

除了C++,我们也在Java中实现了TProactor. JDK1.4, Java仅提供了同步方法C中的select() [78]. Java TProactor基于Java的非阻塞功能(java.nio),类似于C++TProactor使用了select()引擎.

12显示了以 bits/sec为单位的传输速度以及相应的连接数。这些图比较了以下三种方式实现的echo服务器:标准ACE Reactor实现(基于RedHat Linux9.0)TProactor C++/Java实现(Microsoft Windows平台及RedHat v9.0), 以及C#实现。测试的时候,三种服务器使用相同的客户端疯狂地连接,不间断地发送固定大小的数据包。

这几组测试是在相同的硬件上做的,在不同硬件上做的相对结果对比也是类似。

图 1. Windows XP/P4 2.6GHz HyperThreading/512 MB RAM.

图 2. Linux RedHat 2.4.20-smp/P4 2.6GHz HyperThreading/512 MB RAM.

用户代码示例

下面是TProactor Java实现的echo服务器代码框架。总的来说,开发者只需要实现两个接口:一是OpRead,提供存放读结果的缓存;二是OpWrite,提供存储待写数据的缓存区。同时,开发者需要通过回调onReadComplated()onWriteCompleted()实现协议相关的业务代码。这些回调会在合适的时候被调用.

 

class EchoServerProtocol implements AsynchHandler

{

 

  AsynchChannel achannel = null;

 

  EchoServerProtocol( Demultiplexor m,  SelectableChannel channel ) 

  throws Exception

  {

    this.achannel = new AsynchChannel( m, this, channel );

  }

 

  public void start() throws Exception

  {

    // called after construction

    System.out.println( Thread.currentThread().getName() + 

    ": EchoServer protocol started" );

    achannel.read( buffer);

  }

 

  public void onReadCompleted( OpRead opRead ) throws Exception

  {

    if ( opRead.getError() != null )

    {

      // handle error, do clean-up if needed

      System.out.println( "EchoServer::readCompleted: " + 

      opRead.getError().toString());

      achannel.close();

      return;

    }

 

    if ( opRead.getBytesCompleted () <= 0)

    {

      System.out.println("EchoServer::readCompleted: Peer closed " 

       + opRead.getBytesCompleted();

      achannel.close();

      return;

    }

 

    ByteBuffer buffer = opRead.getBuffer();

 

    achannel.write(buffer);

  }

 

  public void onWriteCompleted(OpWrite opWrite) 

  throws Exception

  {

    // logically similar to onReadCompleted

    ...

  }

}

结束语

TProactor为多个平台提供了一个通用、弹性、可配置的高性能通讯组件,所有那些在附录2中提到的问题都被很好地隐藏在内部实现中了。

从上面的图中我们可以看出C++仍旧是编写高性能服务器最佳选择,虽然Java已紧随其后。然而因为Java本身实现上的问题,其在Windows上表现不佳(这已经应该成为历史了吧)

需要注意的是,以上针对Java的测试,都是以裸数据的形式测试的,未涉及到数据的处理(影响性能)

纵观AIOLinux上的快速发展[9], 我们可以预计Linux内核API将会提供大量更加强健的异步API, 如此一来以后基于此而实现的新的Engine/等待策略将能轻松地解决能用性方面的问题,并且这也能让标准ACE Proactor接口受益。

附录 I

TProactor中实现的Engines 和 等待策略

引擎类型

等待策略

操作系统

POSIX_AIO (true async)
aio_read()/aio_write()

aio_suspend()
Waiting for RT signal
Callback function

POSIX complained UNIX (not robust)
POSIX (not robust)
SGI IRIX, LINUX (not robust)

SUN_AIO (true async)
aio_read()/aio_write()

aio_wait()

SUN (not robust)

Emulated Async
Non-blocking read()/write()

select()
poll()
/dev/poll
Linux RT signals
Kqueue

generic POSIX
Mostly all POSIX implementations
SUN
Linux
FreeBSD


附录 II

所有同步等待策略可划分为两组:

· 边缘触发edge-triggered ( Linux实时信号) - socket就绪后信号只发一次

· 水平触发level-triggered ( select(), poll(), /dev/poll) - 始终有效

两组的逻辑:

· 边缘触发组执行 I/O 操作后事件分离器会收不到后续socket就绪事件的通知

· 水平触发组当事件分离器侦测到某socket就绪状态,它负责回调对应的事件处理者。 但在触发回调前,应该先从监听的socket描述符列表中移除之,否则相同的事件可能被派发两次。

· 显然要解决上述这些问题给开发带来了额外的复杂度,而TProactor已经帮大家处理掉。

译注:

关于边缘(下蓝色)和水平(上红色)触发,一图胜千言




边缘触发仅当状态发生变化的时候才获得通知,这里所谓的状态的变化并不包括缓冲区中还有未处理的数据,也就是说,如果要采用边缘触发模式,需要一直read/write直到出错为止,很多人反映为什么采用边缘触发模式只接收了一部分数据就再也得不到通知了,大多因为这样;LT模式是只要有数据没有处理就会一直通知下去的.看来前者适用于数据到达时多而密,后者适用于数据到达时少而稀。

资源

[1] Douglas C. Schmidt, Stephen D. Huston "C++ Network Programming." 2002, Addison-Wesley ISBN 0-201-60464-7

[2] W. Richard Stevens "UNIX Network Programming" vol. 1 and 2, 1999, Prentice Hill, ISBN 0-13- 490012-X

[3] Douglas C. Schmidt, Michael Stal, Hans Rohnert, Frank Buschmann "Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects, Volume 2" Wiley & Sons, NY 2000

[4] INFO: Socket Overlapped I/O Versus Blocking/Non-blocking Mode. Q181611. Microsoft Knowledge Base Articles.

[5] Microsoft MSDN. I/O Completion Ports.
http://msdn.microsoft.com/library/default.asp?url=/library/en- us/fileio/fs/i_o_completion_ports.asp

[6] TProactor (ACE compatible Proactor).
www.terabit.com.au

[7] JavaDoc java.nio.channels
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/package-summary.html

[8] JavaDoc Java.nio.channels.spi Class SelectorProvider 
http://java.sun.com/j2se/1.4.2/docs/api/java/nio/channels/spi/SelectorProvider.html

[9] Linux AIO development 
http://lse.sourceforge.net/io/aio.html, and
http://archive.linuxsymposium.org/ols2003/Proceedings/All-Reprints/Reprint-Pulavarty-OLS2003.pdf

更多Ian Barile "I/O Multiplexing & Scalable Socket Servers", 2004 February, DDJ 

Further reading on event handling
- http://www.cs.wustl.edu/~schmidt/ACE-papers.html

The Adaptive Communication Environment
http://www.cs.wustl.edu/~schmidt/ACE.html

Terabit Solutions
http://terabit.com.au/solutions.php

关于作者

Alex Libman has been programming for 15 years. During the past 5 years his main area of interest is pattern-oriented multiplatform networked programming using C++ and Java. He is big fan and contributor of ACE.

Vlad Gilbourd works as a computer consultant, but wishes to spend more time listening jazz :) As a hobby,he started and runs www.corporatenews.com.au website.


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值