一种正规的性能调优方法:基于等待的调优
 
2008-12-11 作者: Steven Haines 来源: InforQ
 

企业java应用的性能调优是一项艰巨的、有时甚至是徒劳的任务,这是由现代应用的复杂性和缺少正规的调优方法导致的。现代企业应用与十年前的应用相比差距很大,如今这些应用支持多输入、多输出、复杂的框架和业务处理引擎。而十年之前,基于web的企业应用只是通过网络浏览器获得输入信息,然后与数据库或者遗留系统交互进行后台处理,最后把输出结果返回给浏览器(HTML)。现在,输入信息可以来自HTML浏览器、富客户端、移动设备或者网络服务,它可以跨越运行在不同架构下的servlets或者门户容器,这反过来又可能调用企业bean,外部web服务或者把处理委托给业务规则引擎。每一个这样的组件都可能与内容管理系统、缓存层、众多数据库和遗留系统交互。输出的信息通常以独立于展现层的形式保存,随后转化为HTML、XML、WML或者其他任意客户端需要的格式。现代应用比过去包含更多移动部分和“黑盒子”,这对性能调优提出了巨大的挑战。

除了复杂性提高,性能调优技术其艺术性要大于科学性,还因为大多数性能调优指南都侧重于性能指标,有时晦涩难懂,也可能影响用户体验。本文尝试把性能调优活动变成一种“科学”范畴内的行为,提供了一种可重用的关注用户体验的方法,利用“等待点”(也就是应用中引起某请求等待的部分)分析应用架构。总之,基于等待的调优方法允许性能工程师们通过优化用户体验快速实现可度量的性能提高。

性能调优过程

在详细介绍基于等待调优和等待点分析方法之前,本节首先对有效的性能调优过程做一个概述。性能调优可以简单的概括为四步:

1.负载测试

2.容器调优

3.应用调优

4.迭代

像大多数计算机科学一样,性能调优是一个迭代的过程。首先,创建一个合适的负载测试,其中包含了均衡的、具有代表性的服务请求,这都是容器调优实践可以满足的。随着容器被不断调优和测试压力的增大,应用程序的瓶颈逐渐显现出来。随着应用的瓶颈被定位和解决,应用行为会发生变化,这就要求容器再次调优。在容器和应用之间的迭代过程会一直进行到性能到达可以接受的条件(或者直到项目已经到期必须发布时)。

负载测试方法

启动一个性能调优实践的先决条件是创建一整套合适的负载测试集合。每一个负载测试必须满足以下两点:

  • 代表性,必须体现最终用户的业务场景(或期望的场景)
  • 均衡性,必须符合最终用户不同行为的比例分配

也就是说,负载必须能够按照最终用户的实际操作比例来模拟用户动作。为了说明均衡最终用户动作的重要性,请看下面这个例子:在保险索赔部门,员工执行以下操作:

1.用户上午八点登陆系统。

2.上午每人平均处理五个索赔请求。

3.大约80%的用户忘记在吃饭之前注销账号,导致session过期。

4.午饭后,用户重新登录系统。

5.下午每人平均处理五个索赔申请。

6.下班之前生成两个报告。

7.80%的用户回家前注销账号。

这个例子是一个真实应用的简化版,但是它足够在这些服务请求建立一个平衡。这个场景展现的均衡是:两次登陆,十次索赔处理,两次报告和一次注销。

如果负载生成器把压力均匀分布在不同的服务请求上又会怎么样呢?在本例中,用户登陆和注销功能会接收与处理与理赔请求相同的负载。如果是1000个并发用户,登陆功能会很快崩溃,导致企业投资建立一个能够处理这种负载的登陆组件,而实际上这种负载根本不会发生。更糟糕的是,本例中由于最大的瓶颈看似存在于登陆功能上,所以调优的努力会侧重该功能,而忽视索赔处理功能。总之,一个非均衡的负载可能导致调优过程错误的关注于支持那些绝不会发生的负载的组件,而不是那些真正需要调优的部分!

判断一个负载对于应用是均衡的和代表性的标准,对于测试一个已存在的应用(或者一个新版本)还是一个全新的应用是不同的。

已存在应用

已存在应用跟一个全新应用相比,一个明显的优点是:真实的用户行为可以在实际生产环境中观察获得。根据请求的本质和它们如何被应用定义,可以通过两个选择定义最终用户行为:

  • 访问日志
  • 最终用户体验监视器

对于大多数基于web的应用来说,访问日志提供了足够的资源分析服务请求的本质和它们的均衡关系。

