《持续交付 发布可靠软件的系统方法》读书笔记

为了实现部署流水线,我们已经讨论了自动化测试的很多方面。然而,到目前为止,我们主要关注于测试应用程序的行为,这通常称为功能需求测试。本章将讨论非功能需求的测试方法,这主要是关于容量(capacity)、吞吐量(throughput)和性能(performance)的测试。

“性能”是对处理单一事务所花时间的一种度量,既可以单独衡量,也可以在一定的负载下衡量。

“吞吐量”是系统在一定时间内处理事务的数量,通常它受限于系统中的某个瓶颈。在一定的工作负载下,当每个单独请求的响应时间维持在可接受的范围内时,该系统所能承担的最大吞吐量被称为它的容量。

非功能需求的管理

把非功能需求与功能需求区别对待,就很容易把它从项目计划中移除,或者不给予它们足够的分析。然而,这可能就是一个灾难,因为非功能需求常常是项目风险的来源之一。在交付过程的后期才发现应用程序因基本的安全漏洞或很差的性能而导致项目无法验收,这种常见现象会导致项目推迟交付甚至被取消。

非功能需求之间可能彼此排斥:对安全性要求极高的系统常常在易用性上做一些妥协,而非常灵活的系统经常在性能方面有所妥协。

总而言之,在项目一开始,交付过程中的每个人(包括开发人员、运维人员、测试人员和客户)都需要思考一下应用程序的非功能需求,以及它们对系统架构、项目时间表、测试策略和总成本的影响。

当分析非功能需求时,提供适当的细节是至关重要的。只说“响应时间要尽量快”是不够的。“尽量快”根本无法评估到底在这方面要用多少预算来满足需求。“尽量快”意味着要细心处理如何做缓存,以及到底缓存哪些内容?

很多项目都面临一个同样的问题,即没有很好地理解应用程序的验收条件。虽然很明显它们会有一个确切的说明,比如“所有用户交互都要在两秒内作出响应”,或者“系统每小时可以处理 80000 个事务”。然而对于我们的需要来说,这种定义太宽泛了。

涉及“应用程序性能”,人们常用很多含糊的提法来简要地描述性能需求、可用性需求乃至许多其他需求。假如要求“应用程序要在两秒内作出响应”,那么是在所有的情况下都要做到这一点吗?

假如某个数据中心出了问题,我们还必须满足这个“两秒种以内”的要求吗?这个要求对于那些相对很少被用到的交互是一样的,还是只对常用的那些交互有效?当说“两秒以内”时,是指两秒内成功结束本次交互,还是指用户在两秒内可以得到某种反馈就可以了?如果某个地方出了问题,那么是需要在两秒内返回一个错误消息给用户,还是只对那些成功的交互才有这个要求?如果系统遇到较大压力,是否也需要在两秒内作出响应?是在负载达到峰值时也有同样的要求,还是仅需要平均响应时间呢?

如何为容量编程

假如没有很好地分析非功能需求,它们就往往会限制我们的思维,从而导致过分设计和不恰当的优化。

在预测应用程序中哪里有性能瓶颈这一方面,开发人员的表现相当差。他们往往会在代码中引入不必要的复杂性,并且花很多成本来维护,以达到无法确定的性能。

在找到解决方案之前,必须先找出问题的根源。也就是说,我们要知道问题到底是什么。容量测试阶段的关键在于,它要告诉我们是否存在问题,以便我们可以修复它。不要枉自猜测,而要先进行度量。

过早且过分地关注应用程序的容量优化是低效且昂贵的。而且,最终交付的应用系统也很少是高性能的。更糟糕的是,它甚至可能让项目无法交付。

为了解决容量问题,可采取的策略如下:

  1. 为应用程序决定一种架构。通常要特别注意进程、网络边界和I/O。
  2. 了解并使用正确的模式,避免使用那些影响系统容量和稳定性的反模式。
  3. 除了采用适当模式以外,还要确保团队在已经明确的应用架构下进行开发,不要为容量做无谓的优化。鼓励写清晰且简单的代码,而不是深奥难以理解的代码。在没有明确测试结果表明有容量问题时,坚决不能在代码可读性上作出让步。
  4. 注意在数据结构和算法方面的选择,确保它们的属性与应用程序相吻合。比如,只需要O(1)的性能,就不要用一个O(n)的算法。
  5. 处理线程时要特别小心。
  6. 创建一些自动化测试来断言所期望的容量级别。当这些测试失败时,用它们作为向导来修复这些问题。
  7. 使用调测工具主要关注测试中发现的问题,并修复它,不要使用“让它越快越好”这类策略。
  8. 只要有可能,就使用真实的容量数据来做度量。生产环境是唯一真实度量的来源。使用这样的数据,并分析这些数据到底说明了什么。特别要注意系统的用户数,他们的行为模式以及生产环境中的数据量。

    容量度量

    可以做如下的度量:
  • 扩展性测试 - 随着服务器数、服务或线程的增加,单个请求的响应时间和并发用户数的支持会如何变化。
  • 持久性测试 - 这是要长时间运行应用程序,通过一段时间的操作,看是否有性能上的变化。这类测试能捕获内存泄漏或稳定性问题。
  • 吞吐量测试 - 系统每秒能处理多少事务、消息或页面点击。
  • 负载测试 - 当系统负载增加到类似生产环境大小或超过它时,系统的容量如何?

