慢速 Web 应用程序的组成部分

作者:Reuven Lerner

在我的上一篇文章中,我开始讨论如何优化 Web 应用程序,着眼于应用程序的不同方面以及可能出现速度缓慢的地方。我描述了可能导致最终用户感觉 Web 应用程序速度缓慢的几个不同因素:网络速度和延迟、服务器应用程序速度、客户端加载和渲染,最后是客户端 JavaScript 程序的执行。这些因素都会影响用户在使用 Web 应用程序时的速度感,而作为工程师,我们的工作就是尽量减少这些因素。

因此,在本文中,我将介绍许多您可以查看的地方,以尝试减少我描述的问题时间,即您应用程序中可能速度缓慢且可能需要一些调整或改进的特定区域。在我的下一篇文章中,我将介绍您可以用来识别应用程序中存在问题的具体程序和技术,从而帮助改进它们。

您在哪里查找?

有人对程序员说的最令人恼火的事情之一是“它不起作用。” 什么不起作用?它昨天做了什么?是什么导致计算机停止工作?当人们对我说他们的系统“不起作用”,然后期望我找到问题时,我认为这种说法类似于去医生办公室说:“我某处疼。”

同样,如果您要查找系统中的瓶颈和缓慢移动的部分,您需要查看系统的各个部分,并考虑它们可能在多大程度上导致速度缓慢。只有在考虑了各个部分并了解了其中每个部分可能缓慢的原因之后,您才能尝试修复可能存在的任何问题。

基础设施

我不是硬件专家;我经常开玩笑说,当我换灯泡时就达到了我的极限。也就是说,毫无疑问,至少稍微了解您的硬件将有助于您优化软件。

过去,软件开发人员主要考虑的是程序运行的 CPU 速度。但是,对于现代 Web 应用程序来说,I/O 速度和内存更为重要。

您的应用程序——尤其是当它用高级语言编写时,甚至在应用程序负载很重时更是如此——将消耗大量内存。因此,您需要加载大量 RAM。这对于您的数据库也是如此;杀死数据库性能的最佳方法是开始使用虚拟内存。如果您的服务器因为 RAM 用完而正在使用磁盘,您将遭受巨大的性能损失。

相比之下,如果您可以为数据库提供大量内存,它将能够在 RAM 中缓存更多数据,而不必像以前那样频繁地访问磁盘。如果您使用 PostgreSQL 作为数据库,则设置“shared buffers”和“effective cache size”参数至关重要,因为它们告诉数据库它可以预期使用多少内存。这有助于 PostgreSQL 服务器确定它是否应该丢弃已经缓存的数据或将更多数据加载到共享缓冲区中。

这也指出了拥有多台服务器而不是单台服务器的优势。如果您运行的是小型网站,则您的 HTTP 服务器和数据库服务器的 RAM 要求很可能会冲突。当您获得大量流量时,这种情况尤其会显现出来。您的 HTTP 服务器将使用更多内存,您的数据库也会如此,并且在某个时候,它们可能会发生冲突。使用多台计算机不仅可以让您更轻松地扩展 Web 和数据库服务器,而且还可以确保两者都不会相互干扰。

正如我上面提到的,要考虑的另一个因素是 I/O。再说一次,我不是主要的硬件专家,但您需要考虑您正在使用的磁盘速度。今天,随着人们越来越多地转向虚拟化服务器,其中底层硬件被抽象化,有时很难评估,更不用说选择系统运行的硬件了。即使您不能这样做,您也可以并且应该努力避免将任何生产级系统放在共享机器上。

原因很简单。在共享机器上,假设每个应用程序都会与其他应用程序友好相处。如果一个应用程序突然开始大量占用磁盘,那么每个人的 I/O 都会受到影响。我在撰写博士论文时就经历过这种情况。我的软件每小时备份一次数据库,而我大学计算机集群的管理者告诉我,这导致其他用户的性能慢得无法接受。(我们找到了一种资源消耗较少的方式来备份东西。)

许多人过去常常问关于服务器的问题是“购买还是构建?”——意思是,您应该创建自己的专用服务器还是购买现成的服务器。今天,很少有公司构建自己的服务器,因为您通常谈论的是商品硬件。因此,现在的问题是“购买还是租用?”