Web服务器可以配置成抓取最终用户请求信息并存储在一 个日志文件中,称之为“访问日志”(因为该文件通常命名为“access.log”)。使用访问日志定位用户行为的关键是应用交互需要能够通过不同的 URI来区分。例如,如果之前例子的动作采用类似“/login.do”、“/processClaim.do”、“/logout.do”的URI,那 么我们可以非常容易的在访问日志中发现这些行为并确定它们的比例。更进一步,通过最频繁的URI来排序访问日志可以快速发现占比例前n%的的若干请求,这 个“n”%应该在80%左右。

访问日志是文本文件,可以手动检查(不是一个很有效率的任务),可以通过编程解析,也可以通过工具来分析。对此有很多商业解决方案,不过Quest Software有一个产品Funnel Web Analyzer,虽然多年以前已经终止开发,但是由于其很受欢迎的命令,公司就作为将其作为自由软件重新发布。Funnel Web Analyzer可以分析大多数访问日志并显示用于创建合适负载测试的信息。

一些应用不像上面提到的那样简单,其用户交互无法很容易的通过一个URI来定位。例如,考虑一个包含前端控制器servlet的应用,该servlet接受一个XML有效信息——并且其业务处理逻辑就存在于该信息中。在本例中,需要另外的工具来侦测其有效信息以判断其符合哪个业务场景。这可以通过使用 servlet过滤器或者一个称为最终用户体验监视器的硬件设备来完成。

不管用户行为是如何获得的,它都是开始任何性能调优实践之前的关键先决条件。

全新应用

由于全新的应用没有任何最终用户行为可以分析,所以对我们提出了一个独特的挑战。定位新应用的用户行为需要三个步骤,如图1所示。

图 1 评估新应用的最终用户行为

第一步,评估最终用户应该会做什么。这步是“猜一猜”的正式说法,但应该是一个经过培训的猜测过程。评估结果应该来自于以下双方的讨论:应用业务负责人和应用技术负责人。应用业务负责人,通常是一个产品经理,负责细化最终用户应该如何使用该应用程序——例如,他可能报告说最终用户会登陆、处理五个索赔请求、过期、处理五个索赔请求、生成两个报告、然后注销。应用技术负责人,一般是架构师或是技术lead,负责把业务交互的抽象列表翻译成用于生成负载测试的技术步骤——例如,他可能报告说登陆通过“/login.do” URI完成而处理一个索赔请求需要五个URI。这些人(或者小组或者一些大型项目里的委员会)应该一起提供足够的信息来建立一个基准负载测试。

我们建立负载测试,用其调整应用和容器,应用程序部署到生产环境中,这一切做完之后,调优工作并没有结束。下一步是验证负载测试集。这通常是一个多阶段的活动:

冒烟测试验证:在实际运行的一两周之内验证原先的评估值是否符合真正生产环境下最终用户的行为。这步验证是为了确认在评估过程中没有明显的错误。

生产验证:一些应用需要用户花时间才能形成统一的使用方式。这个适应的时间长短因应用而异,可能是一个月或者一个季度,不过一旦用户的行为稳定下来,就需要验证最终用户行为是否与评估一致。

回归验证:最好在应用的生产周期中阶段性的验证用户行为,以防止用户行为改变或者引入新的功能或工作流导致用户行为改变。

最后一步,也是经常被忽视的一步,就是反思。根据实际用户行为来反思评估的精确性是很重要的,因为只有通过反思,用户行为才能更便于理解,评估在以后的应用中才能得到提高。没有反思,相同的错误会一犯再犯,最终会增加调优的工作量。

基于等待的调优方法

建好了负载测试,接下来就是决定把调优精力放在何处。大多数调优指南都会提到“性能比率”或者度量之间的关系。例如,某调优指南可能强调说缓存命中率应该达到80%或者更高,因此负载测试应用时调整缓存大小直到命中率达到80%。然后处理列表上的下一个度量值,但是不要忘记验证调整新的参数不会影响之前已经调好的其他参数。

这项工作不仅困难而且效率很低。例如,调整缓存命中率到80%可能是件好事,但是存在一些更重要的问题:

  • 该应用如何依赖于缓存(与该缓存交互的请求比例是多少)?
  • 这些请求对应用中的其他请求有多大影响力?
  • 被缓存的条目的本质是什么?它们真的需要缓存吗?

基于等待的调优方法提出了一个新的思想,即分析应用的业务交互和实现这些交互的底层结构,然后优化这些业务交互的处理。第一步是分析应用的架构以定位实现业务请求的相关技术。每一个技术代表一个“等待点”,或者说在应用的这个地方,请求可能需要等待一些事情才能继续处理。例如,如果一个请求执行了一次数据库查询,则它必须从连接池中获取一个数据库连接—如果连接池里没有可用的连接,则该请求必须等待直到有连接可用。同样,如果某请求调用了一个web服务,而那个web服务维护了一个请求队列(对应一个线程池处理请求),这会潜在的导致请求等待直到一个处理线程可用。

