Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1852912
  • 博文数量: 274
  • 博客积分: 2366
  • 博客等级: 大尉
  • 技术积分: 1880
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-22 09:37
文章分类

全部博文(274)

文章存档

2022年(1)

2020年(10)

2019年(7)

2018年(18)

2017年(26)

2016年(32)

2015年(43)

2014年(30)

2013年(44)

2012年(36)

2011年(17)

2010年(10)

分类: LINUX

2012-09-26 11:42:00

The C10K problem翻译

如今的web服务器需要同时处理一万个以上的客户端了,难道不是吗?毕竟如今的网络是个big place了。

现在的计算机也很强大了,你只需要花大概$1200就可以买一个1000MHz的处理器,2G的内存, 1000Mbit/sec的网卡的机器。让我们来看看--20000个客户,每个为50KHz,100Kbyes和 50Kbit/sec,那么没有什么比为这两万个客户端的每个每秒从硬盘读取4千字节然后发送到网络上 去更消耗资源的了。可以看出硬件不再是瓶颈了。 (That works out to $0.08 per client, by the way. Those $100/client licensing fees some operating systems charge are starting to look a little heavy!)

在1999年最繁忙的ftp站点,cdrom.com,尽管有G比特的网络带宽,却也只能同时处理10000个 客户端。在2001年,同样的速度可以被所提供,他们预期该趋势会因为大量的商业 用户而变得越来越普遍。

目前的瘦客户端模型也开始又变得流行起来了--服务器运行在Internet上,为数千个客户端服务。

基于以上一些考虑,这里就配置操作系统或者编写支持数千个网络客户端的代码问题提出一些 注意点,该论题是基于类Unix操作系统的--该系统是我的个人爱好,当然Windows也有占有一席之地。

Contents

2003年10月,Felix von Leitner整理了一个很好的和一个 ,该网站介绍了网络的可测量性,完成 了以不同网络系统调用和不同的操作系统为基准的性能比较。其中一项就是2.6版本的Linux内核 击败了2.4的内核,当然还有许多的图片可以给OS的开发者在平时提供点想法。
(See also the Slashdot comments; it'll be interesting to see whether anyone does followup benchmarks improving on Felix's results.)

如果你还没有读过W.Richard Stevens先生的的话,请尽快获取一份 拷贝,该书描述了许多关于编写高性能的服务器的I/O策略和各自的一些缺陷,甚至还讲述 了问题,同时你也可以阅读 的一些 notes。 
(Another book which might be more helpful for those who are *using* rather than *writing* a web server is  by Cal Henderson.)

以下所列的为几个包装好的库,它们概要了几中常见的技巧,并且可以使你的代码与具体操作 系统隔离,从而具有更好的移植性。

  • , 一个重量级的C I/O框架,用面向对象实现了一些I/O策略和其它有用的东西,特别的, 它的Reactor是用OO方式处理非阻塞I/O,而Proactor是用OO方式处理异步I/O的( In particular, his Reactor is an OO way of doing nonblocking I/O, and Proactor is an OO way of doing asynchronous I/O).
  •  一个C 的I/O框架,逐渐成为Boost库的一部分。it's like ACE updated for the STL era。
  •  由Niels Provos用C编写的一个轻量级的I/O框架。它支持kqueue和select,并且很 快就可以支持poll和epoll(翻译此文时已经支持)。我想它应该是只采用了水平触发机制,该机制 有好处当然也有不好的地方。Niels给出了 来说明时间和连接数目在处理一个事件上的功能,从图上可以看出kqueue和sys_epoll明显胜出。
  • 我本人也尝试过轻量级的框架(很可惜没有维持至今):
    •  是一个轻量级的C I/O框架,它使用任何一种准备就绪API(poll, select, /dev/poll, kqueue, sigio)实现水平触发准备就绪API。以其它,Poller的性能 好得多。该链接文档的下面一部分说明了如何使用这些准备就绪API。
    • rn 是一个轻量级的C I/O框架,也是我继Poller后的第二个框架。该框架可以很容易的被用 于商业应用中,也容易的适用于非C 应用中。它如今已经在几个商业产品中使用。
  • Matt Welsh在2000年四月关于在构建服务器方面如何平衡工作线程和事件驱动技术的使用写了 一篇,在该论文中描述了他自己的Sandstorm I/O框架。
  •  - 一个Windows下的异步套接字,文件和管道的库。
