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,尽管有G比特的网络带宽,却也只能同时处理10000个 客户端。在2001年,同样的速度可以被几个ISP服务商所提供,他们预期该趋势会因为大量的商业 用户而变得越来越普遍。
目前的瘦客户端模型也开始又变得流行起来了–服务器运行在Internet上,为数千个客户端服务。
基于以上一些考虑,这里就配置操作系统或者编写支持数千个网络客户端的代码问题提出一些 注意点,该论题是基于类Unix操作系统的–该系统是我的个人爱好,当然Windows也有占有一席之地。
Contents
The C10K problem
相关网站
须首先阅读的书籍
I/O框架
I/O策略
Serve many clients with each thread, and use nonblocking I/O and level-triggered readiness notification
The traditional select()
The traditional poll()
/dev/poll (Solaris 2.7+)
kqueue (FreeBSD, NetBSD)
Serve many clients with each thread, and use nonblocking I/O and readiness change notification
epoll (Linux 2.6+)
Polyakov’s kevent (Linux 2.6+)
Drepper’s New Network Interface (proposal for Linux 2.6+)
Realtime Signals (Linux 2.4+)
Signal-per-fd
kqueue (FreeBSD, NetBSD)
Serve many clients with each thread, and use asynchronous I/O and completion notification
Serve one client with each server thread
LinuxThreads (Linux 2.0+)
NGPT (Linux 2.4+)
NPTL (Linux 2.6, Red Hat 9)
FreeBSD threading support
NetBSD threading support
Solaris threading support
Java threading support in JDK 1.3.x and earlier
Note: 1:1 threading vs. M:N threading
Build the server code into the kernel
Comments
Limits on open filehandles
Limits on threads
Java issues [Updated 27 May 2001]
Other tips
Zero-Copy
The sendfile() system call can implement zero-copy networking.
Avoid small frames by using writev (or TCP_CORK)
Some programs can benefit from using non-Posix threads.
Caching your own data can sometimes be a win.
Other limits
Kernel Issues
Measuring Server Performance
Examples
Interesting select()-based servers
Interesting /dev/poll-based servers
Interesting kqueue()-based servers
Interesting realtime signal-based servers
Interesting thread-based servers
Interesting in-kernel servers
O
ther interesting links
Related Sites
2003年10月,Felix von Leitner整理了一个很好的网站和一个 presentation,该网站介绍了网络的可测量性,完成 了以不同网络系统调用和不同的操作系统为基准的性能比较。其中一项就是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.)
Book to Read First
如果你还没有读过W.Richard Stevens先生的《Unix网络编程:第一卷》的话,请尽快获取一份 拷贝,该书描述了许多关于编写高性能的服务器的I/O策略和各自的一些缺陷,甚至还讲述 了“thundering herd”问题,同时你也可以阅读 Jeff Darcy写的关于高性能服务器设计的一些 notes。
(Another book which might be more helpful for those who are *using* rather than *writing* a web server is Building Scalable Web Sites by Cal Henderson.)
I/O框架
以下所列的为几个包装好的库,它们概要了几中常见的技巧,并且可以使你的代码与具体操作 系统隔离,从而具有更好的移植性。
ACE, 一个重量级的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).
ASIO 一个C++的I/O框架,逐渐成为Boost库的一部分。it’s like ACE updated for the STL era。
libevent 由Niels Provos用C编写的一个轻量级的I/O框架。它支持kqueue和select,并且很 快就可以支持poll和epoll(翻译此文时已经支持)。我想它应该是只采用了水平触发机制,该机制 有好处当然也有不好的地方。Niels给出了一张图 来说明时间和连接数目在处理一个事件上的功能,从图上可以看出kqueue和sys_epoll明显胜出。
我本人也尝试过轻量级的框架(很可惜没有维持至今):
Poller 是一个轻量级的C++ I/O框架,它使用任何一种准备就绪API(poll, select, /dev/poll, kqueue, sigio)实现水平触发准备就绪API。以其它不同的API为基准 ,Poller的性能 好得多。该链接文档的下面一部分说明了如何使用这些准备就绪API。
rn 是一个轻量级的C I/O框架,也是我继Poller后的第二个框架。该框架可以很容易的被用 于商业应用中,也容易的适用于非C++应用中。它如今已经在几个商业产品中使用。
Matt Welsh在2000年四月关于在构建服务器方面如何平衡工作线程和事件驱动技术的使用写了 一篇论文,在该论文中描述了他自己的Sandstorm I/O框架。
Cory Nelson’s Scale! library - 一个Windows下的异步套接字,文件和管道的库。
I/O
Strategies
网络软件设计者往往有很多种选择,以下列出一些:
是否处理多个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)。
下面的五种方式应该是最常用的了。
一个线程服务多个客户端,使用非阻塞I/O和水平触发的就绪通知
一个线程服务多个客户端,使用非阻塞I/O和就绪改变时通知
一个服务线程服务多个客户端,使用异步I/O
一个服务线程服务一个客户端,使用阻塞I/O
把服务代码编译进内核
1. 一个线程服务多个客户端,使用非阻塞I/O和水平触发的就绪通知
…把网络句柄设置为非阻塞模型,然后使用select()或poll()来告知哪个句柄已有数据在等待 处理。此模型是最传统的,在此模型下,由内核告知你某个文件描述符是否准备好,是否已经完 成你的任务自从上次内核告知已准备好以来(“水平触发”这个名字来源计算机硬件设计,与其 相对的是“边缘触发”,Jonathon Lemon在它的关于kqueue() 的论文中介绍了这两个术语)。
注意:牢记内核的就绪通知仅仅只是个提示,当你试图从一个文件描述符读取数据时,该文件 描述符可能并没有准备好。这就是为什么需要在使用就绪通知的时候使用非阻塞模型的原因。
一个重要的瓶颈是read()或sendfile()从磁盘块读取时,如果该页当前并不在内存中。设置磁 盘文件描述符为非阻塞没有任何影响。同样的问题也发生在内存映射磁盘文件中。首先一个服务 需要磁盘I/O时,进程块和所有的客户端都必须等待,因此最初的非线程的性能就被消
耗了。
这也是异步I/O的目的,当然仅限于没有AIO的系统。处理磁盘I/O的工作线程或工作进程也可能遭遇此 瓶颈。一条途径就是使用内存映射文件,如果mincore()指明I/O必需的话,那么要求一个工作线 程来完成此I/O,然后继续处理网络事件。Jef Poskanzer提到Pai,Druschel和Zwaenepoel的 Flash web服务器
使用了这个方法,并且他们就此在 Usenix’99上做了一个演讲,看上去就好像 FreeBSD和Solaris 中提供了mincore()一样,但是它并不是Single Unix Specification的一部分,在Linux的2.3.51 的内核中提供了该方法,感谢Chuck Lever。
在2003.11的 freebsd-hackers list中,Vivek Pei上报了一个不错的成果,他们利用系统剖析 工具剖析它们的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
在非阻塞套接字的集合中,关于单一线程是如何告知哪个套接字是准备就绪的,以下列出了几 种方法:
传统的select()
遗憾的是,select()受限于FD_SETSIZE个句柄。该限制被编译进了标准库和用户程序(有些 版本的C library允许你在用户程序编译时放宽该限制)。See Poller_select (cc, h) for an example of how to use select() interchangeably with other readiness notification schemes.
传统的poll()
poll()虽然没有文件描述符个数的硬编码限制,但是当有数千个时速度就会变得很慢,因为 大多数的文件描述符在某个时间是空闲的,彻底扫描数千个描述符是需要花费一定时间的。有些操作系统(如Solaris 8)通过使用了poll hinting技术改进了poll(),该技术由Niels Provos在1999年实现并利用基准测试程序测试过。
See Poller_poll (cc, h, benchmarks) 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(see patchid 106541) 中就已经存在,不过在Solaris 8 中才公开现身。在750个客户端的情况下,this has 10% of the overhead of poll()。
关于/dev/poll在Linux上有多种不同的尝试实现,但是没有一种可以和epoll相比,不推荐在 Linux上使用/dev/poll。
See Poller_dev
poll (cc, h 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()即可以水平触发,也可以边缘触发,具体请看下面.
2. 一个线程服务多个客户端,使用非阻塞I/O和就绪改变时通知
Readiness change notification(或边缘触发就绪通知)的意思就是当你给内核一个文件描述 符,一段时间后,如果该文件描述符从没有就绪到已经准备就绪,那么内核就会发出通知,告知 该文件描述符已经就绪,并且不会再对该描述符发出类似的就绪通知直到你在描述符上进行一些 操作使得该描述符不再就绪(如直到在send,recv或者accept等调用上遇到EWOULDBLOCK错误,或 者发送/接收了少于需要的字节数)。reactor和epoll
当使用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的 非阻塞客户端更简单,可以试下。
[Banga, Mogul, Drusha '99]详细描述了这种模型.
有几种APIs可以使得应用程序获得“文件描述符已就绪”的通知:
kqueue() 这是在FreeBSD系统上推荐使用边缘触发的方法 (and, soon, NetBSD).FreeBSD 4.3及以后版本,NetBSD(2002.10)都支持 kqueue()/kevent(), 支持边沿触发和水平触发(请查看Jonathan Lemon 的网页和他的BSDCon 2000关于kqueue的论文)。
就像/dev/poll一样,你分配一个监听对象,不过不是打开文件/dev/poll,而是调用kqueue ()来获得。需要改变你所监听的事件或者获得当前事件的列表,可以在kqueue()返回的描述符上 调用kevent()来达到目的。它不仅可以监听套接字,还可以监听普通的文件的就绪,信号和I/O完 成的事件也可以.
Note: 在2000.10,FreeBSD的线程库和kqueue()并不能一起工作得很好,当kqueue()阻塞时, 那么整个进程都将会阻塞,而不仅仅是调用kqueue()的线程。
See Poller_kqueue (cc, h, benchmarks) for an example of how to use kqueue() interchangeably with many other readiness notification schemes.
使用kqueue()的例程和库:
PyKQueue — 一个Python的kqueue()库.
Ronald F.Guilmette的echo的服务器例程; 另外可以查看他在 2000.9.28在freebsd 上发表的帖子。
epoll
这是Linux 2.6的内核中推荐使用的边沿触发poll.2001.7.11, Davide Libenzi提议了一个实时信号的

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。