目标明确的基准式(benchmark-style)容量测试对于代码中某个具体问题的防范或局部代码优化是非常有用的。有时候,它们能提供一些信息,帮助团队进行技术方案选择。然而,它们仅仅是整个视图的一部分。如果对于应用程序来说,性能或吞吐量是一个重要指标的话,我们就需要用一些测试来断言系统能够满足业务需求,而不是通过技术经验来猜测某个特定组件的吞吐量应该是多少。

如何定义容量测试的成功与失败

首先,把目标设定为得到稳定、可重现的结果。只要有可能的话,为容量测试专门准备一个环境,用于度量容量。这会将那些与测试不相关任务对结果的影响降到最低,从而使结果保持一致性。容量测试是少有的几个虚拟技术不太适用的地方之一,除非生产环境也是虚拟环境,因为虚拟环境在性能方面有额外的开销。

然后,一旦某个测试通过了最低验收标准,就把验收标准提高一点儿,调整该测试的成功门槛。这能避免“假阳性”(false-positive)场景。如果提交后测试失败了,而验收门槛刚好高于需求中所定义的要求,那么只要降低容量是能被接受的,直接降低一点儿门槛就行了。当然,该测试仍旧是有价值的,因为它对那些不小心威胁到容量需求的修改起到了保护作用。

为了使测试更好用,而不只是性能度量,每个测试都必须体现一个具体的场景,并且只有达到某个标准门槛时,才能认为该测试通过了。

容量测试环境

理想情况下,系统容量的绝对度量应该在一个尽可能与生产环境相似的环境上执行。

尽管能从不同配置的环境中得到一些有用的信息,但除非这些信息是基于度量的,否则用任何测试环境中的容量信息来推演生产环境中的容量指标都是高度投机行为。

高性能计算系统的行为是一个特殊且复杂的领域。配置变更对于容量特性的影响往往是非线性的。像修改UI连接到应用服务器的会话数和数据库连接数这种简单的事儿,比率只要修改一点儿,就可能增加系统的吞吐量(所以这些都是非常重要的变量)。

现实世界中,在生产环境的一个完整副本里进行容量测试并不总是可行的。有时候,甚至可以说是“不现实的”。比如项目规模太小,或者应用程序的性能问题不值得让客户购买与生产环境一模一样的硬件。

在另一种极端情况下,复制生产环境也是不可能的,比如那些较大的软件即服务(SaaS)提供商。它们的生产环境中常常有数十万台服务器在运行,复制生产环境的话,维护开销就已经很大了,更不用说硬件成本了。

然而,大多数项目的情况应该在这两者之间,即在一个与生产环境尽可能相似的环境中运行容量测试。

另外,也不要依据硬件的某种特定参数对应用程序的扩展性作出线性推论,这是在蒙蔽你自己。比如,仅凭测试环境中的处理器频率是生产服务器处理器的一半,就认为应用程序的性能在生产环境中就会快一倍。

假如真的别无选择,那么,如果可能的话,你还可以尝试缩放范围进行测试,从而找到测试环境和生产环境之间的差异基准。

自动化容量测试

在过去经历的一个项目中,我们曾把容量测试当做一项完全独立的工作:在整个交付流程中为它安排一个专门的测试阶段。这种方法在测试的开发和执行成本上有直接的反映。

对于一个项目来说,当容量非常重要时,那么就请暂且忽视这些成本吧,因为更重要的是,要记住:代码的修改对系统容量的影响与其对功能的影响一样重要。当做了修改之后,要尽早掌握容量会下降多少,这样就能快速且有效地修复它。这就要在部署流水线中加入一个阶段,即容量测试阶段。

如果想在部署流水线中增加容量测试的话,就应该创建一个自动化容量测试套件,并且每次对系统进行修改之后,一旦通过了提交测试和验收测试(可选),就应该执行容量测试。

