C10K问题,我做系统架构的一些原则
现在是网络服务器同时处理一万个客户端的时候了,你不觉得吗?毕竟,网络现在是一个很大的地方。
计算机也很大。您可以花 1200 美元左右购买一台具有 2 GB RAM 和 1000Mbit/sec 以太网卡的 1000MHz 机器。让我们看看 - 在 20000 个客户端时,每个客户端分别为 50KHz、100Kbytes 和 50Kbits/sec。从磁盘中取出 4 KB 并每秒为 2 万个客户端中的每一个将它们发送到网络不应该需要更多的马力。(顺便说一下,这相当于每个客户端 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 内核,但是有很多很多很好的图表可以让 OS 开发人员在一段时间内深思熟虑。(另请参阅Slashdot 评论;看看是否有人对 Felix 的结果进行后续基准测试会很有趣。)
先读书
如果您还没有读过,请出去获取 已故 W. Richard Stevens 所著的Unix 网络编程:网络 API:套接字和 Xti(第 1 卷)的副本 。它描述了许多与编写高性能服务器相关的 I/O 策略和陷阱。它甚至谈到了“雷鸣般的羊群”问题。当您在做这件事时,请阅读Jeff Darcy 关于高性能服务器设计的笔记。
(另一本书可能对那些*使用*而不是*编写*网络服务器的人更有帮助,是 Cal Henderson 的《Building Scalable Web Sites》。)
输入/输出框架
可以使用预先打包的库来抽象下面介绍的一些技术,将您的代码与操作系统隔离并使其更具可移植性。
- 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。我认为它只是电平触发,它既有好的一面也有坏的一面。Niels 有 一个很好的时间图来处理一个事件 作为连接数的函数。它显示 kqueue 和 sys_epoll 是明显的赢家。
- 我自己对轻量级框架的尝试(遗憾的是,没有保持最新):
- Poller是一个轻量级的 C++ I/O 框架,它使用您想要的任何底层就绪 API(轮询、选择、/dev/poll、kqueue 或 sigio)实现级别触发的就绪 API。它对于比较各种 API 的性能的基准测试很有用。 本文档链接到下面的 Poller 子类,以说明如何使用每个就绪 API。
- rn是一个轻量级 CI/O 框架,是我继 Poller 之后的第二次尝试。它是 lgpl(因此在商业应用程序中更容易使用)和 C(因此在非 C++ 应用程序中更容易使用)。它被用于一些商业产品中。
- Matt Welsh 在 2000 年 4 月写了一篇关于在构建可扩展服务器时如何平衡工作线程和事件驱动技术的使用的论文。该论文描述了他的 Sandstorm I/O 框架的一部分。
- 科里·纳尔逊的体重秤!library - 适用于 Windows 的异步套接字、文件和管道 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 完成端口;线程池)
- 是否使用标准的 O/S 服务,或将一些代码放入内核(例如在自定义驱动程序、内核模块或 VxD 中)
以下五种组合似乎很受欢迎:
- 每个线程为多个客户端提供服务,并使用非阻塞 I/O 和级别触发的就绪通知
- 每个线程为多个客户端提供服务,并使用非阻塞 I/O 和就绪更改通知
- 每个服务器线程为多个客户端提供服务,并使用异步 I/O
- 每个服务器线程为一个客户端提供服务,并使用阻塞 I/O
- 将服务器代码构建到内核中
1、每个线程服务多个客户端,使用非阻塞I/O和级别触发的就绪通知
...在所有网络句柄上设置非阻塞模式,并使用 select() 或 poll() 来判断哪个网络句柄有数据等待。这是传统的最爱。使用此方案,内核会告诉您文件描述符是否准备就绪,自上次内核告诉您以来您是否对该文件描述符进行了任何操作。(“级别触发”这个名称来自计算机硬件设计;它与“边缘触发”相反。Jonathon Lemon 在他关于 kqueue() 的 BSDCON 2000 论文中介绍了这些术语 。)
注意:特别重要的是要记住,来自内核的就绪通知只是一个提示;当您尝试从中读取时,文件描述符可能不再准备就绪。这就是为什么在使用就绪通知时使用非阻塞模式很重要的原因。
这种方法的一个重要瓶颈是如果当前页面不在核心中,则来自磁盘的 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 Specification 的一部分。多亏了 Chuck Lever,它从内核 2.3.51 开始作为 Linux 的一部分可用 。
但是 在 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 )。
- 传统的 poll()
对 poll() 可以处理的文件描述符数量没有硬编码限制,但它确实会变慢大约几千,因为大多数文件描述符在任何时候都是空闲的,并且会扫描数千个文件描述符需要时间。一些操作系统(例如 Solaris 8)通过使用轮询提示等技术来加速 poll() 等,该技术 由 Niels Provos在 1999 年为 Linux实现并进行了基准测试。
有关如何将 poll() 与其他就绪通知方案互换使用的示例,请参阅 Poller_poll ( cc , h , benchmarks )。
- /dev/poll
这是推荐的 Solaris 轮询替代。/dev/poll 背后的想法是利用这样一个事实,即经常使用相同的参数多次调用 poll()。使用/dev/poll,您可以获得/dev/poll 的打开句柄,并通过写入该句柄告诉操作系统您对哪些文件感兴趣;从那时起,您只需从该句柄读取当前准备好的文件描述符集。
它在 Solaris 7 中悄然出现(参见 patchid 106541),但它的第一次公开出现是在 Solaris 8 中; 根据 Sun 的说法,在 750 个客户端时,这占 poll() 开销的 10%。
在 Linux 上尝试了 /dev/poll 的各种实现,但它们的性能都没有 epoll 好,并且从未真正完成。不建议在 Linux 上使用 /dev/poll。
有关 如何将 /dev/poll 与许多其他就绪通知方案互换使用的示例,请参阅 Poller_devpoll ( cc , h benchmarks )。(注意 - 该示例适用于 Linux /dev/poll,可能不适用于 Solaris。)
- kqueue()
这是推荐的 FreeBSD(以及很快的 NetBSD)轮询替代品。见下文。 kqueue() 可以指定边沿触发或电平触发。
2.每个线程服务多个客户端,使用非阻塞I/O和就绪变化通知
就绪更改通知(或边缘触发的就绪通知)意味着您给内核一个文件描述符,稍后,当该描述符从not ready转换 为ready 时,内核会以某种方式通知您。然后它假设您知道文件描述符已准备好,并且不会再发送该文件描述符的任何该类型的准备通知,直到您执行某些导致文件描述符不再准备好的操作(例如,直到您收到 EWOULDBLOCK 错误) send、recv 或 accept 调用,或者 send 或 recv 传输的字节数少于请求的字节数)。
当您使用就绪更改通知时,您必须为虚假事件做好准备,因为一种常见的实现是在接收到任何数据包时发出就绪信号,而不管文件描述符是否已经就绪。
这与“级别触发”就绪通知相反。对编程错误的容忍度要低一些,因为如果您只错过一个事件,那么该事件所针对的连接就会永远卡住。尽管如此,我发现边缘触发的就绪通知使使用 OpenSSL 对非阻塞客户端进行编程变得更加容易,因此值得一试。
[Banga, Mogul, Drusha '99] 在 1999 年描述了这种方案。
有几个 API 可以让应用程序检索“文件描述符准备就绪”通知:
- kqueue() 这是推荐用于 FreeBSD(以及很快的 NetBSD)的边缘触发轮询替代。
FreeBSD 4.3 及更高版本,以及截至 2002 年 10 月的 NetBSD-current,支持称为kqueue()/kevent() 的poll() 的通用替代方案 ;它支持边沿触发和电平触发。(另请参阅Jonathan Lemon 的页面 和他关于 kqueue() 的 BSDCon 2000 论文。)
与 /dev/poll 一样,您分配一个侦听对象,但不是打开文件 /dev/poll,而是调用 kqueue() 来分配一个。要更改您正在侦听的事件,或获取当前事件的列表,您可以在 kqueue() 返回的描述符上调用 kevent()。它不仅可以侦听套接字准备情况,还可以侦听普通文件准备情况、信号,甚至 I/O 完成情况。
注意:截至 2000 年 10 月,FreeBSD 上的线程库不能与 kqueue() 很好地交互;显然,当 kqueue() 阻塞时,整个进程都会阻塞,而不仅仅是调用线程。
有关如何将 kqueue() 与许多其他就绪通知方案互换使用的示例,请参阅 Poller_kqueue ( cc , h , benchmarks )。
使用 kqueue() 的示例和库:
- PyKQueue -- kqueue() 的 Python 绑定
- Ronald F. Guilmette 的回声服务器示例;另见 他于 2000 年 9 月 28 日在 freebsd.questions 上发表的帖子。
- epoll
这是 2.6 Linux 内核推荐的边缘触发轮询替代品。2001 年 7 月 11 日,Davide Libenzi 提出了实时信号的替代方案;他的补丁提供了他现在称之为 /dev/epoll www.xmailserver.org/linux-patches/nio-improve.html 的内容。这就像实时信号就绪通知,但它合并了冗余事件,并且具有更有效的批量事件检索方案。
在其接口从 /dev 中的特殊文件更改为系统调用 sys_epoll 后,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。 看:
- Drepper 的新网络接口(Linux 2.6+ 的提案)
在 OLS 2006 上,Ulrich Drepper 提出了一个新的高速异步网络 API。看: - 实时信号
这是 2.4 Linux 内核的推荐边缘触发轮询替代。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, 符号); 标志 = fcntl(fd, F_GETFL); 标志 |= O_NONBLOCK|O_ASYNC; fcntl(fd, F_SETFL, 标志);
当像 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() 循环。
有关如何将 rtsignals 与许多其他就绪通知方案互换使用的示例,请参阅 Poller_sigio ( cc , h )。
有关直接使用此功能的示例代码,请参阅Zach Brown 的 phhttpd。(或者不要;phhttpd 有点难以弄清楚......)
[ Provos、Lever 和 Tweedie 2000 ] 描述了使用 sigtimedwait() 的变体 sigtimedwait4() 的 phhttpd 的最新基准,它允许您通过一次调用检索多个信号。有趣的是,sigtimedwait4() 对他们的主要好处似乎是它允许应用程序衡量系统过载(因此它可以适当地运行)。(请注意, poll() 提供了相同的系统过载度量。)
- Signal-per-fd
Chandra 和 Mosberger 提出了一种对实时信号方法的修改,称为“signal-per-fd”,它通过合并冗余事件来减少或消除实时信号队列溢出。不过,它的表现并不优于 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 ) 以获取有关如何将 signal-per-fd 与许多其他就绪通知方案互换使用的示例。
3.每个服务线程为多个客户端服务,使用异步I/O
这在 Unix 中尚未流行,可能是因为很少有操作系统支持异步 I/O,也可能是因为它(如非阻塞 I/O)需要重新考虑您的应用程序。在标准 Unix 下,异步 I/O 由aio_ 接口提供 (从该链接向下滚动到“异步输入和输出”),它将信号和值与每个 I/O 操作相关联。信号及其值被排队并有效地传递给用户进程。这是来自 POSIX 1003.1b 实时扩展,也在 Single Unix Specification, version 2 中。
AIO 通常与边沿触发的完成通知一起使用,即当操作完成时将一个信号排队。(它也可以通过调用aio_suspend()与级别触发的完成通知一起使用 ,尽管我怀疑很少有人这样做。)
glibc 2.1 及更高版本提供了为符合标准而非性能而编写的通用实现。
自 2.5.32 起,Ben LaHaise 的 Linux AIO 实现已合并到主要的 Linux 内核中。它不使用内核线程,并且具有非常高效的底层 api,但是(从 2.6.0-test2 开始)尚不支持套接字。(还有一个适用于 2.4 内核的 AIO 补丁,但 2.5/2.6 实现有些不同。)更多信息:
- “ Linux 的内核异步 I/O (AIO) 支持”页面试图将有关 2.6 内核的 AIO 实现的所有信息联系在一起(2003 年 9 月 16 日发布)
- 第 3 轮: Benjamin CR LaHaise 的aio vs /dev/epoll(在 2002 OLS 上发表)
- Linux 2.5 中的异步 I/O 支持,作者:Bhattacharya、Pratt、Pulaverty 和 Morgan,IBM;在 OLS '2003 上展示
- Suparna Bhattacharya 的Linux 异步 I/O (aio) 设计说明——将 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支持socket的人来说还是很有用的。
据说 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 常量。有关详细信息,请参阅 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 用户可访问 VM(例如,例如,通常在 x86 上提供的 Linux)。您可以通过给每个线程一个较小的堆栈来解决这个问题,但由于大多数线程库一旦创建就不允许增加线程堆栈,这样做意味着设计您的程序以最大限度地减少堆栈使用。您也可以通过迁移到 64 位处理器来解决此问题。
Linux、FreeBSD 和 Solaris 中的线程支持正在改进,即使对于主流用户来说,64 位处理器也指日可待。也许在不久的将来,那些更喜欢每个客户端使用一个线程的人将能够对 10000 个客户端使用该范例。尽管如此,目前,如果您确实想要支持那么多客户端,那么使用其他范例可能会更好。
有关毫不掩饰的亲线程观点,请参阅 von Behren、Condit 和 Brewer,UCB 在 HotOS IX 上发表的“为什么事件是一个坏主意(对于高并发服务器)”。反线程阵营的任何人都愿意指出一篇反驳这篇论文的论文吗?:-)
Linux线程
LinuxTheads是标准 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的仁慈的 dict^H^H^H^Hmaintainer )和 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 和其他人贡献了 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 人员同步,以确保 NGPT 不会以任何不可接受的方式中断。]
为 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 假设、max_pid 和 pid 分配可扩展性工作,procfs 崩溃。此外,还进行了许多仅针对性能的改进。
本质上,新功能是一种不折不扣的 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 提议 1:1 线程实现成为 FreeBsd 7.x 中的默认实现:
我知道过去已经讨论过这个问题,但我认为随着 7.x 向前推进,是时候再考虑一下了。在许多常见应用程序和场景的基准测试中,libthr 表现出明显优于 libpthread 的性能……libthr 也在我们的更多平台上实现,并且已经在几个平台上成为 libpthread。我们向 MySQL 和其他重度线程用户提出的第一个建议是“切换到 libthr”,这也很有启发性!... 所以稻草人的建议是:使 libthr 成为 7.x 上的默认线程库。
NetBSD 线程支持
根据 Noriyuki Soda 的笔记:
内核支持的基于调度器激活模型的 M:N 线程库于 2003 年 1 月 18 日合并到 NetBSD-current 中。
有关详细信息,请参阅 内森 J. 威廉姆斯 (Wasabi Systems, Inc.) 在 FREENIX '02上发表的 NetBSD 操作系统上调度程序激活的实现。
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”(线程 linUX 网络服务器) 是一个非常快速和灵活的内核空间Ingo Molnar 用于 Linux 的 HTTP 服务器。Ingo 在2000 年 9 月 1 日的公告中 说,可以从ftp://ftp.redhat.com/pub/redhat/tux下载 TUX 的 alpha 版本 ,并解释了如何加入邮件列表以获取更多信息。
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 和MMichal Ostrowski 测量 了简单的基于选择的服务器的各种策略。他们的数据值得一看。
2003 年,Tim Brecht 发布 了 userver 的源代码,这是一个由 Abhishek Chandra、David Mosberger、David Pariag 和 Michal Ostrowski 编写的多台服务器组合而成的小型 Web 服务器。它可以使用 select()、poll()、epoll() 或 sigio。
早在 1999 年 3 月, Dean Gaudet 就发布了:
我一直被问到“为什么你们不使用像 Zeus 这样的基于选择/事件的模型?这显然是最快的。” ...
他的理由归结为“这真的很难,回报也不清楚”。然而,在几个月之内,人们显然愿意为此工作。
Mark Russinovich 撰写 了一篇社论和 一篇文章, 讨论了 2.2 Linux 内核中的 I/O 策略问题。值得一读,即使他在某些方面似乎也被误导了。特别是,他似乎认为 Linux 2.2 的异步 I/O(参见上面的 F_SETSIG)不会在数据准备好时通知用户进程,只有在新连接到达时才通知用户进程。这似乎是一个奇怪的误会。另请参见 上较早草案的意见, 30英格·蒙内的反驳1999年4月, 2 Russinovich的意见1999年5月, 一个反驳的阿伦·考克斯,以及各种 岗位到Linux内核. 我怀疑他是想说 Linux 不支持异步磁盘 I/O,这曾经是真的,但是现在 SGI 已经实现了KAIO,它不再那么真实了。
有关“完成端口”的信息,请参阅sysinternals.com和 MSDN上的这些页面,他说这是 NT 独有的;简而言之,win32的“重叠I/O”级别太低,不方便,“完成端口”是一个包装器,提供完成事件队列,加上试图保持运行次数的调度魔法如果从该端口获取完成事件的其他线程正在休眠(可能正在执行阻塞 I/O),则允许更多线程获取完成事件,从而使线程保持不变。
另请参阅OS/400 对 I/O 完成端口的支持。
1999 年 9 月有一个关于 linux-kernel 的有趣讨论,标题为“ > 15,000 Simultaneous Connections ”(以及该线程的第二周)。强调:
- 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 的内容:“在编写 'pollfd like' 结构的'device like' API 中,'device' 侦听事件并提供 'pollfd like' 结构体如何?你读的时候代表他们?……”
- Rogier Wolff 建议 使用“数字人建议的 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指出,2.3.18ac 中包含 SCT 的 SIGIO 补丁的旧版本。
- Jordan Mendelson发布了一些示例代码,展示了如何使用 F_SETSIG。
- Stephen C. Tweedie继续比较完成端口和 F_SETSIG,并指出:“通过信号出列机制,如果库使用相同的机制,您的应用程序将获得发往各种库组件的信号,”但库可以设置它自己的信号处理程序,所以这不应该影响程序(很多)。
- Doug Royer 指出,当他在 Sun 日历服务器上工作时,他在 Solaris 2.6 上获得了 100,000 个连接。其他人则对 Linux 上需要多少 RAM 以及会遇到哪些瓶颈进行了估计。
有趣的阅读!
打开文件句柄的限制
- 任何 Unix:由 ulimit 或 setrlimit 设置的限制。
- Solaris:请参阅Solaris 常见问题解答,问题 3.46(或相关问题;他们定期对问题重新编号)。
- FreeBSD:
编辑 /boot/loader.conf,添加一行设置 kern.maxfiles=XXXX其中 XXXX 是所需的文件描述符系统限制,然后重新启动。感谢一位匿名读者,他写信说他在 FreeBSD 4.3 上实现了超过 10000 个连接,并说“FWIW:您实际上无法通过 sysctl 微调 FreeBSD 中的最大连接数......您必须在 /boot/loader.conf 文件中进行。
另一位读者说
原因是 zalloci() 调用用于初始化套接字和 tcpcb 结构的区域在系统启动的早期发生,以便区域既类型稳定又可交换。
您还需要将 mbuf 的数量设置得更高,因为您将(在未修改的情况下)内核)为 tcptempl 结构的每个连接咀嚼一个 mbuf,用于实现保活。”“从 FreeBSD 4.4 开始,不再分配 tcptempl 结构;您不再需要担心每个连接会占用一个 mbuf。”
也可以看看:- FreeBSD 手册
- SYSCTL TUNING , LOADER TUNABLES , and KERNEL CONFIG TUNING in ‘ mantuning ’
- The Effects of Tuning a FreeBSD 4.3 Box for High Performance,守护进程新闻,2001 年 8 月
- postfix.org 调优笔记,涵盖 FreeBSD 4.2 和 4.4
- 测量工厂的注释,大约在 FreeBSD 4.3
- OpenBSD:一位读者说
“在 OpenBSD 中,需要额外的调整来增加每个进程可用的打开文件句柄的数量: 需要增加/etc/login.conf 中的 openfiles-cur 参数 。您可以使用 sysctl -w 或在sysctl.conf 但它没有效果。这很重要,因为在发货时,login.conf 限制非常低,非特权进程为 64,特权进程为 128。”
- Linux:请参阅Bodo Bauer 的 /proc 文档。在 2.4 内核上:
回声 32768 > /proc/sys/fs/file-max增加系统对打开文件的限制,以及ulimit -n 32768增加当前进程的限制。在 2.2.x 内核上,
回声 32768 > /proc/sys/fs/file-max 回声 65536 > /proc/sys/fs/inode-max
增加系统对打开文件的限制,以及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。
另请参阅 Oskar 1998 年的帖子,其中讨论了 2.0.36 内核中文件描述符的每个进程和系统范围的限制。
线程限制
在任何架构上,您可能需要减少为每个线程分配的堆栈空间量,以避免耗尽虚拟内存。如果您使用 pthreads,您可以在运行时使用 pthread_attr_init() 设置它。
- Solaris:我听说它支持内存中可以容纳的尽可能多的线程。
- 带有 NPTL 的 Linux 2.6 内核:/proc/sys/vm/max_map_count 可能需要增加以超过 32000 个左右的线程。(不过,除非您使用的是 64 位处理器,否则您需要使用非常小的堆栈线程来获得接近该线程数的任何位置。)请参阅 NPTL 邮件列表,例如主题为“无法创建超过 32K的线程”线程? ”,了解更多信息。
- 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。
- 接口 Java 到虚拟接口架构,由 CC。Chang 和 T. von Eicken 提议更改内存管理以允许使用 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 原语的提议,它摆脱了对许多副本的需求。
- 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 内核。见LWN 2001 年 1 月 25 日。
一位在 Freebsd 中使用 sendfile() 的开发人员报告说,使用 POLLWRBAND 而不是 POLLOUT 会产生很大的不同。
Solaris 8(截至 2001 年 7 月更新)有一个新的系统调用“sendfilev”。 手册页的副本在这里。. Solaris 8 7/01 发行说明 也提到了它。我怀疑这在以阻塞模式发送到套接字时最有用;与非阻塞套接字一起使用会有点痛苦。
- 使用 writev(或 TCP_CORK)避免小帧
Linux 下的新套接字选项 TCP_CORK 告诉内核避免发送部分帧,这会有所帮助,例如当有很多小 write() 调用您无法捆绑在一起时一些理由。取消设置该选项会刷新缓冲区。不过最好使用 writev()...请参阅LWN 2001 年 1 月 25 日,了解有关 linux-kernel 关于 TCP_CORK 和可能的替代 MSG_MORE 的一些非常有趣的讨论的摘要。
- 在超载时要理智行事。
[ Provos、Lever 和 Tweedie 2000 ] 指出,在服务器过载时丢弃传入连接可以改善性能曲线的形状,并降低整体错误率。他们使用平滑版本的“I/O 就绪的客户端数量”作为过载的衡量标准。这种技术应该很容易适用于使用 select、poll 或任何系统调用编写的服务器,这些系统调用会返回每次调用的就绪事件计数(例如 /dev/poll 或 sigtimedwait4())。 - 某些程序可以从使用非 Posix 线程中受益。
并非所有线程都是平等的。例如,Linux 中的 clone() 函数(以及它在其他操作系统中的朋友)允许您创建一个具有自己当前工作目录的线程,这在实现 ftp 服务器时非常有用。有关使用本机线程而不是 pthread 的示例,请参阅 Hoser FTPd。 - 缓存您自己的数据有时可能是一种胜利。
Vivek Sadananda Pai (vivek@cs.rice.edu) 于5 月 9 日在new-httpd上的“Re:修复混合服务器问题” 指出:“我在 FreeBSD 和 Solaris/x86 上比较了基于 select 的服务器与多进程服务器的原始性能。在微基准测试中,软件架构的性能差异很小。select 的巨大性能优势基于服务器的应用程序级缓存。虽然多进程服务器可以以更高的成本完成,但在实际工作负载(与微基准测试相比)上更难获得相同的好处。我将把这些测量作为那会出现在下次的Usenix会议论文。如果你有跋,该文件可在 http://www.cs.rice.edu/~vivek/flash99/ “
其他限制
- 旧的系统库可能使用 16 位变量来保存文件句柄,这会导致超过 32767 个句柄的问题。glibc2.1 应该没问题。
- 许多系统使用 16 位变量来保存进程或线程 ID。将Volano 可扩展性基准移植到 C会很有趣,看看各种操作系统的线程数上限是多少。
- 某些操作系统预先分配了过多的线程本地内存;如果每个线程获得 1MB,并且总 VM 空间为 2GB,则创建的上限为 2000 个线程。
- 查看http://www.acme.com/software/thttpd/benchmarks.html底部的性能比较图 。请注意,即使在 Solaris 2.6 上,各种服务器在超过 128 个连接时也有问题吗?谁知道原因,请告诉我。
注意:如果 TCP 堆栈存在导致 SYN 或 FIN 时间短(200 毫秒)延迟的错误,如 Linux 2.2.0-2.2.6 所具有的,并且操作系统或 http 守护程序对打开的连接数有硬性限制,您会期望这种行为。可能还有其他原因。
内核问题
对于 Linux,看起来内核瓶颈正在不断修复。请参阅Linux Weekly News、 Kernel Traffic、 Linux-Kernel 邮件列表和我的 Mindcraft Redux 页面。
1999 年 3 月,Microsoft 发起了一项基准测试,将 NT 与 Linux 进行比较,为大量 http 和 smb 客户端提供服务,但未能从 Linux 中看到良好的结果。有关 更多信息,另请参阅我关于 Mindcraft 1999 年 4 月基准的文章。
另请参阅Linux 可扩展性项目。他们正在做一些有趣的工作,包括 Niels Provos 的提示投票补丁,以及一些关于雷鸣般的羊群问题的工作。
另请参阅Mike Jagdis 在改进 select() 和 poll() 方面的工作;这是迈克关于它的帖子。
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 et al, 2000]。值得一读。
例子
Nginx是一个 Web 服务器,它使用目标操作系统上可用的任何高效网络事件机制。它越来越流行;甚至还有 关于它的书籍(并且由于此页面最初是编写的,因此还有更多,包括该书的第四版。)
有趣的基于 select() 的服务器
- thttpd 很简单。使用单个进程。它具有良好的性能,但不会随着 CPU 的数量而扩展。也可以使用kqueue。
- mathopd。类似于 thttpd。
- fhttpd
- 蟒蛇
- 罗克森
- Zeus,一个试图成为绝对最快的商业服务器。请参阅他们的调整指南。
- http://www.acme.com/software/thttpd/benchmarks.html 中列出的其他非 Java 服务器
- 测试版FTP
- Flash-Lite - 使用 IO-Lite 的 Web 服务器。
- Flash:一种高效且可移植的 Web 服务器——使用 select()、mmap()、mincore()
- 2003年的 Flash Web 服务器——使用 select()、修改后的 sendfile()、async open()
- xitami - 使用 select() 实现其自己的线程抽象,以便可移植到没有线程的系统。
- Medusa - Python 中的服务器编写工具包,试图提供非常高的性能。
- userver - 可以使用 select、poll、epoll 或 sigio 的小型 http 服务器
有趣的基于 /dev/pol 的服务器
- N. Provos, C. Lever, “Linux 中的可扩展网络 I/O” ,2000 年 5 月。USENIX 2000,加利福尼亚州圣地亚哥(2000 年 6 月)。] 描述了 thttpd 的一个版本,该版本经过修改以支持 /dev/poll。性能与 phhttpd 进行比较。
有趣的基于 epol 的服务器
- 肋骨2
- cmogstored - 大多数网络使用 epoll/kqueue,磁盘和 accept4 线程
有趣的基于 kqueue() 的服务器
- thttpd(从 2.21 版开始?)
- Adrian Chadd 说“我正在做很多工作来让鱿鱼实际上像一个 kqueue IO 系统”;这是一个官方的 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 日的帖子。
有趣的基于线程的服务器
- 霍瑟 FTPD。请参阅他们的基准页面。
- Peter Eriksson 的 phttpd和
- pftpd
- http://www.acme.com/software/thttpd/benchmarks.html列出的基于 Java 的服务器
- Sun 的Java Web Server ( 据报道可以同时处理 500 个客户端)
有趣的内核服务器
- khttpd
- Ingo Molnar 等人的“TUX”(线程 linUX 网络服务器)。对于 2.4 内核。
其他有趣的链接
- Jeff Darcy 关于高性能服务器设计的笔记
- 爱立信的 ARIES 项目——在 1 到 12 个处理器上 Apache 1、Apache 2 和 Tomcat 的基准测试结果
- Peter Ladkin 教授的 Web 服务器性能页面。
- Novell 的 FastCache —— 声称每秒 10000 次点击。相当漂亮的性能图。
- Rik van Riel 的Linux 性能调优站点
我做系统架构的一些原则
工作 20 多年了,这 20 来年看到了很多公司系统架构,也看到了很多问题,在跟这些公司进行交流和讨论的时候,包括进行实施和方案比较的时候,都有很多各种方案的比较和妥协,因为相关的经历越来越多,所以,逐渐形成了自己的逻辑和方法论。今天,想写下这篇文章,把我的这些个人的经验和想法总结下来,希望能够让更多的人可以参考和借鉴,并能够做出更好的架构来。另外,我的这些思维方式和原则都针对于现有市面上众多不合理的架构和方案,所以,也算是一种“纠正”……(注意,这篇文章所说的这些架构上的原则,一般适用于相对比较复杂的业务,如果只是一些简单和访问量不大的应用,那么你可能会得出相反的结论)
原则一:关注于真正的收益而不是技术本身
对于软件架构来说,我觉得第一重要的是架构的收益,如果不说收益,只是为了技术而技术,而没有任何意义。对于技术收益来说,我觉得下面这几个收益是非常重要的:
- 是否可以降低技术门槛加快整个团队的开发流程。能够加快整个团队的工程流程,快速发布,是软件工程一直在解决的问题,所以,系统架构需要能够进行并行开发,并行上线和并行运维,而不会让某个团队成为瓶颈点。(注:就算拖累团队的原因是组织构架,也不妨碍我们做出并行的系统架构设计)
- 是否可以让整个系统可以运行的更稳定。要让整个系统可以运行的更为的稳定,提升整个系统的 SLA,就需要对有计划和无计划的停机做相应的解决方案(参看《关于高可用的架构》)
- 是否可以通过简化和自动化降低成本。最高优化的成本是人力成本,人的成本除了慢和贵,还有经常不断的 human error。如果不能降低人力成本,反而需要更多的人,那么这个架构设计一定是失败的。除此之外,是时间成本,资金成本。
如果一个系统架构不能在上面三个事上起到作用,那就没有意义了。
原则二:以应用服务和 API 为视角,而不是以资源和技术为视角
国内很多公司都会有很多分工,基本上都会分成运维和开发,运维又会分成基础运维和应用运维,开发则会分成基础核心开发和业务开发。不同的分工会导致完全不同的视角和出发点。比如,基础运维和开发的同学更多的只是关注资源的利用率和性能,而应用运维和业务开发则更多关注的是应用和服务上的东西。这两者本来相关无事,但是因为分布式架构的演进,导致有一些系统已经说不清楚是基础层的还是应用层的了,比如像服务治理上的东西,里面即有底层基础技术,也需要业务的同学来配合,包括 k8s 也样,里面即有底层的如网络这样的技术,也有需要业务配合的 readniess和 liveness 这样的健康检查,以及业务应用需要 configMap 等等 ……
这些东西都让我感觉到所谓 DevOps,其实就是因为很多技术和组件已经分不清是 Dev 还是 Ops 的了,所以,需要合并 Dev和 Ops。而且,整个组织和架构的优化,已经不能通过调优单一分工或是单一组件能够有很大提升的了。其需要有一种自顶向下的,整体规划,统一设计的方式,才能做到整体的提升(可以试想一下城市交通的优化,当城市规模到一定程度的时候,整体的性能你是无法通过优化几条路或是几条街区来完成的,你需要对整个城市做整体的功能体的规划才可能达到整体效率的提升)。而为了做到整体的提升,需要所有的人都要有一个统一的视角和目标,这几年来,我觉得这个目标就是——要站在服务和 对外API的视角来看问题,而不是技术和底层的角度。
原则三:选择最主流和成熟的技术
技术选型是一件很重要的事,技术一旦选错,那会导致整个架构需要做调整,而对架构的调整重来都不是一件简单的事,我在过去几年内,当系统越来越复杂的时候,用户把他们的 PHP,Python, .NET,或 Node.js 的架构完全都迁移到 Java + Go 的架构上来的案例不断的发生。这个过程还是非常痛苦的,但是你没有办法,当你的系统越来越复杂,越来越大时,你就再也不能在一些玩具技术上玩了,你需要的更为工业化的技术。
- 尽可能的使用更为成熟更为工业化的技术栈,而不是自己熟悉的技术栈。所谓工业化的技术栈,你可以看看大多数公司使用的技术栈,比如:互联网,金融,电信……等等 ,大公司会有更多的技术投入,也需要更大规模的生产,所以,他们使用的技术通常来说都是比较工业化的。在技术选型上,千万不要被——“你看某个公司也在用这个技术”,或是一些在论坛上看到的一些程序员吐槽技术的观点(没有任何的数据,只有自己的喜好)来决定自己的技术,还是看看主流大多数公司实际在用的技术栈,会更靠谱一些。
- 选择全球流行的技术,而不是中国流行的技术。技术这个东西一定是一个全球化的东西,不是一个局域化的事。所以,一定要选国际化的会更好。另外,千万不要被某些公司的“特别案例”骗过去了,那怕这个案例很性感,关键还是要看解决问题的思路和采用的技术是否具有普世性。只有普世性的技术有更强的生命力。
- 尽可能的使用红利大的主流技术,而不要自己发明轮子,更不要魔改。我见过好些个公司魔改开源软件,比如有个公司同魔改mesos,最后改着改着发现自己发明另一个 kubernetes。我还见过很多公司或技术团队喜欢自己发明自己的专用轮子,最后都会被主流开源软件所取代。完全没有必要。不重新发明轮子,不魔改,不是因为自己技术不能,而是因为,这个世界早已不是自己干所有事的年代了,这个时代是要想尽方法跟整个产业,整个技术社区融合和合作,这样才会有最大的收益。那些试图因为某个特例需要自成一套的玩法,短期没问题,但长期来说,我都不看好。
- 绝大多数情况下,如无非常特殊要求,选 Java基本是不会错的。一方面,这是因为 Java 的业务开发的生产力是非常好的,而且有 Spring 框架保障,代码很难写烂,另外,Java 的社区太成熟了,你需要的各种架构和技术都可以很容易获得,技术红利实在是太大。这种运行在JVM上的语言有太多太多的好处了。在 Java 的技术栈上,你的架构风险和架构的成本(无论是人力成本,时间成本和资金成本)从长期来说都是最优的
在我见过的公司中,好些公司的架构都被技术负责人个人的喜好、擅长和个人经验给绑架了,完全不是从一个客观的角度来进行技术选型。其实,从 0 到 1 的阶段,你用什么样的技术都行,如果你做一个简单的应用,没有事务处理没有复杂的交易流程,比如一些论坛、社交之类的应用,你用任何语言都行。但是如果有一天你的系统变复杂了,需要处理交易了,量也上来了,从 1 到 10,甚至从 10 到 100,你的开发团队也变大了,需要构建的系统越来越大,你可能会发现你只有一个选择,就是 Java。想想京东从.NET 到 Java,淘宝从 PHP 到 Java……
注,一些有主观喜好的人一定会对我上述对 Java 的描述感到不适,我还用一些证据说明一下——全中国所有的电商平台,几百家银行,三大电信运营商,所有的保险公司,劵商的系统,医院里的系统,电子政府系统,等等,基本都是用 Java 开发的,包括 AWS 的主流语言也是 Java,阿里云一开始用 C++/Python 写控制系统,后面也开始用 Java ……你可能会说 B站是用 go语言,但是你可能不知道 B 站的电商和大数据是用 Java……懂着数据分析的同学,建议上各大招聘网站上搜一下 Java 的职位数量,你就知道某个技术是否主流和热门……
原则四:完备性会比性能更重要
我发现好些公司的架构师做架构的时候,首要考虑的是架构的性能是否能够撑得住多大多大的流量,而不是考虑系统的完备性和扩展性。所以,我已经多次见过这样的案例了,一开始直接使用 MongoDB 这样的非关系型数据库,或是把数据直接放在 Redis 里,而直接放弃关系型数据库的数据完备性的模型,而在后来需要在数据上进行关系查询的时候,发现 NoSQL 的数据库在 Join 上都表现的太差,然后就开始各种飞线,为了不做 Join 就开始冗余数据,然而自己又维护不好冗余数据后带来的数据一致性的问题,导致数据上的各种错乱丢失。
所以,我给如下的一些如下的架构原则:
- 使用最科学严谨的技术模型为主,并以不严谨的模型作为补充。对于上面那个案例来说,就是——永远使用完备支持 ACID 的关系型数据库,然后用 NoSQL 作补充,而不是完全放弃关系型数据库。这里的原则就是所谓的“先紧后松”,一开始紧了,你可以慢慢松,但是开始松了,以后你想紧再也紧不过来了。
- 性能上的东西,总是有很多解的。我这么多年的经历告诉我,性能上的事,总是有解的,手段也是最多的,这个比起架构的完备性和扩展性来说真的不必太过担心。
为了追求所谓的性能,把整个系统的完备性丢失掉,相当地得不偿失。
原则五:制定并遵循服从标准、规范和最佳实践
这个原则是非常重要的,因为只有服从了标准,你的架构才能够有更好的扩展性。比如:我经常性的见到很多公司的系统既没有服从业界标准,也没有形成自己公司的标准,感觉就像一群乌合之众一样。最典型的例子就是 HTTP 调用的状态返回码。业内给你的标准是 200表示成功,3xx 跳转,4xx 表示调用端出错,5xx 表示服务端出错,我实在是不明白为什么无论成功和失败大家都喜欢返回 200,然后在 body 里指出是否error(前两年我在微信公众号里看到一个有一定名气的互联网老兵推荐使用无论正确还是出错都返回 200 的做法,我在后台再三确认后,我发现这样的架构师真是害人不浅)。这样做最大的问题是——监控系统将在一种低效的状态下工作。监控系统需要把所有的网络请求包打开后才知道是否是错误,而且完全不知道是调用端出错还是服务端出错,于是一些像重试或熔断这样的控制系统完全不知道怎么搞(如果是 4xx错,那么重试或熔断是没有意义的,只有 5xx 才有意义)。有时候,我会有种越活越退步的感觉,错误码设计这种最基本最基础的东西为什么会没有?并且一个公司会任由着大家乱来?这些基础技能怎么就这样丢掉了?
还有,我还见过一些公司,他们整个组织没有一个统一的用户 ID 的设计,各个系统之间同步用户的数据是通过用户的身份证 ID,是的,就是现实世界的身份证 ID,包括在网关上设置的用户白名单居然也是用身份证 ID。我对这个公司的内的用户隐私管理有很大的担忧。一个企业,一个组织,如果没有标准和规范,也就会有抽象,这一定是要出各种乱子的。
下面,我罗列一些你需要注意的标准和规范(包括但不限于):
- 服务间调用的协议标准和规范。这其中包括 Restful API路径, HTTP 方法、状态码、标准头、自定义头等,返回数据 JSon Scheme……等。
- 一些命名的标准和规范。这其中包括如:用户 ID,服务名、标签名、状态名、错误码、消息、数据库……等等
- 日志和监控的规范。这其中包括:日志格式,监控数据,采样要求,报警……等等
- 配置上的规范。这其中包括:操作系统配置、中间件配置,软件包……等等
- 中间件使用的规范。数据库,缓存、消息队列……等等
- 软件和开发库版本统一。整个组织架构内,软件或开发库的版本最好每年都升一次级,然后在各团队内统一。
这里重要说一下两个事:
- Restful API 的规范。我觉得是非常重要的,这里给两个我觉得写得最好的参考:Paypal 和 Microsoft 。Restful API 有一个标准和规范最大的好处就是监视可以很容易地做各种统计分析,控制系统可以很容易的做流量编排和调度。
- 另一个是服务调用链追踪。对于服务调用链追踪来说,基本上都是参考于 Google Dapper 这篇论文,目前有很多的实现,最严格的实现是 Zipkin,这也是 Spring Cloud Sleuth 的底层实现。Zipkin 贴近 Google Dapper 论文的好处在于——无状态,快速地把 Span 发出来,不消耗服务应用侧的内存和 CPU。这意味着,监控系统宁可自己死了也不能干扰实际应用。
- 软件升级。我发现很多公司包括 BAT,他们完全没有软件升级的活动,全靠开发人员自发。然而,这种成体系的活动,是永远不可能靠大众的自发形成的。一个公司至少一年要有一次软件版本升级的review,然后形成软件版本的统一和一致,这样会极太简化系统架构的复杂度。
原则六:重视架构扩展性和可运维性
在我见过很多架构里,技术人员只考虑当下,但从来不考虑系统的未来扩展性和可运维性。所谓的管生不管养。如果你生下来的孩子胳膊少腿,严重畸形,那么未来是很难玩的。因为架构和软件不是写好就完的,是需要不断修改不断维护的,80%的软件成本都是在维护上。所以,如何让你的架构有更好的扩展性,可以更容易地运维,这个是比较重要的。所谓的扩展性,意味着,我可以很容易地加更多的功能,或是加入更多的系统,而所谓可运维,就是说我可以对线上的系统做任意的变更。扩展性要求的是有标准规范且不耦合的业务架构,可运维性要求的则是可控的能力,也就是一组各式各样的控制系统。
- 通过服务编排架构来降低服务间的耦合。比如:通过一个业务流程的专用服务,或是像 Workflow,Event Driven Architecture , Broker,Gateway,Service Discovery 等这类的的中间件来降低服务间的依赖关系。
- 通过服务发现或服务网关来降低服务依赖所带来的运维复杂度。服务发现可以很好的降低相关依赖服务的运维复杂度,让你可以很轻松的上线或下线服务,或是进行服务伸缩。
- 一定要使用各种软件设计的原则。比如:像SOLID这样的原则(参看《一些软件设计的原则》),IoC/DIP,SOA 或 Spring Cloud 等 架构的最佳实践(参看《SteveY对Amazon和Google平台的吐槽》中的 Service Interface 的那几条军规),分布式系统架构的相关实践(参看:《分布式系统的事务处理》,或微软件的 《Cloud Design Patterns》)……等等
原则七:对控制逻辑进行全面收口
所有的程序都会有两种逻辑,一种是业务逻辑,一种是控制逻辑,业务逻辑就是完成业务的逻辑,控制逻辑是辅助,比如你用多线程,还是用分布式,是用数据库还是用文件,如何配置、部署,运维、监控,事务控制,服务发现,弹性伸缩,灰度发布,高并发,等等,等等 ……这些都是控制逻辑,跟业务逻辑没有一毛钱关系。控制逻辑的技术深度会通常会比业务逻辑要深一些,门槛也会要高一些,所以,最好要专业的程序员来负责控制逻辑的开发,统一规划统一管理,进行收口。这其中包括:
- 流量收口。包括南北向和东西向的流量的调度,主要通过流量网关,开发框架 SDK或 Service Mesh 这样的技术。
- 服务治理收口。包括:服务发现、健康检查,配置管理、事务、事件、重试、熔断、限流……主要通过开发框架 SDK – 如:Spring Cloud,或服务网格Service Mesh等技术。
- 监控数据收口。包括:日志、指标、调用链……主要通过一些标准主流的探针,再加上后台的数据清洗和数据存储来完成,最好是使用无侵入式的技术。监控的数据必须统一在一个地方进行关联,这样才会产生信息。
- 资源调度有应用部署的收口。包括:计算、网络和存储的收口,主要是通过容器化的方案,如k8s来完成。
- 中间件的收口。包括:数据库,消息,缓存,服务发现,网关……等等。这类的收口方式一般要在企业内部统一建立一个共享的云化的中间件资源池。
对此,这里的原则是:
- 你要选择容易进行业务逻辑和控制逻辑分离的技术。这里,Java 的 JVM+字节码注入+AOP 式的Spring 开发框架,会带给你太多的优势。
- 你要选择可以享受“前人种树,后人乘凉”的有技术红利的技术。如:有庞大社区而且相互兼容的技术,如:Java, Docker, Ansible,HTTP,Telegraf/Collectd……
- 中间件你要使用可以 支持HA集群和多租户的技术。这里基本上所有的主流中间件都会支持 HA 集群方式的。
原则八:不要迁就老旧系统的技术债务
我发现很多公司都很非常大的技术债务,这些债务具体表现如下:
- 使用老旧的技术。比如,使用HTTP1.0, Java 1.6,Websphere,ESB,基于 socket的通讯协议,过时的模型……等等
- 不合理的设计。比如,在 gateway 中写大量的业务逻辑,单体架构,数据和业务逻辑深度耦合,错误的系统架构(把缓存当数据库,用消息队列同步数据)……等等
- 缺少配套设施。比如,没有自动化测试,没有好的软件文档,没有质量好的代码,没有标准和规范……等等
来找我寻求技术帮助的人都有各种各样的问题。我都会对他们苦口婆心地说同样的一句话——“如果你是来找我 case-by-case 解决问题,我兴趣不大,因为,你们千万不要寄希望能够很简单的把一辆夏利车改成一辆法拉利跑车,或是把一栋地基没打好的歪楼搞正。以前欠下的技术债,都得要还,没打好的地基要重新打,没建配套设施都要建。这些基础设施如果不按照正确科学的方式建立的话,你是不可能有一个好的的系统,我也没办法帮你 case-by-case 的解决问题……”,一开始,他们都会对我说,没问题,我们就是要还债,但是,最后发现要还的债真多,有点承受不了,就开始现原形了。
他们开始为自己的“欠的技术债”找各种合理化的理由——给你解释各种各样的历史原因和不得以而为之的理由。谈着谈着,让我有一种感觉——他们希望得到一种什么都不改什么都不付出的方式就可以进步的心态,他们宁可让新的技术 low 下来迁就于这些技术债,把新的技术滥用地乱七八糟的。有一个公司,他们的系统架构和技术选型基本都搞错了,使用错误的模型构建系统,导致整个系统的性能非常之差,也才几千万条数据,但他们想的不是还债,不是把地基和配套设施建好,而且要把楼修的更高,上更多的系统——他们觉得现有的系统挺好,性能问题的原因是他们没一个大数据平台,所以要建大数据平台……
我见过很多很多公司,包括大如 BAT 这样的公司,都会在原来的技术债上进行更多的建设,然后,技术债越来越大,利息越来越大,最终成为一个高利贷,再也还不了(我在《开发团队的效率》一文中讲过一个 WatchDog 的架构模式,一个系统烂了,不是去改这个系统,而是在旁边建一个系统来看着它,我很难理解为什么会有这样的逻辑,也许是为了要解决更多的就业……)
这里有几个原则和方法我是非常坚持的,分享给大家:
- 与其花大力气迁就技术债务,不如直接还技术债。是所谓的长痛不如短痛。
- 建设没有技术债的“新城区”,并通过“防腐层 ”的架构模型,不要让技术债侵入“新城区”。
原则九:不要依赖自己的经验,要依赖于数据和学习
有好些人来找我跟我说他们的技术问题,然后希望我能够给他们一个答案。我说,我需要了解一下你现有系统的情况,也就是需要先做个诊断,我只有得到这些数据后,我才可能明白真正的原因是什么 ,我才可能给你做出一个比较好的技术方案。我个人觉得这是一种对对方负责的方法,因为技术手段太多了,所有的技术手段都有适应的场景,并且有各种 trade-off,所以,只有调研完后才能做出决定。这跟医生看病是一样的,确诊病因不能靠经验,还是要靠诊断数据。在科学面前,所有的经验都是靠不住的……
另外,如果有一天你在做技术决定的时候,开始凭自己以往的经验,那么你就已经不可能再成长了。人都是不可能通过不断重复过去而进步的,人的进步从来都是通过学习自己不知道的东西。所以,千万不要依赖于自己的经验做决定。做任何决定之前,最好花上一点时间,上网查一下相关的资料,技术博客,文章,论文等 ,同时,也看看各个公司,或是各个开源软件他们是怎么做的?然后,比较多种方案的 Pros/Cons,最终形成自己的决定,这样,才可能做出一个更好的决定。
原则十:千万要小心 X – Y 问题,要追问原始需求
对于 X-Y 问题,也就是说,用户为了解决 X问题,他觉得用 Y 可以解,于是问我 Y 怎么搞,结果搞到最后,发现原来要解决的 X 问题,这个时候最好的解决方案不是 Y,而是 Z。 这种 X-Y 问题真是相当之多,见的太多太多了。所以,每次用户来找我的时候,我都要不断地追问什么是 X 问题。
比如,好些用户都会来问我他们要一个大数据流式处理,结果追问具体要解决什么样的问题时,才发现他们的问题是因为服务中有大量的状态,需要把相同用户的数据请求放在同一个服务上处理,而且设计上导致一个慢函数拖慢整个应用服务。最终就是做一下性能调优就好了,根本没有必要上什么大数据的流式处理。
我很喜欢追问为什么 ,这种追问,会让客户也跟着来一起重新思考。比如,有个客户来找我评估的一个技术架构的决定,从理论上来说,好像这个架构在用户的这个场景下非常不错。但是,这个场景和这个架构是我职业生涯从来没有见过的。于是,我开始追问这个为什么会是这么一个场景?当我追问的时候,我发现用户都感到这个场景的各种不合理。最后引起了大家非常深刻的研讨,最终用户把那个场景修正后,而架构就突然就变成了一个常见且成熟的的模型……
原则十一:激进胜于保守,创新与实用并不冲突
我对技术的态度是比较激进的,但是,所谓的激进并不是瞎搞,也不是见新技术就上,而是积极拥抱会改变未来的新技术,如:Docker/Go,我就非常快地跟进,但是像区块链或是 Rust 这样的,我就不是很积极。因为,其并没有命中我认为的技术趋势的几个特征(参看《Go,Docker 和新技术 》)。当然,我也不是不喜欢的就不学了,我对区块链和 Rust 我一样学习,我也知道这些技术的优势,但我不会大规模使用它们。另外,我也尊重保守的决定,这里面没有对和错。但是,我个人觉得对技术激进的态度比起保守来说有太多的好处了。一方面来说,对于用户来说,很大程度上来说,新技术通常都表面有很好的竞争力,而且我见太多这样成功的公司都在积极拥抱新的技术的,而保守的通常来说都越来越不好。
有一些人会跟我说,我们是实用主义,我们不需要创新,能解决当下的问题就好,所以,我们不需要新技术,现有的技术用好就行了。这类的公司,他们的技术设计第一天就在负债,虽然可以解决当下问题,但是马上就会出现新的问题,然后他们会疲于解决各种问题。最后呢,最后还是会走到新的技术上。
这里的逻辑很简单 —— 进步永远来自于探索,探索是要付出代价的,但是收益更大。对我而言,不敢冒险才是最大的冒险,不敢犯错才是最大的错误,害怕失去会让你失去的更多……
这多年来我一直在钻研的技术 | 酷 壳 - CoolShell
程序员的谎谬之言还是至理名言? | 酷 壳 - CoolShell
服务级别协议 - 维基百科 service-level agreement (SLA)
“品质在于构建过程”吗? | 酷 壳 - CoolShell
为什么平均数和百分位数很棒
曾经监控或分析过应用程序的任何人都使用或曾经使用过平均值。它们易于理解和计算。我们往往会忽略世界平均描绘的画面是多么错误。为了强调这一点,让我举一个我最近在报纸上读到的表演空间之外的真实例子。
文章解释说,欧洲某个地区的平均工资是 1900 欧元(很明显,这在那个地区会相当不错!)。然而,当仔细观察时,他们发现大多数人,即 10 人中的 9 人,只赚了大约 1000 欧元,而一个人会赚到 10.000 欧元(我当然简化了这一点,但你明白了)。如果你算一下,你会发现这个平均值确实是 1900,但我们都同意这并不代表我们在日常生活中使用的“平均”工资。所以现在让我们将这种想法应用于应用程序性能。
平均响应时间
迄今为止,平均响应时间是应用程序性能管理中最常用的指标。我们假设这代表一个“正常”事务,但是,只有在响应时间始终相同(所有事务以相同速度运行)或响应时间分布大致呈钟形曲线时,这才是正确的。
贝尔曲线表示响应时间的“正态”分布,其中平均值和中位数相同。我很少出现在实际应用中
在钟形曲线中,平均值(平均值)和中位数是相同的。换句话说,观察到的性能将代表大部分(一半或一半以上)的交易。
实际上,大多数应用程序很少有非常严重的异常值。统计学家会说曲线有长尾。长尾并不意味着很多缓慢的交易,但很少有比正常慢的数量级。
这是一个典型的响应时间分布,异常值很少但很重——它有一条长尾。这里的平均值被长尾拖到右边。
我们认识到平均值不再代表大部分交易,但可能远高于中位数。
您现在可以争辩说,只要平均值看起来不比中位数好,这就不是问题。我不同意,但让我们看看我们的许多客户所经历的另一个真实场景:
这是另一个典型的响应时间分布。在这里,我们有很多非常快速的交易,它们将平均值拖到实际中位数的左侧
在这种情况下,相当大比例的交易非常非常快(10-20%),而大部分交易要慢几倍。中位数仍然会告诉我们真实的故事,但突然间平均值看起来比我们大多数交易的实际速度要快得多。这在搜索引擎或涉及缓存时非常典型,一些事务非常快,但批量是正常的。这种情况的另一个原因是失败的事务,更具体地说是快速失败的事务。许多现实世界的应用程序有 1-10% 的失败率(由于用户错误或验证错误)。这些失败的交易通常比真实交易快很多数量级,因此扭曲了平均值。
当然,性能分析师并不愚蠢,他们经常尝试使用更高频率的图表(通过直观地查看较小的聚合来进行补偿)并通过观察到的最小和最大响应时间来进行补偿。然而,我们通常只有在非常了解应用程序的情况下才能这样做,不熟悉应用程序的人可能很容易误解图表。由于这需要知识的深度和类型,因此很难将您的分析传达给其他人 - 想一想 IT 团队之间有多少争论由此引起。那是在我们甚至考虑与业务利益相关者沟通之前!
到目前为止,更好的指标是百分位数,因为它们让我们能够了解分布。但在我们查看百分位数之前,让我们先看看每个生产监控解决方案中的一个关键特性:自动基线和警报。
自动基线和警报
在现实世界环境中,性能不佳时会受到关注,并对业务和用户产生负面影响。但是,我们如何才能快速识别性能问题以防止产生负面影响?我们无法对每一个缓慢的事务发出警报,因为总会有一些。此外,大多数运维团队必须维护大量应用程序,并不熟悉所有应用程序,因此手动设置阈值可能不准确、非常痛苦且耗时。
业界提出了一种称为自动基线的解决方案。基线计算出“正常”的性能,并且仅在应用程序变慢或产生比平时更多的错误时提醒我们。大多数方法依赖于平均值和标准偏差。
无需深入统计细节,此方法再次假设响应时间分布在钟形曲线上:
标准差代表所有交易的 33%,以平均值为中间值。2x 标准偏差代表 66%,因此大多数,外面的一切都可以被视为异常值。然而,大多数现实世界的场景都不是钟形曲线……
通常,超出 2 倍标准偏差的事务被视为缓慢并被捕获以进行分析。如果平均值显着移动,则会发出警报。在钟形曲线中,这将占最慢的 16.5%(您当然可以调整它),但是,如果响应时间分布不代表钟形曲线,它就会变得不准确。我们要么最终得到很多误报(交易比平均值慢很多,但在查看曲线时处于正常范围内),要么我们错过了很多问题(误报)。此外,如果曲线不是钟形曲线,则平均值可能与中位数相差很大,将标准偏差应用于此类平均值可能会导致与您预期的完全不同的结果!
为什么我喜欢百分位数
百分位数告诉我我正在查看曲线的哪个部分以及该指标代表了多少交易。要可视化此外观,请查看以下图表:
该图表显示了第 50 个和第 90 个百分点以及同一交易的平均值。它表明平均值受第 90 次的影响要大得多,因此受异常值影响,而不是受大部分交易的影响
绿线代表平均值。如您所见,它非常不稳定。另外两条线代表第 50个和第90个百分位数。正如我们所看到的,第 50个百分位数(或中位数)相当稳定,但有几个跳跃。这些跳跃代表了大多数 (50%) 事务的实际性能下降。第 90个百分位数(这是“尾部”的起点)的波动性要大得多,这意味着异常值的缓慢度取决于数据或用户行为。这里重要的是,平均值受到第 90个百分位数(尾部)的严重影响(拖累),而不是大部分交易。
如果响应时间的第50个百分位数(中位数)为 500 毫秒,则意味着我 50% 的事务与 500 毫秒一样快或快于 500 毫秒。如果同一事务的第90个百分位数为 1000 毫秒,则意味着 90% 的速度相同或更快,只有 10% 的速度较慢。在这种情况下,平均值可能低于 500 毫秒(在前曲线上)、高得多(长尾)或介于两者之间。百分位数让我对现实世界的表现有更好的了解,因为它向我展示了我的响应时间曲线的一部分。
正是由于这个原因,百分位数非常适合自动基线。如果第 50 个百分位数从 500 毫秒移动到 600 毫秒,我知道我的 50% 的交易的性能下降了 20%。你需要对此做出反应。
在许多情况下,我们看到第 75 个或第 90 个百分位数在这种情况下根本没有变化。这意味着缓慢的事务并没有变慢,只有正常的事务变慢了。在这种情况下,根据您的尾巴有多长,平均值可能根本没有移动!
在其他情况下,我们看到第 98 个百分位数从 1 秒下降到 1.5 秒,而第 95 个百分位数稳定在 900 毫秒。这意味着您的应用程序作为一个整体是稳定的,但一些异常值变得更糟,无需立即担心。基于百分比的警报不会受到误报的影响,波动性要小得多,并且不会错过任何重要的性能下降!因此,使用百分位数的基线方法不需要大量调整变量即可有效工作。
下面的屏幕截图显示了特定事务的中位数(第50个百分位数)从大约 50 毫秒跳到大约 500 毫秒并触发警报,因为它明显高于计算的基线(绿线)。另一方面,标有“慢响应时间”的图表显示了同一事务的第90个百分位数。这些“异常值”也显示响应时间有所增加,但不足以触发警报。
在这里,我们看到一个自动基线仪表板,在第 50 个百分位数处有违规。违规很明显,同时第 90 个百分位(右上图)没有违规。因为异常值比大部分交易慢得多,平均值会受到它们的影响,并且不会像第 50 个百分位那样剧烈反应。我们可能错过了这个明显的违规行为!
我们如何使用百分位数进行调整?
百分位数也非常适合调整,并为您的优化提供特定目标。假设我的应用程序中的某些内容通常太慢,我需要使其更快。在这种情况下,我想专注于降低第 90 个百分位数。这将确保应用程序的整体响应时间下降。在其他情况下,我有不可接受的长异常值,我想专注于降低超过 98% 或 99% 的事务的响应时间(仅异常值)。我们看到许多应用程序在第 90 个百分位的性能完全可以接受,而第 98 个百分位的性能更差。
另一方面,在面向吞吐量的应用程序中,我希望我的大部分事务都非常快,同时接受优化会使一些异常值变慢的事实。因此,我可能会确保 75% 下降,同时试图保持 90% 稳定或不会变得更糟。
我无法对平均值、最小值和最大值进行相同类型的观察,但是对于百分位数,它们确实非常容易。
结论
平均值是无效的,因为它们过于简单和一维。百分位数是了解应用程序真实性能特征的一种非常棒且简单的方法。它们还为自动基线、行为学习和以适当的重点优化您的应用程序提供了很好的基础。简而言之,百分位数很棒!

浙公网安备 33010602011771号