本系列文章
Web应用程序是消费者和企业的主要内容。在用于移动和理解位的许多现有协议中,HTTP具有压倒性的思想份额。当您遇到并了解Web应用程序开发的细微差别时,大多数人可能很少关注最终运行应用程序的操作系统。Dev和Ops的分离只会让情况变得更糟。但随着DevOps文化变得普遍,开发人员负责在云中运行他们的应用程序,更好地理解后端操作系统的细节是一个明显的优势。如果您将后端部署为供个人使用或供少数并发用户使用的系统,您实际上不必费心使用Linux以及后端如何扩展。
我们编写的Web服务中遇到的限制与使Web服务或应用程序工作所需的其他应用程序非常相似。是那些负载平衡器或数据库服务器。所有这些类别的应用程序在高性能环境中都面临着类似的挑战。了解这些基本约束以及如何解决这些约束通常会让您了解Web应用程序或服务的性能和可伸缩性。
我正在编写本系列文章,以回应那些希望成为消息灵通的系统架构师的年轻开发人员提出的问题。如果不深入了解Linux应用程序的基础知识以及构建Linux或Unix网络应用程序的不同方式,就无法全面了解Linux应用程序的性能。虽然有许多类型的Linux应用程序,但我想要探索的是面向Linux网络的应用程序,而不是像浏览器或文本编辑器这样的桌面应用程序。这是因为本系列的读者是希望了解Linux或Unix应用程序如何工作以及如何构建这些服务以实现高性能的Web服务/应用程序开发人员和架构师。
Linux是在服务器操作系统和更多的,往往不是你的应用程序可能在Linux上运行的最后。虽然我说Linux,但大多数时候你可以放心地假设我还包括其他类似Unix的操作系统。但是,我没有在其他类Unix系统上广泛测试附带的代码。所以,如果您对FreeBSD或OpenBSD感兴趣,您的里程可能会有所不同。在我尝试任何特定于Linux的地方,我已尽力指出它。
虽然您当然可以使用这些知识为您想要从头开始编写的新网络应用程序选择最佳结构,但您可能无法启动自己喜欢的文本编辑器并使用C或C ++编写Web服务器来解决问题。在您的组织中提供下一个业务应用程序。这可能是让自己被解雇的有保障的方法。话虽如此,了解这些应用程序结构将极大地帮助您从少数现有应用程序中选择一个,如果您知道它们的结构。在理解了本系列文章后,您将能够理解基于流程的与基于线程的对比基于事件的系统。
ZeroHTTPd:一种学习工具
ZeroHTTPd是我在C中从头开始编写的一个Web服务器,作为一种教学工具。它没有外部依赖,包括Redis访问。我们推出了自己的Redis例程 - 请阅读以下内容。
虽然我们可以谈论很多理论,但没有什么比编写代码,运行它,对它进行基准测试以比较我们发展的每个服务器架构。这应该像其他方法一样巩固你的理解。为此,我们将使用基于流程,基于线程和基于事件的模型开发一个名为ZeroHTTPd的简单Web服务器。我们将对每台服务器进行基准测试,并了解它们相对于彼此的性能。ZeroHTTPd是一个尽可能简单的HTTP服务器,在纯C中从头开始编写,没有外部库依赖。它在单个C文件中实现。对于基于事件的服务器,我包括uthash,这是一个优秀的哈希表实现,它包含在一个头文件中。否则,没有依赖关系,这是为了简单起见。
我对代码进行了大量评论,以帮助理解。ZeroHTTPd也是一个简单的最小Web开发框架,除了是用几百行C编写的简单Web服务器之外。它没有做太多。但是,它可以服务器静态文件和非常简单的“动态”页面。话虽如此,ZeroHTTPd非常适合您了解如何构建Linux应用程序以实现高性能。在一天结束时,大多数Web服务会等待请求,查看该请求是什么并处理它们。这正是我们将使用ZeroHTTPd所做的事情。它是一种学习工具,而不是你将在生产中使用的东西。它也不会因为错误处理,安全最佳实践而赢得奖项(哦,是的,我已经使用过了strcpy
)或C语言的巧妙技巧和捷径,其中有几种。但是,它有望很好地服务于它的目的(双关语意外)。
留言簿应用程序
现代Web应用程序几乎不能用于静态文件。他们与各种数据库,缓存等进行了复杂的交互。为此,我们构建了一个名为“Guestbook”的简单Web应用程序,让客人可以留下他们的名字和评论。留言簿还列出了各位嘉宾以前留下的评论。页面底部还有一个访客柜台。
我们将访客计数器和留言簿条目存储在Redis中。要与Redis交谈,我们不依赖于外部库。我们有自己的自定义C例程来与Redis交谈。当你可以使用已经可用且经过充分测试的东西时,我不是很喜欢推出你自己的东西。但ZeroHTTPd的目标是教授Linux性能和访问外部服务,而在提供HTTP请求的过程中,就性能而言,这会产生巨大的影响。我们需要完全控制我们在构建的每个服务器体系结构中与Redis交流的方式。虽然在一个架构中我们使用阻塞调用,但在其他架构中我们使用基于事件的例程。使用外部Redis客户端库将不允许我们使用此控件。此外,我们将在我们使用Redis的范围内实现我们自己的Redis客户端(获取,设置和增加一个键。获取并附加到数组中)。此外,Redis协议非常优雅和简单。有意识地学习的东西。事实上,您可以实现一个超级快速的协议,它可以在大约100行代码中完成它的工作,这就说明了协议的思考程度。
下图说明了我们遵循的步骤,以便在客户端(浏览器)请求/guestbook
URL 时准备好提供HTML 。
当需要提供留言簿页面时,有一个文件系统调用将模板读入内存,以及三个与Redis网络相关的调用。模板文件包含构成您在上面屏幕截图中显示的留言板页面的大部分HTML内容。它还有一些特殊的占位符,其中来自Redis的内容的动态部分就像客人评论和访客计数器一样。我们从Redis中获取这些内容,将这些替换为模板文件中的占位符,最后,将完整形成的内容写出到客户端。由于Redis返回任何递增键的新值,因此可以避免对Redis的第三次调用。但是,出于我们的目的,当我们将服务器移动到基于事件的异步体系结构时,让服务器忙于阻止大量网络调用是了解事情的好方法。所以,
ZeroHTTPd服务器架构
我们将使用7种不同的体系结构构建ZeroHTTPd,保留相同的功能:
- 迭代
- 分叉(每个请求一个子进程)
- 预分叉服务器(预分叉进程)
- 线程化(每个请求一个线程)
- 预先线程(预先创建的线程)
poll()
基epoll
基于
我们还将测量每个架构的性能,每个架构加载10,000个HTTP请求。但是,随着我们继续与可以处理更多并发性的体系结构进行比较,我们将转而使用30,000个请求进行测试。我们测试3次并考虑平均值。
测试方法
重要的是,这些测试不能与同一台机器上的所有组件一起运行。如果这样做,操作系统将在所有这些组件之间进行调度的额外开销,因为它们争夺CPU。使用每个选定的服务器体系结构测量操作系统开销是本练习最重要的目标之一。添加更多变量将对该过程产生不利影响。因此,上图中描述的设置将最有效。
以下是每个服务器的作用:
- load.unixism.net:这是我们运行的地方
ab
,Apache Benchmark实用程序,它生成测试我们的服务器体系结构所需的负载。 - nginx.unixism.net:有时我们可能想运行多个服务器程序实例。因此,我们使用适当配置的Nginx服务器作为负载均衡器,将来自的负载分散
ab
到我们的服务器进程。 - zerohttpd.unixism.net:这是我们运行服务器程序的地方,这些程序基于上面列出的7种不同的体系结构,一次一个体系结构。
- redis.unixism.net:此服务器运行Redis守护程序,该守护程序存储客户备注和访问者计数器。
所有服务器都有一个CPU核心。我们的想法是看看我们的每个服务器架构能够带来多大的性能。由于我们所有的服务器程序都是针对相同的硬件进行测量的,因此它可以作为我们衡量相对性能或每个服务器架构的基线。我的测试设置包括从Digital Ocean租用的虚拟服务器。
我们在测量什么?
我们可以衡量很多东西。但是,考虑到一定数量的计算资源,我们希望看到在各种增加的并发性水平下我们可以从每个体系结构中挤出多少性能。我们测试最多15,000个并发用户。
检测结果
下图显示了采用不同流程体系结构的服务器在受到各种并发级别时的性能。在y轴上我们有请求/秒,在x轴上我们有并发连接。
这是一张表格,上面列出了数字
并发 | 迭代 | 分叉 | preforked | 螺纹 | prethreaded | 轮询 | epoll的 |
20 | 7 | 112 | 2100 | 1800 | 2250 | 1900 | 2,050 |
50 | 7 | 190 | 2200 | 1700 | 2200 | 2000 | 2000 |
100 | 7 | 245 | 2200 | 1700 | 2200 | 2,150 | 2100 |
200 | 7 | 330 | 2300 | 1750 | 2300 | 2200 | 2100 |
300 | - | 380 | 2200 | 1800 | 2400 | 2250 | 2,150 |
400 | - | 410 | 2200 | 1750 | 2600 | 2000 | 2000 |
500 | - | 440 | 2300 | 1,850 | 2700 | 1900 | 2212 |
600 | - | 460 | 2400 | 1800 | 2500 | 1700 | 2519 |
700 | - | 460 | 2400 | 1600 | 2,490 | 1550 | 2607 |
800 | - | 460 | 2400 | 1600 | 2,540 | 1400 | 2553 |
900 | - | 460 | 2300 | 1600 | 2472 | 1200 | 2567 |
1000 | - | 475 | 2300 | 1700 | 2485 | 1150 | 2439 |
1500 | - | 490 | 2400 | 1550 | 2620 | 900 | 2479 |
2000 | - | 350 | 2400 | 1400 | 2396 | 550 | 2200 |
2500 | - | 280 | 2100 | 1300 | 2453 | 490 | 2262 |
3000 | - | 280 | 1900 | 1250 | 2502 | 广泛的变化 | 2138 |
5000 | - | 广泛的变化 | 1600 | 1100 | 2519 | - | 2235 |
8000 | - | - | 1200 | 广泛的变化 | 2451 | - | 2100 |
万 | - | - | 广泛的变化 | - | 2200 | - | 2200 |
11000 | - | - | - | - | 2200 | - | 2122 |
12000 | - | - | - | - | 970 | - | 1958 |
13000 | - | - | - | - | 730 | - | 1897 |
14000 | - | - | - | - | 590 | - | 1466 |
15000 | - | - | - | - | 532 | - | 1281 |
您可以从上面的图表和表格中看到超过8,000个并发请求我们只有2个竞争者:pre-forked和epoll。事实上,我们基于轮询的服务器比线程服务器更糟糕,即使在相同的并发级别,它也能轻松地击败前者。预先提供的服务器架构为基于epoll的服务器提供了良好的运行,这证明了Linux内核处理大量线程的调度。
ZeroHTTPd源代码布局
您可以在此处找到ZeroHTTPd的源代码。每个服务器架构都有自己的目录。
ZeroHTTPd
│
├── 01_iterative
│ ├── main.c
├── 02_forking
│ ├── main.c
├── 03_preforking
│ ├── main.c
├── 04_threading
│ ├── main.c
├── 05_prethreading
│ ├── main.c
├── 06_poll
│ ├── main.c
├── 07_epoll
│ └── main.c
├── Makefile
├── public
│ ├── index.html
│ └── tux.png
└── templates
└── guestbook
└── index.html
在顶级目录中,除了根据我们讨论的7种不同架构保存ZeroHTTPd代码的7个文件夹之外,还有2个其他目录。“公共”和“模板”目录。“public /”目录包含索引文件和屏幕截图中显示的图像。您可以在此处放置其他文件和文件夹,ZeroHTTPd应该没有问题地为这些静态文件提供服务。当在浏览器中输入的路径组件与“public”文件夹中的路径匹配时,ZeroHTTPd将在放弃之前在该目录中查找“index.html”文件。我们的留言簿应用程序,可通过路径访问/guestbook
,是一个动态应用程序,意味着它的内容是动态生成的。它只有一个主页面,该页面的内容基于文件“templates / guestbook / index.html”。很容易向ZeroHTTPd添加更多动态页面并对其进行扩展。这个想法是用户可以在此目录中添加更多模板,并根据需要扩展ZeroHTTPd。
要构建所有7台服务器,您只需从顶级目录运行“make all”,并构建所有7台服务器并将其放置在顶级目录中。可执行文件期望运行它们的同一目录中的“公共”和“模板”目录。
Linux API
如果你不太了解Linux API,你仍然应该能够遵循这个系列并获得足够的理解。但是,我建议您阅读有关Linux编程API的更多信息。在这方面有无数的资源可以帮助你,就本系列文章而言,这超出了范围。虽然我们将涉及Linux的几个API类别,但我们主要关注的是流程,线程,事件和网络领域。如果您不熟悉Linux API,我建议您阅读系统调用和库函数的手册页,除了阅读有关其用法的书籍和文章之外。
性能和可扩展性
一想到了性能和可扩展性。从理论上讲,它们之间没有任何关系。您可以拥有一个性能非常好的Web服务,在几毫秒内响应,但根本不能扩展。同样,可能会有一个性能不佳的Web应用程序需要几秒钟才能响应,但可以扩展以处理成千上万的并发用户。话虽如此,高性能,高度可扩展的服务组合非常强大。高性能应用程序通常会节省大量资源,从而有效地为每台服务器提供更多并发用户,从而降低成本,这是一件好事。
CPU和I / O绑定任务
最后,计算中始终只有两种可能的任务类型:I / O绑定和CPU绑定。通过Internet(网络I / O)获取请求,提供文件(网络和磁盘I / O),与数据库(网络和磁盘I / O)通信都是I / O绑定活动。但是,有几种类型的数据库查询可以使用一些CPU(排序,计算一百万个结果的平均值等)。您将构建的大多数Web应用程序都将受I / O限制,并且很少会将CPU用于其全部容量。当您在I / O绑定应用程序中看到大量CPU使用时,它很可能指向糟糕的应用程序体系结构。这可能意味着CPU主要用于进程管理和上下文切换开销 - 这并不是非常有用。如果你正在做重图像处理,音频文件转换或机器学习推理,那么您的应用程序将倾向于受CPU限制。但是,对于大多数应用程序而言,情况并非如此。
深入了解服务器架构
via https://unixism.net/2019/04/linux-applications-performance-introduction/