服务端经典的C10k问题(译)

7,455 阅读46分钟

服务端经典的C10K问题

最近看了一下Unix网络编程相关的内容,然后发现了一篇非常经典的文章, 内容可能不是很新,不过真的很经典,C10K问题,简单翻译了一下(markdown转换过来格式可能存在少量问题)对掌握linux io和 linux 线程会有更深的理解.(文内存在大量的链接.) 原文链接 The C10K problem

现在 web 服务器需要同时处理上万请求,难道不是吗?毕竟如今的网络将会有很大的发展空间. 计算机也同样强大.你可以花1200美元买一台 1000MHz,2G 内存和1000Mbits/sec的网卡的机器.让我们来看看-- 20000 客户端,每个客户端 50KHz, 1000Kb 和每秒 50Kb,那没有什么比这两万个客户端每个每秒从磁盘中取出4千字节并将它们每秒发送到网络上去更消耗资源了.(顺便说一下,每个客户端0.0.8美元,一些操作系统收费的单个客户端 美元的许可费看起来有点贵)所以硬件不再是一种瓶颈.

在1999年,最繁忙的 ftp 网站之一, cdrom.com, 实际上通过一个千兆以太网网卡同时处理 10000个客户端.现在相同的速度也被ISP 提供,他们希望它变得越来越受大型企业客户的欢迎.

轻量级的客户端计算模型似乎又开始变得流行起来了 - 服务器在互联网上运行,为数千个客户提供服务.

基于以上的一些考虑,这有一些关于如何配置操作系统或者编写支持数千客户端的代码问题提出了一些注意点. 讨论的中心的主要是围绕着类Unix操作系统,因为这是我个人感兴趣的领域,但是Windows也会涉及一点.

内容

  • C10K问题
    • 内容]
    • 相关网站
    • 预读书籍
    • I/O 框架
    • I/O 策略
      • 1. 一个线程服务多个客户端,使用非阻塞 IO 和水平触发的就绪通知
      • 2. 一个线程服务多个客户端,使用非阻塞 IO 和就绪改变通知
      • 3. 一个线程服务多个客户端,使用异步 I/O
      • 4. 一个线程服务一个客户端
      • Linux线程
        • NGPT: Linux的下一代 Posix 线程
        • NPTL: Linux原生的 Posix 线程库
        • FreeBSD 线程支持
        • NetBSD 线程支持
        • Solaris 线程支持
        • JDK 1.3.x及更早版本中的Java线程支持
        • 注意:1:1 线程与 M:N线程
      • 5. 将服务端代码构建到内核中
    • 将 TCP 协议栈带入用户空间
    • 评论
    • 打开文件句柄的限制
    • 线程限制
    • Java问题]
    • 其他建议
    • 其他限制
    • 内核问题
    • 测试服务性能
    • 例子
    • 有趣的基于 select() 的服务器
    • 有趣的基于 /dev/poll 的服务器
    • 有趣的基于 epoll 的服务器
    • 有趣的基于 kqueue() 的服务器
    • 有趣的基于实时信号的服务器
    • 有趣的基于线程的服务器
    • 有趣的内核服务器
    • 其他有趣的链接

相关网站

参阅下 Nick Black 的杰出的 快速的Unix服务器网页,了解大约2009的情况.

在2003年10月,Felix von Leitner 整理了一个优秀的关于网络可扩展性的网站演示,完成了多种不同的网络系统调用和操作系统的性能比较.其中一个发现是 linux 2.6 内核击败了 2.4 内核,当然这里有很多很好的图片会让操作系统开发人员在平时提供点想法.

预读书籍

如果你没有阅读过the late W. Richard Stevens的Unix网络编程: 网络Apis:套接字和Xti(第1卷)的拷贝,请尽快获取一份,它描述了很多的于 I/O 策略和编写高性能服务器的陷阱.它甚至谈到了 'thundering herd'问题.当你在阅读它时,请阅读 Jeff Darcy写的关于高性能服务器设计.