我必须说,直到最近,我仍然认为拥有自己的服务器(您可以在其上拥有相对完全的控制权)才是正确的选择。但我必须承认,在从事几个可扩展性项目之后,我越来越倾向于部署大量相同的 VM 的想法。每个单独的 VM 可能不是很强大,但随时随地扩展和缩减的能力可能比这种需求更重要。

最重要的是,当您研究服务器时,(一如既往)有很多不同的方向可以探索。但是,如果您认为您的系统可能需要快速扩展,您应该认真考虑使用“云”平台。比 CPU 更重要的是 RAM 的数量,并确保您的虚拟机是您的物理机器的唯一用户。

哦,您应该设置多少台服务器?这始终是一个难以回答的问题,即使您知道您期望有多少用户访问也是如此。这是因为服务器在不同的负载下会有不同的行为,这取决于各种各样的因素。无论如何,您都应该给自己留出舒适的误差范围,并制定应急计划,以便在公关非常成功的情况下如何扩展。

HTTP 服务器

现在您有了一些硬件,您需要考虑您的 HTTP 服务器。这取决于品味、个人偏好和大量争论,而且在很大程度上还取决于您使用的技术。多年来,我一直使用 Apache httpd 作为我的 Web 服务器——不是因为它速度超快,而是因为它非常容易配置并且有许多可用的插件模块。但是,即使我也不得不承认 nginx 比 Apache 更具可扩展性。Phusion Passenger 是 Apache 和 nginx 的插件,可以与 Ruby 和 Python 一起使用,并且非常容易安装,这一事实说服我切换到 nginx。

Apache 使用多个线程或进程来处理其连接,而 nginx 使用单线程来处理,这被称为“reactor 模式”。因此,它通常更具可扩展性。

如果您尝试消除系统中的潜在瓶颈,那么拥有高性能的 HTTP 服务器将是必要的。当然,这还不够;您还希望服务器尽可能快地运行并尽可能少地被征税。

为了使服务器运行得更快,您需要检查其配置并删除任何您不需要的模块。调试很棒,额外的功能也很棒,但是当您尝试使某些东西尽可能高效时,您需要修剪掉任何对其功能不重要的东西。如果您正在使用 Apache 并且包含使调试更容易和更快的模块,您应该删除它们,(当然)将它们保留在您的开发和暂存系统中。

您还需要尽可能少地占用 HTTP 服务器的资源,以便它可以集中精力为 HTTP 客户端提供服务。有很多方法可以做到这一点,但它们基本上都涉及确保请求永远不会到达服务器。换句话说,您需要使用各种缓存。

在您的应用程序中,您需要缓存数据库调用和页面,以便只有在真正必要时,请求系统页面的用户才会转向 HTTP 服务器。现代框架(如 Rails 和 Django)旨在让您在外部系统(如 memcached 或 Redis)中缓存页面,这样,从缓存中提供来自服务器的 /faq 请求。

除了您要在应用程序中进行的缓存之外,您可能还需要在服务器和外部世界之间放置前端 Web 缓存(如 Varnish)。这样,用户请求的任何静态资产(如 JavaScript、CSS 或图像)都将来自该缓存,而不是必须转到服务器。

更进一步,越来越多的大型网站正在采取一种举措,您可以(并且可能应该)使用内容分发网络 (CDN),您的静态资产驻留在该网络上。这样,访问您网站的人只会访问您的服务器以获取应用程序的动态部分;其他一切都由第三方提供。您的服务器可以将其所有时间都花在担心应用程序本身上,而不是所有使其对最终用户来说漂亮且实用的东西。

数据库

另一个引起广泛争论的点,以及一个无底洞(或者,如果您是顾问,则是一个无尽的机会!)的工作,是数据库。无论您使用的是关系数据库、NoSQL 数据库还是两者的组合,所有数据库都是非常复杂的软件,它们需要大量的配置、维护和关注。

实际上,这不正确。在许多情况下,您可以不调整数据库配置。但是,如果您想扩展系统以处理高负载级别,您需要尽可能多地将数据库保存在内存中,并尽可能多地调整查询。

