前言
《C10K问题》这篇经典文章深入探讨了如何在现代硬件条件下构建能够处理数万并发连接的高性能服务器系统。文章开篇即点明核心观点:随着硬件性能的飞速发展,单个服务器已具备同时服务大量客户端的物理能力,真正的挑战在于软件架构设计和操作系统机制优化。
作者首先回顾了技术发展背景,指出在二十世纪末期,千兆以太网和高速硬件已经普及,但软件架构却成为制约服务器并发能力的主要瓶颈。接着文章系统性地提出了五种主流的技术方案,每种方案都代表了不同的设计哲学和实现路径。
最传统的方案是每个客户端分配一个独立线程或进程,这种模式简单直观但资源消耗大,在连接数增多时会面临内存和调度压力。第二种方案采用单线程配合非阻塞I/O和电平触发就绪通知,通过select或poll系统调用监控多个连接状态,有效减少了线程开销,但在海量连接下效率会下降。
第三种方案在非阻塞I/O基础上引入边缘触发机制,这是文章重点讨论的先进方案。作者详细介绍了各操作系统特有的高性能接口:Linux的epoll、FreeBSD的kqueue、Solaris的/dev/poll等。这些机制通过事件驱动模型,只在连接状态真正变化时通知应用程序,避免了不必要的轮询开销,特别适合高并发场景。
第四种方案采用异步I/O模式,将I/O操作完全交由操作系统处理,完成后通过回调或信号通知应用程序。这种模式理论上性能最优,但在当时的主流操作系统中支持尚不完善。第五种则是激进的内核级服务器方案,将部分或全部服务器逻辑移入内核空间,最大限度减少上下文切换开销,但带来了复杂性和可维护性问题。
文章不仅停留在理论探讨,还提供了丰富的实践指导。作者详细分析了各种方案在不同操作系统下的具体实现细节,包括如何设置非阻塞模式、如何处理信号竞争、如何优化缓冲区管理等。特别是在性能优化方面,文章强调了零拷贝技术的重要性,介绍了sendfile等系统调用的使用技巧,以及如何通过writev减少小数据包传输开销。
在系统配置方面,作者指出了常见的限制因素和调优方法。包括如何调整操作系统级别的文件描述符限制、线程数量限制、内存分配策略等。对于Java开发者,文章专门讨论了JDK版本对NIO支持的发展历程和性能影响。
文章最后部分通过丰富的案例研究,展示了各种技术方案在实际服务器中的运用效果。从轻量级的thttpd到高性能的Nginx,从基于select的传统服务器到采用epoll的现代架构,作者通过对比分析揭示了不同技术选择带来的性能差异。特别值得关注的是对TUX内核级Web服务器的讨论,展示了极端优化可能达到的性能高度。
整篇文章体现了系统化思考的工程精神,既重视理论深度,又强调实践可行性。作者不仅介绍了各种技术方案的优势,也客观分析了它们的局限性和适用场景。更重要的是,文章揭示了高性能服务器设计的基本规律:需要在资源利用率、实现复杂度、可维护性之间找到最佳平衡点。
这篇写于网络应用爆发初期的文章,其核心思想至今仍然具有重要指导意义。随着物联网和云计算的发展,现代服务器需要处理的并发连接数已从万级迈向百万级,但文章所探讨的基本原理和设计思路仍然适用。从某种意义上说,C10K问题开启了高性能网络编程系统化研究的新时代,为后来C100K、C1000K等更大规模并发问题的解决奠定了理论基础和方法框架。
C10K问题
现在是时候让Web服务器能够同时处理一万个客户端了,你不觉得吗?毕竟,现在的网络已经非常庞大了。
电脑也变得非常强大。你可以花大约1200美元购买一台1000MHz的机器,配备2GB内存和1000Mbit/sec的以太网卡。算一下——对于两万个客户端,每个客户端分配50KHz、100KB和50Kbits/sec的带宽。每秒钟为两万个客户端从磁盘读取4KB数据并发送到网络上,这应该不需要比这更多的计算能力。(顺便说一句,每个客户端的成本约为0.08美元。某些操作系统收取的每客户端100美元的授权费开始显得有点沉重了!)因此,硬件不再是瓶颈。
1999年,最繁忙的FTP站点之一cdrom.com实际上通过千兆以太网管道同时处理了10000个客户端。到2001年,多家ISP已经开始提供同样的速度,他们预计大型企业客户会越来越青睐这种服务。
此外,瘦客户端计算模型似乎又重新流行起来——这次服务器部署在互联网上,为成千上万的客户端提供服务。
考虑到这一点,以下是一些关于如何配置操作系统和编写代码以支持成千上万客户端的说明。讨论主要围绕类Unix操作系统,因为这是我个人感兴趣的领域,但Windows也会涉及一些。
相关网站
参见Nick Black优秀的《Fast UNIX Servers》页面,了解2009年左右的情况。
2003年10月,Felix von Leitner整理了一个关于网络可扩展性的优秀网页和演示文稿,其中包含比较各种网络系统调用和操作系统的基准测试。他的一个观察是,Linux 2.6内核确实比2.4内核更优,但其中许多优秀的图表将为操作系统开发者提供长时间的思考素材。(另请参见Slashdot评论;如果有人基于Felix的结果进行了后续的改进基准测试,那将非常有趣。)
推荐阅读书籍
如果你还没有读过,请购买一本已故的W. Richard Stevens所著的《Unix网络编程:网络API:套接字与XTI(第1卷)》。该书描述了许多与编写高性能服务器相关的I/O策略和陷阱,甚至谈到了“惊群”问题。此外,你还可以阅读Jeff Darcy关于高性能服务器设计的笔记。
(对于那些使用而非编写Web服务器的人来说,Cal Henderson的《构建可扩展的网站》一书可能更有帮助。)
I/O框架
以下提供了一些预封装库,它们抽象了下面介绍的部分技术,使你的代码与操作系统隔离,并提高可移植性:
- ACE:一个重量级的C++ I/O框架,包含了一些I/O策略的面向对象实现以及其他许多有用的功能。特别是,其Reactor是执行非阻塞I/O的面向对象方式,而Proactor是执行异步I/O的面向对象方式。
- ASIO:一个C++ I/O框架,正在成为Boost库的一部分。它就像是针对STL时代更新的ACE。
- 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之后的第二次尝试。它采用lgpl许可证(因此在商业应用中更容易使用),并使用C语言(因此在非C++应用中更容易使用)。它曾用于一些商业产品。
- Matt Welsh在2000年4月撰写了一篇关于在构建可扩展服务器时如何平衡工作线程和事件驱动技术的论文。该论文描述了他的Sandstorm I/O框架的一部分。
- Cory Nelson的Scale!库——一个用于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。
如何控制为每个客户端服务的代码
- 每个客户端一个进程(经典的Unix方法,自1980年左右开始使用)
- 一个操作系统级线程处理多个客户端;每个客户端由以下控制:
- 一个用户级线程(例如GNU状态线程、使用绿色线程的经典Java)
- 状态机(有些深奥,但在某些圈子里很流行;我最喜欢)
- 延续(有些深奥,但在某些圈子里很流行)
- 每个客户端一个操作系统级线程(例如使用原生线程的经典Java)
- 每个活动客户端一个操作系统级线程(例如带有Apache前端的Tomcat、NT完成端口、线程池)
是否使用标准的操作系统服务,或将部分代码放入内核(例如在自定义驱动程序、内核模块或VxD中)
以下五种组合似乎很受欢迎:
- 每个线程服务多个客户端,使用非阻塞I/O和电平触发就绪通知
- 每个线程服务多个客户端,使用非阻塞I/O和边沿触发就绪通知
- 每个服务器线程服务多个客户端,使用异步I/O
- 每个服务器线程服务一个客户端,使用阻塞I/O
- 将服务器代码构建到内核中
1. 每个线程服务多个客户端,使用非阻塞I/O和电平触发就绪通知
在所有网络句柄上设置非阻塞模式,并使用select()或poll()来告知哪个网络句柄有数据等待。这是传统上最受欢迎的方法。在这种方案中,内核会告诉你文件描述符是否就绪,无论自上次内核通知你以来你是否对该文件描述符进行了任何操作。(“电平触发”一词来自计算机硬件设计;它是“边沿触发”的反义词。Jonathan Lemon在他的BSDCON 2000关于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上就此发表了演讲。看起来mincore()在BSD衍生的Unix系统(如FreeBSD和Solaris)中可用,但不是Single Unix规范的一部分。从内核2.3.51开始,多亏了Chuck Lever,它也可用于Linux。
但在2003年11月的freebsd-hackers列表中,Vivek Pei等人报告了通过系统级分析其Flash Web服务器攻击瓶颈的非常好的结果。他们发现的一个瓶颈是mincore(猜猜这毕竟不是一个好主意)。另一个瓶颈是sendfile在磁盘访问时阻塞;他们通过引入一个修改版的sendfile()来改进性能,当获取的磁盘页面尚未在内存中时,它会返回类似EWOULDBLOCK的错误。(不确定如何告诉用户页面现在已驻留……在我看来,这里真正需要的是aio_sendfile()。)他们优化的最终结果是在1GHz/1GB的FreeBSD机器上获得了约800的SpecWeb99分数,这比spec.org上存档的任何结果都要好。
有几种方法可以让单个线程判断一组非阻塞套接字中哪些已经准备好进行I/O:
传统的select()
遗憾的是,select()仅限于FD_SETSIZE个句柄。这个限制被编译到标准库和用户程序中。(某些版本的C库允许你在用户应用编译时提高这个限制。)
参见Poller_select(cc、h)以了解如何将select()与其他就绪通知方案互换使用。传统的poll()
poll()可以处理的文件描述符数量没有硬编码限制,但在几千个时会变慢,因为大多数文件描述符在任何时候都是空闲的,扫描数千个文件描述符需要时间。
某些操作系统(例如Solaris 8)通过使用如poll hinting等技术来加速poll()等,Niels Provos在1999年为Linux实现并进行了基准测试。
参见Poller_poll(cc、h、基准测试)以了解如何将poll()与其他就绪通知方案互换使用。/dev/poll
这是Solaris推荐的poll替代方案。
/dev/poll背后的思想是利用poll()经常使用相同参数被调用多次的事实。使用/dev/poll,你可以获得一个打开/dev/poll的句柄,并通过写入该句柄一次性告诉操作系统你感兴趣的文件;之后,你只需从该句柄读取当前就绪的文件描述符集。
它悄悄出现在Solaris 7中(参见补丁ID 106541),但首次公开出现是在Solaris 8中;根据Sun的说法,在750个客户端时,其开销仅为poll()的10%。
在Linux上尝试了各种/dev/poll的实现,但都没有epoll表现得好,并且从未真正完成。不建议在Linux上使用/dev/poll。
参见Poller_devpoll(cc、h、基准测试)以了解如何将/dev/poll与许多其他就绪通知方案互换使用。(注意——该示例适用于Linux的/dev/poll,可能无法在Solaris上正常工作。)kqueue()
这是FreeBSD(以及很快的NetBSD)推荐的poll替代方案。
参见下文。kqueue()可以指定边沿触发或电平触发。
2. 每个线程服务多个客户端,使用非阻塞I/O和边沿触发就绪通知
边沿触发就绪通知意味着你向内核提供一个文件描述符,之后当该描述符从“未就绪”转换为“就绪”时,内核会以某种方式通知你。然后内核会认为你已经知道该文件描述符就绪,并且在你执行某些操作导致文件描述符不再就绪之前(例如,直到你在send、recv或accept调用上收到EWOULDBLOCK错误,或者send或recv传输的字节数少于请求的数量),不会再次发送该类型的就绪通知。
使用边沿触发通知时,你必须准备好处理虚假事件,因为一种常见的实现方式是:无论文件描述符是否已经就绪,只要收到任何数据包就会发出就绪信号。
这与“电平触发”就绪通知相反。它对编程错误的要求更严格,因为如果你错过一个事件,该事件对应的连接可能会永久卡住。不过我发现,在使用OpenSSL编写非阻塞客户端时,边沿触发就绪通知使编程变得更容易,值得尝试。
[Banga, Mogul, Drusha ‘99] 在1999年描述了这种方案。
有几种API允许应用程序检索“文件描述符变为就绪”的通知:
kqueue()
这是FreeBSD(以及即将到来的NetBSD)推荐的边沿触发的poll替代方案。 FreeBSD 4.3及更高版本,以及截至2002年10月的NetBSD-current,支持一种称为kqueue()/kevent()的通用poll()替代方案;它同时支持边沿触发和电平触发。(另请参阅Jonathan Lemon的页面以及他在BSDCon 2000上关于kqueue()的论文。)
与/dev/poll类似,你需要分配一个监听对象,但不是打开/dev/poll文件,而是调用kqueue()来分配一个。要更改正在监听的事件或获取当前事件列表,你可以在kqueue()返回的描述符上调用kevent()。它不仅可以监听套接字就绪,还可以监听普通文件就绪、信号,甚至I/O完成。
注意:截至2000年10月,FreeBSD上的线程库与kqueue()的交互不太好;显然,当kqueue()阻塞时,整个进程都会阻塞,而不仅仅是调用线程。
参见 Poller_kqueue (cc, h, 基准测试) 以了解如何将 kqueue() 与其他许多就绪通知方案互换使用的示例。
使用 kqueue() 的示例和库:
- PyKQueue —— Python 的 kqueue() 绑定
- Ronald F. Guilmette 的示例回声服务器;另请参见他于 2000年9月28日 在 freebsd.questions 上的帖子。
epoll
这是 2.6 Linux 内核推荐的边沿触发的 poll 替代方案。 2001年7月11日,Davide Libenzi 提出了实时信号的替代方案;他的补丁提供了他后来称为 /dev/epoll 的功能(www.xmailserver.org/linux-patches/nio-improve.html)。这与实时信号就绪通知类似,但它合并了冗余事件,并具有更高效的批量事件检索方案。
Epoll 的接口从 /dev 中的特殊文件更改为系统调用 sys_epoll 后,于 2.5.46 版本合并到 2.5 内核树中。旧版本的 epoll 补丁可用于 2.4 内核。
2002年万圣节前后,linux-kernel 邮件列表上关于统一 epoll、aio 和其他事件源进行了一场漫长的辩论。这可能最终会发生,但 Davide 目前正专注于首先巩固 epoll 的整体实现。
Polyakov 的 kevent (Linux 2.6+)
新闻快讯:2006年2月9日和2006年7月9日,Evgeniy Polyakov 发布了似乎统一了 epoll 和 aio 的补丁;他的目标是支持网络 AIO。参见:
- 关于 kevent 的 LWN 文章
- 他7月的公告
- 他的 kevent 页面
- 他的 naio 页面
- 最近的讨论
Drepper 的新网络接口 (Linux 2.6+ 提案)
在 OLS 2006 上,Ulrich Drepper 提出了一个新的高速异步网络 API。参见:
- 他的论文《对异步、零拷贝网络 I/O 的需求》
- 他的幻灯片
- 7月22日的 LWN 文章
实时信号
这是 2.4 Linux 内核推荐的边沿触发的 poll 替代方案。 2.4 Linux 内核可以通过特定的实时信号传递套接字就绪事件。以下是如何启用此行为:
/* 屏蔽 SIGIO 和你想要使用的信号。 */
sigemptyset(&sigset);
sigaddset(&sigset, signum);
sigaddset(&sigset, SIGIO);
sigprocmask(SIG_BLOCK, &m_sigset, NULL);
/* 对于每个文件描述符,调用 F_SETOWN、F_SETSIG,并设置 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);当像 read() 或 write() 这样的普通 I/O 函数完成时,会发送该信号。要使用此功能,编写一个普通的 poll() 外循环,在其中处理完 poll() 注意到的所有 fd 后,循环调用 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) 以了解如何将 rtsignals 与其他许多就绪通知方案互换使用的示例。
参见 Zach Brown 的 phhttpd,了解直接使用此功能的示例代码。(或者不看也行;phhttpd 有点难懂…)
[Provos, Lever, and Tweedie 2000] 描述了使用 sigtimedwait() 的一个变体 sigtimedwait4() 对 phhttpd 进行的基准测试,该变体允许一次调用检索多个信号。有趣的是,对于他们来说,sigtimedwait4() 的主要好处似乎是它允许应用程序衡量系统过载情况(从而做出适当的行为)。(注意:poll() 也提供了相同的系统过载衡量方法。)
每个文件描述符一个信号
Chandra 和 Mosberger 提出了对实时信号方法的修改,称为“每个文件描述符一个信号”,通过合并冗余事件来减少或消除实时信号队列溢出。不过,它的性能并不优于 epoll。他们的论文(www.hpl.hp.com/techreports/2000/HPL-2000-174.html)比较了此方案与 select() 和 /dev/poll 的性能。 Vitaly Luban 于 2001年5月18日 宣布了一个实现此方案的补丁;他的补丁位于 www.luban.org/GPL/gpl.html。(注意:截至 2001年9月,在重负载下,此补丁可能仍存在稳定性问题。大约 4500 个用户时的 dkftpbench 可能会触发 oops。)
参见 Poller_sigfd (cc, h) 以了解如何将“每个文件描述符一个信号”与其他许多就绪通知方案互换使用的示例。
3. 每个服务器线程服务多个客户端,并使用异步 I/O
这在 Unix 中尚未普及,可能是因为支持异步 I/O 的操作系统很少,也可能是因为它(像非阻塞 I/O 一样)需要重新思考你的应用程序。在标准 Unix 下,异步 I/O 由 aio_ 接口提供(从该链接向下滚动到“异步输入和输出”),它将一个信号和值与每个 I/O 操作关联起来。信号及其值被高效地排队并传递给用户进程。这来自 POSIX 1003.1b 实时扩展,也包含在 Single Unix Specification 版本 2 中。
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 的实现有些不同。)更多信息:
- “Linux 内核异步 I/O (AIO) 支持”页面,该页面试图整合有关 2.6 内核 AIO 实现的所有信息(发布于 2003年9月16日)
- 第三轮:aio 对比 /dev/epoll,作者 Benjamin C.R. LaHaise(在 2002 OLS 上展示)
- Linux 2.5 中的异步 I/O 支持,作者 Bhattacharya, Pratt, Pulaverty, 和 Morgan, IBM;在 OLS ‘2003 上展示
- 关于 Linux 异步 I/O (aio) 的设计说明,作者 Suparna Bhattacharya —— 比较了 Ben 的 AIO 与 SGI 的 KAIO 以及其他一些 AIO 项目
- Linux AIO 主页 —— Ben 的初步补丁、邮件列表等
- linux-aio 邮件列表存档
- libaio-oracle —— 在 libaio 之上实现标准 Posix AIO 的库。首次由 Joel Becker 于 2003年4月18日 提及。
- Suparna 还建议看看 DAFS API 处理 AIO 的方法。
- Red Hat 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 不提供无阻塞打开文件的方法;如果你关心打开磁盘文件导致的休眠,Linus 建议你只需在另一个线程中执行 open(),而不是期望有一个 aio_open() 系统调用。
在 Windows 下,异步 I/O 与术语“重叠 I/O”和 IOCP 或“I/O 完成端口”相关联。Microsoft 的 IOCP 结合了先前技术中的技巧,如异步 I/O(像 aio_write)和排队完成通知(像使用 aio_sigevent 字段与 aio_write),以及一个新想法:保留一些请求,试图使与单个 IOCP 关联的运行线程数量保持恒定。更多信息,请参见 sysinternals.com 上 Mark Russinovich 的《Inside I/O Completion Ports》,Jeffrey Richter 的书籍《Programming Server-Side Applications for Microsoft Windows 2000》(亚马逊,MSPress),美国专利 #06223207,或 MSDN。
4. 每个服务器线程服务一个客户端
… 并让 read() 和 write() 阻塞。缺点是为每个客户端使用整个堆栈帧,这会消耗内存。许多操作系统在处理超过几百个线程时也有困难。如果每个线程获得 2MB 的堆栈(这不是不常见的默认值),在具有 1GB 用户可访问虚拟内存的 32 位机器上(例如,通常在 x86 上发货的 Linux),你会在 (2^30 / 2^21) = 512 个线程时耗尽 虚拟内存。你可以通过为每个线程分配更小的堆栈来解决这个问题,但由于大多数线程库不允许在创建后增长线程堆栈,这样做意味着设计你的程序以最小化堆栈使用。你也可以通过转向 64 位处理器来解决这个问题。
Linux、FreeBSD 和 Solaris 的线程支持正在改进,64 位处理器即使对主流用户来说也即将到来。也许在不久的将来,那些更喜欢每个客户端一个线程的人将能够使用该范式处理 10000 个客户端。然而,在当前,如果你真的想要支持那么多客户端,你可能最好使用其他范式。
关于一个毫不掩饰支持线程的观点,请参见 von Behren, Condit, 和 Brewer, UCB 的《Why Events Are A Bad Idea (for High-concurrency Servers)》,在 HotOS IX 上展示。反线程阵营的人想指出一篇反驳这篇文章的论文吗? :-)
LinuxThreads
LinuxThreads 是标准 Linux 线程库的名称。自 glibc2.0 起,它已集成到 glibc 中,并且大部分符合 Posix 标准,但性能和信号支持不太理想。
NGPT:Linux 的下一代 Posix 线程
NGPT 是 IBM 启动的一个项目,旨在为 Linux 带来良好的符合 Posix 标准的线程支持。现在处于稳定的 2.2 版本,并且运行良好……但 NGPT 团队已宣布他们将 NGPT 代码库置于仅支持模式,因为他们认为这是“长期支持社区的最佳方式”。NGPT 团队将继续努力改进 Linux 线程支持,但现在专注于改进 NPTL。(向 NGPT 团队的良好工作和他们优雅地向 NPTL 让步表示敬意。)
NPTL:Linux 的原生 Posix 线程库
NPTL 是 Ulrich Drepper(glibc 的仁慈独裁者^H^H^H^H维护者)和 Ingo Molnar 的一个项目,旨在为 Linux 带来世界级的 Posix 线程支持。 截至 2003年10月5日,NPTL 现已作为附加目录合并到 glibc cvs 树中(就像 linuxthreads 一样),因此几乎肯定会随下一个 glibc 版本一起发布。
第一个包含 NPTL 早期快照的主要发行版是 Red Hat 9。(这对一些用户来说有点不便,但总得有人打破僵局…)
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 和其他人贡献了构成原生 Posix 线程库(NPTL)的 glibc 和内核更改。NPTL 使用了为 NGPT 设计的所有内核增强功能,并利用了一些新的功能。Ingo Molnar 描述内核增强如下:
“虽然 NPTL 使用了 NGPT 引入的三个内核功能:getpid() 返回 PID、CLONE_THREAD 和 futexes;但 NPTL 还使用(并依赖)一组更广泛的新内核功能,这些功能是作为该项目的一部分开发的。 NGPT 在 2.5.8 左右引入内核的一些项目得到了修改、清理和扩展,例如线程组处理(CLONE_THREAD)。[与 NGPT 团队同步了影响 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 线程方法——内核现在在所有可以提高线程性能的地方提供帮助,并且我们精确地为每个基本线程原语执行最小必要的上下文切换和内核调用。”
两者之间的一个大区别是 NPTL 是 1:1 线程模型,而 NGPT 是 M:N 线程模型(见下文)。尽管如此,Ulrich 的初步基准测试似乎表明 NPTL 确实比 NGPT 快得多。(NGPT 团队期待看到 Ulrich 的基准测试代码以验证结果。)
FreeBSD 线程支持
FreeBSD 同时支持 LinuxThreads 和一个用户空间线程库。此外,在 FreeBSD 5.0 中引入了一个名为 KSE 的 M:N 实现。有关概述,请参见 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 提议在 FreeBSD 7.x 中将 1:1 线程实现作为默认设置: “我知道过去已经讨论过这个问题,但随着 7.x 向前推进,我认为是时候再次考虑它了。在许多常见应用程序和场景的基准测试中,libthr 比 libpthread 表现出明显更好的性能…… libthr 也在更多平台上实现,并且已经是几个平台上的 libpthread。我们对 MySQL 和其他重度线程用户的第一个建议是‘切换到 libthr’,这也是有暗示性的!……因此,初步建议是:在 7.x 上将 libthr 作为默认线程库。”
NetBSD 线程支持
根据 Noriyuki Soda 的说明: “基于调度器激活模型的内核支持 M:N 线程库已于 2003年1月18日 合并到 NetBSD-current 中。” 详情请参见 Nathan J. Williams, Wasabi Systems, Inc. 的《An Implementation of Scheduler Activations on the NetBSD Operating System》,在 FREENIX ‘02 上展示。
Solaris 线程支持
Solaris 中的线程支持正在发展……从 Solaris 2 到 Solaris 8,默认线程库使用 M:N 模型,但 Solaris 9 默认使用 1:1 模型线程支持。参见 Sun 的多线程编程指南 以及 Sun 关于 Java 和 Solaris 线程的说明。
JDK 1.3.x 及更早版本的 Java 线程支持
众所周知,Java 直到 JDK1.3.x 除了每个客户端一个线程之外,不支持任何处理网络连接的方法。Volanomark 是一个很好的微基准测试,它测量不同数量并发连接下的每秒消息吞吐量。截至 2003年5月,来自各个供应商的 JDK 1.3 实现实际上能够处理一万个并发连接——尽管性能显著下降。参见表 4,了解哪些 JVM 可以处理 10000 个连接,以及随着连接数增加,性能如何下降。
注:1:1 线程与 M:N 线程
实现线程库时有一个选择:你可以将所有线程支持都放在内核中(这称为 1:1 线程模型),或者你可以将相当一部分移到用户空间(这称为 M:N 线程模型)。曾几何时,M:N 被认为性能更高,但它太复杂了,很难做好,而且大多数人正在远离它。
- 为什么 Ingo Molnar 更喜欢 1:1 而不是 M:N
- 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 和静态网页这样做,而“TUX”(Threaded linUX webserver)是 Ingo Molnar 为 Linux 编写的一个极快且灵活的内核空间 HTTP 服务器。Ingo 2000年9月1日 的公告称,TUX 的 alpha 版本可以从 ftp://ftp.redhat.com/pub/redhat/tux下载,并解释了如何加入邮件列表以获取更多信息。 linux-kernel 列表一直在讨论这种方法的优缺点,共识似乎是:与其将 Web 服务器移入内核,不如让内核添加最小的钩子来提高 Web 服务器性能。这样,其他类型的服务器也可以受益。参见例如 Zach Brown 关于用户空间与内核 HTTP 服务器的评论。似乎 2.4 Linux 内核为用户程序提供了足够的能力,因为 X15 服务器运行速度与 Tux 差不多,但不需要任何内核修改。
将 TCP 协议栈移至用户空间
参见例如 netmap 数据包 I/O 框架,以及基于它的 Sandstorm 概念验证 Web 服务器。
评论
Richard Gooch 撰写了一篇讨论 I/O 选项的论文。
2001年,Tim Brecht 和 Michal Ostrowski 测量了基于简单 select 的服务器的各种策略。他们的数据值得一看。
2003年,Tim Brecht 发布了 userver 的源代码,这是一个由 Abhishek Chandra、David Mosberger、David Pariag 和 Michal Ostrowski 编写的几个服务器组装而成的小型 Web 服务器。它可以使用 select()、poll()、epoll() 或 sigio。
早在 1999年3月,Dean Gaudet 发帖:
“我一直被问到‘为什么你们不像 Zeus 那样使用基于 select/event 的模型?这显然是最快的。’……”
他的理由归结为“这真的很难,而且回报不明确”。然而,几个月后,人们显然愿意为此努力。
Mark Russinovich 撰写了一篇社论和一篇文章,讨论 2.2 Linux 内核中的 I/O 策略问题。值得一读,尽管他在某些观点上似乎有误解。特别是,他似乎认为 Linux 2.2 的异步 I/O(见上面的 F_SETSIG)不会在数据就绪时通知用户进程,只会在新连接到达时通知。这似乎是一个奇怪误解。另请参阅对早期草稿的评论、Ingo Molnar 于 1999年4月30日 的反驳、Russinovich 于 1999年5月2日 的评论、Alan Cox 的反驳,以及 linux-kernel 上的各种帖子。我怀疑他是想说 Linux 不支持异步磁盘 I/O,这在过去是真的,但现在 SGI 已经实现了 KAIO,就不再那么正确了。
参见 sysinternals.com 和 MSDN 上关于“完成端口”的页面,他说这是 NT 独有的;简而言之,win32 的“重叠 I/O”被证明太低层级而不方便,而“完成端口”是一个包装器,它提供一个完成事件队列,加上调度魔法,该魔法试图通过允许更多线程从该端口获取完成事件来保持运行线程数量恒定(如果其他已从该端口获取完成事件的线程正在休眠,可能正在执行阻塞 I/O)。
另请参见 OS/400 对 I/O 完成端口的支持。
1999年9月,linux-kernel 上有一个有趣的讨论,标题为“> 15,000 个并发连接”(以及该线程的第二周)。亮点:
- Ed Hall 发布了一些关于他经验的说明;他在运行 Solaris 的 UP P2/333 上实现了 >1000 次连接/秒。他的代码使用了一个小型线程池(每个 CPU 1 或 2 个),每个线程使用“基于事件的模型”管理大量客户端。
- Mike Jagdis 发布了对 poll/select 开销的分析,并说“当前的 select/poll 实现可以显著改进,特别是在阻塞情况下,但开销仍将随描述符数量增加,因为 select/poll 没有,也不能记住哪些描述符是有趣的。用新的 API 很容易解决这个问题。欢迎建议……”
- Mike 发布了他改进 select() 和 poll() 的工作。
- Mike 发布了一些关于可能替代 poll()/select() 的 API 的信息:“怎么样,一个‘类似设备’的 API,你写入‘类似 pollfd’的结构,‘设备’监听事件,并在你读取它时传递代表这些事件的‘类似 pollfd’的结构?……”
- Rogier Wolff 建议使用“digital 的家伙们建议的 API”,http://www.cs.rice.edu/~gaurav/papers/usenix99.ps
- Joerg Pommnitz 指出,任何这类新 API 都应该能够等待不仅仅是文件描述符事件,还包括信号和可能 SYSV-IPC。我们的同步原语当然应该至少能够做 Win32 的 WaitForMultipleObjects 能做的事。
- Stephen Tweedie 断言 F_SETSIG、排队实时信号和 sigwaitinfo() 的组合是 http://www.cs.rice.edu/~gaurav/papers/usenix99.ps中提出的 API 的超集。他还提到,如果你关心性能,你应该始终阻塞该信号;信号不会异步传递,而是进程使用 sigwaitinfo() 从队列中获取下一个。
- Jayson Nordwick 比较了完成端口与 F_SETSIG 同步事件模型,并得出结论它们非常相似。
- Alan Cox 指出,SCT 的 SIGIO 补丁的旧版本包含在 2.3.18ac 中。
- Jordan Mendelson 发布了一些示例代码,展示如何使用 F_SETSIG。
- Stephen C. Tweedie 继续比较完成端口和 F_SETSIG,并指出:“使用信号出队机制,如果你的应用程序使用与库相同的机制,你的应用程序将收到针对各种库组件的信号,”但库可以设置自己的信号处理程序,因此这不应该(太)影响程序。
- Doug Royer 指出,他在 Sun 日历服务器上工作时,在 Solaris 2.6 上实现了 100,000 个连接。其他人加入了关于这在 Linux 上需要多少 RAM 以及会遇到什么瓶颈的估计。
有趣的阅读!
打开文件句柄的限制
- 任何 Unix:由 ulimit 或 setrlimit 设置的限制。
- Solaris:参见 Solaris FAQ,问题 3.46(或大约那个位置;他们定期重新编号问题)。
- FreeBSD:
编辑
/boot/loader.conf,添加一行:其中 XXXX 是系统对文件描述符的限制,然后重启。感谢一位匿名读者,他写信来说他在 FreeBSD 4.3 上实现了远超过 10000 个连接,并说: “顺便说一下:在 FreeBSD 中,你不能通过 sysctl 轻松调整最大连接数……你必须通过 /boot/loader.conf 文件来设置。 原因是,用于初始化套接字和 tcpcb 结构的 zalloci() 调用发生在系统启动的非常早期,以便区域既是类型稳定的,又是可交换的。 你还需要将 mbufs 的数量设置得更高,因为(在未修改的内核上)你会为每个连接消耗一个 mbuf 用于 tcptempl 结构,这些结构用于实现 keepalive。” 另一位读者说: “从 FreeBSD 4.4 开始,不再分配 tcptempl 结构;你不再需要担心每个连接消耗一个 mbuf。” 另请参见:set kern.maxfiles=XXXX- FreeBSD 手册
- ‘man tuning’ 中的 SYSCTL TUNING, LOADER TUNABLES, 和 KERNEL CONFIG TUNING
- The Effects of Tuning a FreeBSD 4.3 Box for High Performance, Daemon News, 2001年8月
- postfix.org 调优说明,涵盖 FreeBSD 4.2 和 4.4
- Measurement Factory 的说明,大约 FreeBSD 4.3 时期
- OpenBSD:一位读者说: “在 OpenBSD 中,需要额外的调整来增加每个进程可用的打开文件句柄数量:需要增加 /etc/login.conf 中的 openfiles-cur 参数。你可以使用 sysctl -w 或在 sysctl.conf 中更改 kern.maxfiles,但没有效果。这很重要,因为默认情况下,login.conf 对非特权进程的限制是相当低的 64,对特权进程是 128。”
- Linux:参见 Bodo Bauer 的 /proc 文档。在 2.4 内核上:增加系统对打开文件的限制,并且
echo 32768 > /proc/sys/fs/file-max增加当前进程的限制。 在 2.2.x 内核上:ulimit -n 32768增加系统对打开文件的限制,并且echo 32768 > /proc/sys/fs/file-max echo 65536 > /proc/sys/fs/inode-max增加当前进程的限制。 我验证了在 Red Hat 6.0(大约 2.2.5 加补丁)上,一个进程可以通过这种方式打开至少 31000 个文件描述符。另一个人验证了在 2.2.12 上,一个进程可以通过这种方式打开至少 90000 个文件描述符(具有适当的限制)。上限似乎是可用内存。 Stephen C. Tweedie 发布了关于如何在启动时使用 initscript 和 pam_limit 全局或按用户设置 ulimit 限制的信息。 然而,在较旧的 2.2 内核中,即使进行了上述更改,每个进程的打开文件数仍限制为 1024。 另请参见 Oskar 于 1998年 的帖子,该帖子讨论了 2.0.36 内核中每个进程和系统范围内的文件描述符限制。ulimit -n 32768
线程限制
在任何架构上,你可能需要减少为每个线程分配的堆栈空间量,以避免耗尽虚拟内存。如果你使用 pthreads,可以在运行时使用 pthread_attr_init() 设置。
- Solaris:我听说它支持内存能容纳的任意多线程。
- Linux 2.6 内核与 NPTL:/proc/sys/vm/max_map_count 可能需要增加才能超过 32000 个左右的线程。(不过,你需要使用非常小的堆栈线程才能接近那个线程数,除非你在 64 位处理器上。)参见 NPTL 邮件列表,例如主题为“Cannot create more than 32K threads?”的线程,获取更多信息。
- Linux 2.4:/proc/sys/kernel/threads-max 是最大线程数;在我的 Red Hat 8 系统上默认为 2047。你可以像往常一样通过向该文件写入新值来增加,例如
"echo 4000 > /proc/sys/kernel/threads-max" - Linux 2.2:即使是 2.2.13 内核也限制线程数量,至少在 Intel 上。我不知道其他架构上的限制是什么。Mingo 为 Intel 上的 2.1.131 发布了一个移除此限制的补丁。它似乎已集成到 2.3.20 中。
- 另请参见 Volano 关于在 2.2 内核中提高文件、线程和 FD_SET 限制的详细说明。哇。这个文档逐步指导你完成很多你自己很难弄清楚的东西,但有些过时了。
- Java:参见 Volano 的详细基准测试信息,以及他们关于如何调整各种系统以处理大量线程的信息。
Java 相关问题
直到 JDK 1.3,Java 的标准网络库主要提供每个客户端一个线程的模型。有一种方法可以进行非阻塞读取,但没有方法进行非阻塞写入。
2001年5月,JDK 1.4 引入了包 java.nio,以提供对非阻塞 I/O 的全面支持(以及其他一些好东西)。参见发行说明了解一些注意事项。试试看,并向 Sun 提供反馈!
HP 的 Java 还包括一个线程轮询 API。
2000年,Matt Welsh 为 Java 实现了非阻塞套接字;他的性能基准测试表明,在服务许多(最多 10000)连接的服务器中,它们比阻塞套接字有优势。他的类库称为 java-nbio;它是 Sandstorm 项目的一部分。提供了 10000 个连接下的性能基准测试。
另请参见 Dean Gaudet 关于 Java、网络 I/O 和线程的文章,以及 Matt Welsh 关于事件与工作线程的论文。
在 NIO 之前,有几个改进 Java 网络 API 的提案:
- Matt Welsh 的 Jaguar 系统提出了预序列化对象、新的 Java 字节码和内存管理更改,以允许在 Java 中使用异步 I/O。
- C-C. Chang 和 T. von Eicken 的《Interfacing Java to the Virtual Interface Architecture》提出了内存管理更改,以允许在 Java 中使用异步 I/O。
- JSR-51 是 Sun 的项目,提出了 java.nio 包。Matt Welsh 参与了(谁说 Sun 不听?)。
其他技巧
零拷贝
通常,数据在从这里到那里的过程中会被复制多次。任何将这些复制减少到最低物理极限的方案都称为“零拷贝”。
- Thomas Ogrisegg 针对 Linux 2.4.17-2.4.20 下内存映射文件的零拷贝发送补丁。声称它比 sendfile() 更快。
- IO-Lite 是一组 I/O 原语的提案,旨在消除许多复制的需要。
- Alan Cox 在 1999年 指出,零拷贝有时不值得麻烦。(不过,他确实喜欢 sendfile()。)
- Ingo 在 2000年7月 为 TUX 1.0 在 2.4 内核中实现了一种形式的零拷贝 TCP,并表示他很快会使其可用于用户空间。
- Drew Gallatin 和 Robert Picco 向 FreeBSD 添加了一些零拷贝功能;其思想似乎是,如果你在套接字上调用 write() 或 read(),指针是页对齐的,传输的数据量至少为一页,并且你没有立即重用缓冲区,则会使用内存管理技巧来避免复制。但请参见 linux-kernel 上对此消息的后续讨论,人们对这些内存管理技巧的速度表示担忧。
- 根据 Noriyuki Soda 的说明: “发送端零拷贝自 NetBSD-1.6 版本起通过指定 ‘SOSEND_LOAN’ 内核选项得到支持。此选项现在是 NetBSD-current 的默认选项(你可以在 NetBSD_current 上通过指定 ‘SOSEND_NO_LOAN’ 内核选项来禁用此功能)。使用此功能,如果要发送的数据超过 4096 字节,会自动启用零拷贝。”
sendfile()系统调用可以实现零拷贝网络。
Linux和FreeBSD中的sendfile()函数让你可以指示内核发送文件的一部分或全部。这使得操作系统能够以最高效的方式完成。它可以同等良好地应用于使用线程或非阻塞I/O的服务器。(在Linux中,目前文档较少;可使用_syscall4来调用它。Andi Kleen正在编写新的手册页来涵盖此内容。另请参见Jeff Tranter在Linux Gazette第91期中的文章《探索sendfile系统调用》。)有传言称,ftp.cdrom.com从sendfile()中受益显著。
sendfile()的零拷贝实现正在2.4内核中开发。参见2001年1月25日的LWN文章。
一位使用sendfile()的FreeBSD开发者报告称,使用POLLWRBAND代替POLLOUT会带来巨大差异。
Solaris 8(截至2001年7月的更新)引入了一个新的系统调用’sendfilev’。其手册页副本可在此处查看。Solaris 8 7/01发行说明也提到了它。我怀疑这在以阻塞模式发送到套接字时最为有用;在非阻塞套接字上使用会有些麻烦。
通过使用writev(或TCP_CORK)避免小数据帧 Linux的一个新套接字选项TCP_CORK,告诉内核避免发送部分帧,这在某些情况下会有所帮助,例如当有许多小的write()调用出于某种原因无法捆绑时。取消该选项会刷新缓冲区。不过,更好的方法是使用writev()… 参见2001年1月25日的LWN文章,其中总结了linux-kernel上关于TCP_CORK和可能的替代方案MSG_MORE的一些非常有趣的讨论。
在过载时采取明智行为。 [Provos, Lever, and Tweedie 2000]指出,在服务器过载时丢弃传入连接改善了性能曲线的形状,并降低了整体错误率。他们使用了“具有I/O就绪状态的客户端数量”的平滑版本作为过载的度量。这种技术应该很容易应用于使用select、poll或任何每次调用返回就绪事件计数的系统调用(例如/dev/poll或sigtimedwait4())编写的服务器。
某些程序可以通过使用非Posix线程而受益。
并非所有线程都是平等的。Linux中的clone()函数(以及其他操作系统中的类似函数)允许你创建一个拥有自己当前工作目录的线程,例如,这在实现FTP服务器时非常有用。参见Hoser FTPd以了解使用原生线程而非pthreads的示例。
有时缓存自己的数据可以带来优势。 Vivek Sadananda Pai ( vivek@cs.rice.edu) 于5月9日在new-httpd上的帖子《Re: fix for hybrid server problems》中提到: “我在FreeBSD和Solaris/x86上比较了基于select的服务器与多进程服务器的原始性能。在微基准测试中,仅存在源于软件架构的边际性能差异。基于select的服务器的巨大性能优势来自应用级缓存。虽然多进程服务器可以以更高的成本实现它,但在实际工作负载(与微基准测试相对)中获得相同的优势要困难得多。我将在下一篇Usenix会议上发表的论文中展示这些测量结果。如果你有postscript,论文可在 http://www.cs.rice.edu/~vivek/flash99/获取。”
其他限制
- 旧的系统库可能使用16位变量来存储文件句柄,这在超过32767个句柄时会导致问题。glibc2.1应该没问题。
- 许多系统使用16位变量来存储进程或线程ID。将Volano可扩展性基准测试移植到C,并查看各种操作系统的线程数量上限将会很有趣。
- 某些操作系统为每个线程预分配了过多的线程本地内存;如果每个线程获得1MB,而总虚拟内存空间为2GB,那就产生了2000个线程的上限。
- 查看 http://www.acme.com/software/thttpd/benchmarks.html底部的性能比较图。注意到各种服务器在超过128个连接时遇到困难,即使在Solaris 2.6上也是如此吗?如果有人弄清楚原因,请告诉我。 注意:如果TCP协议栈存在一个在SYN或FIN时导致短暂(200ms)延迟的错误,就像Linux 2.2.0-2.2.6曾有过的那样,并且操作系统或HTTP守护进程对打开连接数有硬性限制,你将会看到这种行为。可能还有其他原因。
内核问题 对于Linux,看起来内核瓶颈在不断被修复。参见Linux Weekly News、Kernel Traffic、Linux-Kernel邮件列表以及我的Mindcraft Redux页面。
1999年3月,微软赞助了一项基准测试,比较了NT与Linux在服务大量HTTP和SMB客户端方面的表现,他们在其中未能从Linux看到良好的结果。另请参见我关于Mindcraft 1999年4月基准测试的文章以获取更多信息。
另请参见The Linux Scalability Project。他们正在进行有趣的工作,包括Niels Provos的poll hinting补丁,以及一些关于“惊群”问题的工作。
另请参见Mike Jagdis改进select()和poll()的工作;这是Mike关于此事的帖子。
Mohit Aron ( aron@cs.rice.edu) 写道,TCP中的基于速率的时钟调度可以将HTTP在“慢速”连接上的响应时间提高80%。
测量服务器性能 有两个测试特别简单、有趣且困难:
- 原始连接每秒(每秒可以提供多少个512字节的文件?)
- 许多慢速客户端下载大文件时的总传输速率(在性能恶化之前,你的服务器可以同时支持多少个28.8k调制解调器客户端下载?)
Jef Poskanzer发布了比较许多Web服务器的基准测试。有关他的结果,请参见 http://www.acme.com/software/thttpd/benchmarks.html。
我还有一些关于比较thttpd与Apache的旧笔记,可能对初学者有兴趣。
Chuck Lever不断提醒我们关于Banga和Druschel的Web服务器基准测试论文。值得一读。
IBM有一篇优秀的论文《Java服务器基准测试》[Baylor等人,2000年]。值得一读。
示例
- Nginx 是一个Web服务器,它使用目标操作系统上可用的任何高效网络事件机制。它越来越受欢迎;甚至有关于它的书籍(并且自本页面最初编写以来,出现了更多相关书籍,包括那本书的第四版)。
- 有趣的基于select()的服务器
- thttpd 非常简易。使用单个进程。性能良好,但不随CPU数量扩展。也可以使用kqueue。
- mathopd 与thttpd类似。
- fhttpd
- boa
- Roxen
- Zeus,一个试图成为绝对最快的商业服务器。参见他们的调优指南。
- http://www.acme.com/software/thttpd/benchmarks.html上列出的其他非Java服务器。
- BetaFTPd
- Flash-Lite - 使用IO-Lite的Web服务器。
- Flash: An efficient and portable Web server – 使用 select(), mmap(), mincore()
- The Flash web server as of 2003 – 使用 select(), 修改的 sendfile(), 异步 open()
- xitami - 使用 select() 来实现其自己的线程抽象,以便移植到没有线程的系统。
- Medusa - 一个用Python编写的服务器开发工具包,试图提供非常高的性能。
- userver - 一个小型HTTP服务器,可以使用 select, poll, epoll 或 sigio。
- 有趣的基于/dev/poll的服务器
- N. Provos, C. Lever, “Scalable Network I/O in Linux,” May, 2000. [FREENIX track, Proc. USENIX 2000, San Diego, California (June, 2000).] 描述了一个修改以支持 /dev/poll 的thttpd版本。性能与phhttpd进行了比较。
- 有趣的基于epoll的服务器
- ribs2
- cmogstored - 使用 epoll/kqueue 处理大部分网络,使用线程处理磁盘和 accept4
- 有趣的基于kqueue()的服务器
- thttpd (版本2.21起?)
- Adrian Chadd说:“我正在做很多工作,让squid真正喜欢kqueue I/O系统”;这是Squid的一个官方子项目;参见 http://squid.sourceforge.net/projects.html#commloops。(这显然比Benno的补丁更新。)
- 有趣的基于实时信号的服务器
- Chromium的X15。这使用了2.4内核的SIGIO功能以及sendfile()和TCP_CORK,据报道达到了比TUX更高的速度。源代码可在社区源代码(非开源)许可下获得。参见Fabio Riccardi的原始公告。
- Zach Brown的phhttpd - “一个快速的Web服务器,编写用来展示sigio/siginfo事件模型。如果你试图在生产环境中使用此代码,请认为它是高度实验性的,并且你自己可能有点疯狂。” 使用了2.3.21或更高版本的siginfo功能,并包含了早期内核所需的补丁。传言甚至比khttpd更快。参见他于1999年5月31日的帖子以获取一些说明。
- 有趣的基于线程的服务器
- Hoser FTPD。参见他们的基准测试页面。
- Peter Eriksson的phttpd和pftpd
- http://www.acme.com/software/thttpd/benchmarks.html上列出的基于Java的服务器。
- Sun的Java Web Server(据报道可以处理500个并发客户端)
- 有趣的内核服务器
- khttpd
- “TUX” (Threaded linUX webserver) 由 Ingo Molnar 等人开发。适用于2.4内核。
其他有趣的链接
- Jeff Darcy关于高性能服务器设计的笔记
- Ericsson的ARIES项目——Apache 1 vs. Apache 2 vs. Tomcat在1到12个处理器上的基准测试结果
- Prof. Peter Ladkin的Web Server Performance页面。
- Novell的FastCache——声称每秒10000次点击。非常漂亮的性能图表。
- Rik van Riel的Linux Performance Tuning网站
翻译
- 白俄罗斯语翻译由Ucallweconn的Patric Conrad提供
更新日志
2011/07/21
添加了nginx.org
$Log: c10k.html,v $
Revision 1.212 2006/09/02 14:52:13 dank
添加了asio
Revision 1.211 2006/07/27 10:28:58 dank
链接到Cal Henderson的书。
Revision 1.210 2006/07/27 10:18:58 dank
列出Polyakov链接,添加Drepper的新提案,注明FreeBSD 7可能转向1:1
Revision 1.209 2006/07/13 15:07:03 dank
链接到Scale!库,更新了Polyakov链接
Revision 1.208 2006/07/13 14:50:29 dank
链接到Polyakov的补丁
Revision 1.207 2003/11/03 08:09:39 dank
链接到Linus不赞成aio_open想法的消息
Revision 1.206 2003/11/03 07:44:34 dank
链接到userver
Revision 1.205 2003/11/03 06:55:26 dank
链接到Vivek Pei的新Flash论文,提到出色的specweb99分数
版权所有 1999-2018 Dan Kegel
dank@kegel.com
最后更新:2014年2月5日
(小修正:2019年5月28日)
[返回 www.kegel.com]