(另外一本书构建可扩展的网站可能会对使用而不是编写一个web服务器的人会有帮助)

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 为基础测试,Poll的性能好的多.文档链到下面的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 调用
    • 不处理;使用阻塞和同步调用,尽可能的使用多个线程和进程实现并发.
    • 使用非阻塞调用(如,在一个socket write()上设置 O_NONBLOCK) 去启动 I/O,就绪通知(如,poll() 或则 /dev/poll)知道什么时候通道是 OK 的然后开启下一个 I/O.通常这只能用于网络 I/O,而不能用于磁盘 I/O.
    • 使用异步调用(如,aio_write())去启动 I/O,完成通知(如,信号或完成端口)去通知 I/O 完成.这同时适用于网络和磁盘 I/O.
  • 如何控制每个客户的服务
    • 一个进程服务一个客户(经典的 Unix 方法,从1980年左右就开始使用)
    • 一个系统级别线程服务多个客户;每个客户通过以下控制:
      • 一个用户级别线程(如. GNU 状态线程, 带绿色线程的经典 java)
      • 状态机(有点深奥,但在某些圈子里很受欢迎; 我的最爱)
      • continuation (有点深奥,但在某些圈子里很受欢迎; 我的最爱)
    • 一个系统级线程服务单个客户(如,经典的带有原生线程的Java)
  • 一个系统级线程服务每个活跃的客户(如. Tomcat与apache的前端;NT完成端口; 线程池)
  • 是否使用标准系统服务,或者构建服务到内核中(如,在一些自定义驱动,内核模块,或者 VxD)

下边的5中组合似乎非常流行:

  1. 一个线程服务多个客户端.使用非阻塞 I/O 和水平触发就绪通知.
  2. 一个线程服务多个客户端.使用非阻塞 I/O 和就绪更改通知.
  3. 一个线程服务多个客户端. 使用异步 I/O.
  4. 一个线程服务多个客户端.使用阻塞 I/O
  5. 将服务端代码构建到内核

1. 一个线程服务多个客户端,使用非阻塞 IO 和水平触发就绪通知

... 在所有的网络句柄上都设置为非阻塞模式,使用 select() 或则 poll() 去告知哪个网络句柄处理有数据等待.此模型是最传统的.这种模式下,内核告诉你是否一个文件描述符就绪,自从上次内核告诉你它以来,你是否对该文件描述符做了任何事情.('水平触发'这个名词来自计算机硬件设计;它与'边缘触发'相反).Jonathon Lemon在他的关于BSDCON 2000 paper kqueue()的论文中介绍了这些术语

注意: 牢记来自内核的就绪通知只是一个提示,这一点尤为重要;当你尝试去读取文件描述符的时候,它可能没有就绪.这就是为什么需要在使用就绪通知时使用非阻塞模式的原因.

一个重要的瓶颈是 read()或 sendfile() 从磁盘块读取时,如果该页当前并不在内存中.在设置非阻塞模式的磁盘文件处理是没有影响的.内存映射磁盘文件也是如此.首先一个服务需要磁盘 I/O时,他的处理块,所有客户端必须等待,因此原生非线程性能将会被浪费了.

这也就是异步 I/O 的目的,当然仅限于没有 AIO 的系统上,用多线程和多进程进行磁盘 I/O 也可能解决这个瓶颈.一种方法是使用内存映射文件,如果 mincore() 表示需要 I/O,让一个工作线程去进行 I/O 操作,并继续处理网络流量.Jef Poskanzer 提到 Pai, Druschel, and Zwaenepoel的1999 Flash web服务器使用这个技巧;他们在Usenix '99发表了关于它的演讲.看起来 mincore() 在BSD-derived Unixes 上是可用的,如像FreeBSD和Solaris,但它不是单Unix规范的一部分.从kernel2.3.51 开始,它也开始是linux的一部分,感谢Chuck Lever.