您还需要确保数据库在缓存查询和结果方面做得最好。数据库自然会尽可能多地这样做;将查询结果保存在内存中是提高速度的一种方法。但是许多数据库允许您配置此内存,并调整其分配方式。例如,PostgreSQL 使用其统计信息(通过 VACUUM 收集)以便它知道应该将什么保留在内存中以及可以删除什么。但在应用程序级别,您可以缓存查询及其结果,从而允许应用程序完全绕过数据库,从而减轻其负载。

在数据库方面,还有另一个复杂之处,即许多现代 Web 应用程序框架使用对象关系映射器 (ORM) 为您自动生成 SQL。诚然,在大多数情况下,ORM 生成的 SQL 对于该任务来说绰绰有余,并且动态生成 SQL 的开销以及执行此操作所需的许多对象层是值得的。

但是,在许多情况下,ORM 生成的 SQL 效率不高,通常是因为程序员的假设与 ORM 的假设不符。这方面的一个经典示例是在 Ruby on Rails 中,如果您在查询后从数据库中检索大量对象。从 Ruby 代码来看,感觉就像您只是在迭代大量对象。但是从 SQL 方面来看,您迭代的每个对象都会触发 SQL 查询,这可能会阻塞数据库。

使用慢查询日志或日志记录(如 PostgreSQL 允许的那样)所有花费时间超过某个最小阈值的查询是开始查找数据库中花费大量时间的事情的好方法。

但即便如此,您也可能找不到性能问题。我最近与客户合作,帮助他们优化数据库,我们发现数据库执行大量查询需要很长时间。然而,当我们查看日志时,没有任何内容写入那里。问题不在于每个单独的查询都花费了很长时间,而是存在大量小查询。我们的优化并没有加快他们的查询速度,而是用一个大型查询替换了一个“for”循环,在该循环中,数据库被重复查询。执行速度的差异确实令人惊叹,它表明,为了调试 ORM 问题,仅仅了解高级语言是不够的。您确实需要了解 SQL 以及 SQL 在数据库中的执行方式。

在某些情况下,您无法阻止应用程序访问数据库——并且通过大量查询来猛烈访问数据库。在这种情况下,您可能需要使用主从模型,其中所有只读查询都定向到一组从(只读)数据库,而写查询都定向到主服务器。主从配置假设大多数查询将是读取而不是写入,如果您的系统是这种情况,那么您很幸运。

如果您的数据库不是这种情况,并且主从模式不起作用,那么您的解决方案将受到您使用的数据库的限制。有些提供主主复制,为您提供多个数据库,您可以向其发送写请求。其中一些,尤其是在 NoSQL 世界中,会自动分片。但是没有完美的解决方案,尤其是在主主复制方面,即使对于最有经验的数据库专家来说,事情也可能变得有点棘手。

最重要的是,您的数据库很可能成为系统中的最大瓶颈。尽量让应用程序远离它,尽可能缓存任何东西。并且,尽量使数据库尽可能调整,使用您正在使用的系统的当前最佳实践,以便它以最佳性能运行。

结论

在我的上一篇文章中,我谈到了扩展 Web 应用程序可能存在问题的基本高级位置。在这里,我更详细地讨论了当您的应用程序正在显示或可能显示扩展问题时,您可以做哪些事情。下个月,我将研究一些具体的工具和技术,您可以使用这些工具和技术来了解您的应用程序在大量用户来使用它之前的可扩展性。

资源

Martin Abbott 和 Michael Fisher 撰写的可扩展性艺术第 2 版(由 Addison-Wesley 出版)详细描述了 Web 应用程序的不同部分以及您可以并且应该如何扩展它。他们描述了一种思维过程,而不是一组技术,用于分析 Web 应用程序的架构。如果这是您感兴趣的主题,那么很可能值得阅读本书。

Reuven M. Lerner 是一位长期从事 Web 开发的开发者,提供 Python、Git、PostgreSQL 和数据科学方面的培训和咨询服务。他撰写了两本编程电子书(Practice Makes Python 和 Practice Makes Regexp),并在 http://lerner.co.il/newsletter 上为程序员发布免费的每周新闻通讯。Reuven 的 Twitter 账号是 @reuvenmlerner,与他的妻子和三个孩子住在以色列的 Modi’in。

加载 Disqus 评论