|
级别: 中级 Sean A. Walberg, 高级网络工程师, P.Eng 2009 年 4 月 13 日 了解 Web 应用程序的各种组件如何交互,以及在哪些地方可能发现性能瓶颈。开发人员和管理员都可以从本文受益,因为获得更好的性能是他们的责任。 动态的 Web 应用程序能够存储大量信息,让用户能够通过熟悉的界面立即访问这些信息。但是,随着应用程序越来越受欢迎,可能会发现对请求的响应速度没有以前那么快了。开发人员应该了解 Web 应用程序处理 Web 请求的方式,知道在 Web 应用程序开发中可以做什么,不能做什么,这有助于减少日后的麻烦。 静态的 Web 请求(比如图 1 所示的请求)很容易理解。客户机连接服务器(通常通过 TCP 端口 80),使用 HTTP 协议发出一个简单的请求。
服务器解析这个请求,把它映射到文件系统上的一个文件。然后,服务器向客户机发送一些描述有效负载(比如网页或图像)的响应头,最后向客户机发送文件。 在上面的场景中可能出现几个瓶颈。如果请求的变化很大,导致无法有效地使用操作系统的磁盘缓存,那么服务器的磁盘会很忙,到了某种程度之后,就会减慢整个过程。如果为客户机提供数据的网络通道饱和了,就会影响所有客户机。但是,除了这些状况之外,“接收请求,发送文件” 过程还是相当高效的。 通过做一些假设,可以大致体会静态服务器的性能。假设一个请求的服务时间是 10ms(主要受到磁头寻道时间的限制),那么大约每秒 100 个请求就会使磁盘接近饱和(10msec/request / 1 second = 100 requests/second)。如果要发送 10K 的文档,就会产生大约 8mbit/sec 的 Web 通信流(100 requests/second * 10 KBytes/request * 8bits/byte)。如果可以从内存缓存中获取文件,就可以降低平均服务时间,因此增加服务器每秒能够处理的连接数。如果您有磁盘服务时间或平均请求延时的真实数据,可以把它们放进上面的算式,从而计算出更准确的性能估计值。 既然服务器的处理容量是平均请求服务时间的倒数,那么如果服务时间加倍,服务器的处理容量(每秒处理的连接数)就会减半。请记住这一点,下面看看动态应用程序的情况。 动态应用程序的流程依赖于应用程序的具体情况,但是一般情况下与图 2 相似。
与前一个示例中的客户机一样,图 2 中的客户机首先发出一个请求。静态请求和动态请求之间实际上没什么差异(有时候 .php 或 .cgi 等扩展名可能意味着动态请求,但是它们可能引起误解)。如何处理请求是由 Web 服务器决定的。 在 图 2 中,请求被发送到一个应用服务器,比如运行一个 Java™ 应用程序的 Solaris 系统。应用服务器执行一些处理,然后向数据库查询更多的信息。得到这些信息之后,应用服务器生成一个 HTML 页面,这个页面由 Web 服务器转发给客户机。因此,这个请求的服务时间是几个部分的总和。如果数据库访问花费 7ms,应用服务器花费 13ms,Web 服务器花费 5ms,那么网页的服务时间就是 25ms。根据前面介绍的倒数规则,各个组件的容量分别是每秒 142、77 和 200 个请求。因此,瓶颈是应用服务器,它使这个系统每秒只能处理 77 个连接;超过这个数量之后,Web 服务器被迫等待,连接开始排队。 但是,一定要注意一点:因为系统每秒只能分派 77 个连接,而一个连接需要的处理时间是 25ms,所以并非每个应用程序用户的请求都能够在 25ms 内得到处理。每个组件每次只能处理一个连接,所以在高峰负载下,请求不得不等待 CPU 时间。在上面的示例中,考虑到排队时间和 25ms 的处理时间,平均请求服务时间最终会超过 1.1 秒。关于解决这些排队问题的更多信息,请参见 参考资料。 通过这些示例可以得出以下结论:
本文的其余部分将深入讨论这些问题。 用于动态站点的 N 层体系结构 应用程序(包括 Web 应用程序)的体系结构常常按照层来描述。静态站点可以被看作只有一层 —— Web 服务器。如果用 Web 服务器运行某种脚本语言(比如 PHP),从而连接数据库,那么这可以看作两层。前一节中的示例有三层,即前端 Web 服务器、应用服务器和数据库。 一个软件也可能由多层组成,这取决于您谈话的对象。例如,PHP 脚本可能使用一个模板引擎把业务逻辑与表示分隔开,它可以被看作单独的两层。Java 应用程序可能通过 Java servlet 执行表示任务,servlet 通过与 Enterprise Java Bean (EJB) 通信执行业务逻辑,EJB 通过连接数据库获取更多信息。因此,换一个角度来看,三层体系结构可能是另一副样子,尤其是在涉及不同的工具集时。 常见的体系结构 尽管应用程序的体系结构各不相同,但是有一些常见的体系结构趋势。在一般情况下,应用程序需要四个功能层:
在 Web 应用程序中,客户机层由 Web 浏览器处理。浏览器显示 HTML 并执行 Javascript(以及 Java applet、ActiveX 或 Flash applet),从而向用户显示信息和收集用户信息。表示层是从服务器到客户机的接口,它负责控制输出的格式,让输出可以在客户机上显示。业务逻辑层实施业务规则(比如计算和工作流),从而驱动应用程序。最后,数据访问层是持久化的数据存储,比如数据库或文件存储。 大多数应用程序需要所有这四层的功能,尽管它们可能不需要明显完整地实现这些层。 另一种流行的体系结构是 Model-View-Controller,这是一种用于分隔应用程序组件的模式。在 MVC 模式中,模型封装业务逻辑层,并与框架一起封装数据层。视图负责处理发送给客户机的数据表示。控制器的作用是控制应用程序流程。 层的容量扩展 扩展 Web 应用程序的容量意味着让它能够处理更多的通信流。容量扩展的一个方面是如何根据需求部署硬件。另一个方面是应用程序如何响应新的硬件环境。从概念上说,在出现性能问题时,往往首先想到使用功能更强的服务器;但是应用程序本身很可能造成其他瓶颈。把应用程序划分为一系列层有助于收缩问题的范围,可以简化容量扩展。 现在先不考虑应用程序瓶颈。扩展应用程序的硬件通常有两种方式:水平扩展和垂直扩展。水平扩展意味着在一层中添加更多的服务器。在前面的示例中,应用服务器的瓶颈把请求速率限制在每秒 77 个请求,通过添加第二个应用服务器并在两个服务器之间共享负载,可能可以解决此问题。这会把理论容量提高到每秒 154 个请求,瓶颈位置就会转到数据库。 另一方面,垂直扩展意味着使用功能更强的计算机。可以使用功能更强的计算机运行应用服务器的两个实例,或者更快地处理请求。 初看上去,您可能会完全排除垂直扩展方式,因为购买多台小型计算机通常比不断购买更高级的服务器便宜。但是,在许多情况下,垂直扩展是更好的方法。如果您有通过逻辑分区 (LPAR) 支持硬件分区的 IBM® Power® 服务器,就可以把空闲的容量添加到应用服务器层。 应用程序的需求也可能促使您选择垂直扩展。在一台服务器上很容易通过共享内存段共享用户的会话状态。如果使用两台服务器,就需要通过其他方式共享状态,比如数据库。数据库访问比内存访问慢,所以两台服务器的处理速度达不到一台服务器的两倍。 数据库是另一个常常适合使用垂直扩展的场合。让数据集跨越不同的服务器需要在应用程序层做大量工作,比如跨两个数据库联结列并确保数据是一致的。使用更强大的数据库服务器要容易得多,而且不需要通过重新构建应用程序来支持分散的数据。 把 Web 应用程序建模为队列 根据前面对应用程序体系结构的讨论可以看出,Web 请求会通过多个阶段,每个阶段花费一定的执行时间。请求排队通过每个步骤,完成一个步骤之后,再排队进入下一个步骤。每个步骤很像人们在商店里排队结帐的情况。 可以把 Web 应用程序建模为一系列步骤(称为 “队列”)。应用程序的每个组件都是一个队列。建模为一系列队列的典型 WebSphere 应用程序如图 3 所示。
图 3 显示请求等待 Web 服务器处理它们,然后等待 Web 容器,依此类推。如果进入某个队列的请求速率超过了此队列处理请求的速率,请求就会聚集起来。当出现请求聚集时,服务时间是不可预测的,用户会察觉到浏览器会话延迟。图 3 中的队列代表最糟糕的情况,因为 Web 服务器可以自己处理一些请求,即不需要访问数据库。 队列在 UNIX® 环境中很常见。当应用程序发出磁盘请求的速率快于磁盘返回数据的速率时,操作系统会让磁盘请求排队,还可能调整请求的次序以降低寻道时间。另一个队列是运行队列,其中包含等待运行的进程的有序列表。应用程序会等待轮到它们使用某些有限的资源(比如 CPU)。 因此,队列调优是一种平衡的艺术。队列太小,就会在仍然有富余容量的情况下拒绝用户。队列太大,就会试图为过多的用户提供服务,导致性能很差。 导致情况更复杂的另一个因素是,这些排队位置并不是无成本的。保留排队位置会导致内存开销,对于应用服务器,这会与正在处理请求的线程争用内存。因此,在一般情况下,在应用服务器上排队并不是好方法。推荐的方法是在应用服务器之前(比如在 Web 服务器上)排队。这意味着 Web 服务器要保持与 Web 客户机的连接,并在应用服务器空闲时发出请求。应用服务器只需处理它能够及时派发的请求。 IBM 的文档中推荐了 Web 应用程序布局方法和各种队列的调优方法。但是注意,IBM 建议应该避免在 WebSphere 中排队。这意味着应该把发送给 WebSphere 应用服务器的请求速率控制在能够立即处理的范围内。Web 服务器(或 Web 服务器前面的代理服务器)应该限制过多的连接,让它们等待处理。这确保负载比较重的应用服务器队列能够把时间花在为有限的请求提供服务上,而不是试图同时为所有请求提供服务。 针对开发人员的提示 作为开发人员,应该按照一些一般原则提高应用程序的可伸缩性。这些原则可以应用于大多数 Web 应用程序。 度量设施 应用程序应该以某种方式向收集系统提供度量值(即使收集系统仅仅是日志文件)。这些度量值包括访问应用程序中某个函数的频率或处理一个请求花费的时间等。这并不会使应用程序运行得更快,但是有助于了解应用程序为什么会变慢以及代码的哪些部分花费的时间最长。了解什么时候调用某些函数,这有助于把在系统上观察到的现象(比如 CPU 忙或磁盘活动量高)与应用程序中的活动(比如上传图像)联系起来。 能够了解站点上发生的情况,这是扩展站点容量的关键。您认为不够优化的代码部分可能不会造成问题。只有通过适当的度量,才能发现真正的瓶颈。 会话 Web 在本质上是无状态的。用户发出的每个请求都独立于以前的请求。但是,应用程序常常是有状态的。用户必须登录应用程序以证明自己的身份,在访问站点期间可能要维护购物车的状态,还可能要填写供以后使用的个人信息。跟踪会话是一种成本很高的操作,尤其是在涉及多个服务器的情况下。 在单一服务器上运行的 Web 应用程序可以把会话信息放在内存中,在服务器上运行的任何 Web 应用程序实例都可以访问共享内存。常常会给用户分配一个标志,这个标志标识内存中的会话。考虑一下在涉及第二个应用服务器时会发生什么。如果用户的第一个请求发送给一个服务器,第二个请求发送给另一个服务器,那么会存在两个单独的会话,它们并不相同。 此问题的常用解决方案是,把会话存储在数据库而不是内存中。这种方法导致的问题是,对于每个请求,需要增加数据库读操作,还可能涉及数据库写操作。每个 Web 应用服务器都需要这个数据库。 一个解决方案是,只在需要会话的地方使用会话。应用程序并不为每个请求装载会话,而是只在需要会话时装载会话。这会减少对后端数据库的请求数量。 另一个方法是加密会话数据并把它发送回客户机,这样就不需要在本地存储会话。在用户的 cookie 中能够存储的数据量是有限的,但是 RFC 2109 规定客户机应该能够为每个域名存储至少 20 个 cookie,每个 cookie 至少可以保存 4K 字节的数据。 如果发现用数据库存储的会话是性能瓶颈,而且无法消除它们,那么应该考虑把它们分散到单独的数据库,甚至是多个数据库。例如,可以在一个数据库中存储偶数的会话 ID,在另一个数据库中存储奇数的会话 ID。 缓存 与其他部分相比,应用程序的某些部分会更频繁地修改数据。新闻网站可能每个月只修改顶级分类列表一次。因此,对于每个请求都通过查询数据库获取最新的分类列表是很浪费的。同样,包含新闻稿的页面在其整个生命周期中可能只修改一两次,所以不需要为每个请求重新生成它。 缓存意味着把处理成本很高的请求的结果存储起来,供以后使用。可以缓存分类列表或整个页面。 在考虑缓存时,问自己一个问题:“这些信息必须是最新的吗?” 如果不是这样,就可以考虑使用缓存。在新闻最初出现时,能够及时改变新闻稿可能很重要;但是在以后,每分钟检查一次修改并通过缓存提供页面,就足够了。 一种补充方法是,当底层数据改变时,让缓存的数据项失效。如果修改了新闻稿,在保存它时可以删除缓存的版本。对于下一个请求,由于没有缓存的版本,所以会生成新的数据项。 在使用缓存时,必须注意在缓存项过期或被删除时发生的情况。如果有许多请求在请求缓存项,那么在缓存项过期时,会为许多用户重新生成缓存项。为了解决这个问题,可以只为第一个请求重新生成缓存,而其他用户使用过时的版本,直到新的缓存项可用为止。 memcached 是一种流行的分布式内存缓存系统,在 UNIX 环境中部署的许多应用程序都使用它。服务器运行 memcache 守护进程的实例,这些进程分配一块可以通过一种简单的网络协议访问的 RAM。希望在 memcache 中存储或获取数据的应用程序首先对键进行散列计算,这告诉它们应该使用 memcache 池中的哪个服务器。然后,通过连接这个服务器检查或存储数据,这比磁盘或数据库访问快得多。 在寻找应该缓存的数据时,还应该考虑是否确实需要直接提供这些信息。需要在每个页面上显示用户的购物车吗?只显示总金额怎么样?或者只显示一个简单的链接 “view the contents of your cart”。 Edge-Side Includes (ESI) 是一种标记语言,可以用它把网页划分为单独的可缓存的实体。应用程序负责生成包含 ESI 标记的 HTML 文档,还负责生成组件。Web 应用程序前面的代理缓存根据各个部分重新组装最终的文档,负责缓存一些组件并为其他组件发出请求。清单 1 给出一个 ESI 文档示例。 清单 1. ESI 示例
尽管这个示例非常简单,但是 清单 1 说明了如何把两个文档拼接在一起,这两个文档有自己的缓存规则。 异步处理 还有一个问题与 “这些信息必须是最新的吗?” 相关:“必须在处理完请求时更新这些信息吗?” 在许多情况下,可以获取用户提交的数据并把处理延后几秒,而不需要在处理信息时让用户一直等待装载页面。这称为异步处理。一种常用方法是,让应用程序把数据发送给一个消息队列,比如 IBM WebSphere MQ,等待到资源可用时处理数据。这样就可以立即把一个页面返回给用户,尽管数据处理的结果还是未知的。 请考虑一个电子商务应用程序,用户会在这个程序中提交订单。立即返回信用卡检验结果可能是很重要的,但是不需要让订单系统马上确认订单的所有内容都是有效的。可以把订单放进一个队列中等待处理,这可能会在几秒内发生。如果发生了错误,可以通过电子邮件通知用户,如果用户仍然在网站上,甚至可以把错误通知插入他的会话。另一个示例是报告。不需要让用户一直等待生成报告,而是可以返回 “please check the reports page in a few minutes” 消息,同时在另一台服务器上异步地生成报告。 结束语 应用程序常常采用分层方式编写。表示逻辑与业务逻辑分隔开,业务逻辑又与持久化存储分隔开。这种方式可以提高代码的可维护性,但是也会导致一些开销。在扩展应用程序的容量时,应该了解数据在分层环境中的流动并寻找出现瓶颈的位置。 缓存和异步处理等技术可以重用以前的结果或把工作转移到另一台计算机上,从而降低应用程序的工作负载。在应用程序中提供度量设施,有助于及时了解 “热点”。 应用服务器环境的工作方式与排队网络很相似,一定要仔细地管理队列的大小,确保一层不会对另一层施加过大的压力。IBM 建议尽可能在应用服务器之前排队,比如在外部 Web 服务器或代理服务器上。 仅仅靠投入更多的硬件,很少能够有效地扩展应用程序的容量。常常需要综合应用这些技术,才能让新的硬件发挥作用。 参考资料 学习
获得产品和技术
讨论
|