但是在2003年十一月的 freebsd-hackers list, Vivek Pei 等人报道了使用他们的 Flash web服务器有一个很好的结果.然后在攻击其瓶颈,其中发现一个瓶颈是 mincore(猜测之后这不是一个好办法),另外一个就是 sendfile 阻塞磁盘访问;他们一种修改的 sendfile(),当他的访问磁盘页尚未处于核心状态时返回类似 EWOULDBLOCK 的内容,提升了性能.(不知道怎么告诉用户页现在是常驻的...在我看来真正需要的是aio_sendfile().)他们优化的最终结果是在 1GHZ/1GB 的FreeBSD盒子上 SpecWeb99 得分约为800,这比spec.org上的任何文件都要好.

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

  • 传统的 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, benchmarks)是一个如何使用 poll() 与其他就绪通知模式交互的示例.
  • /dev/poll
    这是推荐在Solaris 代替poll的方法
    /dev/poll 的背后思想就是利用 poll() 在大部分的调用时使用相同的参数.使用/dev/poll,获取一个 /dev/poll 的文件描述符,然后把你关心的文件描述符写入到/dev/poll的描述符;然后,你就可以从该句柄中读取当前就绪文件描述符集.
    它悄悄的出现在 Solaris 7 中(看 patchid 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系统上推荐使用的代替poll的方法(很快,NetBSD).
    看下边 kqueue() 可以指定边缘触发或水平触发.

2. 一个线程服务多个客户端, 使用非阻塞 IO 和就绪改变通知

就绪改变通知(或边缘就绪通知)意味着你向内核提供文件描述符,然后,当该描述符从 not ready 转换为 ready 时,内核会以某种方式通知你.然后它假定你已知文件描述符已准备好,同时不会再对该描述符发送类似的就绪通知,直到你在描述符上进行一些操作使得该描述符不再就绪(例如,直到你收到 EWOULDBLOCK 错误为止)发送,接收或接受呼叫,或小于请求的字节数的发送或接收传输).

当你使用就绪改变通知时,你必须准备处理好虚假事件,因为最常见的实现是只要接收到任何数据包都发出就绪信号,而不管文件描述符是否准备就绪.

这与"水平触发"就绪通知相反.它对编程错误的宽容度要低一些,因为如果你只错过一个事件,那么事件的连接就会永远停滞不前.可以尽管如此,我发现边缘触发的就绪通知能让使用OpenSSL编程非阻塞客户端变得更容易,因此还是值得尝试.

