高性能服务器中的C10K问题

原文:

The C10K problem

---------------------------------------------------------------------------------------------------------------------------

C10K 问题


是时候让 Web 服务器同时处理一万个客户端了,你不觉得吗?毕竟,网络现在是一个很大的地方。

电脑性能也大大超出以前了。你可以花 1200 美元左右购买一台 1000MHz 的机器,它有 2GB 的 RAM 和一个 1000Mbit/sec 的以太网卡。让我们看看 - 在 20000 个客户端,即每个客户端 50KHz、100Kbytes 和 50Kbits/sec。不应该比从磁盘中取出 4 KB 并为每两万个客户端每秒一次将它们发送到网络的能力更多。 (顺便说一下,每个客户端 0.08 美元。一些操作系统收取的 100 美元/客户端许可费开始看起来有点沉重!)所以硬件不再是瓶颈。

1999 年,最繁忙的 ftp 站点之一 cdrom.com 实际上通过千兆以太网管道同时处理了 10000 个客户端。到 2001 年,几家 ISP 提供了同样的速度,他们预计它会越来越受到大型企业客户的欢迎。

计算的瘦客户端模式似乎又回来了——这次是服务器在互联网上,为成千上万的客户端提供服务。

考虑到这一点,这里有一些关于如何配置操作系统和编写代码以支持数千个客户端的说明。讨论集中在类 Unix 操作系统上,因为这是我个人感兴趣的领域,但也涵盖了一些 Windows。

相关网站

请参阅 Nick Black 出色的 Fast UNIX Servers 页面,了解大约 2009 年的情况。
2003 年 10 月,Felix von Leitner 整理了一个关于网络可扩展性的优秀网页演示文稿,其中包含比较各种网络系统调用和操作系统的基准。 他的观察之一是 2.6 Linux 内核确实优于 2.4 内核,但是有很多很多好的图表会让操作系统开发人员思考一段时间。 (另请参阅Slashdot上的评论;看看是否有人对 Felix 的结果进行后续基准测试将会很有趣。)


必读书籍和资料

如果您还没有阅读过,请阅读已故的 W. Richard Stevens 所著的Unix Network Programming : Networking APIs: Sockets and Xti (Volume 1)。 它描述了许多与编写高性能服务器相关的 I/O 策略和陷阱。 它甚至谈到了“thundering herd”问题。 当你在做的时候,去阅读Jeff Darcy 关于高性能服务器设计的笔记

(另一本书可能对那些*使用*而不是*编写* Web 服务器更有帮助的人是 Cal Henderson 的 Building Scalable Web Sites。) 

I/O框架