网络软件设计者往往有很多种选择,以下列出一些:
  • 是否处理多个I/O?如何处理在单一线程中的多个I/O调用?
    • 不处理,从头到尾使用阻塞和同步I/O调用,可以使用多线程或多进程来达到并发效果。
    • 使用非阻塞调用(如在一个设置O_NONBLOCK选项的socket上使用write)读取I/O,当I/O完 成时发出通知(如poll,/dev/poll)从而开始下一个I/O。这种主要使用在网络I/O上,而不是磁盘的I/O上。
    • 使用异步调用(如aio_write())读取I/O,当I/O完成时会发出通知(如信号或者完成端口),可以同时使用在网络I/O和磁盘I/O上。
  • 如何控制对每个客户的服务?
    • 对每个客户使用一个进程(经典的Unix方法,自从1980年一直使用)
    • 一个系统级的线程处理多个客户,每个客户是如下一种:
      • a user-level thread (e.g. GNU state threads, classic Java with green threads)
      • a state machine (a bit esoteric, but popular in some circles; my favorite)
      • a continuation (a bit esoteric, but popular in some circles)
    • o一个系统级的线程对应一个客户端(e.g. classic Java with native threads)
    • 一个系统级的线程对应每一个活动的客户端(e.g. Tomcat with apache front end; NT完成端口; 线程池)
  • 是否使用标准的操作系统服务,还是把一些代码放入内核中(如自定义驱动,内核模块,VxD)。

下面的五种方式应该是最常用的了。

  1. 一个线程服务多个客户端,使用非阻塞I/O和水平触发的就绪通知
  2. 一个线程服务多个客户端,使用非阻塞I/O和就绪改变时通知
  3. 一个服务线程服务多个客户端,使用异步I/O
  4. 一个服务线程服务一个客户端,使用阻塞I/O
  5. 把服务代码编译进内核

...把网络句柄设置为非阻塞模型,然后使用select()或poll()来告知哪个句柄已有数据在等待 处理。此模型是最传统的,在此模型下,由内核告知你某个文件描述符是否准备好,是否已经完 成你的任务自从上次内核告知已准备好以来(“水平触发”这个名字来源计算机硬件设计,与其 相对的是“边缘触发”,Jonathon Lemon在它的中介绍了这两个术语)。

注意:牢记内核的就绪通知仅仅只是个提示,当你试图从一个文件描述符读取数据时,该文件 描述符可能并没有准备好。这就是为什么需要在使用就绪通知的时候使用非阻塞模型的原因。

一个重要的瓶颈是read()或sendfile()从磁盘块读取时,如果该页当前并不在内存中。设置磁 盘文件描述符为非阻塞没有任何影响。同样的问题也发生在内存映射磁盘文件中。首先一个服务 需要磁盘I/O时,进程块和所有的客户端都必须等待,因此最初的非线程的性能就被消耗了。 
这也是异步I/O的目的,当然仅限于没有AIO的系统。处理磁盘I/O的工作线程或工作进程也可能遭遇此 瓶颈。一条途径就是使用内存映射文件,如果mincore()指明I/O必需的话,那么要求一个工作线 程来完成此I/O,然后继续处理网络事件。Jef Poskanzer提到Pai,Druschel和Zwaenepoel的 使用了这个方法,并且他们就此在 上做了一个演讲,看上去就好像 的一部分,在Linux的2.3.51 的内核中提供了该方法,感谢。

在上报了一个不错的成果,他们利用系统剖析 工具剖析它们的Flash Web服务器,然后再攻击其瓶颈。其中找到的一个瓶颈就是mincore(猜测 毕竟不是好办法),另外一个就是sendfile在磁盘块访问时。他们修改了sendfile(),当需要读 取的页不在内存中时则返回类似EWOULDBLOCK的值,从而提高了性能。The end result of their optimizations is a SpecWeb99 score of about 800 on a 1GHZ/1GB FreeBSD box, which is better than anything on file at spec.org.