从以上称之为等待点分析的方法中,可以定位两种类型的等待点:

  • 基于层次的等待点
  • 基于技术的等待点

本节首先概述了基于等待点的架构分析方法,然后分别研究了不同类型的等待点。

等待点架构分析

从上面讨论中得出的最重要的结论就是性能调优必须在应用架构的环境中执行。这也是调优性能比率为何如此低效的一个原因:主观的调整一个性能参数到一个最佳值,对应用可能是好事也可能是坏事——因为这可能会也可能不会有利于最终用户体验。

基于等待点的分析是一个分析应用中主要请求处理路径的过程,借此定位潜在影响该请求可能会等待的资源。在等待点分析实践中最有效的办法是定位并标出应用中核心处理路径。这包括一个请求可能访问的所有层次、请求交互的所有外部服务、被做成池的所有对象和全部缓存对象。

基于层次的等待点

一个请求穿过一个物理层,比如在web层和业务层之间切换,或者调用外部服务器,比如web服务,每当这种时候,都意味着伴随着转换,这里存在一个隐式等待点。如果服务器在某一时刻只处理单个请求是没有效率的,因此他们使用了多线程方法。典型的,一个服务器在某个socket端口监听访问的请求——每当收到一个请求它就会把请求放在队列中,然后返回监听其他请求。请求队列对应着一个线程池,负责从队列中提取请求并处理。图2描述了对于三层结构(web服务器、动态web层和业务层)的执行过程。

图 2 基于层次的等待点

因为请求穿越层次的动作会引起请求队列,由相应的线程池处理,这种线程池也是一个潜在的等待点。每一个线程池的大小的调优必须基于以下考虑:

池必须足够大使得访问的请求无需不必要的等待一个线程。

池不能大到耗尽服务器。过多的线程会迫使服务器花费大量时间用于在不同的线程上下文中切换,真正处理请求的时间反而更少了。这种情况通常表现为CPU使用率很高,但是处理请求的吞吐量却降低了。

池的大小不能透支与之交互的后台资源。例如,如果数据库对于单个服务器只支持50个请求,那么服务器不应该向数据库发送超过50个数量的请求。

服务器线程池的最佳尺寸的标准是:对受限制的依赖资源产生足够的负载—最大化它们的使用率而不让其透支。下面的“后退调优”一节有更多调整受限制资源池大小的内容。

基于技术的等待点

基于层次的等待点考虑的是在不同服务器之间传递请求,而基于技术的等待点关注的则是在单个服务器中如何通过有效地内部工作来传递请求。基于层次的调优,类似于IBM的队列调优,只是调整应用的有效第一步,如果忽略了调优应用服务器的内部工作,则会对应用性能产生巨大的影响。这就类似于调整JDBC连接池以发送最佳数量的负载给数据库,但是忽略了检查执行的SQL语句——如果查询需要连接十个表单,每个表单有一百万个记录,则最佳负载可能是两个连接,但是如果我们优化了查询,则数据库可能支持二百个连接。深入研究应用服务器和应用使用的潜在技术,可能存在以下通用的基于技术的等待点:

池对象(比如无状态session bean或者其他应用放入池的业务对象)

缓存设施

持久化存储或外部依赖池

通讯基础设施

垃圾收集

大多数情况下,无状态session bean池的大小被应用服务器优化,不会是一个明显的等待点,除非池大小被手工错误的配置了。但是也存在一些池对象必须手动配置大小——这些可能成为有效的等待点。当一个应用需要一个池化的资源,它必须从池里获取一个资源实例,使用它,然后释放给池。如果池太小,所有的对象实例都在被使用,则请求不得不等待一个实例可用。显而易见,等待一个池化的资源会增加响应时间,如果越来越多的请求被堵塞在等待池化资源,会导致明显的性能下降。另一方面,如果池很大,它可能消耗过多内存,对总体JVM的性能产生差的影响。池的最佳大小需要权衡,只能在对池的利用情况做彻底的分析才能决定。

池化对象是无状态的,这意味着应用从池中得到哪个实例都无所谓——任何实例都行。另一方面,缓存的对象都是有状态的,也就是说当应用从缓存中请求一个对象时,它的目标是一个特定对象。举一个很粗糙的类比说明一下区别:考虑人们生活当中两种常见的活动:超市购物和接孩子放学。在超市中,任何收银员都可以接待每一位顾客,无论顾客选择哪位收银员都可以顺利结账。因此收银员可以池化。但是在接孩子放学时,每一个父母只想要他们自己的孩子,别的孩子是不行的。因此孩子可以被缓存。