可以使用预打包的库来抽象下面介绍的一些技术,使您的代码与操作系统隔离并使其更具可移植性。

  • ACE (原链接http://www.cs.wustl.edu/~schmidt/ACE.html打不开了,请参考https://github.com/qzeno/ACE)是一个重量级的 C++ I/O 框架,包含其中一些 I/O 策略和许多其他有用的东西的面向对象的实现。特别是他的Reactor是一种OO方式做非阻塞I/O,Proactor是一种OO方式做异步I/O。
  • ASIO是一个 C++ I/O 框架,它正在成为 Boost 库的一部分。这就像为 STL 时代更新的 ACE。
  • libevent是 Niels Provos 开发的轻量级 C I/O 框架。它支持 kqueue 和 select,很快将支持 poll 和 epoll。我认为它只是水平触发(level-triggered的,它既有好的一面,也有坏的一面。 Niels 有一个很好的时间图表(如下图)来处理一个事件作为连接数的函数。它显示 kqueue 和 sys_epoll 是明显的赢家。

  • 我自己对轻量级框架的尝试(遗憾的是,没有及时更新):

               (1) Poller 是一个轻量级 C++ I/O 框架,它使用您想要的任何底层就绪 API(poll、select、/dev/poll、kqueue 或 sigio)来实现水平触发(level-triggered)的就绪 API。它作为比较各种 API 的性能表现的基准是很有用的。(Microbenchmark comparing poll, kqueue, and /dev/poll - 24 Oct 2000)本文档链接到下面的 Poller 子类,以说明如何使用每个就绪 API。

                (2)rn 是一个轻量级的 C I/O 框架,是我在 Poller 之后的第二次尝试。它是 lgpl(因此更易于在商业应用程序中使用)和 C(因此更易于在非 C++ 应用程序中使用)。它曾被用于一些商业产品。

  • Matt Welsh 在 2000 年 4 月写了一篇关于在构建可扩展服务器时如何平衡使用工作线程和事件驱动技术的论文。这篇论文描述了他的 Sandstorm I/O 框架的一部分。
  • Cory Nelson's Scale! library - Windows 的异步套接字、文件和管道 I/O 库

输入输出策略

网络软件的设计者有很多选择。这里有几个:

  • 是否以及如何从单个线程发出多个 I/O 调用

        (1)别; 始终使用阻塞/同步调用,并尽量使用多个线程或进程来实现并发
        (2)使用非阻塞调用(例如在设置为 O_NONBLOCK 的套接字上进行write()操作)来启动                 I/O,并使用就绪通知(例如 poll() 或 /dev/poll)来了解何时可以在该通道上启动下一个                 I/O .通常只能用于网络 I/O,不能用于磁盘 I/O。
        (3)使用异步调用(例如 aio_write())来启动 I/O,并使用完成通知(completion                 notification)(例如信号或完成端口)来了解 I/O 何时完成。适用于网络和磁盘 I/O。

  • 如何控制服务每个客户端的代码

       (1)每个客户端一个进程(经典的 Unix 方法,自 1980 年左右开始使用)
       (2) 一个操作系统级线程处理多个客户端;每个客户端由以下人员控制:
                ♦用户级线程(例如 GNU 状态线程、带有绿色线程的经典 Java)
                ♦状态机(有点深奥,但在某些圈子里很流行;我最喜欢的)
                ♦延续(a continuation)(有点深奥,但在某些圈子里很流行)
        (3)每个客户端都有一个操作系统级线程(例如,带有原生线程的经典 Java)
        (4)每个活动客户端都有一个操作系统级线程(例如,带有 apache 前端的 Tomcat;NT                 完成端口;线程池)

  • 是使用标准的 O/S 服务,还是将一些代码放入内核(例如,在自定义驱动程序、内核模块或 VxD 中)


以下五种组合比较受欢迎:

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

​接下来详细介绍这五种组合。 

1. 每个线程服务多个客户端,使用非阻塞 I/O 和级别触发的就绪通知

在所有网络句柄上设置非阻塞模式,并使用 select() 或 poll() 来判断哪个网络句柄有数据等待。这是传统的最爱。使用这种方案,内核会告诉您文件描述符是否准备就绪,自上次内核告诉您以来您是否对该文件描述符进行了任何操作。(“水平触发”这个名称来自计算机硬件设计;它与“边缘触发”相反。Jonathon Lemon 在他的论文 BSDCON 2000 paper on kqueue() 中介绍了这些术语。)

注意:特别重要的是要记住来自内核的就绪通知只是一个提示;当您尝试从中读取文件描述符时,它可能不再准备就绪。这就是为什么在使用就绪通知时使用非阻塞模式很重要的原因。

此方法的一个重要瓶颈是如果页面此时不在核心中,则从磁盘读取(read())或发送文件(sendfile())会阻塞;在磁盘文件句柄上设置非阻塞模式无效。内存映射(memory-mapped)的磁盘文件也是如此。当服务器第一次需要磁盘 I/O 时,它的进程阻塞,所有客户端都必须等待,而原始的非线程性能就被浪费了。
这就是异步 I/O 的用途,但在缺乏 AIO 的系统上,执行磁盘 I/O 的工作线程或进程也可以绕过这个瓶颈。一种方法是使用内存映射文件,如果 mincore() 指示需要 I/O,请让工作线程执行 I/O,并继续处理网络流量。 Jef Poskanzer 提到 Pai、Druschel 和 Zwaenepoel 的 1999Flash web 服务器使用了这个技巧。他们在 Usenix '99上发表了演讲。就像mincore() 在 BSD-drived  Unixs (如 FreeBSD 和 Solaris)中可用,但不是单一 Unix 规范的一部分。从内核 2.3.51 开始,它可以作为 Linux 的一部分使用,这要感谢 Chuck Lever

但是在 2003 年 11 月的 freebsd-hackers 列表中,Vivek Pei 等人报告了,使用他们的 Flash web 服务器的系统范围分析,来打破瓶颈,获得了非常好的结果。他们发现的一个瓶颈是 mincore(我猜这毕竟不是一个好主意)另一个是 sendfile 阻塞磁盘访问的事实。他们通过引入修改后的 sendfile() 来提高性能,当它正在获取的磁盘页面尚未进入核心时,它会返回类似 EWOULDBLOCK 的内容。 (不确定您如何告诉用户该页面现在是常驻的……在我看来,这里真正需要的是 aio_sendfile())他们优化的最终结果是在 1GHZ/1GB FreeBSD 机器上的 SpecWeb99 得分约为 800,这比 spec.org 上其他任何人,在文件传输方面的优化,做的都要好。

单个线程有几种方法可以判断一组非阻塞套接字中的哪一个已准备好进行 I/O:

  • 传统的 select()

不幸的是,select() 仅限于 FD_SETSIZE 句柄。这个限制被编译到标准库和用户程序中。 (某些版本的 C 库允许您在用户应用程序编译时提高此限制。)
有关如何将 select() 与其他就绪通知方案互换使用的示例,请参阅Poller_select (cc, h)

class Poller_select

http://www.kegel.com/dkftpbench/dkftpbench-0.44/Poller_select.cc

http://www.kegel.com/dkftpbench/dkftpbench-0.44/Poller_select.h

  • 传统的 poll()

poll() 可以处理的文件描述符的数量没有硬编码限制,但它确实会变慢,因为有时大多数文件描述符都是空闲的,扫描数千个文件描述符需要花些时间。
一些操作系统(例如 Solaris 8)通过使用poll hinting等技术来加速 poll() 等,该技术由 Niels Provos于 1999 年为 Linux 实现并进行了基准测试。

有关如何将 poll() 与其他就绪通知方案互换使用的示例,请参阅 Poller_poll (cc, h, benchmarks):

class Poller_poll

http://www.kegel.com/dkftpbench/dkftpbench-0.44/Poller_poll.cc

http://www.kegel.com/dkftpbench/dkftpbench-0.44/Poller_poll.h

Microbenchmark comparing poll, kqueue, and /dev/poll - 24 Oct 2000

  • /dev/poll

在Solaris系统中, 推荐使用/dev/poll 作为poll的替换。
/dev/poll 背后的想法是利用 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 benchmarks) 以了解如何将 /dev/poll 与许多其他就绪通知方案互换使用的示例。 (注意 - 该示例适用于 Linux /dev/poll,可能无法在 Solaris 上正常工作。)

  • kqueue()

这是 FreeBSD(以及不久将发行的 NetBSD)中poll()的推荐替代品。
见下文。 kqueue() 可以指定边沿触发或水平触发。

2. 每个线程服务多个客户端,并使用非阻塞 I/O 和就绪更改通知

就绪变化通知(或边缘触发的就绪通知/or edge-triggered readiness notification)意味着您给内核一个文件描述符,然后,当该描述符从未就绪(not ready)转换为就绪(ready)时,内核会以某种方式通知您。然后它假设您知道文件描述符已准备好,并且不会再为该文件描述符发送任何该类型的就绪通知,直到您执行导致文件描述符不再就绪的操作(例如,直到您在执行send、recv 或 accept 调用时收到 EWOULDBLOCK 的报错,或者 send 或 recv 传输少于请求的字节数)。

当您使用就绪改变通知(readiness change notification)时,您必须为虚假事件(spurious events)做好准备,因为一种常见的实现是在收到任何数据包时发出就绪信号,而不管文件描述符是否已经就绪。

这与“水平触发”就绪通知相反。对编程错误的宽容度要低一些,因为哪怕您只错过了一个事件,那么该事件所针对的连接就会永远卡住。尽管如此,我发现对于那些使用 OpenSSL 的非阻塞客户端,使用边缘触发的就绪通知的编程方式会更加容易,因此值得一试。

[Banga, Mogul, Drusha '99] 在 1999 年描述了这种方案。

有几个 API 可以让应用程序检索“文件描述符就绪(file descriptor became ready)”通知:

#未完

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值