在非阻塞套接字的集合中,关于单一线程是如何告知哪个套接字是准备就绪的,以下列出了几 种方法:

  •  
    遗憾的是,select()受限于FD_SETSIZE个句柄。该限制被编译进了标准库和用户程序(有些 版本的C library允许你在用户程序编译时放宽该限制)。

    See Poller_select (cch) for an example of how to use select() interchangeably with other readiness notification schemes.

  •  
    poll()虽然没有文件描述符个数的硬编码限制,但是当有数千个时速度就会变得很慢,因为 大多数的文件描述符在某个时间是空闲的,彻底扫描数千个描述符是需要花费一定时间的。

    有些操作系统(如Solaris 8)通过使用了poll hinting技术改进了poll(),该技术由测试过。

    See Poller_poll (cchbenchmarks) for an example of how to use poll() interchangeably with other readiness notification schemes.

  • /dev/poll
    这是在Solaris中被推荐的代替poll的方法。

    /dev/poll的背后思想就是利用poll()在大部分的调用时使用相同的参数。使用/dev/poll时 ,首先打开/dev/poll得到文件描述符,然后把你关心的文件描述符写入到/dev/poll的描述符, 然后你就可以从/dev/poll的描述符中读取到已就绪的文件描述符。

    /dev/poll 在Solaris 7() 中就已经存在,不过在 中才公开现身。,this has 10% of the overhead of poll()。

    关于/dev/poll在Linux上有多种不同的尝试实现,但是没有一种可以和epoll相比,不推荐在 Linux上使用/dev/poll。

    See Poller_devpoll (cch benchmarks ) for an example of how to use /dev/poll interchangeably with many other readiness notification schemes. (Caution - the example is for Linux /dev/poll, might not work right on Solaris.)

  • kqueue()
    这是在FreeBSD系统上推荐使用的代替poll的方法(and, soon, NetBSD).

    kqueue()即可以水平触发,也可以边缘触发,具体请看下面.

Readiness change notification(或边缘触发就绪通知)的意思就是当你给内核一个文件描述 符,一段时间后,如果该文件描述符从没有就绪到已经准备就绪,那么内核就会发出通知,告知 该文件描述符已经就绪,并且不会再对该描述符发出类似的就绪通知直到你在描述符上进行一些 操作使得该描述符不再就绪(如直到在send,recv或者accept等调用上遇到EWOULDBLOCK错误,或 者发送/接收了少于需要的字节数)。

当使用Readiness change notification时,必须准备好处理乱真事件,因为最常见的实现是只 要接收到任何数据包都发出就绪信号,而不管文件描述符是否准备就绪。

这是水平触发的就绪通知的相对应的机制。It's a bit less forgiving of programming mistakes, since if you miss just one event, the connection that event was for gets stuck forever. 然而,我发现edge-triggered readiness notification可以使编写带OpenSSL的 非阻塞客户端更简单,可以试下。

详细描述了这种模型.

