如今的web服务器需要同时处理上万个客户端了,难道不是吗?毕竟如今的网络已经非常庞大了。
现在的计算机也很强大了,你只需要花大概1200美元就可以买一个有1000MHz处理器、2G内存、1000Mbit/sec网卡的机器。让我们看看 —— 分配给20000个客户端,每个50KHz、100Kbytes、50Kbit/sec。没有什么应用比为这两万个客户端的每一个,每秒从硬盘读取4Kbytes然后发送到网络上去更消耗资源的了。(每个客户端的开销大概0.08美元,相比之下某些操作系统每台客户端100美元的许可费看起来有点多!)因此硬件已经不再是瓶颈了。
在1999年最繁忙的ftp站点,cdrom.com,通过Gbit的网络同时满足10000个客户端的请求。在2001年,同样的速度可以被几个ISP服务商所提供,预期该趋势会因为大量的商业用户而变得越来越普遍。
瘦客户端模型开始又变得流行起来了 —— 这次服务器运行在Internet上,为数千个客户端服务。
针对上面的问题,这里就配置操作系统和编写支持数千个网络客户端的代码整理了一些笔记。出于我个人的爱好,这些讨论是围绕类Unix操作系统的,当然这方面Windows也有占有一席之地。
相关站点
见Nick Black的精彩的 Fast UNIX Servers ,描述了大约2009年的情况。
2003年10月,Felix von Leitner就网络的可扩展性整理了一个很好的 网页 和一份 PPT ,包括不同系统和各种网络系统调用的性能基准对比。其中一个结果是2.6 Linux内核确实击败了2.4内核,当然还有很多非常好的图片可以给OS开发者一些参考。(可以到 Slashdot 论坛看看是否有人做一些新的基准测试来改进Felix的结果。)
需要先看的书
如果你还没有读过W. Richard Stevens先生的 《Unix网络编程》(第一卷) 的话,请尽快获取一份拷贝,该书描述了关于编写高性能的服务器的很多I/O策略和各自的一些缺陷,甚至还讨论了 “惊群” 问题。可以同时读读Jeff Darcy关于高性能服务器设计的 一些笔记 。
(对于用而不是写web服务器的人来说,Cal Henderson的 《搭建可扩展的Web站点》 可能更有用。)
I/O框架
以下为几个打包好的库,它们包含了后面给出的几种技巧,可以使你的代码与具体操作系统隔离,从而具有更好的移植性。
- ACE ,一个重量级的C++ I/O框架,包含一些I/O策略和很多其它有用的东西的面向对象的实现。它的Reactor用OO方式处理非阻塞I/O,而Proactor用OO方式处理异步I/O。
- ASIO 是一个C++的I/O框架,并逐渐成为Boost库的一部分。它就像是ACE更新到STL版本。
- libevent 是Niels Provos用C编写的一个轻量级的I/O框架。它支持 kqueue 和 select ,并且很快就可以支持poll 和 epoll (目前已经支持)。我想它应该是只采用了水平触发机制,该机制有好处当然也有不好的地方。Niels绘制了 一张图 来给出每个事件的处理时间随连接数的变化,从图上可以看出 kqueue 和 sys_epoll 明显胜出。
- 我本人也尝试过写轻量级的框架(很可惜没有维持至今):
- Matt Welsh在2000年4月写了一篇关于构建可扩展服务器时如何平衡工作线程和事件驱动技术的使用的 论文 ,论文中描述了他自己的Sandstorm I/O框架的部分内容。
- Cory Nelson's Scale! library —— 一个Windows下的异步套接字,文件和管道的库。
I/O策略
网络软件设计者往往有很多种选择,以下列出一些:
- 是否并且如何在单线程中处理多个I/O调用
- 不处理,从头到尾使用阻塞/同步I/O调用,可能用多线程或多进程来达到并发
- 使用非阻塞调用(如在一个设置 O_NONBLOCK 选项的套接字上使用 write() )打开I/O,并使用就绪通知(如 poll() 或 /dev/poll )来知道什么时候开始下一个I/O。一般这只对网络I/O有用,而不是磁盘I/O。
- 使用异步调用(如 aio_write() )打开I/O,并使用完成通知(如信号或者完成端口)来知道什么时候I/O完成。对网络I/O和磁盘I/O都很好用。
- 如何控制对每个客户端的服务
- 对每个客户端使用一个进程(经典的Unix方法,自从1980年左右开始一直使用)
- 一个系统级的线程处理多个客户端,每个客户端采用:
- 一个用户级的线程(如GNU状态线程,经典的绿色线程Java)
- 一个状态机(有点深奥,但在某些圈子很流行;我喜欢的方式)
- 一个延续(有点深奥,但在某些圈子很流行)
- 对每个客户端使用一个系统级的线程(如经典的本地线程Java)
- 对每个活动的客户端使用一个系统级的线程(如用apache做前端的Tomcat,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的1999年的 Flash web服务器使用了这个方法,他们还在 Usenix'99 上做了一个相关的演讲。好像 FreeBSD 和Solaris之类的BSD Unix有 mincore() ,但它并不在 Single Unix Specification 中。在Linux的2.3.51内核中提供了该方法,这要感谢 Chuck Lever 。
但在2003年11月的freebsd-hackers list中, Vivek Pei等人 给出了利用系统分析工具分析Flash web服务器的瓶颈的结果。他们发现其中一个瓶颈就是 mincore (猜测毕竟不是一个好办法),另外一个是 sendfile 访问磁盘时阻塞。他们通过改写 sendfile() 提高了性能,方法是在读取不在内存中的磁盘页面时返回类似于 EWOULDBLOCK 的值。(不确定如何告诉用户页面现在已经常驻内存... 可能这里真正需要的是 aio_sendfile() 。)他们优化的最后结果是在1GHz/1GB FreeBSD机器上SpecWeb99得分大概在800左右,这比spec.org上的任何结果都要好。
单线程有几种方法来告诉我们一组非阻塞套接字中哪个是I/O准备就绪的:
-
传统的 select()
遗憾的是, select() 的句柄数受限于 FD_SETSIZE 。该限制被编译进了标准库和用户程序(有些版本的C库允许你在用户程序编译时放宽该限制)。
作为例子,见 Poller_select (cc, h)。
-
传统的 poll()
poll() 虽然没有文件描述符个数的硬编码限制,但是当达到数千个时速度就会变得很慢,因为大多数的文件描述符在某个时间是空闲的,而扫描全部数千个描述符需要花费一定的时间。
有些操作系统(如Solaris 8)通过使用poll hinting之类的技术改进了 poll() ,该技术由 Niels Provos等人 于1999年在Linux上实现并测量了性能。
作为例子,见 Poller_poll (cc, h, benchmarks)。
-
/dev/poll
这是Solaris中推荐的 poll 的替代方案。
/dev/poll 的背后思想就是利用 poll() 在大部分的调用时使用相同的参数。使用 /dev/poll 时,首先打开/dev/poll 得到文件描述符,然后把你关心的文件写入到该描述符,接下来你就可以从该描述符中读取到已就绪的文件描述符。
在Solaris 7(见 patchid 106541 )中它就已经存在,不过在 Solaris 8 中才公布。按照 Sun的说法 ,在750个客户端的情况下,性能比 poll() 高10%。
/dev/poll 在Linux上有多种不同的实现尝试,但是性能都比不上 epoll ,而且没有真正完成实现。不推荐在Linux上使用 /dev/poll 。
作为例子,见 Poller_devpoll (cc, h, benchmarks)。(注意:例子是针对Linux的,可能在Solaris上会有问题。)
-
kqueue()
这是在FreeBSD系统上推荐使用的 poll 的替代方案(不久后包括NetBSD)。
见后面。 kqueue() 即可以水平触发,也可以边缘触发。
2. 一个线程服务多个客户端,使用非阻塞I/O和就绪改变时通知
就绪改变时通知(或边缘触发就绪通知)的意思就是当你给内核一个文件描述符,一段时间后,如果该描述符从没有就绪到已经就绪,那么内核就会发出通知。它假定你知道文件描述符已经就绪,不会再为该描述符发出更多的就绪通知,直到你在描述符上进行一些操作使得该描述符不再就绪(如直到在 send , recv 或者 accept 等调用上遇到EWOULDBLOCK 错误,或者发送/接收了少于请求字节数的数据)。
当使用就绪改变时通知时,必须准备好处理乱真事件,因为一个常见的实现是只要接收到任何数据包都发出就绪信号,而不管文件描述符是否已经就绪。
这和水平触发的就绪通知相反。它对编程错误的容忍度低些,只错过一个事件,对应的连接就会卡住。但是我发现边缘触发就绪通知使编写带OpenSSL的非阻塞客户端变得更容易,因此值得一试。
[Banga, Mogul, Drusha '99] 在1999年描述了这种方案。
有几种API可以让应用程序得到“文件描述符已就绪”的通知:
-
kqueue()
这是在FreeBSD系统上推荐使用的边缘触发的 poll 替代(不久后包括NetBSD)。
FreeBSD 4.3和以后的版本,还有 NetBSD(自Oct 2002版本起) ,都支持 kqueue()/kevent() 作为 poll() 的一般替代方法,它同时支持边缘触发和水平触发。(请看 Jonathan Lemon的页面 和他的 关于kqueue()的BSDCon 2000论文 。)
就像 /dev/poll 一样,分配一个监听对象,但不是打开文件 /dev/poll ,而是调用 kqueue() 来分配。如果需要改变监听的事件或者获取当前事件的列表,可以在 kqueue() 返回的描述符上调用 kevent() 。它不仅可以监听套接字的就绪,还包括普通文件的就绪、信号,甚至是I/O完成事件。
Note: 截至2000年10月,FreeBSD的线程库和 kqueue() 的兼容有问题。当 kqueue() 阻塞时,整个进程都将会阻塞,而不仅仅是调用 kqueue() 的线程。
作为例子,见 Poller_kqueue (cc, h, benchmarks)。
使用 kqueue() 的例程和库:
- PyKQueue —— 一个Python的 kqueue() 库
- Ronald F.Guilmette的echo服务器示例 ,也可以参看2000年9月28日他在 freebsd.questions上发表的帖子 。
-
epoll
这是Linux 2.6内核推荐使用的边缘触发的 poll 替代。
2001年7月11日,Davide Libenzi提议了一个实时信号的替代方案,他称之为 /dev/epollhttp://www.xmailserver.org/linux-patches/nio-improve.html , 该方法类似于实时信号就绪通知机制,但是结合了其它更多的事件,从而在大多数的事件获取上拥有更高的效率。
在 epoll 的接口从/dev下的特定文件改为系统调用 sys_epoll 之后,它就被合并到了2.5版(2.5.46)的内核中。2.4版的内核也有一个老版本 epoll 的补丁。
2002年万圣节时,linux-kernel邮件列表里就 统一epoll, aio和其他事件方法 持续了很长的争论。Davide正努力使 epoll 变得更加稳定。
-
Polyakov的 kevent (Linux 2.6+)的最新进展:在2006年2月9日和2006年7月9日,Evgeniy Polyakov发布了一个看起来是统一 epoll 和 aio 的补丁,他的目标是支持网络AIO。见:
-
Drepper的最新的网络接口(针对Linux 2.6+)
在OLS 2006上,Ulrich Drepper提议了一种新的高速异步网络API。见:
-
实时信号
这是Linux 2.4内核推荐使用的边缘触发的 poll 替代。
Linux 2.4内核可以通过一个特殊的实时信号来分派套接字就绪事件。下面是使用示例:
/* 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() 循环。
如果 sigwaitinfo 或 sigtimedwait 返回了实时信号,那么 siginfo.si_fd 和 siginfo.si_band 给出的信息和调用 poll() 后 pollfd.fd 和 pollfd.revents 的几乎一样。因此处理该I/O,然后继续调用 sigwaitinfo() 。
如果 sigwaitinfo 返回了传统的 SIGIO ,那么信号队列溢出了,你必须通过临时改变信号处理程序为SIG_DFL来刷新信号队列,然后返回到外层的 poll() 循环。
作为例子,见 Poller_sigio (cc, h)。
参考 Zach Brown的phhttpd 作为示例来了解如何直接使用这个特性。(不过phhttpd有点难懂...)
[Provos, Lever, and Tweedie 2000]给出了使用 sigtimedwait() 的一个变种 —— sigtimedwait4() 的新的phhttpd的基准测试,它可以使你在一次调用中获得多个信号。有趣的是, sigtimedwait4() 的主要好处好像是它允许应用程序测量系统负载(因此它能够表现正常)。(注意 poll() 也提供了同样的系统负载测量。)
-
Signal-per-fd
Signal-per-fd是由Chandra和Mosberger提出的对实时信号的一种改进,它可以通过合并多余的事件减少甚至消除实时信号的队列溢出。然而它的性能没有 epoll 好。他们的论文(http://www.hpl.hp.com/techreports/2000/HPL-2000-174.html)比较了这种方案和 select() 、 /dev/poll 的性能。
Vitaly Luban在2001年5月18日发布了一个实现该方案的补丁 ,授权见 http://www.luban.org/GPL/gpl.html 。(注意:到2001年9月,在高负载情况下仍然存在稳定性问题,用 dkftpbench 在约4500个用户情况下测试时会出错。)
作为例子,见 Poller_sigfd (cc, h)。
3. 一个服务线程服务多个客户端,使用异步I/O
该方法目前还没有在Unix上普遍使用,可能因为很少有操作系统支持异步I/O,或者因为它(就像非阻塞I/O)需要你改写应用程序。在标准Unix下,异步I/O是由 aio_ 接口 提供的,它把一个信号和值与每一个I/O操作关联起来。信号和其值的队列被有效地分配到用户进程上。异步I/O是POSIX 1003.1b实时标准的扩展,也属于Single Unix Specification第二版。
AIO使用的是边缘触发的完成时通知,即当一个操作完成时向队列加入一个信号。(也可以通过调用 aio_suspend()使用水平触发的完成时通知,不过我想很少有人会这么做。)
glibc 2.1和后续版本提供了一个一般性的实现,为了兼容标准而不是提高性能。
Ben LaHaise编写的Linux AIO实现合并到了Linux 2.5.32的内核中,它并没有采用内核线程,而是使用了一个高效的底层API,但是目前(2.6.0-test2)它还不支持套接字。(2.4内核也有一个AIO的补丁,但是2.5/2.6的实现有些不同。)更多信息如下:
- "Kernel Asynchronous I/O (AIO) Support for Linux" 尝试整理了2.6内核的AIO实现的全部信息(发表于2003年9月16日)
- Benjamin C.R. LaHaise的 Round 3: aio vs /dev/epoll (于2002 OLS)
- Bhattacharya, Pratt, Pulaverty, 和Morgan, IBM的 Asynchronous I/O Suport in Linux 2.5 (于2003 OLS)
- Suparna Bhattacharya的 Design Notes on Asynchronous I/O (aio) for Linux —— 比较了Ben的使用SGI的KAIO的AIO和一些其他的AIO项目
- Linux AIO home page - Ben的初步的补丁和邮件列表等等
- linux-aio mailing list archives
- libaio-oracle - 基于libaio实现的标准Posix AIO库。 由Joel Becker于2003年4月18日首先提出 。
Suparma建议先看看 AIO的DAFS API的方法 。
RedHat AS 和Suse SLES都在2.4内核中提供了高性能的实现,与2.6内核中的实现相似,但并不完全一样。
2006年2月,出现了一个提供网络AIO的新的尝试,见上文Evgeniy Polyakov的基于kevent的AIO的笔记。
1999年,SGI为Linux实现了一个 高速的AIO 。到1.1版本时,据说已经可以很好地支持磁盘I/O和套接字。看起来它使用了内核线程。对于不能等待Ben的AIO提供套接字支持的人来说,它仍然不错。
O'Reilly的 POSIX.4: Programming for the Real World 一书对aio做了很好的介绍。
在 Sunsite 上有一个指南介绍了Solaris上早期的非标准的aio实现。可能值得一看,但是请记住你得把 aioread 转换为aio_read ,之类。
注意AIO并没有提供无阻塞的磁盘I/O打开文件的方法,如果你在意打开磁盘文件时的休眠的话, Linus建议你应该在另外一个线程中调用 open() ,而不是找 aio_open() 这样的系统调用。
在Windows下,异步I/O与术语“重叠I/O”和“IOCP”(I/O完成端口)有一定的联系。Microsoft的IOCP结合了先前的如异步I/O(像 aio_write )的技术,把完成时通知加入队列(就像使用了 aio_sigevent 字段的 aio_write ),目的是通过阻止一些请求来尝试控制和单一IOCP常数相关的运行线程的数量。更多信息见Mark Russinovich在sysinternals.com上的 Inside I/O Completion Ports ,Jeffrey Richter的书"Programming Server-Side Applications for Microsoft Windows 2000"( Amazon, MSPress), U.S. patent #06223207, 或 MSDN 。
4. 一个服务线程服务一个客户端,使用阻塞I/O
... 让 read() 和 write() 阻塞。这样做的缺点是需要为每个客户端使用一个完整的栈帧,比较浪费内存。许多操作系统在处理数百个线程时还存在一定的问题。如果每个线程使用一个2MB的栈,那么当你在32位的机器上运行(2^30 / 2^21) = 512个线程时,你就会用光有1GB的用户可访问虚拟内存的全部虚拟内存(让Linux运行在x86上)。你可以减小每个线程所拥有的栈内存大小,但是由于大部分线程库在线程创建后就不能增大线程栈大小,所以这样做就意味着你必须使你的程序最小程度地使用内存。当然你也可以把程序运行在64位的处理器上来解决。
Linux、FreeBSD和Solaris系统的线程库一直在更新,64位的处理器也已经开始在主流用户中使用。也许在不远的将来,这些喜欢使用一个线程来服务一个客户端的人也有能力服务10000个客户了。但是在目前,如果你想支持更多的客户,你最好还是使用其它的方法。
作为仍然赞成使用线程的观点。可以看UCB的von Behren、Condit和Brewer的 Why Events Are A Bad Idea (for High-concurrency Servers) ,发表在HotOS IX。反对使用线程的阵营的人们,有人愿意找一篇反驳它的论文吗?:-)
-
LinuxThreads
LinuxTheads 是标准Linux线程库的名字。它从glibc2.0开始已经集成在glibc库中,并且高度兼容Posix标准,不过在性能和信号的支持上稍逊一筹。
-
NGPT:Linux的下一代Posix线程
NGPT 是一个由IBM发起的项目,其目的是提供更好的Posix兼容的Linux线程支持。现在已到2.2稳定版,并且运行良好... 但是NGPT开发组已经宣布他们正在把NGPT的代码改为support-only模式,因为他们觉得这才是支持社区长久运行的最好方式。NGPT小组将继续改进Linux的线程支持,但现在主要关注NPTL。(感谢NGPT开发组的工作和他们的大无私精神。)
-
NPTL:Linux的原生Posix线程库
NPTL 是由 Ulrich Drepper ( glibc 的主要维护人员)和 Ingo Molnar 发起的项目,目的是为Linux提供世界级的Posix线程支持。
2003年10月5日,NPTL作为一个add-on目录(就像linuxthreads一样)被合并到glibc的cvs树中,所以很有可能随glibc的下一个稳定版一起发布。
Red Hat 9是最早的包含NPTL的主要发行版本(对一些用户来说有点不太方便,但是必须有人来打破这个沉默...)
NPTL的链接:
- NPTL讨论的邮件列表
- NPTL源码
- NPTL的最初发表
- 最初的描述NPTL目标的白皮书
- 修改后的NPTL的最后设计的白皮书
- Ingo Molnar的最早的基准测试 表明它可以处理10^6个线程
- Ulrich的基准测试 比较了LinuxThreads、NPTL和IBM的NGPT的性能,结果看来NPTL比NGPT快的多。
这是我尝试写的描述NPTL历史的文章(也可以参考 Jerry Cooperstein的文章 ):
2002年3月,NGPT小组的Bill Abt,glibc的维护者Ulrich Drepper和其它人召开了个会议 来探讨LinuxThreads的发展,会议产生的一个想法就是要改进锁的性能。 Rusty Russell 等人 随后实现了 快速用户区的锁(futexes) ,如今已在NGPT和NPTL中使用了。与会的大部分人都认为NGPT应该合并到glibc中。
然而Ulrich Drepper并不怎么喜欢NGPT,他认为他可以做得更好。(对那些曾经想提供补丁给glibc的人来说,这应该不会令他们感到惊讶:-) )于是在接下来的几个月里,Ulrich Drepper、Ingo Molnar和其它人致力于glibc和内核的改变,然后就弄出了原生Posix线程库(NPTL)。NPTL使用了NGPT设计的所有内核改进,并且采用了几个最新的改进。Ingo Molnar 描述了 几个内核改进:
NPTL使用了三个由NGPT引入的内核特征: getpid() 返回 PID , CLONE_THREAD 和 futexes ;NPTL还使用了(并且依赖于)也是该项目的一部分的一个更大的内核特征集。
一些由NGPT引入内核2.5.8的项也被修改,清除和扩展,例如线程组的处理( CLONE_THREAD )。[影响到NGPT兼容性的 CLONE_THREAD 的变更同步到了NGPT,以保证它不出错。]
这些为NPTL开发的并且后来在NPTL中使用的内核特征都描述在设计白皮书中,http://people.redhat.com/drepper/nptl-design.pdf ...
一个简要的列表:TLS的支持,各种克隆的扩展( CLONE_SETTLS, CLONE_SETTID, CLONE_CLEARTID),POSIX线程-信号的处理, sys_exit() 的扩展(基于VM的TID futex), sys_exit_group() 系统调用,sys_execve() 的增强和对分离线程的支持。
也对PID空间做了扩展,比如64K PID假设造成的procfs崩溃,max_pid,PID分配的可扩展性的工作。此外还有很多性能方面的改进。
本质上,新特性是绝对的1:1线程的方法:对每个基本的线程原语,内核现在为改进线程竭尽所能,而我们只做最基本的上下文切换和内核调用。
NGPT和NPTL的一个最大的不同就是NPTL是1:1的线程模型,而NGPT是M:N的线程模型(具体请看下面)。尽管这样,Ulrich的最初的基准测试 还是表明NPTL比NGPT快很多。(NGPT小组期待查看Ulrich的测试程序来核实他的结果。)
-
FreeBSD线程支持
FreeBSD同时支持LinuxThreads和用户空间的线程库。另外,一个称为KSE的M:N的实现引入了FreeBSD 5.0中。介绍可以看 www.unobvious.com/bsd/freebsd-threads.html 。
2003年3月25日, Jeff Roberson在freebsd-arch上发表了 :
... 感谢Julian、David Xu、Mini、Dan Eischen和其它每一位参加了KSE和libpthread开发的成员所提供的基础,Mini和我已经开发出了一个1:1的线程实现。它可以和KSE并行工作而不会带来任何影响。实际上,通过测试共享位它可以帮助更容易完成M:N线程。
然后2006年7月, Robert Watson提议1:1的线程实现应该作为FreeBSD 7.x的默认实现 :
我知道曾经讨论过这个问题,但是我认为随着7.x的向前推进,这个问题应该重新考虑了。在很多通常的应用程序和场景的基准测试中,libthr明显比libpthread在性能上要好得多。libthr也在大量的平台上被实现了,而libpthread却仍只在少数平台上。最主要的是MySQL和其它的大量线程的使用者“转换到libthr”,这也是暗示!which is suggestive, also! ... 所以strawman提议:让libthr成为7.x上的默认线程库。
-
NetBSD线程支持
根据Noriyuki Soda的描述:
内核支持的基于调度器激活模型的N:N线程库于2003年1月18日合并到了NetBSD-current中。
细节见Wasabi Systems, Inc.公司的Nathan J. Williams在FREENIX'02上的演讲 An Implementation of Scheduler Activations on the NetBSD Operating System 。
-
Solaris线程支持
Solaris的线程支持正在发展中... 从Solaris 2到Solaris 8,默认的线程库使用的都是M:N模型,但是Solaris 9却默认使用了1:1模型。见 Sun的多线程编程指南 和 Sun的关于Java和Solaris线程的文章 。
-
Java在JDK 1.3.x及更早版本的线程支持
大家都知道,Java一直到JDK1.3.x都没有除了每个客户端一个线程之外的任何处理网络连接的方法。Volanomark 是一个不错的微型基准测试工具,可以用来测量不同并发连接数的每秒消息吞吐量。在2003年5月,JDK 1.3的实现实际上可以同时处理10000个连接,但是性能却严重下降了。从 表4 可以看到JVM可以处理10000个连接,以及随着连接数的增长的性能变化。
-
Note: 1:1线程 vs. M:N线程
在实现线程库的时候有一个选择:你可以把所有的线程支持都放到内核中(也就是所谓的1:1线程模型),也可以把一些移到用户空间上去(也就是所谓的M:N线程模型)。从某个角度来说,M:N被认为拥有更好的性能,但是由于很难被正确的编写,所以大部分人都远离了该方法。
- 为什么Ingo Molnar相对于M:N更喜欢1:1
- Sun改为1:1线程
- NGPT 是针对Linux的M:N线程库
- 尽管 Ulrich Drepper计划在新的glibc线程库中使用M:N线程 ,但还是 选用了1:1线程模型 。
- MacOSX也将使用1:1线程
- FreeBSD 和 NetBSD 仍然将使用M:N线程... 孤独的坚持者?但FreeBSD 7.0也倾向于使用1:1线程(见上面),因此可能M:N线程的拥护者最后被证明是错的。
5. 把服务代码编译进内核
Novell和Microsoft都宣称已经在不同时期完成了该工作,至少NFS的实现完成了该工作。 khttpd 在Linux下为静态WEB页面完成了该工作,Ingo Molnar开发了 "TUX"(Threaded linUX WEB服务器) ,这是一个Linux下的快速的可扩展的内核空间的HTTP服务器。Ingo在 2000年9月1日宣布 alpha版本的TUX可以在ftp://ftp.redhat.com/pub/redhat/tux 下载,并且介绍了如何加入邮件列表来获取更多信息。
在Linux内核的邮件列表上讨论了该方法的好处和缺点,多数人认为不应该把WEB服务器放进内核中,相反内核应该加入尽可能小的钩子来提高WEB服务器的性能。这样对其它形式的服务器就有益。见 Zach Brown的讨论 ,他对比了用户级别和内核的http服务器。在2.4的Linux内核中为用户程序提供了足够的动力,比如 X15 服务器运行的速度和TUX几乎一样,但是它没有对内核做任何修改。
评论
Richard Gooch曾经写了一篇讨论I/O选项的论文。
在2001, Tim Brecht和MMichal Ostrowski为使用简单的select的服务器 做了各种策略的测度 测试的数据值得看一看。
在2003, Tim Brecht发表了 userver的源码, 该服务器是整合了Abhishek Chandra, David Mosberger, David Pariag和Michal Ostrowski所写的几个服务器而成的, 可以使用select(), poll(), epoll()和sigio.
回到1999.3, Dean Gaudet发表:
我一直在问“为什么你们不使用基于select/event的模型,它明显是最快的。”...
他们的理由是“太难理解了,并且其中关键部分(payoff)不清晰”,但是几个月后,当该模型变得易懂时人们就开始愿意使用它了。
Mark Russinovich写了 一篇评论和 文章讨论了在2.2的linux内核只能够I/O策略问题。 尽管某些地方似乎有点错误,不过还是值得去看。特别是他认为Linux2.2的异步I/O (请看上面的F_SETSIG) 并没有在数据准备好时通知用户进程,而只有在新的连接到达时才有。 这看起来是一个奇怪的误解。 还可以看看 早期的一些comments, Ingo Molnar在1999.4.30所举的反例, Russinovich在1999.5.2的comments, Alan Cox的 反例,和各种 linux内核邮件. 我怀疑他想说的是Linux不支持异步磁盘I/O,这在过去是正确的,但是现在SGI已经实现了KAIO,它已不再正确了。
查看页面 sysinternals.com和 MSDN了解一下“完成端口”, 据说它是NT中独特的技术, 简单说,win32的"重叠I/O"被认为是太低水平而不方面使用,“完成端口”是提供了完成事件队列的封装,再加上魔法般的调度, 通过允许更多的线程来获得完成事件如果该端口上的其它已获得完成事件的线程处于睡眠中时(可能正在处理阻塞I/O),从而可以保持运行线程数目恒定(scheduling magic that tries to keep the number of running threads constant by allowing more threads to pick up completion events if other threads that had picked up completion events from this port are sleeping (perhaps doing blocking I/O).
查看OS/400的I/O完成端口支持.
在1999.9,在linux内核邮件列表上曾有一次非常有趣的讨论,讨论题目为 "15,000 Simultaneous Connections" (并且延续到第二周). Highlights:
- Ed Hall 发表了一些他自己的经验:他已经在运行Solaris的UP P2/333上完成>1000个连接每秒。 他的代码使用了一个很小的线程池(每个cpu 1或者2个线程池),每个线程池使用事件模型来管理大量的客户端连接。
- Mike Jagdisposted an analysis of poll/select overhead, and said "The current select/poll implementation can be improved significantly, especially in the blocking case, but the overhead will still increase with the number of descriptors because select/poll does not, and cannot, remember what descriptors are interesting. This would be easy to fix with a new API. Suggestions are welcome..."
- Mike posted about his work on improving select() and poll().
- Mike posted a bit about a possible API to replace poll()/select(): "How about a 'device like' API where you write 'pollfd like' structs, the 'device' listens for events and delivers 'pollfd like' structs representing them when you read it? ... "
- Rogier Wolff suggested using "the API that the digital guys suggested",http://www.cs.rice.edu/~gaurav/papers/usenix99.ps
- Joerg Pommnitz pointed out that any new API along these lines should be able to wait for not just file descriptor events, but also signals and maybe SYSV-IPC. Our synchronization primitives should certainly be able to do what Win32's WaitForMultipleObjects can, at least.
- Stephen Tweedie asserted that the combination of F_SETSIG, queued realtime signals, and sigwaitinfo() was a superset of the API proposed in http://www.cs.rice.edu/~gaurav/papers/usenix99.ps. He also mentions that you keep the signal blocked at all times if you're interested in performance; instead of the signal being delivered asynchronously, the process grabs the next one from the queue with sigwaitinfo().
- Jayson Nordwick compared completion ports with the F_SETSIG synchronous event model, and concluded they're pretty similar.
- Alan Cox noted that an older rev of SCT's SIGIO patch is included in 2.3.18ac.
- Jordan Mendelson posted some example code showing how to use F_SETSIG.
- Stephen C. Tweedie continued the comparison of completion ports and F_SETSIG, and noted: "With a signal dequeuing mechanism, your application is going to get signals destined for various library components if libraries are using the same mechanism," but the library can set up its own signal handler, so this shouldn't affect the program (much).
- Doug Royer noted that he'd gotten 100,000 connections on Solaris 2.6 while he was working on the Sun calendar server. Others chimed in with estimates of how much RAM that would require on Linux, and what bottlenecks would be hit.
Interesting reading!
打开文件句柄的限制
-
Any Unix: the limits set by ulimit or setrlimit.
-
Solaris: see the Solaris FAQ, question 3.46 (or thereabouts; they renumber the questions periodically).
-
FreeBSD:
Edit /boot/loader.conf, add the line
set kern.maxfiles=XXXX
where XXXX is the desired system limit on file descriptors, and reboot. Thanks to an anonymous reader, who wrote in to say he'd achieved far more than 10000 connections on FreeBSD 4.3, and says
"FWIW: You can't actually tune the maximum number of connections in FreeBSD trivially, via sysctl.... You have to do it in the /boot/loader.conf file.
The reason for this is that the zalloci() calls for initializing the sockets and tcpcb structures zones occurs very early in system startup, in order that the zone be both type stable and that it be swappable.
You will also need to set the number of mbufs much higher, since you will (on an unmodified kernel) chew up one mbuf per connection for tcptempl structures, which are used to implement keepalive."
Another reader says
"As of FreeBSD 4.4, the tcptempl structure is no longer allocated; you no longer have to worry about one mbuf being chewed up per connection."
See also:
- the FreeBSD handbook
- SYSCTL TUNING, LOADER TUNABLES, and KERNEL CONFIG TUNING in 'man tuning'
- The Effects of Tuning a FreeBSD 4.3 Box for High Performance, Daemon News, Aug 2001
- postfix.org tuning notes, covering FreeBSD 4.2 and 4.4
- the Measurement Factory's notes, circa FreeBSD 4.3
-
OpenBSD: A reader says
"In OpenBSD, an additional tweak is required to increase the number of open filehandles available per process: the openfiles-cur parameter in /etc/login.conf needs to be increased. You can change kern.maxfiles either with sysctl -w or in sysctl.conf but it has no effect. This matters because as shipped, the login.conf limits are a quite low 64 for nonprivileged processes, 128 for privileged."
-
Linux: See Bodo Bauer's /proc documentation. On 2.4 kernels:
echo 32768 > /proc/sys/fs/file-max
increases the system limit on open files, and
ulimit -n 32768
increases the current process' limit.
On 2.2.x kernels,
echo 32768 > /proc/sys/fs/file-max echo 65536 > /proc/sys/fs/inode-max
increases the system limit on open files, and
ulimit -n 32768
increases the current process' limit.
I verified that a process on Red Hat 6.0 (2.2.5 or so plus patches) can open at least 31000 file descriptors this way. Another fellow has verified that a process on 2.2.12 can open at least 90000 file descriptors this way (with appropriate limits). The upper bound seems to be available memory.
Stephen C. Tweedie posted about how to set ulimit limits globally or per-user at boot time using initscript and pam_limit.
In older 2.2 kernels, though, the number of open files per process is still limited to 1024, even with the above changes.
See also Oskar's 1998 post, which talks about the per-process and system-wide limits on file descriptors in the 2.0.36 kernel.
线程的限制
On any architecture, you may need to reduce the amount of stack space allocated for each thread to avoid running out of virtual memory. You can set this at runtime with pthread_attr_init() if you're using pthreads.
-
Solaris: it supports as many threads as will fit in memory, I hear.
-
Linux 2.6 kernels with NPTL: /proc/sys/vm/max_map_count may need to be increased to go above 32000 or so threads. (You'll need to use very small stack threads to get anywhere near that number of threads, though, unless you're on a 64 bit processor.) See the NPTL mailing list, e.g. the thread with subject "Cannot create more than 32K threads?", for more info.
-
Linux 2.4: /proc/sys/kernel/threads-max is the max number of threads; it defaults to 2047 on my Red Hat 8 system. You can set increase this as usual by echoing new values into that file, e.g. "echo 4000 > /proc/sys/kernel/threads-max"
-
Linux 2.2: Even the 2.2.13 kernel limits the number of threads, at least on Intel. I don't know what the limits are on other architectures. Mingo posted a patch for 2.1.131 on Intel that removed this limit. It appears to be integrated into 2.3.20.
See also Volano's detailed instructions for raising file, thread, and FD_SET limits in the 2.2 kernel. Wow. This document steps you through a lot of stuff that would be hard to figure out yourself, but is somewhat dated.
-
Java: See Volano's detailed benchmark info, plus their info on how to tune various systems to handle lots of threads.
Java的问题
Up through JDK 1.3, Java's standard networking libraries mostly offered the one-thread-per-client model. There was a way to do nonblocking reads, but no way to do nonblocking writes.
In May 2001, JDK 1.4 introduced the package java.nio to provide full support for nonblocking I/O (and some other goodies). See the release notes for some caveats. Try it out and give Sun feedback!
HP's java also includes a Thread Polling API.
In 2000, Matt Welsh implemented nonblocking sockets for Java; his performance benchmarks show that they have advantages over blocking sockets in servers handling many (up to 10000) connections. His class library is called java-nbio; it's part of the Sandstorm project. Benchmarks showing performance with 10000 connections are available.
See also Dean Gaudet's essay on the subject of Java, network I/O, and threads, and the paper by Matt Welsh on events vs. worker threads.
Before NIO, there were several proposals for improving Java's networking APIs:
- Matt Welsh's Jaguar system proposes preserialized objects, new Java bytecodes, and memory management changes to allow the use of asynchronous I/O with Java.
- Interfacing Java to the Virtual Interface Architecture, by C-C. Chang and T. von Eicken, proposes memory management changes to allow the use of asynchronous I/O with Java.
- JSR-51 was the Sun project that came up with the java.nio package. Matt Welsh participated (who says Sun doesn't listen?).
其他技巧
-
Zero-Copy
Normally, data gets copied many times on its way from here to there. Any scheme that eliminates these copies to the bare physical minimum is called "zero-copy".
-
Thomas Ogrisegg's zero-copy send patch for mmaped files under Linux 2.4.17-2.4.20. Claims it's faster than sendfile().
-
IO-Lite is a proposal for a set of I/O primitives that gets rid of the need for many copies.
-
Alan Cox noted that zero-copy is sometimes not worth the trouble back in 1999. (He did like sendfile(), though.)
-
Ingo implemented a form of zero-copy TCP in the 2.4 kernel for TUX 1.0 in July 2000, and says he'll make it available to userspace soon.
-
Drew Gallatin and Robert Picco have added some zero-copy features to FreeBSD; the idea seems to be that if you call write() or read() on a socket, the pointer is page-aligned, and the amount of data transferred is at least a page, and you don't immediately reuse the buffer, memory management tricks will be used to avoid copies. But see followups to this message on linux-kernel for people's misgivings about the speed of those memory management tricks.
According to a note from Noriyuki Soda:
Sending side zero-copy is supported since NetBSD-1.6 release by specifying "SOSEND_LOAN" kernel option. This option is now default on NetBSD-current (you can disable this feature by specifying "SOSEND_NO_LOAN" in the kernel option on NetBSD_current). With this feature, zero-copy is automatically enabled, if data more than 4096 bytes are specified as data to be sent.
-
The sendfile() system call can implement zero-copy networking.
The sendfile() function in Linux and FreeBSD lets you tell the kernel to send part or all of a file. This lets the OS do it as efficiently as possible. It can be used equally well in servers using threads or servers using nonblocking I/O. (In Linux, it's poorly documented at the moment; use _syscall4 to call it. Andi Kleen is writing new man pages that cover this. See also Exploring The sendfile System Call by Jeff Tranter in Linux Gazette issue 91.) Rumor has it, ftp.cdrom.com benefitted noticeably from sendfile().
A zero-copy implementation of sendfile() is on its way for the 2.4 kernel. See LWN Jan 25 2001.
One developer using sendfile() with Freebsd reports that using POLLWRBAND instead of POLLOUT makes a big difference.
Solaris 8 (as of the July 2001 update) has a new system call 'sendfilev'. A copy of the man page is here.. The Solaris 8 7/01 release notes also mention it. I suspect that this will be most useful when sending to a socket in blocking mode; it'd be a bit of a pain to use with a nonblocking socket.
-
-
Avoid small frames by using writev (or TCP_CORK)
A new socket option under Linux, TCP_CORK, tells the kernel to avoid sending partial frames, which helps a bit e.g. when there are lots of little write() calls you can't bundle together for some reason. Unsetting the option flushes the buffer. Better to use writev(), though...
See LWN Jan 25 2001 for a summary of some very interesting discussions on linux-kernel about TCP_CORK and a possible alternative MSG_MORE.
-
Behave sensibly on overload.
[Provos, Lever, and Tweedie 2000] notes that dropping incoming connections when the server is overloaded improved the shape of the performance curve, and reduced the overall error rate. They used a smoothed version of "number of clients with I/O ready" as a measure of overload. This technique should be easily applicable to servers written with select, poll, or any system call that returns a count of readiness events per call (e.g. /dev/poll or sigtimedwait4()).
-
Some programs can benefit from using non-Posix threads.
Not all threads are created equal. The clone() function in Linux (and its friends in other operating systems) lets you create a thread that has its own current working directory, for instance, which can be very helpful when implementing an ftp server. See Hoser FTPd for an example of the use of native threads rather than pthreads.
-
Caching your own data can sometimes be a win.
"Re: fix for hybrid server problems" by Vivek Sadananda Pai (vivek@cs.rice.edu) on new-httpd, May 9th, states:
"I've compared the raw performance of a select-based server with a multiple-process server on both FreeBSD and Solaris/x86. On microbenchmarks, there's only a marginal difference in performance stemming from the software architecture. The big performance win for select-based servers stems from doing application-level caching. While multiple-process servers can do it at a higher cost, it's harder to get the same benefits on real workloads (vs microbenchmarks). I'll be presenting those measurements as part of a paper that'll appear at the next Usenix conference. If you've got postscript, the paper is available at http://www.cs.rice.edu/~vivek/flash99/"
其他限制
-
Old system libraries might use 16 bit variables to hold file handles, which causes trouble above 32767 handles. glibc2.1 should be ok.
-
Many systems use 16 bit variables to hold process or thread id's. It would be interesting to port the Volano scalability benchmark to C, and see what the upper limit on number of threads is for the various operating systems.
-
Too much thread-local memory is preallocated by some operating systems; if each thread gets 1MB, and total VM space is 2GB, that creates an upper limit of 2000 threads.
-
Look at the performance comparison graph at the bottom ofhttp://www.acme.com/software/thttpd/benchmarks.html. Notice how various servers have trouble above 128 connections, even on Solaris 2.6? Anyone who figures out why, let me know.
Note: if the TCP stack has a bug that causes a short (200ms) delay at SYN or FIN time, as Linux 2.2.0-2.2.6 had, and the OS or http daemon has a hard limit on the number of connections open, you would expect exactly this behavior. There may be other causes.
内核的问题
For Linux, it looks like kernel bottlenecks are being fixed constantly. See Linux Weekly News, Kernel Traffic, the Linux-Kernel mailing list, and my Mindcraft Redux page.
In March 1999, Microsoft sponsored a benchmark comparing NT to Linux at serving large numbers of http and smb clients, in which they failed to see good results from Linux. See also my article on Mindcraft's April 1999 Benchmarks for more info.
See also The Linux Scalability Project. They're doing interesting work, including Niels Provos' hinting poll patch, and some work on the thundering herd problem.
See also Mike Jagdis' work on improving select() and poll(); here's Mike's post about it.
Mohit Aron (aron@cs.rice.edu) writes that rate-based clocking in TCP can improve HTTP response time over 'slow' connections by 80%.
测试服务器性能
Two tests in particular are simple, interesting, and hard:
- raw connections per second (how many 512 byte files per second can you serve?)
- total transfer rate on large files with many slow clients (how many 28.8k modem clients can simultaneously download from your server before performance goes to pot?)
Jef Poskanzer has published benchmarks comparing many web servers. Seehttp://www.acme.com/software/thttpd/benchmarks.html for his results.
I also have a few old notes about comparing thttpd to Apache that may be of interest to beginners.
Chuck Lever keeps reminding us about Banga and Druschel's paper on web server benchmarking. It's worth a read.
IBM has an excellent paper titled Java server benchmarks [Baylor et al, 2000]. It's worth a read.
例子
Nginx is a web server that uses whatever high-efficiency network event mechanism is available on the target OS. It's getting popular; there are even two books about it.
基于 select() 的服务器
- thttpd Very simple. Uses a single process. It has good performance, but doesn't scale with the number of CPU's. Can also use kqueue.
- mathopd. Similar to thttpd.
- fhttpd
- boa
- Roxen
- Zeus, a commercial server that tries to be the absolute fastest. See their tuning guide.
- The other non-Java servers listed at http://www.acme.com/software/thttpd/benchmarks.html
- BetaFTPd
- Flash-Lite - web server using IO-Lite.
- Flash: An efficient and portable Web server -- uses select(), mmap(), mincore()
- The Flash web server as of 2003 -- uses select(), modified sendfile(), async open()
- xitami - uses select() to implement its own thread abstraction for portability to systems without threads.
- Medusa - a server-writing toolkit in Python that tries to deliver very high performance.
- userver - a small http server that can use select, poll, epoll, or sigio
基于 /dev/poll 的服务器
-
- Provos, C. Lever, "Scalable Network I/O in Linux," May, 2000. [FREENIX track, Proc. USENIX 2000, San Diego, California (June, 2000).] Describes a version of thttpd modified to support /dev/poll. Performance is compared with phhttpd.
基于 kqueue() 的服务器
- thttpd (as of version 2.21?)
- Adrian Chadd says "I'm doing a lot of work to make squid actually LIKE a kqueue IO system"; it's an official Squid subproject; see http://squid.sourceforge.net/projects.html#commloops. (This is apparently newer than Benno's patch.)
基于实时信号的服务器
- Chromium's X15. This uses the 2.4 kernel's SIGIO feature together with sendfile() and TCP_CORK, and reportedly achieves higher speed than even TUX. The source is available under a community source (not open source) license. See the original announcement by Fabio Riccardi.
- Zach Brown's phhttpd - "a quick web server that was written to showcase the sigio/siginfo event model. consider this code highly experimental and yourself highly mental if you try and use it in a production environment." Uses the siginfo features of 2.3.21 or later, and includes the needed patches for earlier kernels. Rumored to be even faster than khttpd. See his post of 31 May 1999 for some notes.
基于线程的服务器
- Hoser FTPD. See their benchmark page.
- Peter Eriksson's phttpd and
- pftpd
- The Java-based servers listed at http://www.acme.com/software/thttpd/benchmarks.html
- Sun's Java Web Server (which has been reported to handle 500 simultaneous clients)
编译进内核的服务器
- khttpd
- "TUX" (Threaded linUX webserver) by Ingo Molnar et al. For 2.4 kernel.
其他一些链接
- Jeff Darcy's notes on high-performance server design
- Ericsson's ARIES project -- benchmark results for Apache 1 vs. Apache 2 vs. Tomcat on 1 to 12 processors
- Prof. Peter Ladkin's Web Server Performance page.
- Novell's FastCache -- claims 10000 hits per second. Quite the pretty performance graph.
- Rik van Riel's Linux Performance Tuning site