为性能做规划

为性能做规划

作者/Christian Antognini

资深数据库专家,从1995年就开始致力于探究Oracle数据库引擎的工作机制。长期关注逻辑与物理数据库的设计、数据库与Java应用程序的集成、查询优化器以及与性能管理和优化相关的各个方面。目前任瑞士苏黎世Trivadis公司首席顾问和性能教练,是OakTable网站核心成员。

如果仔细分析程序开发各阶段所需开展的工作,你也许会注意到每个阶段都有性能要求。即便如此,在实际开发过程中,开发团队还是会时常忘记性能要求,直到性能问题浮现出来。而那时也许为时已晚。因此,接下来的内容将从性能的角度出发,介绍在下一次开发应用程序时不应该再忽视的内容。

需求分析

简单来讲,需求分析(requirements analysis)就是确定应用程序的主要目标以及藉此期望达成的目的。进行需求分析之前,通常要对多个利益相关方进行调研。这一步十分必要,因为单独一方不太可能确定所有的业务需求和技术需求。由于需求的来源不一,因此必须对需求进行仔细分析,尤其需要找出不同需求间是否存在潜在冲突。在进行需求分析时,不仅要关注应用程序需要提供的功能,仔细确定这些功能的使用率也是至关重要的。对于每一个具体的功能,需要预估与之交互的用户数量、用户的使用频率以及每次使用时的预期响应时间。换句话说,你必须确定预期的性能指标。

这些性能需求不仅仅作为核心要素贯穿于应用程序开发的各个阶段,稍后也可将其作为定义服务级别协议以及制定容量规划的基础。

服务级别协议

服务级别协议(SLA)是用来明确服务提供商和用户之间关系的契约。它描述的内容包括服务项目,其在运行时间和停机时间的可用性、响应时间、客户支持水平,以及一旦服务提供商无法履行协议时相应的处理方式。

只有在能够验证响应时间的情况下,才能根据响应时间制定服务级别协议。这需要定义清晰的、可测量的性能数据以及与之相关的目标。这些性能数据通常被称作关键性能指标(Key Performance Indicator,KPI)。最理想的情况是使用一种监控工具收集、存储和评估这些指标数据。事实上,这样做的目的不仅是为了在某个目标没有达到时进行标识,还能为日后出具报告和制定容量规划而记录下依据。为了收集这些性能数据,可以采用两种主要的技术手段。第一种是利用监测代码(instrumentation code)的输出结果;第二种是使用响应时间监控工具。

分析与设计

架构设计师根据需求设计解决方案。开始的时候,为了定义架构,需要考虑所有的需求。事实上,对于一个需要承受高负载的应用程序,在设计之初就应考虑负载需求。当设计时用到诸如并行化、分布式计算或结果重用等技术时更应如此。例如,设计一个支持少数用户每分钟执行十几个事务的C/S应用程序,与设计一个支持成千上万用户每秒执行数百个事务的分布式系统完全是两码事。

有时需求也会通过在某一资源的使用上施加限制来影响架构。例如,如果一个应用程序用于通过低速网络连接到服务器的移动设备,那么其架构设计必须考虑能够支持较大延迟和较低吞吐量。通常,架构设计师不仅需要预见到一个方案可能出现的瓶颈,还要衡量这些瓶颈是否会危及需求的实现。如果架构设计师没有掌握足够的信息来进行这样的关键预先评估,就应该开发一个或甚至多个原型。在这方面,如果没有前一阶段收集的性能数据,将很难做出明智的决定。我所说的明智的决定是指那些能够实现以最小投资支撑预期负载的架构/设计的决定:简单方案处理简单问题,精简方案处理复杂问题。

编码和单元测试

专业开发人员编写的代码应具有下面这些特点。

稳定性:拥有应对意外情况的能力是所有软件都应具备的特性。为了达到预期质量,必须定期进行单元测试。这一点对于迭代型生命周期尤为重要。实际上,在这类模型中,快速重构现有代码的能力是不可或缺的。例如,在调用某个子程序时,如果传递的参数值超过指定范围,系统就必须能够做出相应处理而不至于崩溃。如有必要,应该同时生成有意义的错误信息。

可维护性:能够长期运行、结构良好、已文档化的可读代码比没有文档化的糟糕代码维护起来要容易得多(维护费用也更低)。例如,有人将多个操作写成单独一行晦涩的代码,这样的开发人员其实选错了展示才华的方式。