容量测试应该达到以下几点目标:

  • 测试具体的现实场景,这样就不会因为测试太抽象而错过真实应用场景中那些重要的 bug。
  • 预先设定成功的门槛,这样就能判定容量测试是否通过了。
  • 尽可能让测试运行时间短一些,从而保证容量测试在适当时间内完成。
  • 在变更面前要更健壮一些,从而避 免因对应用程序的频繁修改而不断返工。
  • 组合成大规模的复杂场景,这样就可以模拟现实世界中的用户使用模式。
  • 是可重复的,并且既能串行执行,也能并行执行,以便这些测试既可以做负载测试,也可以做持久性测试。

    将容量测试加入到部署流水线中

    大多数容量测试不适合放在部署流水线的提交测试阶段,因为它们通常需要的时间太长,资源占用太多。如果容量测试相当简单,并且花的时间不长,可以将其增加到验收测试阶段,尽管我们并不建议这么做,原因如下:

  • 为了得到真正有效的结果,容量测试需要在它自己的环境上运行。如果其他自动化测试与容量测试同时运行在同一个环境上,那么要找到某版本不符合性能要求的原因,所需成本就太高了。有些持续集成系统让你能够为测试指定环境。你可以使用这种功能对容量测试进行分组,让它们与验收测试一起并行执行。

  • 某些类型的容量测试可能要运行很长时间,这样可能会耽误验收测试结果的反馈时间。
  • 在验收测试之后的很多质量保障活动可以和容量测试并行执行,比如演示最新版本的可工作软件、手工测试、集成测试,等等。对于很多项目来说,没有必要等到容量测试成功之后才做这些事情,那样的话,效率很低。
  • 对于一些项目来说,也没有必要像验收测试那样,频繁运行容量测试。

由于项目的不同,对待部署流水线中容量测试阶段的方式也各不相同。对于某些项目来说,应该像对待验收测试那样,把容量测试也作为自动部署流水线上的一个关卡。除非容量测试成功,否则没有人为批准的话,绝对不能部署这个版本的应用程序。这种方式对于高性能或高扩展性的应用程序来说是最合适的,因为如果无法满足容量需求,软件就失去了存在的意义。

容量测试系统的附加价值

容量测试系统通常是与你所期望的生产系统最接近的。因此,它也是一个非常有价值的资源。并且,如果你遵循我们的建议,把容量测试设计成为一系列组合式的、基于场景的测试,那么实际上这已经是生产系统的一个精密模拟系统了。

假如有很多通用方法来标定具体且很技术性的交互的话,迭代地完成这件事也是值得的。基于场景的测试是对与系统的真实交互的模拟。可以将这些场景组合成更加复杂的场景,在类生产环境中高效执行你希望做的检查和验证。

我们曾用这种方法执行了各种各样的任务,如下所述:

  • 重现生产环境中发现的复杂缺陷。
  • 探测并调试内存泄漏。
  • 持久性(longevity)测试。
  • 评估垃圾回收(garbage collection)的影响。
  • 垃圾回收的调优。
  • 应用程序参数的调优。
  • 第三方应用程序配置的调优,比如操作系统、应用程序服务器和数据库配置。
  • 模拟非正常的、最糟糕情况的场景。
  • 评估一些复杂问题的不同解决方案。
  • 模拟集成失败的情况。
  • 度量应用程序在不同硬件配置下的可扩展性。
  • 与外部系统进行交互的负载测试,即使容量测试的初衷是与桩替身接口(stubbed interface)打交道。
  • 复杂部署的回滚演练。
  • 有选择地使系统的部分或全部瘫痪,从而评估服务的优雅降级(graceful degradation)。
  • 在短期可用的生产硬件上执行真实世界的容量基准,以便能计算出长期且低配的容量测试环境中更准确的扩展因素。

这并不是一个完整的列表,但每项都来自真实的项目。

小结

如何设计出满足非功能需求的系统是一个很复杂的问题。很多非功能需求的横切本质(crosscutting nature)意味着,很难管理它们给项目中带来的风险。结果,这也经常导致两种不适当的做法:从项目一开始就没有足够注意它们,或者是另一个极端,预防性架构和过分设计。

非功能需求就好比是建造桥梁时对大梁的选择,它一定要足够强劲以支撑所期望的交通压力和各种天气。这些需求是现实的,必须要考虑这些需求,但这些需求并不是业务人员为大桥付钱的理由。

业务人员只是想有某种东西可以让他们从河的一边到达另一边,并且这个东西看起来还不错。也就是说,作为技术人员,我们必须警惕自己更倾向于首先出现在脑海中的那种技术解决方案。我们必须和客户及用户紧密合作,共同确定应用程序中的敏感问题,并根据真实的业务价值定义详细的非功能需求。

推荐阅读