有几种APIs可以使得应用程序获得“文件描述符已就绪”的通知:

  •  这是在FreeBSD系统上推荐使用边缘触发的方法 (and, soon, NetBSD).

    FreeBSD 4.3及以后版本,都支持 kqueue()/kevent(), 支持边沿触发和水平触发(请查看 的网页和他的BSDCon 2000关于的论文)。

    就像/dev/poll一样,你分配一个监听对象,不过不是打开文件/dev/poll,而是调用kqueue ()来获得。需要改变你所监听的事件或者获得当前事件的列表,可以在kqueue()返回的描述符上 调用kevent()来达到目的。它不仅可以监听套接字,还可以监听普通的文件的就绪,信号和I/O完 成的事件也可以.

    Note: 在2000.10,FreeBSD的线程库和kqueue()并不能一起工作得很好,当kqueue()阻塞时, 那么整个进程都将会阻塞,而不仅仅是调用kqueue()的线程。

    See Poller_kqueue (cchbenchmarks) for an example of how to use kqueue() interchangeably with many other readiness notification schemes.

    使用kqueue()的例程和库:

    •  -- 一个Python的kqueue()库.
    • ; 另外可以查看他在  上发表的帖子。


  • 这是Linux 2.6的内核中推荐使用的边沿触发poll.

    2001.7.11, Davide Libenzi提议了一个实时信号的可选方法,他称之为/dev/epoll< /a>, 该方法类似与实时信号就绪通知机制,但是结合了其它更多的事件,从而在大多数的事件获取上拥有更高的效率。

    epoll在将它的接口从一个/dev下的指定文件改变为系统调用sys_epoll后就合并到2.5版本的 Linux内核开发树中,另外也提供了一个为2.4老版本的内核可以使用epoll的补丁。

     展开了很久的争论,it may yet happen,but Davide is concentrating on firming up epoll in general first.

  •  (Linux 2.6 )  See:

  •  (proposal for Linux 2.6 )
    在2006 OLS上,Ulrich Drepper提议了一种最新的高速异步网络API. See:
    • his paper, ""


  • Linux2.4内核中推荐使用的边沿触发poll.

    2.4的linux内核可以通过实时信号来分派套接字事件,示例如下:

    /* Mask off SIGIO and the signal you want to use. */ sigemptyset(&sigset); sigaddset(&sigset, signum); sigaddset(&sigset, SIGIO); sigprocmask(SIG_BLOCK, &m_sigset, NULL); /* For each file descriptor, invoke F_SETOWN, F_SETSIG, and set O_ASYNC. */ fcntl(fd, F_SETOWN, (int) getpid()); fcntl(fd, F_SETSIG, signum); flags = fcntl(fd, F_GETFL); flags |= O_NONBLOCK|O_ASYNC; fcntl(fd, F_SETFL, flags); 当正常的I/O函数如read()或write()完成时,发送信号。要使用该段的话,在外层循环中编写 一个普通的poll(),在循环里面,当poll()处理完所有的描述符后,进入 循环。 如果sigwaitinfo()或sigtimedwait()返回了实时信号,那么siginfo.si_fd和 siginfo_si_band给出的信息和调用poll()后pollfd.fd和pollfd.revents的几乎一样。如果你处 理该I/O,那么就继续调用sigwaitinfo()。 
    如果sigwaitinfo()返回了传统的SIGIO,那么信号队列溢出了,你必须通过临时,然后返回到外层的poll()循环。

    See Poller_sigio (cch) for an example of how to use rtsignals interchangeably with many other readiness notification schemes.

    See Zach Brown's phhttpd 示例代码来如何直接使用这些特点. (Or don't; phhttpd is a bit hard to figure out...)

    [] 描述了最新的phhttp的基准测试,使用了不同的sigtimewait()和sigtimedwait4(),这些调用可以使你只用一次调用便获得多个信号。 有趣的是,sigtimedwait4()的主要好处是它允许应用程序测量系统负载(so it couldbehave appropriately)(poll()也提供了同样的系统负载 测量)。


  • Signal-per-fd是由Chandra和Mosberger提出的对实时信号的一种改进,它可以减少甚至削除实 时信号的溢出通过oalescing redundant events。然而是它的性能并没有epoll好. 论文() 比较了它和select(),/dev/poll的性能.

    ; 授权见. (到2001.9,在很重的负载情况下仍然存在稳定性问题,利用dkftpbench测试在4500个用户时将引发问题.

    See Poller_sigfd (cch) for an example of how to use signal-per-fd interchangeably with many other readiness notification schemes.

该方法目前还没有在Unix上普遍的使用,可能因为很少的操作系统支持异步I/O,或者因为它需 要重新修改应用程序(rethinking your applications)。 在标准Unix下,异步I/O是由 提供的,它把一个信号和值与每一个I/O操作关联起来。信号和其值的队列被有效地分配到用户的 进程上。异步I/O是POSIX 1003.1b实时标准的扩展,也属于Single Unix Specification,version 2.

AIO使用的是边缘触发的完成时通知,例如,当一个操作完成时信号就被加入队列(也可以使用 水平触发的完成时通知,通过调用即可, 不过我想很少人会这么做).

glibc 2.1和后续版本提供了一个普通的实现,仅仅是为了兼容标准,而不是为了获得性能上的提高。

Ben LaHaise编写的Linux AIO实现合并到了2.5.32的内核中,它并没有采用内核线程,而是使 用了一个高效的underlying api,但是目前它还不支持套接字(2.4内核也有了AIO的补丁,不过 2.5/2.6的实现有一定程序上的不同)。更多信息如下:

  • The page "" which tries to tie together all info about the 2.6 kernel's implementation of AIO (posted 16 Sept 2003)
  • Round 3: aio vs /dev/epoll by Benjamin C.R. LaHaise (presented at 2002 OLS)
  • , by Bhattacharya, Pratt, Pulaverty, and Morgan, IBM; presented at OLS '2003
  •  by Suparna Bhattacharya -- compares Ben's AIO with SGI's KAIO and a few other AIO projects
  •  - Ben's preliminary patches, mailing list, etc.
  •  - library implementing standard Posix AIO on top of libaio. .
Suparma建议先看看.

RedHat AS和Suse SLES都在2.4的内核中提供了高性能的实现,与2.6的内核实现相似,但并不完全一样。

2006.2,在网络AIO有了一个新的尝试,具体请看Evgeniy Polyakov的基于kevent的AIO.

1999, 

一书对aio做了很好的介绍.

 有一个指南介绍了早期的非标准的aio实现,可以看看,但是请记住你得把"aioread"转换为"aio_read"。

注意AIO并没有提供无阻塞的为磁盘I/O打开文件的方法,如果你在意因打开磁盘文件而引起 sleep的话, 你在另外一个线程中调用open()而不是把希望寄托在对aio_open()系统调用上。

在Windows下,异步I/O与术语"重叠I/O"和"IOCP"(I/O Completion Port,I/O完成端口)有一定联系。Microsoft的IOCP结合了 先前的如异步I/O(如aio_write)的技术,把事件完成的通知进行排队(就像使用了aio_sigevent字段的aio_write),并且它 为了保持单一IOCP线程的数量从而阻止了一部分请求。(Microsoft's IOCP combines techniques from the prior art like asynchronous I/O (like aio_write) and queued completion notification (like when using the aio_sigevent field with aio_write) with a new idea of holding back some requests to try to keep the number of running threads associated with a single IOCP constant.) 更多信息请看 Mark russinovich在sysinternals.com上的文章 , Jeffrey Richter的书"Programming Server-Side Applications for Microsoft Windows 2000" (, ), , or .

... 让read()和write()阻塞. 这样不好的地方在于需要为每个客户端使用一个完整的栈,从而比较浪费内存。 许多操作系统仍在处理数百个线程时存在一定的问题。如果每个线程使用2MB的栈,那么当你在32位的机器上运行 512(2^30 / 2^21=512)个线程时,你就会用光所有的1GB的用户可访问虚拟内存(Linux也是一样运行在x86上的)。 你可以减小每个线程所拥有的栈内存大小,但是由于大部分线程库在一旦线程创建后就不能增大线程栈大小,所以这样做 就意味着你必须使你的程序最小程度地使用内存。当然你也可以把你的程序运行在64位的处理器上去。

Linux,FreeBSD和Solaris系统的线程库一直在更新,64位的处理器也已经开始在大部分的用户中所使用。 也许在不远的将来,这些喜欢使用一个线程来服务一个客户端的人也有能力服务于10000个客户了。 但是在目前,如果你想支持更多的客户,你最好还是使用其它的方法。

For an unabashedly pro-thread viewpoint, see Why Events Are A Bad Idea (for High-concurrency Servers) by von Behren, Condit, and Brewer, UCB, presented at HotOS IX. Anyone from the anti-thread camp care to point out a paper that rebuts this one? :-)

 是标准Linux线程库的命名。 它从glibc2.0开始已经集成在glibc库中,并且高度兼容Posix标准,不过在性能和信号的支持度上稍逊一筹。是一个由IBM发起的项目,其目的是提供更好的Posix兼容的Linux线程支持。 现在已到2.2稳定版,并且运行良好...但是NGPT team 他们正在把NGPT的代码基改为support-only模式,因为他们觉得这才是支持社区长久运行的最好的方式。 NGPT小组将继续改进Linux的线程支持,但主要关注NPTL方面。 (Kudos to the NGPT team for their good work and the graceful way they conceded to NPTL.)是由  ( 的主要维护人员)和 发起的项目,目的是提供world-class的Posix Linux线程支持。

2003.10.5,NPTL作为一个add-on目录(就像linuxthreads一样)被合并到glibc的cvs树中,所以很有可能随glibc的下一次release而 一起发布。

Red Hat 9是最早的包含NPTL的发行版本(对一些用户来说有点不太方便,但是必须有人来打破这沉默[break the ice]...)

NPTL links:

  • 最初的基准测试表明可以处理10^6个线程
  •  比较了LinuxThreads,NPTL和IBM的NGPT的各自性能,结果看来NPTL比NGPT快的多。
这是我尝试写的描述NPTL历史的文章(也可以参考):

管理员在2009年8月13日编辑了该文章文章。

-->
阅读(981) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~