运行速度:代码应该进行优化,以期尽可能提高运行速度。在预期负载很高的情况下更应如此。代码应该具有可伸缩性,进而能够利用额外的硬件资源应对用户或事务的不断增加。例如,应该避免不必要的操作、串行程序,以及低效或不适合的算法。然而,一定不要掉进过早优化的陷阱。

精明的资源利用:代码应尽最大可能利用可访问资源。注意,这并不总是意味着使用最少的资源。比如,应用程序使用并行化操作比串行化操作要消耗更多的资源,但是有时候并行化也许是解决苛刻负载的唯一途径。

安全性:毋庸置疑,代码要拥有保证数据机密性和完整性,以及对用户进行验证和授权的能力。有时,不可抵赖性也是需要考虑的问题。例如,可能需要用到数字签名来防止终端用户否认通信或合同的有效性。

可检测性:检测的目的有两方面。其一,更易于分析出现的功能问题和性能问题(即使是精心设计的系统也无法避免这些问题的出现);其二,有策略地添加代码以提供应用程序的性能信息。例如,通常情况下,添加用以获取某一操作所耗时间的代码非常简单。这是一个验证应用程序是否能够满足必要性能需求的简单有效的办法。

不仅这些特性彼此之间确实存在一些冲突,而且预算通常是有限的(有时甚至非常有限)。因此,我们通常有必要在这些特性之间做个优先级排序,在其中找到平衡点,以便在有限的预算下实现预期的需求。

集成和验收测试

集成和验收测试的目的是验证应用程序的功能需求、性能需求以及系统稳定性。性能测试和功能测试同等重要,这一点无论如何强调都不过分。从各方面来看,一个性能差的应用程序和没有实现功能需求的应用程序一样糟糕。在这两种情况下,应用程序都是无用的。然而,只有明确定义过性能需求才有可能去验证它。

缺少正式的性能需求会导致两个主要问题。第一,极有可能在集成和验收测试阶段没有执行严格的、有条不紊的压力测试,这样,应用程序就会在不知道是否能够支持预期负载的情况下交付生产;第二,就性能而言,无法明确什么样的表现可以接受,什么样的表现不能接受。通常,只有在极端情况下(也就是说,性能非常好或非常糟糕),不同的人才会达成统一意见。如果无法达成共识,冗长、恼人、徒劳的会议以及人际冲突就会随之出现。

在实践中,设计、实现和执行良好的集成和验收测试来验证应用程序的性能表现并非易事。要想取得成功必须面对下面三个主要挑战。

设计压力测试时应该考虑能够产生典型的负载。对此主要有两种方法:一是让真实的用户做真实的工作;二是用工具来模拟用户。两种方法各有优缺点,应该根据具体情况进行具体分析。某些情况下,两种方法可同时用于对不同模块进行压力测试,或者用两种方法进行互补。

要产生典型的负载,就需要典型的测试数据。不仅数据行的数量和大小要符合预期的量,数据分布情况和数据内容也应与真实数据一致。例如,如果属性中含有城市名称,那么用真实的城市名称就比用像Aaaacccc或Abcdefghij这样的字符串要好得多。这样做很重要,因为很多情况下应用程序和数据库都会因为不同的数据导致不同的表现(例如,索引或作用于数据的函数)。

测试的基础设施要尽可能与生产环境的基础设施保持一致。这对于高度分布的系统和需要与许多其他系统协作的系统来说尤其困难。

在顺序型生命周期模型中,项目开发接近尾声时才进入集成和验收测试阶段。如果导致性能问题的重大系统架构缺陷此时才被发现,问题会比较棘手。为避免这样的问题,在编码和单元测试阶段也应该进行压力测试。注意,迭代型生命周期模型不存在这种问题。事实上,根据“迭代型生命周期模型”的定义,每一次迭代都应该执行压力测试。

 

{%}

《Oracle性能诊断艺术(第2版)》是Oracle数据库优化专家Christian Antognini的一部继往开来的里程碑式著作。书中的最佳实践和诸多建议全部来源于作者在实战一线的丰富积累,不仅简单实用,而且发人深省,堪称一座“宝库”,适合各层次读者研读和发掘。