对J2EE应用而言,所选择的基本架构对性能有本质性的影响。假若整体架构充斥着不必要的数据库访问,假若在Java对象和XML文档之间进行过度的转换,或者假若过多地进行远程调用(这是三个最常见的J2EE性能问题的根源),那么你针对某个方法进行单独优化不会带来多大性能提升。 从性能和可伸缩的角度来说,最重要的架构方面的选择是: 应用是否是分布式的?
如果需要在集群环境中运行,采用何种方式进行集群? 访问持久化数据的方式?
让我们一一讨论这些问题。前两个问题有着密切的关系。
对象分布、集群和农场
企业应用通常不会只在一台服务器上运行。水平伸缩(在多台对等服务器上运行)的主要动机是通过联合更多的CPU性能来增加吞吐量;另一个(可能更重要的)目的是为了让程序运行得更稳定,因为单机失效无关大局。
水平伸缩是件复杂的事情,有必要区分出不同类型的水平伸缩,特别是: 对象分布,在这种类型中,具有远程接口的组件是主要的分布单元。应用
程序构筑在分布式组件的外围。 针对部署的集群,这种类型中应用程序的多份部署是分布的主要单元。当
一台服务器接收到请求,它只调用其内部的组件来完成处理。这是我提倡的方式,特别是对于web应用而言。 我们还会看到,根据“希望在集群内复制的内容”不同,这两种类型各自还会有不同的集群风格。 定义
对象分布(object distribution)是J2EE的经典做法:通过针对组件(远程EJB)而非部署来获得水平伸缩。这种模型的目标是,通过把web和业务对象(远程EJB)分布到集群中的不同机器中,来获得负载平衡和可伸缩性。在这种模型中,所有的业务调用都通过RMI/IIOP、或是XML和web service来进行。 针对部署的集群(deployment clustering)是把所有的组件都部署到集群中的每个节点中去。在web应用中,这会导致每个节点都包含“从web层直到数据库访问”的完整J2EE栈。这种模型下,在请求到达节点之前就已经进行了路由分配,而不是在单独的组件调用时进行分配。Martin Fowler在他的Patterns of Enterprise Application Architecture一书的第页强烈建议使用这种方式,并列举出它从编程模型直到性能的优点。这种风格的集群中,当控制权从一台单独的转发服务器中转到具体处理的服务器后,所有的调用都是本地的(通过引用调用)。
不管哪一种情况下,EIS层的资源(比如数据库)都是在的服务器上。虽然这会牺牲部分性能,但可以获得管理上的优势。既然我们终归无法J2EE中间层的同一进程中运行企业级数据库,因此无论如何都无法避免进程间通讯的性能损失。
不管是对象分布还是对部署分布,很多情况下我们都需要在集群的各个节点间通讯。比如,各个节点需要复制数据缓存。稍后我会简单说明这个问题。 集群的风格也是多种多样的,比如一种特别的风格是农场(farm)。在一个农场中,整个应用运行在多台服务器上,但是每台服务器都不知道其他服务器的存在,除了共享资源(比如数据库)外也不需要进行通讯。也就是说,在服务器之间不进行状态复制。 分布式对象带来的麻烦
对象分布的关键问题是,它获得可伸缩性的方式是以大幅度牺牲性能为代价的,结果苦心积虑获得的可伸缩性大半被用来解决系统本身的性能低下了。 在第11章中提到过,真正的远程方法调用起来很慢。(不包括那些对位于本机的远程EJB进 行的伪远程调用。它们速度很快的原因是因为实际上进行的并非远程调用,而是本地方法调用。)因为网络传输以及序列化、反序列化的开销,每次远程调用的代价 都比本地调用高出好几个数量级。因此尽管从理论上来说我们可以拥有任意数量的远程业务对象,但比起“在集群的每台服务器上部署同一个应用”的做法来,前者 需要更多的硬件才能达到同样的吞吐量。 有时候远程调用不是通过RMI,而是通过XML或者web service协议来进行。这还是同样缓慢,而且带来了更多的复杂性。(远程EJB已经让对象分布变得尽可能不带来麻烦了。)
对付远程调用的高昂代价,传统的J2EE方法是通过批处理来尽量减少分布式调用的次数:比如,使用传输对象(transfer object) 一次性传送大量的数据。每次传输的数据多,进行远程调用的次数就少,比“大量的远程调用而每次只传送很少的数据”要快,因为进行远程调用时,在底层发生的 基础设施开销往往要比实际上传输数据本身的开销更大。然而,如果可能的话,如果完全取消分布式边界,性能会提高得更多。使用传输对象也会带来困难,它要求 我们精确地知道每个远程客户端需要多少数据、该在持久化对象的关系图中追溯多深。 因为对象分布可以让承担大量负载的组件分布到多台服务器,所以它轻率地承诺可以从根本上解决可伸缩性问题。然而,从理论上来说,对象的分布只应该针对瓶颈部分,但实际上把整个应用都部署到多台服务器上会更好,因为这样可以减少远程调用开销,也能让目标组件获得足够的CPU能力。
根据我的经验,分布对象在性能上真正起到正面作用,这样的案例非常罕见。在典型的成功案例中,必定会存在一些非常消耗CPU时间的操作,以至于和这些操作相比远程调用的代价可以忽略不计。比如说,有些财务应用中的风险计算会需要极多的CPU能力,大量服务器组成的专用集群能够很好的应付它的需求。对于这种案例来说,远程EJB是一种良好的实现策略。但是这种案例本身就非常罕见。而且,对于需要很长时间才能完成的操作来说,通过消息队列(message queue)进行异步分布可能是比远程方法调用更好的选择。
集群的挑战
就算我们决定采用集群,下面的主要挑战就是如何运行一个集群。这里最大的难题包括:
路由。发送到集群的请求如何被路由到具体的服务器?
复制。有哪些状态(或者其他资源)需要在集群中复制共享?Session状
态是最重要的复制需求之一,另一个需求是确保在集群中使用的共享对象缓存保持一致。 第一个问题不难解决。实际上,路由问题甚至不必由软件来解决。但是,复制是集群中的主要问题和最大的。 实施水平伸缩时,你必须考虑如下的:
性能增长不是线性的。在三台服务器上运行程序带来的吞吐量通常不可能
是在单台服务器上运行的三倍,除非你采用农场模式。如果使用对象分布,从伪远程调用(在同一台机器中)转换到真实的远程调用会显著降低性能,可能还会增加CPU负担,导致降低吞吐量;如果采用集群分布,结果会好些,但也不可能做到性能的线性增长,因为需要session复制,可能还会需要访问数据库这类共享的资源。维护数据缓存在单台服务器上很简单,但是要维护共享数据的缓存则要复杂得多,会带来显著的运行时开销。 在一个集群中能运行服务器的数量通常也有极限,因为通常有管理开销。
比如说,在集群中的服务器越多,复制带来的开销就越大。虽然我们可以通过使用数据库来共享状态,从而降低复制开销,但是对小型集群来说这样性能更差。说到底,除非我们的EIS层资源有无穷的可伸缩性,否则我们在增加中间层的时候仍然会受其拖累。 高可伸缩性的应用首先必须是一个高效率的程序。因为水平伸缩很难得到线性回报,每单位的硬件若能得到更高的吞吐量,整个应用就更具可伸缩性。 一个经常会被忽略的重要问题是,EJB常常并非是集群环境下需要考虑的最重要问题。下表中列出了当应用程序面对集群和复制问题时各个层面的问题:
结构层 Web层 问题 在哪里解决 讨论 请求如何被路由到web容器? 硬件路由器 Apache等web服务器的插件 Web容器,例如假若不考虑状态WebLogic的路由本身倒HttpClusterServlet 复制,是相对简单。状态复制给“选择哪个服务器来处理请求”增加了。 路由可能按照不同的算法进行,比如轮换、随机和负载估计。 如何复制HTTP 由Web容器处理。可能session状态,采取的策略有: 来提供错误恢 在集群内广播改变复能力? 了的数据 把状态保存在数据库 把状态备份到一立的“备用”服务器 不同的方案在可靠性、性能和可伸缩性方面有不同的取舍。比如,把状态备份到数据库是一个可靠的方案,也可以支持很大的集群规模(假设数据库能够处理这么大的负载),但是会对性能造成严重的负面影响。 “经典”的分布式J2EE应用通常包含web层缓存来尽量减少对远程EJB业务对象的调用。假若我们取消远程EJB,这就变得不那么重要了,因为调用业务对象不再会大量消耗性能。 若能用HTTP session对象的形式来管理状态会更好。因为web层需要保持有状态session bean的引用,而如果我们又在业务层复制状态,通常我们就需要处理两个(而不是一个)复制问题。假若使用本地业务对象,就不会有“从web层为它们传递状态”的性能损耗;而对于远程session bean 对频繁显示的通常由web层的代码提数据是否有缓供缓存来处理 存,来避免对业务层的重复操作? 业务对象层 复制有状态的业务对象的状态 EJB容器会管理有状态session bean的实例 来说,使用有状态的业务对象会带来负面性能影响。 把请求路由到无状态业务对象实例 在节点间维护一致的缓存 EJB容器把调用路由到远程EJB 只在分布式架构中需要。 O/R mapping层 O/R mapping工具,比要维护一个针对如TopLink、Hibernate事务的分布式缓或者JDO实现。 存是个复杂的问题。 若使用CMP entity bean,则由EJB容器负O/R mapping 责解决。 产品通常把问题交给第三方产品,比如Tangosol Coherence去解决。 CMP entity bean的实现通常不比上面的方案更成熟。比如,在每次对集群中的实体操作前必须调用ejbLoad():这是巨大的管理开销 数据库 水平伸缩,横跨RDBMS厂商。Oracle 9i 多台物理服务RAC就是这样的产品之器的数据库仍一。 然可以作为一个逻辑服务器提供服务 数据库的这种伸缩能力是非常重要的,否则数据库可能会变成整个应用伸缩的瓶颈。 从上面这些“集群和复制带来的难题”中,你可以看出什么?对于不使用entity
bean、运行在单一节点的应用,EJB容器无法解决上述的任何难题。因此,我们可以得出一个重要的结论:EJB容器对可伸缩性没有任何贡献——既没有正面的,也没有反面的。
实际上,复制的难题全然没有涉及到业务对象层。最重要的复制服务是由以下部分提供的:
web容器,复制任何session状态。
O/R mapping 层,它通过复制数据来确保在数据库之前有一致而连贯
的缓存。 数据库,本身就拥有跨越多台物理服务器的可伸缩性。
在这样的应用中,使用EJB但是不采用entity bean(这也是目前EJB社群公认的最佳实践),我们可以使用web容器来管理HTTP session状态,使用与JDO结合的本地session bean来管理持久化。在这样的结构中,最重要的复制在两个地方发生:
web容器之间交换session状态。
在各个服务器节点的JDO二级(PersistenceManagerFactory级别)
缓存之间。这种复制常常由专用的第三方缓存产品提供,比如
SolarMetric Kodo JDO常常与Tangosol Coherence一起提供高效率的事务性缓存。
在这一体系结构中,本地SLSB间没有复制或者路由。因此EJB容器不在集群管理中,关键的分布式数据缓存并不是由应用服务器处理的,而是由持久化技术提供的。
通过减少在HTTP session对象中保存的数据,以及使用足够细化的session对象(而非一个庞大的对象),可以帮助提高web容器更好地进行状态复制。因为这样可以只复制那些被更改过了的对象,而不是每当session状态有所改变,就序列化整个庞大的对象。
无状态业务对象不需要复制,并且只有在为远程客户提供服务时才需要路由。其他情况下,路由选择早在业务对象被调用之前就已经完成了。
数据访问常常是水平伸缩的关键。集群的成功往往是由下面几件事情决定的: 如果有O/R mapping层,它在分布式环境中缓存数据的效率如何?这是大多数entity bean实现表现差劲的地方。
数据库是否能承担整体的负载?有效的数据缓存能够保护数据库,但是我
们也应该记得数据库自身往往也包含有高效的缓存,特别是高端数据库——比如Oracle 9i RAC——本身就可以通过集群部署获得高度的可伸缩性。 有些应用自己维护分布式数据缓存很难获得好处:比如,假若数据是写多过于读的,这种情况下可能直接使用JDBC操作数据库更合适。
在集群的J2EE应用中,解决复制问题的常常并不是EJB容器,而是web容器和数据访问层。被广泛信奉的“EJB是集群所必需的”这一信念,其实是个谬误。
希望通过牺牲性能而获得更好的伸缩性——通过更多的远程调用——是危险的。不仅仅是我们无法获得那些失去的性能,我们还需要更大数量的服务器才能获得同样性能,因为每台服务器都在浪费资源,用于为网络中传送的数据进行打包和解包。也就是说,我们可能达到水平伸缩的极限。
不保存服务器端状态的应用,其可伸缩性是最佳的。比如在web应用中,如果我们只需要保存很少一点用户状态,我们可以把它保存在cookie中,就不再需要任何形式的HTTP session状态复制了。这样的应用是高度可伸缩的,也是极为稳定的。硬件路由设备可以高效地在服务器之间平衡负载,或者替换任何发现出问题的服务器。 数据访问
数据访问的方式对性能有重大影响。假设使用关系数据库,拥有下列特性是很重要的:
高效的数据库结构,能够快速执行常见的查询 数据库更新的次数要尽能少
每次更新都要高效,牵扯的内容越少越好
高效的数据缓存,如果应用程序的数据访问有责任进行缓存的话。 从可管理性和性能两方面来考虑的话,理想的数据访问是:使用一个自然而高效的RDBMS数据结构,以及一个自然而高效的对象模型。也就是说如果采用O/R mapping,我们就需要一个成熟的解决方案,它应该能够映射对象继承关系、能够利用目标数据库的优化能力、还能在必要的时候访问储存过程。
基于以上原因,entity bean很少能够具有成熟的O/R mapping方案同样高的性能。(虽然只是一个令人遗憾的方案,但entity bean通常还是被用作O/R mapping。)BMP entity bean通常天生低效,因为存在“n+1查找问题”(n+1 finder problem)。CMP entity bean会好一点,但是性能往往也不够高:
EJB QL不像SQL等查询语言一样富有表现力。有些在SQL和Hibernate
HQL这样的类SQL语言中能够高效进行的操作,在EJB QL中就无法高效进行。 如果需要将多个持久化对象关联起来,entity bean的处理效率比基于
POJO的O/R mapping要低得多。 entity bean模型必须使用事务,而这可能并不是必须的。
和最好的O/R mapping技术相比,entity bean实现并不能做到同样高
效的分布式缓存。 通过Hibernate或者优秀的JDO实现所能做到的、真实的O/R mapping常常很有价值,是获得一个良好的领域模型的最佳方法。然而也要记住,O/R mapping并非到处都合适。
“ORM到处都适合”的观念,代表的是J2EE架构师对关系数据库广泛的不信任。在Expert One-on-One J2EE Design and Development一书和其他场合,我不断在质疑这一点。不可理解,为何这种不信任在J2EE社群中占据如此重要的地位?关系数据库对有些事情处理得非常好,特别是在针对集合进行的操作上。还有,储存过程有时候能够减少对数据库的过多使用,提供高效的关系操作。
关系数据库不会因为J2EE社群不怎么喜欢它们就消失。早在J2EE出现之前很久,这些数据库就已经在那里,而且在可预见的将来它们还会在那里,用户已经在RDBMS上投入了大量的资金。
当然,如果可能的话,我们不希望把业务逻辑也放到数据库中。但是持久化逻辑是另一回事,关于它是否应该属于数据库的一部分还存在很大争议。比如说,如果用储存过程来更新数据库表、从而将表的细节隐藏起来,我们就可能通过改进设计来把我们的Java代 码和数据库结构分离开来。只需要保证储存过程具有同样的参数,我们就可以自由改变底层的数据库结构,或者迁移到另一个数据库。只要在储存过程中不进行业务 操作——譬如说,不需要生成主键,不需要针对一笔新数据更新好几张表,也不需要根据条件或者角色不同来选择进行操作——这种方法就可以继续使用。
一般来说,假若我们发现某个操作需要通过O/R mapping对很多记录进行枚举的迭代操作,比如计算统计值,那么最好不要通过O/R mapping来进行这个操作,尽可能将它放到数据库内部去。不过,如果我们需要在客户端显示这些记录,就应该产生代表它们的Java对象实例。 其他体系结构方面的问题
还有一些结构性的问题可能影响性能。在实际应用中,我最常看到的三个最重要的问题分别是:XML处理、表现层技术、以及如何选择应用服务器和其他产品的专有(proprietary)特性。
无论如何,为整个技术架构的每个部分进行性能评测是非常重要的。也就是说,必须把系统耗费的全部时间细化,分别衡量每部分占用的时间。本章的后面会讨论采样评测。
XML的使用
XML现在几乎变成Internet的通用语了。在用于把不同技术实现的系统松散连接起来时——特别是web service成为事实标准后——XML是富有价值的技术。但是,在J2EE应用内部采用XML进行通讯很不合适,虽然这种做法现在像感冒一样流行。
Java对象和XML之间的相互转换代价是非常高昂的。XML数据绑定能降低代价,但是不能消除它。
目前流行的J2EE体系结构中,“在应用程序中大量使用XML”被认为是一种好的做法,但这种做法直接损害了系统性能。
表现层技术
表现层技术(以及我们使用这些技术的方式)对性能有重大影响。比如说渲染一个JSP,可能比从数据库访问获取必须的数据花费的时间还长。一些不良习惯(比如过度使用自定义标签)也会降低性能。
使用XSLT有一些其他的好处,但是XSLT转换对性能而言也是昂贵的。如果它被用来产生表现层的结果,会导致显著的性能下降。XSLT引擎的性能参差不齐,因此选择好的引擎至关重要。
因为表现层技术及其使用会显著影响性能,对表现层单独进行评测会很有用处。
可移植性与性能
有 时,可移植性的目标与性能互相冲突。特别是当无法用标准的技术应对所有需求时,这个问题就显得更加突出。架构师往往以“纯净”的理由拒绝使用产品或厂商专 有的扩展,但这可能是以性能为代价的。如果你采用的技术平台对某种特殊操作提供了专有的、非标准的高效方法,你就应该考虑这个操作是否有性能上的要求、是 否应该采用这种非标准的技术。同时,你应该尽可能遵循良好的OO实践,把这些不可移植的部分隐藏在便于移植的接口后面。
不同实现的选择
在说到“架构”这个词的时候,我们指的是一些大问题,比如是否需要分布?是否采用O/R mapping?完成这些选择之后,我们还要针对具体实现进行选择。 对于本地业务对象,采用本地EJB还是采用AOP注入操作代码后的POJO? 使用EJB容器还是IoC容器? 使用Hibernate还是JDO实现?
本节中,我会从性能的角度来思考这些选择,也会说明为何这些选择通常不像“根本性的架构选择”那样重要。我将重点介绍:摆脱EJB对性能造成怎样的影响。 随后,我将带领读者考虑代码优化的问题,以及如何以尽量少的工作量和代价获得尽量大的性能提升。
摆脱EJB服务设施对性能的影响
抛弃EJB而采用我们在本书中所提倡的体系结构,对于性能和可伸缩性会大有帮助。这样的观点是否让你吃惊了?每当听到别人批评EJB在其它方面的弱点(例如复杂度)时,EJB的鼓吹者们总是会反驳说:如果容忍那些缺陷,EJB具有比其他竞争对手更好的可伸缩性和吞吐量潜力。难道这不是事实吗? 在本节中,我会进行一些质疑,看看到底如何。
请考虑下列情况:我们有一个服务接口,可以选择采用本地无状态session bean,这样就可以利用CMT等EJB企业服务;也可以选择POJO,它能够利用具有AOP能力的轻型容器(例如Spring)提供的企业服务。作为参考比较,稍后我也会考虑真正的远程EJB调用的性能结果。
虽然声明式服务比较引人注目,但这里主要关注业务对象调用的性能问题。声明式服务的性能开销——不管是对于EJB还是其他技术——只有在大量细粒度对象(例如一个巨大结果集中返回的持久化对象)的情况下才会成为一个问题。稍后我也会讨论整个应用程序的性能潜力,并通过web层行为来客观衡量它。 基准评测方法
下面的讨论基于一次基准测试,被测目标是一个访问关系数据库的web应用。被测应用会进行下列操作,来模拟典型web应用常见的几种操作。
处理请求,这个请求需要对业务对象进行反复的调用,而业务对象则会迅
速返回结果,以模拟“返回缓存数据”的情况。这一操作不需要事务。 建立订单、更新库存,这需要更新两张数据库表。这是通过一个PL/SQL
储存过程进行的。 用户发起的订单查询。
由于这些情况不能从缓存中获得好处——基准测试也不是为了比较缓存实现——因此业务对象会借助DAO访问数据库,后者直接使用JDBC。应用程序会从事先拟定的范围中随机选取用户和物品ID来进行“建立订单”和“订单查询”的操作。
被选择进行测试的实现包括:
远程部署的远程SLSB,使用CMT进行事务管理
本地部署的SLSB,使用CMT——对于大多数web应用,这是推荐的EJB
架构 全局共享单一实例的POJO,通过Spring AOP引入事务功能:这是我们
对大多数情况推荐的架构 通过Spring AOP基础设施进行池管理的POJO,也用AOP引入事务功能 全局共享单一实例的POJO,通过自编程实现事务管理,没有任何的声明式
服务 所有的POJO都部署到一个web容器中,该web容器与EJB容器在同一个应用服务器中运行。所有的情况下,都使用同样的JSP来生成内容。底层的事务基础设施都是应用服务器的JTA实现。不同的实现都从同样的容器DataSource获取数据库联接。数据库每次运行后都清空。
每种情况的业务对象中的代码几乎都是同样的。 EJB扩展了POJO实现,并实现session bean生命周期必须的方法,具体实现采用了Decorator模式,本章稍后会讨论这个模式。为了尽量减少JNDI查询的次数,用于获得EJB引用的服务定位器实现缓存了EJBHome对象。
不管是EJB还是AOP,唯一需要的声明式中间件服务只有事务管理服务。在“模拟返回缓存数据”的情况下,无须事务管理。
负载测试模拟具有长延时的重负载,同时测试稳定性和吞吐量。下面将要列出的结果是采用一个流行的开源应用服务器测试得出的。我们也使用一个高端的商业产品进行了测试,各项数据都有了相当显著的提高,但同样显示本地EJB不如Spring方案——但差距已经缩小了很多。
下面的基准测试运行于Sun JDK 版本1.4.2_03,操作系统是RedHat
Enterprise Linux 3.0。应用服务器设置使用HotSpot Server JVM,其垃圾收集和堆栈设置已经预先设置成为服务器应用优化。应用服务器运行于双Xeon 2.8GHz处理器的机器,配置2GB内存和两个SCSI硬盘,并且使用RAID 0来优化性能。因为基准测试包括远程SLSB,一台类似的机器被作为客户端,两
台机器通过GB速度的电缆相连来消除网络开销。数据库(MySQL 4.0.16)也为测试配置了合适的缓存和缓冲区大小。所有的测试执行时都使用干净的数据库,之前服务器会被重启,等待一段预热时间。测试使用ApacheBench进行。 我们也在不同的系统上执行测试,也换过不同的数据库(把MySQL换成Oracle),操作系统则是从Red Hat换成Windows 2000/XP,结果大同小异。如果对这次基准测试有更多的兴趣,可以点击Spring网站
(www.springframework.org)上的“Performance”链接,下载源代码和详细的结果。
感谢Alef Arendsen提供了生产级别的硬件并且采集大多数性能结果。 声明式中间件模型和编程式中间件模型
在讨论线程模型之前,首先我们看看事务管理的方法。
使用CMT的远程SLSB
当EJB真正以远程方式运行时,除非业务操作本身就很慢(比如,需要插入到数据库),否则远程调用的总体开销对吞吐量和响应时间有巨大的负面影响。
使用CMT的本地SLSB
本地SLSB的性能会好很多,吞吐量的提高引人注目。与执行业务对象所需要的代价相比较,在同一进程中执行EJB调用的代价就很低了。
用AOP进行事务管理
Spring AOP方式的性能比本地EJB又有了显著提高——当然,就更不用说比远程EJB好得多了。Spring AOP框架实现声明式服务的代价就算与本地EJB调用比也小得多。区别主要体现在调用“不使用声明式服务的方法”时,此时Spring AOP实现的开销仅是开源EJB容器的零头,也比商业EJB容器稍微少一点点。这是一个很有趣的结果,因为商业EJB容器使用Java代码生成和预编译来避免使用任何反射操作,而Spring AOP使用了动态代理或者CGLIB代理。在Java 1.4 JVM中,反射不再是代价高昂的操作了。
不出所料,假若被拦截的业务方法进行了昂贵的操作(比如数据库插入),AOP和EJB调用的开销之间的差别就变得不那么明显。此时,不管AOP还是EJB基础设施的开销都被执行数据库操作的时间所掩盖了。然而,拦截的性能越高,就意味着越多的CPU资源可以用于I/O操作,因此J2EE服务器所占据的时间也变少了,所以仍然会得到显著的回报。也可以在类似Tomcat这样的web容器中运行这个基准测试,此时便不使用JTA,而是使用Spring为JDBC提供的PlatformTransactionManager和第三方连接池(例如Commons DBCP)。假若有不止一个需要事务的数据源,这种方法就不能奏效,但这仍然是一个有用的选择,因为这样在任何web容器上都可以允许声明式事务管理,不需要改变编程模型。(和EJB不同,Spring AOP既可以向高端伸展,也可以向低端收缩。)此时的性能有一部分取决于你选择的连接池的效率,但结果仍然可以证实:Spring拦截的性能比EJB的性能更高。
编程实现事务管理
这里我们使用一个事务装饰器(decorator)子类。比如,我们覆盖业务facade上的placeOrder()方法,在之前开始事务,在之后提交或者回滚事务,并且仍然调用超类的实现来完成必要的业务逻辑。对“返回缓存住的结果”的模拟操作来说,就不需要提供任何事务装饰器。
我们之前说过,JTA是一个难用的API,我们使用Spring事务API作为JTA之上的一个薄中间层。我们的代码是这样的:
public void placeOrder(long userid, Order order) throws NoSuchUserException,
NoSuchItemException, InsufficientStockException {
TransactionStatus txStatus = txManager.getTransaction(new
DefaultTransactionDefinition()); try {
super.placeOrder(userid, order); // Leave txStatus alone
} catch (DataAccessException ex) { txStatus.setRollbackOnly(); throw ex;
} catch (NoSuchUserException ex) { txStatus.setRollbackOnly(); throw ex;
} catch (NoSuchItemException ex) {
txStatus.setRollbackOnly(); throw ex;
} catch (InsufficientStockException ex) {
txStatus.setRollbackOnly(); throw ex;
} finally {
// Close transaction
// May have been marked for rollback txManager.commit(txStatus); } }
注意我们使用了Spring TransactionStatus对象的setRollbackOnly()方法,在捕获到意外的时候进行回滚。
我们使用了简化的抽象API,因此以上的代码看上去没那么复杂。如果某个简单的应用只有一两个需要事务的方法,这个方法比设置EJB或者Spring AOP以便使用声明式事务管理还要来得简便。当然,如果很多方法中都需要这样的代码,编写这种代码就令人痛苦,犯错误的机会也更多。
如果用到了装饰器模式,通常暗示着可能应该采用AOP方法。
这次基准测试在底层使用Spring JtaTransactionManager实现。这是一个基于JTA的很薄的层。因此在所有的测试中,这和使用基本的事务设施没什么不同。
和Spring AOP的方法一样,Spring的编程式事务管理方法也可以使用非JTA的底层事务(比如JDBC),事务机制的转换不需要修改程序代码。
我们可能认为,自己编程会比声明式方式更快,但实际上这里只有2%到10%的性能提高。这个结果说明,相对于花费在自己编写事务管理的力气而言,回报实在太少。付出了程序开发和维护上的代价,获得这很少的性能提升,实在太不值得了。
相比而言,自己编程管理事务是一种复杂的编程模型,除非有必要的理由才能这么做。不仅仅是因为编写的代码越多就越容易有潜在的错误,而且我们的代码还会变得难于测试,因为为了测试我们的业务逻辑,必须提供事务基础设施的测试替代(stub)或者模仿(mock)对象。如果我们使用Spring对事务的抽象,能减少一些痛苦,因为它把JTA这样难于配置和替代的底层事务设施隔离开来。但是,更好的做法是:根本不编写、也就无需测试事务代码! 线程模型
现在来看看我在第12章中建议的线程模型。SLSB实例池和下面的替代方案比较,结果会如何呢?
全局共享的、可以多线程执行的对象(不是像SLSB实例池那样,而是用
一个对象应对所有的客户端)
对POJO提供透明的实例池,而不使用EJB 下面的讨论会和前面谈到的声明式服务模型结合起来。
对非EJB的选择,我主要关注Spring,因为它令程序员从业务对象的生命周期管理中脱离出来——同样,除了Spring IoC容器之外,这里也有AOP的功劳。 稍后我会把这个基准测试和事务管理的基准测试放在一起讨论。
共享的多线程可执行对象
第12章中我提出过一个论断:对于大多数无状态服务对象,“全局共享唯一实例”是最合适的线程模型。
正常情况下,由于这样的对象没有“既可读又可写”的状态,因此无须同步。如前所述,即使多线程执行的对象需要一些同步,只要同步代码能快速运行,性能也不会受到显著影响。
因为基准测试的目的是进行仿真测试,不准备包含需要同步的情况,因此我们不包括同步。
在所有的测试中,共享的多线程执行对象都比并置的EJB对象池具有更好的性能,不管是在开源应用服务器还是在商用服务器上。
Spring实例池
下面看看基于Spring的实例池。这需要使用Spring AOP和
org.springframework.aop.TargetSource接口的实现——在这里是基于Commons Pool 1.1的实现。
org.springframework.aop.target.CommonsPoolTargetSource类作为Spring的一部分发布。这提供了类似EJB的编程模型,业务对象在编写时可以假设为是单线程的。我们不需要修改目前的任何代码,因为业务对象中没有线程问题。我们把池的大小设置为和在EJB 测试中SLSB的池大小一致。 在我们的测试中,假若web容器的配置是合理的,Spring实例池要比单个共享的实例的性能差。如果web容器允许非常多的连接(这应该算是一种不恰当的配置),业务对象的实例池会提高性能。
在开源应用服务器上,Spring池的性能比EJB实例池有非常明显的优势。在高端商业服务器中,Spring池的性能还有一些差距,说明服务器的EJB池实现极为高效,击败了Spring的池实现所使用的Commons Pool 1.1。(假若能够使用其它的池产品来重新运行Spring基准测试,应该会很有趣。)不管怎样,看上去EJB容器的实例池并非魔术,Spring可以对POJO达到同样的效果。Spring的池性能会有一些优势,除非EJB容器特别高效。
因为Spring IoC容器提供的解耦能力,在共享实例和实例池模型间切换的时候,客户端代码和业务实现代码都不需要任何改变。
结果总结
开源应用服务器和Spring方案的结果对比如图15-1和15-2所示,我对“响应时间”数据做过一些处理以便于图形展示。左边的两个结果来自远程EJB和本地EJB,之后两个来自Spring共享实例和实例池方案,在最右边是没有使用EJB和Spring声明式服务、而是自己编程管理事务的结果。
图15-1显示了“业务操作瞬时完成”时的性能,这种情况是在模拟返回缓存中的值。这测试了业务facade中部分操作无需事务、而另一些则必须具有声明式事务管理的情形(因此,可能对所有的方法都需要某种形式的代理)。吞吐量和响应时间的差别在远程访问和最慢的本地访问之间大约是10倍。当然,需要两台服务器才能获得远程访问的结果,因此它需要的硬件资源两倍于其它的所有测试。
测试显示Spring AOP实例池获得了最佳的性能。在多个环境下运行测试,我们后来发现如果能优化web容器的线程池大小,就算在最重的负荷下,共享实例也能胜过实例池。然而,因为我们不能在前面介绍过的硬件配置条件下重新运行所有的测试,我们这里采用初始的测试结果。
图15-2显示了如果业务操作本身代价高昂时的性能。此时我们使用JDBC查询数据库(没有缓存),获得平均50个对象的结果集,此时远程和本地调用的差别就小多了。创建订单的结果对比情况非常相似。硬件和软件配置与前面的测试一模一样。
对于典型应用,远程调用的真实代价应该在这两者之间。远程业务对象的有些方法会反应迅速,远程调用的代价就显得很高;对于慢速的操作,远程调用的代价则似乎不是问题所在。假若机器之间使用慢速网络相连,远程调用的代价会变得非常高。(以上测试中的连接带宽是1GB。)在所有的测试中,远程调用的CPU占用率都比本地调用高,表示虽然测试结果是通过两台性能很高的机器得出的,远程调用还是比本地调用消耗了更多的服务器能力,这也许能对“对于成为性能瓶颈的业务操作,远程调用可以充分利用CPU计算能力”这一论断提供一个反证。
在所有的测试中,对比使用同样的JTA事务管理的本地EJB调用,在吞吐量和响应时间两个方面,Spring方案都清楚地表明了其优越性。 可以大致总结如下:
“调用EJB很慢”这个假设是错误的。本地EJB和并置运行的远程EJB性
能都不错。在现代EJB容器中,在本地使用EJB并不会造成很大的开销。 远程EJB调用有很高的性能代价——对于大多数web应用,这个代价都太高了。 非EJB的方案能比本地EJB获得更好的性能,还能用更灵活的方式达到同
样功能。除非你能用得起最高端的应用服务器,没有任何测试可以证明本地EJB比提供同样的声明式服务的Spring AOP效率更高,实际上EJB的性能看上去明显要低。Spring AOP方法能够提供声明式事务管理,而开销比本地EJB CMT小得多。 不采用声明式服务、而是自己编程管理事务,无法得到合理的性能回报。
在我们的测试中,这只获得了不到10%的性能提升——不足以抵消付出的复杂性和潜在的错误的代价。编程获得事务要比声明式配置麻烦得多。 假若web容器的线程池大小得当,实例池比全局共享的单个实例性能要差。 虽然我们建议使用多线程运作的共享对象作为大多数业务对象的首选模型,假若你希望使用单线程模型,也未必使用EJB。在我们的测试中,Spring的池比开源应用服务器的SLSB实例池的性能好得多,也足以和高端商业服务器相提并论。 在Spring和EJB的声明式服务之间的性能比较,部分取决于你使用的应
用服务器的品质。尽管在所有的基准测试中Spring的性能都比EJB来得好,如果转到不那么高效的应用服务器中,它的优势将更加明显。 没有证据表明对于声明式事务或单线程模型来说,本地EJB的性能更好。别的方案也可以让我们给POJO加上同样的行为,而且更简单、更灵活,性能也至少与EJB相当(甚至更好)。
选择EJB也不是一个稳定的方案。在我们的测试中,当操作需要耗费很长时间时,Spring实现在各种线程模型中负载并发会话的能力与EJB相当,并且占用
的CPU更少。最引人注目的是,它的响应时间更短、更具一致性,说明它可能比EJB更加稳定。(当然,必须说明:EJB和Spring方案都很稳定,即便负载量很大也是如此。)
缓存和代码优化
现在,让我们简要考虑一下其他的实现问题,主要是与代码优化相关的问题。
代码优化,以及为何要避免它
代码优化不仅仅是提升性能的错误方法:实际上,在大多数情况下,它都是有害的。
它很困难:代码优化会需要很多工作量 它可能降低可维护性,引入bug 它常常是不必要的
杰出的计算机科学家高德纳(Donald Knuth)写道:“我们应该忘记大约97%的小优化,不成熟的优化是万恶之源”(着重号为本书作者所加)。这真是明智的建议。
我 们应该对优化采取保守的态度,小心对待它。优化——不管是架构性的还是实现性的——都只应该针对执行缓慢的操作来考虑。虽然这可能是一个显而易见的观点, 但我常常见到开发者在一些代码上殚精竭虑,仅仅因为他们知道这一小段代码还可以运行得更快——虽然这些代码在任何现实场景中都算得上“足够快”。应该考虑 从架构和实现中加以优化的、最明显的慢速操作的代表包括: 过多的数据库访问 远程方法调用
导致最大性能损失的通常是整体架构,而不是糟糕的实现代码。
当 然这不是说我们在编写程序的时候应该把性能因素扔在一边。有些开发者不管处理什么问题都是用最慢的方法,但是根据我的经验,有能力的开发者想出的最简单直 接的办法通常会是最优的。通常会有更快、但稍微绕点弯子(因此比较难于维护)的解决方法,但是获得的回报通常不足以抵消其复杂性。 需要优化的是设计,而非代码。
缓存
缓存是一种介于架构和代码级别优化之间的重要优化。
虽然合理使用缓存能获得很大的性能提升,但缓存是复杂的,很容易引发错误。如果没有强烈的需要,最好不要实现任何缓存。Expert One-on-One J2EE Design and Development的第15章详细讨论了缓存的优缺点,包括缓存和采样的例子。这里不会再次重复,只关注一些缓存的基本方针。
只应该在对业务需求有明确认识的前提下才实施缓存。是否能容忍部分数
据过期?如果出现竞争,有什么隐藏的含义? 数据读超过写的程度越多,缓存就越有价值。如果对被缓存的数据进行大
量写操作,缓存会变得难以实施,获得的性能提升也会大幅下降。 在结构的每一层都可以进行缓存。最好避免重复缓存,要考虑整个应用结
构,而非只关注某个特定层。从EIS层开始,可以进行缓存的地方有: 数据库
O/R mapping层 业务对象 web层对象
过滤器与JSP标签缓存
通过HTTP头控制的浏览器web缓存
假若不采用分布式架构,数据访问或者业务层缓存能获得更大的好处。在传统的分布式J2EE应用中,数据访问缓存与web层距离太远了。 简单场景的缓存往往能带来很大的效益,比如对只读数据的缓存。 如果在应用程序中需要考虑复杂的并发问题,可以考虑采用第三方缓存产
品,或者至少使用一个并发类库来协助处理线程问题。 对业务层来说,AOP是一个优秀的缓存选择。
很多J2EE开发者(包括我在内)都有一种近乎直觉的假设:即便一次方法调用命中了被缓存的中间层业务对象,仍然会造成高昂的开销。这可能是因为使用分布式EJB的经验留下的后遗症:对分布式EJB进行的调用都是远程方法调用,因此方法调用本身开销也很大。如果使用并置的架构,特别是如果你发现自己正在用复杂的设计避免调用中间层,就应该问问自己:是不是这种后遗症又开始发作了。
当然,你仍然需要控制数据访问,因为这的确很慢。但是,此时进行缓存的最佳位置可能是中间层业务对象,而非web层。
不妨把上面列表中的最后一点(即AOP)稍做展开讨论。广为人知的缓存方法之一就是使用Decorator设计模式:实现一个缓存装饰器(decorator)类,它与目标类实现同样的接口,但是会进行一些数据缓存,从而减少对目标调用的次数。(这也可以被认为是缓存代理。)
如果使用Decorator模 式,我们可以编写这样一个实现,将所有方法委托给原来的对象,并对某些方法进行缓存。我们也可以继承原对象,并覆盖一些方法来提供缓存。这两种方法都不够 优雅。使用第一种方式,即使只需要处理某几个方法,也需要对所有方法编写代码,造成很多重复的代码。继承的方式能避免这个问题,但是无法为同一接口、不同 实现类型的目标对象提供缓存,也就是说,缓存被紧密绑定到一个具体的类。
AOP用来对付这种横切注入非常理想。我们可以使用缓存注入代码(advice)来把缓存方面(aspect)的代码从客户代码中完全分离出来。通过切点
(pointcut),AOP给了我们有力的手段来指定哪些方法是缓存的目标。我们也可以考虑采用元数据属性(metadata attribute)来标注可以缓存的值。(比如,指出getHeadline()的方法可以在10分钟之内保持缓存。)
这种通用的缓存甚至可以在集群环境中提供一致性。可以切换不同的缓存实现,而无需改变应用程序。
这种缓存可以对任何业务对象都有效,不会丢失任何强类型检查。 在Spring和其他基于的AOP框架中,这可能体现为缓存(caching interceptor)的形式。
潜在代码优化
通过下列技巧,可以让危险变得最小,而代码优化获得的回报最大: 只进行必要的优化。使用基准测试和采样来确定代码优化的关注点。 关注容易进行的优化。常常通过简单的优化就可以得到很好的回报。应该
避免采用复杂的优化,以免得不偿失,还降低可维护性。 有一个详尽的回归测试套件,才是安全的。如果详尽的测试套件无需更改
就能通过,你就可以知道:你的代码做到了原来所有的事情,而且更快,这真是太痛快了! 有很多优化技巧,我们不可能在这里罗列他们。我们会选取一些简单易用见效快的优化。请参见本章后面的“资源”部分,那里有很多有用的参考资料。 从算法开始
在考虑底层代码细节之前,先检查运行缓慢的方法所使用的算法。是否有冗余?是否有被忽略的缓存的机会?对此问题是否有更好的算法? 选用正确的集合类型
有时候,选择正确的集合类型可以大幅度提高性能。选择正确的数据结构——比如,在合适的时候选择哈希表而不是列表——应该成为你的直觉。但是,同样数据结构的不同实现可能也有很大的性能差异。比如,如果你需要读入大量元素,预先获取一个大的ArrayList可能比使用LinkedList快得多。在ArrayList中进行随机访问也比LinkedList快。
看一下java.util包中它们的不同实现就知道为什么会这样了。有时候看看标准类库的源代码也是大有裨益的。 避免不必要的字符串操作
Java中的字符串相加是很慢的。字符串是不可变的,也就是说不可以被修改其中的内容,每次需要相加的时候都会创建一个新对象。通常使用StringBuffer比String效率高。 不必要的log 请看下面的语句:
for (int i = 0; i < myArray.length; i++) {
foo.doSomething(myArray[i]);
logger.debug(“Foo object [“ + foo + “] processing [“
+ myArray[i] + “]”);
}
这段代码中写log的部分看起来没什么问题,结果耗时却比数组操作还多。为什么呢?
因为字符串操作很慢,而且常常没有在编程的时候考虑效率问题(可能会显示一些很少使用的对象状态,而这些状态值需要额外的计算才能得到),而且toString()调用通常是很慢的。因此上面的代码中,在循环中不加保护地使用log输出并不合适。
因此,至少我们需要加一点保护:
for (int i = 0; i < myArray.length; i++) {
foo.doSomething(myArray[i]); if (logger.isDebugEnabled()) {
logger.debug(“Foo object [“ + foo + “] processing [“
+ myArray[i] + “]”);
} }
这样好多了,因为“检查一个特定的log级别是否打开”要比字符串处理快得多。但是在这种情况下,值得去想一下:这里的log有什么用处吗?是否真的有人会
打开log察看这么长的输出?而且,在这个循环中输出那么多信息,就会淹没其他的调试记录,让它们难以使用。
如果你大量使用调试log,就算对log类库的isDebugEnabled()这样的方法调用可能都是昂贵的。(这些方法很快,但是仍然会消耗一些资源。)这时,应该把方法调用结果放在一个boolean变量中。 有争议的优化
上面的优化方法,从代码质量的角度来说都是完全的保守方法。但是,在特殊情况下,也可以使用一些激进的代码优化方法,比如:
控制继承树的深度。虽然太深的继承树可能暗示设计很糟糕,不过我们通
常不想让性能因素影响面向对象设计。但是,在对性能要求苛刻的场合,消除继承也能明显提高性能。在很特殊的情况下,可以考虑是否可以容忍靠拷贝粘贴部分代码,而不是把它们重构到一个基类中去。 直接操作对象字段,而非通过方法访问。通过让AOP代理直接访问
org.springframework.aop.framework.AdvisedSupport代理管理器类的字段、而不是通过方法调用访问,我降低了Spring AOP大约20%的开销。这需要把AdvisedSupport中的一些变量的访问级从private变成protected(通常为了加强类继承树中的封装,默认访问级都是private)。这种情况下,获得这样的性能提升是值得的。而在一般情况下,不应该这么做,不应该把类之间联系得如此之紧。
用局部变量来代替实例变量。局部变量在堆栈中分配,访问起来快得多。如果某个实例变量被反复访问,把它复制到局部变量中能提高性能。 只有在你绝对以性能优先的时候以上这些技巧才是有价值的。因为Spring AOP框架是很多应用程序的基础设施,因此它的确对性能要求苛刻;然而,很少有应用程序的代码需要这样做。只有在有足够的证据时,你才能考虑使用这些具有侵入性的技巧来优化特定部分的代码。
一般来说,要避免出现会让代码变得复杂、或者增加维护工作量的优化。 改进测试的机会
有时候,进行一项优化可能破坏你或者你的同事编写某个测试套件时的假设,比如可能会把“每次创建一个新对象”改为“始终重用同一个对象”。可能会带来原先的测试未曾覆盖的失败情况,比如对象状态可能不一致。这些情况就提醒你应该在进行优化之前编写更多的测试,将这些失败情况检查出来。无论优化是否成功,新加的测试都可以对防止未来的bug有所帮助。
好的开发者总是不断寻找让测试变得更严格的机会。有时候在进行优化之前强化测试套件是很重要的。
调优和部署
通常,在深入优化程序代码之前,最好能检查一下部署的调优选项。调优部署的工作量要小得多,也不会带来复杂的维护问题。有时候它能提供非常惊人的效果,同时提高程序稳定性。
JVM
首先,为你的应用服务器和应用程序选择合适的JVM。我已经发现BEA JRockit有很高的性能,对长时间运行的服务器端应用,大概会比标准的Sub JRE快2到4倍。但是,你应该运行你自己的基准程序,因为不同的JVM有不同的长短之处,没有一个适合所有的应用。
下一步,正确地配置JVM。查阅它的文档,以及你的应用服务器提供的文档,确保你针对应用服务器和应用程序正确地配置了那些重要的设置,包括: 初始堆尺寸与最大堆尺寸 垃圾收集选项 线程选项
下面的连接有一些示例:
http://edocs.bea.com/wljrockit/docs81/tuning/:“调优WebLogic
JRockit 8.1 JVM http://publib7b.boulder.ibm.com/wasinfo1/en/info/aes/ae/urun_rconfproc_jvm.html:“Java虚拟机设置:WebSphere应用服务器” 在项目生命周期的前期就要进行基准测试,选择合适的JVM,进行合适的设置。
应用服务器
首先,针对你的性能需求选择合适的应用服务器。不同应用服务器的性能大相径庭,在同样硬件上的性能可能差别很大,也就是说:在应用服务器这里多花一点钱,也许就可以在硬件这里少花一点钱。对于典型的web应用,不同应用服务器的性能可能相差3~4倍。
假若你必须选用不是那么高效的服务器,轻型容器方案就更为吸引人了。把低效的EJB容器换成Spring AOP,你可以避免服务器中的很多低效的代码,同时JTA实现与容器数据源等重要特性仍然得到保留。
第二步,针对你的应用特点定制应用服务器配置。应用服务器设置对性能和吞吐量有重要的影响,包括:
数据库连接池大小。这对吞吐量有至关重要的影响。连接池太小会导致线
程因等待连接释放而阻塞;而太大的连接池在集群环境中可能带来问题,数据库可能耗光连接。 线程池大小。线程池太小会浪费CPU能力,太大的线程池可能反而降低性
能。 使用SLSB时的实例池大小。它的影响和线程池相似。不要把它设置得比
线程池更大。 使用SLSB时的事务描述符。除非业务对象的所有方法都是需要是事务的,
不要对所有方法都使用同样的事务声明。确定每个方法都有合适的事务
属性:如果不需要事务就是none。
HTTP session复制选项。根据你的应用程序选择是把session保存到数
据库还是保持在内存中,仔细检查你的应用服务器提供的选项。 JTA选项。假若只使用一个数据库,必须确保你没有使用开销巨大的XA
事务。 log设置。应用服务器和应用程序一样都会产生log输出,这可能造成巨大
的开销,记住让你的服务器只产生必要的信息log。 应用服务器厂商通常会提供关于可用选项的良好文档。
框架配置
我们前面说过,持久化技术对性能和可伸缩性有巨大影响,因此是否正确配置它们极为重要,特别是使用缓存(和分布式缓存)的时候。
如果你使用Spring之类的框架来取代中间件的一些职责,而不是采用EJB容器,你需要确认你正确配置了它。它的原理和EJB是相似的,但是具体的设置有所不同。
比如说,如果你使用前面讨论过的Spring池,和决定无状态session bean 的池大小类似,你需要设置合理的池大小。Spring AOP框架中的不同选项也可能影响性能,不过默认配置在大多数主流应用中都运作良好。重要的Spring设置还包括事务AOP代理的事务设置。和SLSB一样,不要为那些无须事务的方法创建不必要的事务,这很重要。
对于所有参数,都要有能方便重复运行的基准测试来检查配置变更的影响。理想的基准测试是长时间的负载测试,在测试新配置的性能的同时检查代码是否稳定。
数据库配置
数据库是否正确配置极为重要。数据库服务器是复杂的产品,DBA对于项目成功至关重要。每种数据库有不同的设置,重要的参数有:
线程模型 池 索引
表结构(Oracle等数据库为不同的使用模式提供很多不同的表结构) 查询优化选项
数据库产品通常会提供采样工具,有助于指出哪些地方可以提高性能。
一种循证的性能策略
性能问题可以并且应该以科学的方式来解决。然而,现实生活中却常常不是这样的。我不断看到这样的问题: 没有尽早收集性能的客观证据 根据没有证据的猜测来进行性能优化
第一个问题会浪费工作量和时间——比如,一个需要验证的结构没有及早得到验证,到后来才发现它有性能问题。前一章中,我讨论过在项目周期中及早创建垂直切片(vertical slice)或者可执行架构(executable architecture)的重要性。在性能问题上,它们能够帮助验证性能指标,这是很重要的。现代方法学——即便在实践上有所不同,例如RUP与XP——都强调这类架构验证的重要性。不幸的是,实际生活中这还是做得太不够了。
假若没有及早发现并避免性能问题,等到后来才来解决可能就太晚了;但事情常常还没这么简单:整件事情可能会演变成一场政治辩论,你得花费更多努力才能找到问题所在,而解决问题的难度就更大了。(我看见过好几个项目在出现问题时将其掩盖起来,而不是集中精力解决问题。)
在项目早期,要做垂直切片试验,来验证备选架构的性能,确认它能够满足需求。
因为性能的重要性、以及优化的代价和风险,不应该在没有证据基础的情况下做出性能判断,因为那有可能导致浪费时间在优化那些根本不造成任何问题的代码上。
在没有实证的时候,千万不要做出重要的决策。
让我们看看有哪些方法可以用来收集必须的证据,来指导我们的调优过程。
基准测试
在项目的早期,要进行基准测试来确认能达到性能目的。我经常看到架构师把基准测试步骤推迟到项目有显著进展之后,因为他们之前没办法得到生产级别的硬件,这不能成为理由:在开发者的桌面计算机上,已经可以获得很多有关性能的信息了。能够运行现代IDE的机器,只要能够快速连接数据库之类的资源,就有能力为服务器端应用提供很高的吞吐量。
基准测试本质上用于减轻风险。假若能发现危险的瓶颈,就发挥出了它们的用处。假若一台P4桌面计算机只能得到每秒两次事务(2TPS)的性能,那么就不难得出这样的结论:再多的硬件也无法满足100TPS这一要求。
在开发者的桌面计算机上运行起来慢得令人担心的应用程序,在生产硬件上也会很慢。
基准测试必须仔细规划,因为有很多的变数需要控制(之前就讨论过一些),比如:
JVM版本
应用服务器配置(假若需要在部署环境中运行基准测试) log级别 网络配置 数据库配置 负载测试工具
可以用于基准测试的工具有很多。但是,讨论不同的工具已经超出了本章的内容。 理想情况是,很多测试可以在容器外针对应用程序代码运行。但是,针对现实的发布配置进行基准测试也是很重要的。通常发布基准测试应该先运行,然后根据分段基准测试的结果来确定哪些部分可以改善。
对于有web界面的并置应用,在容器内运行基准测试非常容易,因为有很多web负载测试工具可供选择,比如有:
Microsoft Web Application Stress Tool(WAS,
www.microsoft.com/technet/treeview/default.asp?url=/technet/itsolutions/intranet/downloads/webstres.asp):有些陈旧了,但是易于使用,也是免费的负载测试工具。 Apache JMeter(http://jakarta.apache.org/jmeter/):纯Java测
试工具,可以用于测试纯Java类。配置起来相当复杂。 The Grinder(http://grinder.sourceforge.net/):另一个Java测试
工具,也可以用来测试纯Java类。配置起来相当复杂。 ApacheBench
(http://perl.apache.org/docs/1.0/guide/performance.html#ApacheBench):这个工具在任何Apache web服务器发行包中都已经包括。这是一个很简单的命令行测试工具,可以把服务器推至极限。没有提供复杂的脚本功能。 性能测试要像功能测试那样可以重复,相关的方法已经有人整理成文。注意别犯常见的错误,比如:
不要轻信单次测试的结果。结果必须可以重复。 小心别让负载测试软件扭曲了测试结果
注意别让负载测试软件与运行被测试应用程序的应用服务器竞争CPU时间。
对于web应用来说,最好在其他的一台或多台机器上运行负载测试,以消除任何此类影响。 假如能对程序的不同层面——比如展示层、业务对象(访问测试数据库或者替换的DAO)和数据库查询——分别进行基准测试,这会很有帮助,这能让我们清楚地看到:应该在哪些部分集中进行采样测试。
对 于令人担忧的基准测试结果,随后的两节——“采样”和“诊断”——介绍了后续应该采取的步骤。假若你的应用已经能够满足性能要求,就不要浪费时间来采样和 优化了。当然,进行优化可以让它更快,而且可以享受挑战自我的满足快感。(我个人很喜欢采样和性能优化。幸运的是,因为的很多时间是在编写基础代码,我比 大多数应用程序开发者更经常享受这种乐趣。)但是,没有理由的话,不用让它变得更快。
可以进行一次快速的采样来看看是否有明显的瓶颈,但是不要在性能上浪费太多努力。总是有好多其他事情等着你做呢。
采样(Profiling)
如果我们发现了问题,如何逐步逼近它呢?
我强烈建议使用采样器,它可以帮助你明确分析每个方法使用了多少时间。 为什么要使用采样器?
在你发现性能问题后,为什么需要采样器呢?直接检查代码不能够发现瓶颈所在吗?
当然,对于一些常见的问题,可能可以预见到问题所在。比如: 我们知道,字符串操作很慢。
我们知道没有isInfoEnabled()保护的log语句假若调用了速度缓慢的
toString()方法、或者简单的字符串相加,也会变得很慢。
但是,我们并不知道是否这些“慢速”的代码是否真的造成了问题,我们也不知道解决这些问题是否值得。对toString()值进行缓存可能造成问题,除非我们确信要这么做,不然这不是个好主意。
坚实的证据是无可替代的。采样可以指出哪一段慢速代码真的在惹祸。比如,我对性能采样有很多经验,对于特定方法我可能可以精确的给出各个片断执行时间的预测。但是我不能精确预测整个应用中需要照管的数千个方法。计算机处理这些任务比人可好多了。采样器可以轻松做到这件事。
采样器必定也可以指出哪些慢速的代码可以忽略不管。对于性能问题,很多开发者都抱持某些信念,这些信念或多或少有有一定的事实根据,比如:
反射很慢 对象创建很慢
我曾经看到过:在缺乏有效证据的情况下,开发者就把编写得非常漂亮的反射代码删除了,而用更复杂和晦涩的代码取而代之;或者实现复杂的池机制,以避免创建对象。这些假定很有可能是错误的。(在现代的JVM中,相比于实际运作的方法,反射已经相当快了。对象创建也很少再造成问题。)
因此,对应用程序代码采样是很重要的,不管是否在部署环境里都是如此。 采样要基于现实的应用场景。不要人为地制造那种会大量触发慢速代码、但不大可能在现实中出现的场景。
一开始,我通常在应用服务器内运行基准测试;而当我知道需要调整哪段程序的时候,会创建很多在外部运行的采样过程(在IDE里面)。通常我可以从调用堆栈中猜测出现问题的区域,通过与此关联的基准测试来确认它。假若这不可行,我就会在应用服务器中对应用程序采样。
我会尽量减少在应用服务器中运行采样的次数。虽然在IDE内部运行应用服务器、或者通过特殊的JVM配置来启动一个可以远程debug的应用服务器都是可以做到的,但这种做法有几个很重要的缺点:
和基础设施一起启动JVM会把整个现场都变得非常的慢。也就是说你的应
用服务器启动的时间可能是以分钟计,而非以秒计。 很难把“杂音”过滤出去,比如应用服务器自己的代码。
尽量减少代码对EJB容器或者应用服务器的依赖的好处之一就是,你可以从容器外运行采样。比如,遵循本书建议的结构指南,你可以在 IDE中对绝大多数应用代码进行采样,而不需要启动应用服务器。
对于调试来说,也能获得同样的好处。能够在应用服务器之外进行调试会变得容易许多。当然,有时候我们需要知道代码和J2EE服务器是如何交互的,但是更多的是要追踪那些肯定位于应用程序代码内部的错误。这时就和单元测试一样,我们也希望避免任何外部关系。
记住,因为采样器本身的消耗,也会拖慢性能曲线。因此要经常重新运行不包含采样的初始基准测试,来验证是否有性能提高。
几 乎可以肯定,通过采样逐次得到的性能回报会逐步减少。通常你可以很轻松地摘下那些“较低的果子”,比如说去掉所有不必要的字符串操作等等。做完这些以后, 你可能会发现自己已经取得了不小的成果,看到基准测试的切实提高会让你兴奋不已。但是,你会发现下面需要进行越来越多的努力,得到的性能提升却越来越少。 你也会发现:为了得到这样很少的提高,你开始试图采用一些对可维护性会产生伤害的优化方法。我是唯一一个感受到这种诱惑的程序员吗?我深表怀疑。采样和代 码覆盖分析一样,假若使用得当都是非常有用的工具,但是千万不要让它诱惑你,毁了你的正常工作方式。
采样对定位问题来说是无价之宝。它也能指出设计的哪一部分需要修改。
采样也是一种观察调用栈的有趣途径。这可能有助于理解代码的动态行为,也是观察代码静态结构的有益补充。你可能发现简单的代码错误,比如,不小心调用了一个方法两次,而应该使用第一次调用返回的对象引用。
因此,就算我没有遇到性能问题,我也喜欢进行采样——虽然这种情况下我不会花很多时间在上面。 采样实战
下面的笔记出自于我自己对Spring和其他项目使用采样工具的实际经验。在进行采样时,我依照下面的步骤:
1. 运行一次有实际意义的,典型的基准测试。
2. 使用采样器再运行同样的基准测试,可能减少负载量或操作数,以便平衡
采样器对性能带来的影响。 3. 按照采样的结果修改代码,每次修改后重新运行完整的单元测试套件。 4. 去掉采样,经常重复第2步和第3步来检查是否性能得到了提升。第1步
就不需要经常重做了。 一些小提示:
确保你的采样测试运行了足够多的操作,避免测试启动带来开销造成结果
失真。 配置好你的采样器,把类库排除在外,以免淹没你自己应用程序的信息。 我通常使用Eclipse Profiler插件
(http://eclipsecolorer.sourceforge.net/index_profiler.html)进行采样。对Eclipse用户来说,这个工具容易配置,用起来也很直观。这个采样器可以运行任何Java应用程序(也就是说,有一个main入口点),并且完全支持从采样结果跳到对应的源代码。
图15-3和15-4展示了对Spring AOP框架的一百万次调用的采样结果——这足够消除启动对结果带来的影响。我展开了线程调用树,来显示所有占用超过5%运行时间的方法。这个线程调用树同时也指出了运行时的结构。
因为采样器在IDE内运行,所以可以从调用树直接跳转到源代码元素去,也可以用图形化的方式察看“热点”方法。
另一个有用的视图是“反转调用树”(图15-4):展示调用链末端的方法,并按照它们的执行时间排序。这对于观察哪些方法值得优化很有帮助。
以前我用过Sitraka JProbe,作为商业产品它很优秀,但同时也很昂贵。我发现Eclipse Profiler对于大多数要求都可以满足了:特别是我喜欢在容器外采样。但是,商业产品通常对在应用服务器中采样提供了更好的支持,也能生成更详细的图表。
诊断
除了采样,我们也可以进行一些诊断,特别是对于部署环境。
好的采样工具可以让我们察看每个类创建对象的个数。从JVM也可以获取这种信息。对部署后的环境这会特别有用。假如你发现问题是因为过多的对象分配和垃圾收集造成的,检查一下你的JVM的文档,看看有什么标志你可以设置(比如-verbosegc),通过这些启动标志,你可以得到垃圾收集进程的信息记录。 通过应用服务器的控制台和其他监视工具,你也能在部署后的环境中观察连接池以及正在进行的事务的行为。
诊断对于生产系统特别重要,因为性能情况可能和负载测试的时候有所不同。要能够在不对性能或者稳定性造成负面影响的前提下进行行为分析,这是很重要的(因此不能使用采样)。
资源
对代码优化和其他性能问题来说,我建议继续阅读下面的参考资料:
Java Performance Tuning,Jack Shirazi著(O'Reilly出版)。这本书提
供对Java性能问题的精彩讨论。虽然这本书主要关注底层代码优化技巧,没有详细覆盖J2EE,仍然值得任何对编写高性能代码有兴趣的开发者阅读。 Shirazi的web站点:www.javaperformancetuning.com。这个站点
有很多有用的链接,也比上面的书多了很多有关J2EE的内容。 Expert One-on-One J2EE Design and Development,第15章(“性
能测试与应用调优”),本人所著。这本书讨论了对J2EE有用的代码优化技巧,包括采样和负载测试工具,以及——假若你必须使用分布式结构的话——提高序列化性能的技巧。 J2EE Performance Testing with BEAWebLogic Server,Peter
Zadrozny、Philip Aston和Ted Osborne 所著,Expert Press 2002年出版。这本书对很多基于实证的案例进行了精彩讨论,虽然有点过时了,仍然值得一看。
总结
基础架构的选择很大程度上决定了企业应用的性能特征。没有比“分布式架构还是并置架构”更基本、更重要的抉择了。我们的基准测试显示,在真实的web应用场景中,远程EJB调用比本地EJB调用慢整整一个数量级。
如果你关注性能,除非商业上需要,不要采用分布式架构。“依靠分布式架构获得可伸缩性”的说法是非常值得怀疑的。
通过并置部署所有应用组件,我们可以避免远程调用的高昂代价。假若必要,我们可以对整个应用部署进行集群。
除了远程调用的巨大开销之外,只要使用得当,EJB中没有任何部分是天生缓慢的。现代的EJB相当高效,对本地EJB调用没有很大的开销——不过毫无疑问,在开发、部署和测试方面,EJB带来的成本很高。因此,假若使用EJB,我们不必担心调用本地EJB的性能代价,也不应该认为本地EJB会增加开销而减少EJB的使用。
但是,从性能角度而言,EJB背后没有什么魔术。在本章中,我针对典型的web应用的场景,比较了本地EJB和Spring AOP架构的基准测试成绩,后者也提供了声明式事务管理和——可选的——线程管理。这二者在使用底层J2EE服务上是等价的,因为Spring部署也配置为使用JTA作为底层事务基础设施。 Spring方案比EJB方案的性能更高。我在所有的EJB容器中都得到了同样的结果,包括高端的商业产品。随EJB容器不同,性能的差异程度也有所不同,在某个不是特别高效的EJB容器中表现得特别明显——那是一个流行的开源产品。说得更具体点,Spring方案比EJB方案提供了更短的响应时间,反应也更一致。 这是个重要的结果。性能是一个非常重要的考虑因素。我介绍的轻量级Spring解决方案在几乎任何重要的方面都比本地EJB更好——开发效率;成熟的IoC方案;容易测试,与TDD兼容;避免OOP的那些不好的效果;更加灵活的声明式事务管理;真正的AOP框架。现在,我们又亲眼看到EJB方案不能提供更好——甚至哪怕只是与Spring方案相同——的性能和可伸缩性,这就连选择EJB的最后一个论点都驳斥了。
EJB的声明式服务(比如CMT)和为POJO提供同样服务的高效的AOP实现相比,没有效率或者可伸缩性上的优点。我们已经在比较本地EJB方案和Spring AOP方案时得出了这一结论。
数据访问方法对性能和可伸缩性有重要影响。这里两个重要的选择是:是否适合采用O/R mapping,以及如何在分布式环境中获得一致的数据缓存。O/R mapping对于基于集合的关系操作不合适。在适合采用O/R mapping的场合,我们建议采用专业的O/R mapping产品,比如Kodo JDO或者Hibernate,或许应该和一个成熟的分布式缓存产品一起使用。
不用硬性要求使用O/R mapping。假若你使用O/R mapping,就应该使用一个成熟和灵活的O/R mapping工具。
总而言之,要记住简单通常有助于带来良好的性能——通常也有助于按时交付、并获得良好的可管理性。使用EJB常常带来了不必要的复杂性。杰出的J2EE作家和咨询师Bruce Tate在接受JavaPerformanceTuning.com的一次采访时表明了这一点
(www.javaperformancetuning.com/news/interview036.shtml)。Tate重申了简单对性能带来的功效。记者问他:“你所见过的获得最大性能提升的项目中,采用了什么变化?”他的回答是:“我们抛弃了EJB,用简单的POJO方案来代替。”
因为性能和可伸缩性对项目的成败有重要影响,所以必须根据坚实的证据来做出性能方面的决策。我的建议是:
在项目的早期,对垂直切片进行基准测试
在开发周期中,要反复进行性能测试,检查是否有性能衰退 使用采样工具来检查瓶颈,确保性能优化都是有的放矢
我也介绍了一些代码级别的优化技巧,它们可以获得性能提升。但是要警告你,这些技巧也许会损害代码质量和可维护性,并且只能得到很少的性能提升,你必须做出权衡。
更明显的性能提升很可能并非来自代码优化。如果你能选择最适合需求的应用服务器,同时确保服务器、JVM和其他核心软件(比如数据库)都被配置在最佳状态,你得到的性能提升将胜过任何代码优化。
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- zrrp.cn 版权所有 赣ICP备2024042808号-1
违法及侵权请联系:TEL:199 1889 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务