如前面所述,缓存提出了一个新的调优挑战。简单说,缓存的目的是在本地内存中存储对象,使应用可以随时读取它们,而不是在需要的时候才获取他们。一个合适大小的缓存可以对通过远程调用加载对象的行为提供明显的性能改善。但是,一个不合适大小的缓存,可能产生明显的性能阻碍。因为缓存维护有状态的对象,所以重要的一点是在缓存中存储最频繁访问的对象,同时保留额外的空间来处理非频繁访问对象。试想如果缓存太小,请求会怎样:

  • 请求检查缓存中是否存在某对象,结果失败。
  • 请求需要查询外部资源获取对象数据。
  • 因为缓存通常维护最频繁访问的数据,所以这个新对象需要添加到缓存中(它正在被访问)。
  • 但是如果缓存满了,必须利用“最少最近访问”算法选择一个对象移除。
  • 如果缓存对象的状态与外部资源不一致,则缓存对象移除之前必须更新外部资源。
  • 新的对象此刻添加到缓存中。
  • 新的对象最终返回给请求。

这是一个低效的过程,如果大多数请求都要执行这些步骤,那么缓存肯定会降低性能。缓存必须调整到足够大以最小化缓存的“不命中率”,一次不命中意味着需要执行前面提到的七个步骤,但是也不能太大导致占用太多JVM内存。如果缓存需要非常非常大才能满足性能需要,那么最好是重新考虑被缓存对象的实质和它们到底是否值得缓存。

类似对象池,外部资源池,比如数据库连接池,也必须足够大以满足请求不会被迫等待池中的一个连接变为可用状态,但是也不能太大,导致应用浪费外部资源。“后退调优”一节讨论了如何决定这些池的最佳大小,但是在本节的上下文中,牢记它们代表了一个明显的等待点。

调优通讯基础设施远远超出了本文讨论的范围,因为其具体实现因产品不同而存在明显的区别,这包括诸如MSMQ、MQSeries、TIBCO等主流产品。但是请记住,如果一个应用与某消息服务器交互,它必须经过合适的调优或者它也代表了一个等待点。

最后一个明显影响JVM性能的等待点是垃圾收集。它不太适用本文中描述的等待点分析过程(检查一个请求,定位导致该请求等待的技术),但是由于它可以对性能产生显著的影响,所以把它列在这里。不同的JVM实现和不同的垃圾收集策略决定了垃圾收集如何执行,但是在很多情况下,一次主垃圾收集(或者说标记—清除—压缩垃圾收集)可能导致整个JVM暂停直到垃圾收集完成。一个显著提高JVM性能的办法就是优化垃圾收集。如果想了解更多垃圾收集的信息,请加入GeekCap讨论应用基础设施调优。

后退调优

现在关于基于层次的和基于技术的等待点的一切都介绍完了,最后一步就是优化每一个等待点的配置。这一步有时被称为“后退调优”,其思想非常简单:

1. 开放所有基于层次的等待点和外部依赖池——也就是配置它们允许过多的负载经过服务器。

2. 根据应用生成均衡的和具有代表性的服务请求。

3. 定位首先透支的等待点,通常是外部依赖,比如数据库。

4. 减小配置以控制等待点允许足够的负载经过外部依赖而不透支。

5. 调整所有其他基于层次的等待点,发送足够的负载经过服务器,最大化受限制的等待点,但是也不让请求等待。

6. 允许所有其他请求在业务逻辑层之上等待,比如web服务器端。

此处的原则就是应用应该发送一定数量的负载给它的外部依赖资源以最大化它们的使用率又不导致透支—并且所有其他等待点应该合理配置以发送足够的负载给这些受限制的等待点。例如,如果数据库对每一个应用服务器最多支持50个连接(例如,配置池容纳40或45个连接)。接下来,如果80个线程产生40个数据库连接,则应用的线程池应配置为80。最后,web服务器在任意时刻应该发送不超过80个请求给每一个应用服务器。

所有基于技术的等待点,比如对象池、缓存和垃圾回收,应该调整到最大化请求的吞吐量使得尽可能快的穿越服务器或者基于层析的等待点之间。

总结

性能调优曾经是“艺术性”多于“科学性”,但是通过结合抽象分析和尝试并产生错误,基于等待的调优方法已经证明能够使该过程更具科学性和更有效率。基于等待的调优首先执行一个应用架构的等待点分析,以此定位有可能导致请求等待的某个技术。等待点来自两方面:基于层次的等待点,代表着跨越应用层次的转换;基于技术的等待点,代表着可能提高或降低性能的技术,比如缓存、池和通讯基础设施。一旦定位好了一系列等待点,调优过程就此开始:开放所有基于层次的等待点和外部依赖池,产生均衡的和具有代表性的负载,然后后退调优,收紧等待点以最大化该请求最薄弱的一环的性能,但是不要透支。基于等待的调优方法在生产环境中已经一次又一次得到了证明,不仅仅是高效的,而且允许性能工程师快速实现可度量的性能优化。


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织