[Banga, Mogul, Drusha '99]在1999年描述了这种类型的模式.

有几种API使应用程序检索"文件描述符准备就绪"通知:

/* 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);
  • 当 read() 或 write() 等普通 I/O 函数完成时,发送该信号.要使用该段的话,在循环里面,当poll()处理完所有的描述符后,进入 sigwaitinfo()sigwaitinfo() 循环.
    如果 sigwaitinfo 或 sigtimedwait 返回你的实时信号,siginfo.si_fd 和 siginfo.si_band 提供的信息几乎与 pollfd.fd 和 pollfd.revents 在调用 poll() 之后的信息相同,如果你处理该 I/O,那么就继续调用sigwaitinfo()
    如果 sigwaitinfo 返回传统的 SIGIO,那么信号队列溢出,你必须通过临时将信号处理程序更改为SIG_DFL来刷新信号队列,然后回到外层poll()循环.
    参阅Poller_sigio (cc, h)是一个如何使用 rtsignals 与其他就绪通知模式交互的示例.
    参阅Zach Brown的phhttpd,例如直接使用此功能的代码.(还是不要; phhttpd有点难以弄清楚......)
    [Provos,Lever和Tweedie 2000]描述了 phhttpd 的最新基准,使用的不同的sigtimedwait(),sigtimedwait4(),它允许你通过一次调用检索多个信号.有趣的是 sigtimedwait4() 对他们的主要好处似乎是允许应用程序来衡量系统过载(因此它可以行为恰当).(请注意,poll()也提供了同样的系统负载测量.)

每个fd一个信号
Signal-per-fd是由Chandra和Mosberger提出的对实时信号的一种改进,它通过合并冗余事件来减少或消除实时信号队列溢出.但它并没有超越 epoll.他们的论文 (www.hpl.hp.com/techreports…)将此方案的性能与select() 和 /dev/poll 进行了比较.
Vitaly Luban于2001年5月18日宣布了一项实施该计划的补丁;他的补丁产生于www.luban.org/GPL/gpl.htm….(注意:截至2001年9月,这个补丁在高负载下可能存在稳定性问题.dkftpbench在大约4500个用户可能会触发oops.)
参阅Poller_sigfd (cc,h)是一个如何使用 signal-per-fd 与其他就绪通知模式交互的示例.

3. 一个线程服务多个客户端,使用异步 I/O.

这在Unix至今都没有流行起来,可能是因为较少的操作系统支持了异步 I/O,也可能是因为(像非阻塞 I/O)它要求重新思考应用程序.在标准 Unix 下,异步 I/O 被aio_ 接口提供(从该链接向下滚动到"异步输入和输出"),它将信号和值与每个 I/O操作相关联.信号及其值排队并有效地传递给用户进程.这是来自 POSIX 1003.1b 实时扩展,也是单Unix规范第二版本.

AIO通常与边缘触发完成通知一起使用,即当操作完成时,信号排队.(它也可以通过调用aio_suspend()与水平触发的完成通知一起使用,虽然我怀疑很少有人这样做.)

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

截止linux内核 2.5.32,Ben LaHaise的 Linux AIO 实现已合并到主 Linux 内核中.它不使用内核线程,同时还具有非常高效的底层api,但是(从2.6.0-test2开始)还不支持套接字.(2.4内核还有一个 AIO 补丁,但 2.5/2.6 实现有些不同.)更多信息:

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: 真实世界的编程据说涵盖了对aio的一个很好的介绍.

Solaris早期非标准的aio实现的教程在线Sunsite.这可能值得一看,但请记住,你需要在精神上将"aioread"转换为"aio_read"等.

请注意,AIO不提供在不阻塞磁盘 I/O 的情况下打开文件的方法; 如果你关心打开磁盘文件导致休眠,Linus建议你只需在另一个线程中执行 open()而不是是进行 aio_open() 系统调用.

在Windows下,异步 I/O 与术语"重叠 I/O "和 IOCP 或"I/O完成端口"相关联.微软的 IOCP 结合了现有技术的技术,如异步 I/O(如aio_write)和排队完成通知(如将 aio_sigevent 字段与 aio_write 一起使用时),以及阻止某些请求尝试保持运行线程数量相关的新想法具有单个 IOCP 常量.欲获得更多信息,请参阅 sysinternals.com 上的 Mark Russinovich 撰写的I/O 完成端口的内部,Jeffrey Richter的书 "为Microsoft Windows 2000编写服务端程序"(Amazon, MSPress), U.S. patent #06223207, 或者MSDN.

4. 一个线程服务多个客户端

...让 read() 和 write() 阻塞.每个客户端使用整个栈侦会有很大的缺点,就是消耗内存.很多操作系统也难以操作处理上百个线程.如果每个线程获得2MB堆栈(不是非常见的默认值),则在 32 位机器上的 (2^30/2 ^21)= 512 个线程上会耗尽虚拟内存,具有 1GB 用户可访问的VM(比如,Linux 通常在 x86 上允许)你可以通过提供更小的栈解决这个问题,但是线程一旦创建,大多数线程库都不允许增加线程栈,所以这样做就意味着你必须使你的程序最小程度地使用内存.你也可以通过转移到64位处理器来解决这个问题.

在Linux, FreeBSD, Solaris上的线程支持是正在完善,即使对于主流用户来说,64位处理器也即将到来.也许在不就的将来,那些喜好每个客户端使用一个线程的人也有能力服务10000个客户端了.然而,在目前这个时候,如果你真的想要支持那么多客户,你可能最好还是使用其他一些方法.

对于毫不掩饰的亲线程观点的人,请参阅为什么事件是一个坏主意(对于高并发服务器)由von Behren,Condit和Brewer,UCB,在HotOS IX上发布.有反线营地的任何人能指出一篇反驳这篇论文的论文吗?:-)

Linux 线程

Linux线程是标准Linux线程库的名称.从 glibc2.0 开始,它就集成到 glibc 中,主要是符合 Posix 标准,但性能和信号支持度上都不尽如人意.

NGPT: Linux 的下一代的 Posix 线程

NGPT是 IBM 启动的为 Linux 带来更好的 Posix 线程兼容性的项目.他目前的稳定版本是2.2,工作的非常好...但是 NGPT 团队宣布他们将 NGPT 代码库置于support-only模式,因为他们觉得这是"长期支持社区的最佳方式". NGPT团队将会继续改进 Linux 的线程支持,但是现在主要集中在NPTL.(感谢NGPT团队的出色工作以及他们以优雅的方式转向NPTL.)

NPTL: Linux 原生的 Posix 线程库

NPTL是由Ulrich Drepper(glibc的维护人员)和Ulrich Drepper发起的,目的是为Linux带来的world-class Posix线程库支持.

截至2003年10月5日,NPTL 现在作为附加目录合并到 glibc cvs 树中(就像linux线程),所以它几乎肯定会与 glibc 的下一个版本一起发布.

Red Hat 9是最早的包含NPTL的发行版本(这对某些用户来说有点不方便,但有人不得不打破僵局...)

NPTL 链接:

这是我尝试写的描述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 和内核的变化,这些变化构成了 Native Posix线程库(NPTL).NPTL使用了NGPT设计的所有内核改进,并利用一些新功能:

> NPTL使用NGPT引入的三个内核特性:getpid()返回 PID,CLONE_THREAD 和 futexes;NPTL还使用(并依赖)更广泛的新内核功能,作为该项目的一部分开发.

> 引入 2.5.8 内核的 NGPT 中的一些项目得到了修改,清理和扩展,例如线程组处理(CLONE_THREAD).[影响 NGPT 兼容性的 CLONE_THREAD 更改与 NGPT 人员同步,以确保NGPT不会以任何不可接受的方式破坏.]

> NPTL开发和使用的内核功能在设计白皮书中有描述,people.redhat.com/drepper/npt… ...

> 简短列表:TLS支持,各种克隆扩展(CLONE_SETTLS,CLONE_SETTID,CLONE_CLEARTID),POSIX线程信号处理,sys_exit()扩展(在VM发布时发布TID futex)sys_exit_group()系统调用,sys_execve()增强功能 并支持分离的线程.

> 还有扩展 PID 空间的工作 - 例如,procfs由于 64K PID 的设计,为max_pid 和 pid 分配可伸缩性的工作而崩溃.此外,还进行了许多仅针对性能的改进.

> 本质上,新功能完全是使用1:1线程方法 - 内核现在可以帮助改进线程的所有内容,并且我们为每个基本线程原语精确地执行最低限度必需的上下文切换和内核调用.

FreeBSD线程支持

FreeBSD同时支持 linux 线程和用户空间线程库.此外,在 FreeBSD 5.0 中引入了一个名为 KSE 的 M:N 实现.概述,参阅www.unobvious.com/bsd/freebsd….

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",这也是暗示性的! ...所以草书建议是:使libthr成为7.x上的默认线程库.

NetBSD线程支持

根据Noriyuki Soda的说明:

内核支持 M:N 基于 Scheduler Activations 模型线程库将于2003年1月18日合并到NetBSD-current中.

更多细节,看由NethanD系统公司的 Nathan J. Williams在2002年的FREENIX上的演示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及以后开始支持

众所周知,直到 JDK1.3.x 的 Java 不支持处理除每个客户端一个线程之外的任何网络连接方法.Volanomark是一个很好的微基准测试,它可以在不同数量连接中测量每秒消息的吞吐量.截至2003年5月,来自不同供应商的 JDK 1.3实际上能够处理一万个同时连接 - 尽管性能显着下降.请参阅表4,了解哪些 JVM 可以处理10000个连接,以及随着连接数量的增加性能会受到影响.

注意:1:1 线程与 M:N 线程

在实现线程库时有一个选择: 你可以将所有线程支持放在内核中(这称为 1:1 线程模型),或者您可以将其中的相当一部分移动到用户空间(这称为 M:N 线程模型).有一点,M:N被认为是更高的性能,但它太复杂了,很难做到正确,大多数人都在远离它.

5. 将服务器代码构建到内核中

据说 Novell 和微软已经在不同的时间做过这个,至少有一个 NFS 实现是这样做的,khttpd为Linux和静态网页做了这个,"TUX"(线程linux web服务器)是Ingo Molnar为Linux的一个快速且灵活的内核空间HTTP服务器. Ingo的2000年9月1日公告表示可以从ftp://ftp.redhat.com/pub/redhat/tux 下载 TUX 的alpha版本,并解释如何加入邮件列表以获取更多信息.

linux-kernel列表一直在讨论这种方法的优点和缺点,而且似乎不是将 Web 服务器移动到内核中,内核应该添加最小的钩子来提高Web服务器的性能.这样,其他类型的服务器可以受益.参见例如Zach Brown的评论关于 userland 与内核 http 服务器的关系.似乎2.4 linux 内核为用户程序提供了足够的功能,因为X15服务器的运行速度与Tux一样快,但不使用任何内核修改.

Bring the TCP stack into userspace

例如,参见netmap数据包 I/O 框架和Sandstorm基于这个概念验证Web服务器.

评论

Richard Gooch已经写了一篇关于讨论 I/O 选项的论文.

在2001年,Tim Brecht和MMichal Ostrowski测试了多种策略为简化基于 select 的服务器.他们的数据值得一看.

在2003年,Tim Brecht发布了userver的源代码,由Abhishek Chandra, David Mosberger, David Pariag 和 Michal Ostrowski 编写的几台服务器组成的小型Web服务器.它能使用select(), poll(),或者sigio.

早在1999年3月,Dean Gaudet的文章:

我不断被问到"为什么你们不使用像Zeus这样的基于select/event的模型?它显然是最快的."...

他的理由归结为"这真的很难,收益还不清楚",然而,在几个月内,很明显人们愿意继续努力.

Mark Russinovich 写了一篇社论文章讨论在 linux内核2.2 中的 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.comMSDN上的这些网页,他说这是NT独有的;简而言之,win32的"重叠 I/O "结果太低而不方便,"完成端口"是一个提供完成事件队列的包装器,加上调试魔术试图保持运行的数量,如果从该端口获取完成事件的其他线程正在休眠(可能阻塞I/O)则允许更多线程获取完成事件,从而使线程保持不变。

另请参阅OS/400对I/O完成端口的支持

1999年9月对linux-kernel进行了一次有趣的讨论"> 15,000个同时连接"(和线程的第二周).强调:

  • Ed Hall发布关于他的经历的一些笔记; 他在运行Solaris的UP P2/333上实现了>1000次连接/秒.他的代码使用了一小块线程(每个CPU1或2个),每个线程使用"基于事件的模型”管理大量客户端.
  • Mike Jagdis 发布了对 poll/select 性能开销的分析,并说"当前的select/poll 实现可以得到显着改善,特别是在阻塞情况下,但由于 select/poll 没有,因此开销仍会随着描述符的数量而增加,并且不能,记住哪些描述符很有趣的.这很容易用新的API修复.欢迎提出建议......"
  • Mike发布关于他改进select()和poll()的工作.
  • Mike 发布了一些可能的API来替换poll()/select(): "你可以编写'pollfd like'结构的'device like'API,'device'监听事件并在你读它时提供代表它们的'pollfd like'结构?..."
  • Rogier Wolff 建议使用"数字家伙建议的API",www.cs.rice.edu/~gaurav/pap…
  • Joerg Pommnitz 指出沿着这些线路的任何新API应该不仅能够等待文件描述符事件,还能够等待信号和SYSV-IPC.我们的同步原语至少应该能够做到Win32的WaitForMultipleObjects.
  • Stephen Tweedie断言,_SETSIG,排队的实时信号和 sigwaitinfo() 的组合是 www.cs.rice.edu/~gaurav/pap… 中提出的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, 增加行
    set kern.maxfiles=XXXX
    其中XXXX是文件描述符所需的系统限制,并重新启动.感谢一位匿名读者,他写道,他说他在FreeBSD 4.3上获得了超过10000个连接,并说
    "FWIW: 你实际上无法通过sysctl轻松调整FreeBSD中的最大连接数....你必须在/boot/loader.conf文件中这样做.
    这样做的原因是 zalloci() 调用初始化套接字和 tcpcb 结构区域在系统启动时很早就发生了,这样区域既可以是类型稳定的又可以交换。
    您还需要将 mbuf 的数量设置得更高,因为您在(在未修改的内核上)为 tcptempl 结构每个连接消耗一个mbuf,用于实现 keepalive."

    其他的读者说到:
    "从FreeBSD 4.4开始,不再分配 tcptempl 结构; 你不再需要担心每个连接都会被消耗一个mbuf

    也可以看看:


  • OpenBSD: 读者说
    "在OpenBSD,需要额外的调整来增加每个进程可用的打开文件句柄的数量: /etc/login.conf 的openfiles-cur参数需要被增加. 您可以使用sysctl -w 或 sysctl.conf 更改 kern.max 文件,但它不起作用.这很重要,因为对于非特权进程,login.conf限制为非常低的64,对于特权进程为128

  • Linux: 参阅Bodo Bauer的 /proc 文档. 在2.4内核
    echo 32768 > /proc/sys/fs/file-max
    增大系统打开文件的限制.和
    ulimit -n 32768
    ulimit -n 32768
    增大当前进程的限制
    On 2.2.x kernels,
    在 2.2.x 内核,
    echo 32768 > /proc/sys/fs/file-max echo 65536 > /proc/sys/fs/inode-max
    增大系统打开文件的限制.和
    ulimit -n 32768
    ulimit -n 32768
    增大当前进程的限制
    我验证了 Red Hat 6.0 上的进程(2.2.5 左右加补丁)可以通过这种方式打开至少31000 个文件描述符.另一位研究员已经证实,2.2.12 上的进程可以通过这种方式打开至少90000 个文件描述符(具有适当的限制).上限似乎是可用的内存。
    Stephen C. Tweedie 发表 关于如何使用 initscript 和 pam_limit 在引导时全局或按用户设置 ulimit 限制.
    在 2.2 更老的内核,但是,即使进行了上述更改,每个进程的打开文件数仍限制为1024
    另见Oskar1998年的帖子,其中讨论了 2.0.36 内核中文件描述符的每个进程和系统范围限制。

线程限制

在任何体系结构上,您可能需要减少为每个线程分配的堆栈空间量,以避免耗尽虚拟内存.如果使用pthreads,可以使用pthread_attr_init() 在运行时设置它。

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 Gaude关于 Java , 网络 I/O, 和线程主题的文章,和 Matt Welsh 写的关于事件对比工作线程的论文

在 NIO 之前,有几个改进Java的网络API的建议:

  • Matt Welsh 的Jaguar 系统提出预序列化对象,新的 Java 字节码和内存管理更改允许使用 Java 进行异步I/O.
  • C-C. Chang and T. von Eicken写的将Java连接到虚拟接口体系结构提出内存管理更改允许 Java 使用异步 I/O.
  • JSR-51是提出 java.nio 包的Sun工程项目. Matt Welsh参加了(谁说Sun不听?).

其他建议

  • 零拷贝
    通常情况下,数据会从一处到其他处多次复制.任何将这些副本消除到裸体物理最小值的方案称为"零拷贝".
    • Thomas Ogrisegg 在 Linux 2.4.17-2.4.20 下为 mmaped 文件发送零拷贝发送补丁.声称它比 sendfile() 更快.
    • IO-Lite 是一组 I/O 原语的提议,它摆脱了对许多副本的需求.
    • 在1999年, Alan Cox指出零拷贝有时是不值的会遇到麻烦.(但他确实喜欢sendfile())
  • Ingo于2000年7月在 2.4 内核中为 TUX 1.0实现了一种零拷贝TCP,他说他很快就会将其提供给用户空间.
    • Drew Gallatin and Robert 已经为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 正在写覆盖该内容的 man 页面.另请参阅Jeff Tranter在Linux Gazette issue 91中探索 sendfile 系统调用.) 有传言称 ftp.cdrom.com 受益于 sendfile().
      sendfile() 的零拷贝实现正在为2.4内核提供.看LWN Jan 25 2001.
      一个开发者在 Freebsd 下使用 sendfile() 的报告显示使用 POLLWRBAND 而不是 POLLOUT 会产生很大的不同.
      Solaris 8 (截至2001年7月更新) 有一个新的系统调用'sendfilev'.手册页的副本在这里. Solaris 8 7/01 发版说明 也提到了它.我怀疑这在以阻塞模式发送到套接字时最有用;使用非阻塞套接字会有点痛苦。
  • 使用writev避免使用小帧(或者 TCP_CORK)
    一个新的在 Linux 下的套接字选项, TCP_CORK,告诉内核去避免发送部分帧,这有点帮助,例如当有很多小的 write() 调用时,由于某种原因你不能捆绑在一起.取消设置选项会刷新缓冲区.最好使用writev(),但......
    LWN Jan 25 2001,关于TCP-CORK和可能的替代MSG_MORE的关于linux-kernel的一些非常有趣的讨论的摘要.
  • 在过载时表现得智能.
    [Provos, Lever, and Tweedie 2000]提到在服务器过载时丢弃传入连接可以改善性能曲线的形状,并降低整体错误率.他们使用平滑版本的" I/O 就绪客户端数"作为过载的衡量标准.此技术应该很容易应用于使用 select, poll 或任何系统调用编写的服务器,这些调用返回每次调用的就绪事件技术(例如 /dev/poll 或 sigtimedwait4()).
  • 某些程序可以从使用非Posix线程中受益.
    并非所有线程都是相同的.例如,Linux 中的 clone() 函数(及其在其他操作系统中的朋友)允许您创建具有其自己的当前工作目录的线程,这在实现ftp服务器时非常有用.有关使用本机线程而不是 pthreads 的示例,请参阅 Hoser FTPd。
  • 缓存自己的数据有时可能是一个胜利.
    Vivek Sadananda Pai(vivek@cs.rice.edu)在 new-httpd"回复: 修复混合服务器问题",5月9日,声明:
    "我在 FreeBSD 和 Solaris/x86 上比较了基于 select 的服务器和多进程服务器的原始性能.在微基准测试中,软件架构的性能差异很小.基于 select 的服务器的巨大性能优势源于进行应用程序级缓存.虽然多进程服务器可以以更高的成本实现,但实际工作负载(与微基准测试相比)更难获得相同的好处.我将把这些测量结果作为论文的一部分展示,这些论文将出现在下一届Usenix会议上.如果你有后记,那么论文可以在www.cs.rice.edu/~vivek/flas…"

其他限制

  • 旧系统库可能使用16位变量来保存文件句柄,这会导致32767句柄之上的麻烦.glibc2.1应该没问题。
  • 许多系统使用16位变量来保存进程或线程ID.将Volano可伸缩性基准测试 移植到C会很有意思,看看各种操作系统的线程数上限是多少.
  • 某些操作系统预先分配了过多的线程本地内存;如果每个线程获得1MB,并且总VM空间为2GB,则会创建2000个线程的上限.
  • 查看www.acme.com/software/th… 底部的性能对比图.请注意各种服务器如何在 128个 以上的连接上出现问题,甚至在 Solaris 2.6上 知道原因的人,让我知道.

注意: 如果TCP堆栈有一个bug,导致 SYN 或 FIN 时间更短(200ms)延迟,如 Linux 2.2.0-2.2.6 所示,并且操作系统或 http 守护程序对连接数有硬限制,你会期待这种行为.可能还有其他原因.

内核问题

对于Linux,看起来内核瓶颈正在不断修复.看Linux Weekly News,Kernel Traffic, the Linux-Kernel mailing list,和my Mindcraft Redux page.

1999年3月,微软赞助了一项比较 NT 和 Linux 的基准测试,用于服务大量的 http 和 smb客户端,linux的结果不如人意.另见关于Mindcraft 1999年4月基准测试的文章了解更多信息

另请参见Linux可扩展性项目.他们正在做有趣的工作.包括Niels Provos的暗示民意调查补丁,关于雷鸣般的群体问题的一些工作.

另请参与Mike Jagdis致力于改进 select() 和 poll();这是Mike关于它的帖子.

Mohit Aron(aron@cs.rice.edu)写道,TCP中基于速率的时钟可以将"缓慢"连接上的HTTP响应时间提高80%

测量服务器性能

特别是两个测试简单,有趣,而且很难:

  1. 每秒原始连接数(每秒可以提供多少512字节文件?)
  2. 具有许多慢速客户端的大型文件的总传输速率(在性能进入底池之前,有多少 28.8k 调制解调器客户端可以同时从服务器下载?)

Jef Poskanzer发布了比较许多Web服务器的基准测试.看他的结果www.acme.com/software/th…

我也有关于将thttpd与Apache比较的一些旧笔记可能对初学者感兴趣.

Chuck Lever不断提醒我们关于Banga和Druschel关于Web服务器基准测试的论文.值得一读。

IBM有一篇名为Java服务器基准测试的优秀论文.[Baylor 等,2000年].值得一读。

例子

Nginx 是一个web服务器,它使用目标操作系统上可用的任何高效网络事件机制.它变得非常流行;这甚至有关于它的两本书

有趣的基于 select() 的服务器

有趣的基于 /dev/poll 服务器

  • N.Provos,C.Lever,"Scalable Network I/O in Linux"2000年5月.[ FREENIX track,Proc.USENIX 2000,San Diego,California(2000年6月).]描述了被修改为支持 /dev/poll的 thttpd 版.将性能与phhttpd进行比较。

有趣的基于 epoll 服务器

  • ribs2
  • cmogstored - 对大多数网络使用epoll/kqueue,对磁盘和accept4使用线程

有趣的基于 kqueue() 服务器

有趣的基于实时信号服务器.

  • Chromium 的 X15.使用2.4内核 SIGIO 功能以及 sendfile() 和 TCP_CORK,据报道甚至比TUX实现更高的速度. 在社区许可下的源码是可用的.看 Fabio Riccardi 原始公告
  • Zach Brown 的 phhttpd - "一个更快的服务服务器, 它用于展示 sigio/siginfo 事件模型.如果你尝试在生产环境中使用它,请将此代码视为高度实验性的,同时您自己也格外注意" ,使用 2.3.21或之后的 siginfo 特性, 包含了需要的更新内核补丁.据传甚至比khttpd更快.见他1999年5月31日的一些说明

有趣的基于线程的服务器

有趣的基于内核的服务器

其他有趣的链接