性能优化模式(1)——入门介绍

时间:2019-09-07 来源:www.0419fc.com

关于性能优化的文章和图书已有很多,但就我所知,还没有采用模式的方式去讲解的。本文借鉴《设计模式》(*“Design Patterns-Elements of Reusable Object-Oriented Software”*)对设计模式的阐述方式,首先为每一种性能优化模式取一个贴切的名字,便于读者快速理解和深刻记忆,接着讲解该模式的动机和原理,然后结合作者在美团的具体工作案例进行深度剖析,最后总结采用该模式的优点以及需要付出的代价。简而言之,本文采用“命名C>原理和动机C>具体案例C>缺点和优点”的四阶段方式进行性能优化模式讲解。与其他方式相比,采用模式进行讲解有两个方面的优点:一方面,读者不仅仅能够掌握优化手段,而且能够了解采用该手段进行性能优化的场景以及所需付出的代价,这有利于读者全面理解和灵活应用;另一方面,模式解决的是特定应用场景下的一类问题,所以应用场景描述贯穿于模式讲解之中。如此,即使读者对原理不太了解,只要碰到的问题符合某个特定模式的应用场景(这往往比理解原理要简单),就可以采用对应的手段进行优化,进一步促进读者对模式的理解和掌握。

文章的所有案例都来自于美团的真实项目。出于两方面的考虑,作者做了一定的简化和抽象:一方面,系统可以优化的问题众多,而一个特定的模式只能解决几类问题,所以在案例分析过程中会突出与模式相关的问题;另一方面,任何一类问题都需要多维度数据去描述,而应用性能优化模式的前提是多维度数据的组合值超过了某个临界点,但是精确定义每个维度数值的临界点是一件很难的事情,更别说多维度数据组合之后临界点。因此有必要对案例做一些简化,确保相关取值范围得到满足。基于以上以及其他原因,作者所给出的解决方案只是可行性方案,并不保证其是所碰到问题的最佳解决方案。

案例涉及的所有项目都是基于Java语言开发的,严格地讲,所有模式适用的场景是基于Java语言搭建的服务。从另外一方面讲,Java和C++的主要区别在于垃圾回收机制,所以,除去和垃圾回收机制紧密相关的模式之外,文章所描述的模式也适用于采用C++语言搭建的服务。对于基于其他语言开发的服务,读者在阅读以及实践的过程中需要考虑语言之间的差别。

必须说明,本文中各种模式所要解决的问题之所以会出现,部分是因为工程师运用了某些深层次的设计原则。有些设计原则看上去和优秀的设计理念相悖,模式所解决的问题似乎完全可以避免,但是它们却被广泛使用。“存在即合理”,世界上没有完美的设计方案,任何方案都是一系列设计原则的妥协结果,所以本文主要关注点是解决所碰到的问题而不是如何绕过这些设计原则。下面对文中重要的设计原则进行详细阐述,在后面需要运用该原则时将不再解释。

最小可用原则(快速接入原则)有两个关注点:1. 强调快速接入,快速完成;2. 实现核心功能可用。这是一个被普遍运用的原则,其目标是缩短测试周期,增加试错机会,避免过度设计。为了快速接入就必须最大限度地利用已有的解决方案或系统。从另外一个角度讲,一个解决方案或系统只要能够满足基本需求,就满足最小可用原则的应用需求。过度强调快速接入原则会导致重构风险的增加,原则上讲,基于该原则去设计系统需要为重构做好准备。

经济原则关注的是成本问题,看起来很像最小可用原则,但是它们之间关注点不同。最小可用原则的目标是通过降低开发周期,快速接入而实现风险可控,而快速接入并不意味着成本降低,有时候为了实现快速接入可能需要付出巨大的成本。软件项目的生命周期包括:预研、设计、开发、测试、运行、维护等阶段。最小可用原则主要运用在预研阶段,而经济原则可以运用在整个软件生命周期里,也可以只关注某一个或者几个阶段。例如:运行时经济原则需要考虑的系统成本包括单次请求的CPU、内存、网络、磁盘消耗等;设计阶段的经济原则要求避免过度设计;开发阶段的经济原则可能关注代码复用,工程师资源复用等。

代码复用原则分为两个层次:第一个层次使用已有的解决方案或调用已存在的共享库(Shared Library),也称为方案复用;第二个层次是直接在现有的代码库中开发,也称之为共用代码库。

方案复用是一个非常实用主义的原则,它的出发点就是最大限度地利用手头已有的解决方案,即使这个方案并不好。方案的形式可以是共享库,也可以是已存在的服务。方案复用的例子参见避免蚊子大炮模式的具体案例。用搜索引擎服务来解决查找附近商家的问题是一个性能很差的方案,但仍被很多工程师使用。方案复用原则的一个显著优点就是提高生产效率,例如:Java之所以能够得到如此广泛应用,原因之一就是有大量可以重复利用的开源库。实际上“Write once, run anywhere”是Java语言最核心的设计理念之一。基于Java语言开发的代码库因此得以在不同硬件平台、不同操作系统上更广泛地使用。

共用代码库要求在同一套代码库中完成所有功能开发。采用这个原则,代码库中的所有功能编译时可见,新功能代码可以无边界的调用老代码。另外,原代码库已存在的各种运行、编译、测试、配置环境可复用。主要有两个方面地好处:1. 充分利用代码库中已有的基础设施,快速接入新业务;2. 直接调用原代码中的基础功能或原Z,避免网络或进程间调用开销,性能更佳。共用代码库的例子参见垂直分割模式的具体案例。

从设计的角度上讲,方案复用类似于微服务架构(Microservice Architecture,有些观点认为这是一种形式的SOA),而共用代码库和Monolithic Architecture很接近。总的来说,微服务倾向于面向接口编程,要求设计出可重用性的组件(Library或Service),通过分层组织各层组件来实现良好的架构。与之相对应,Monolith Architecture则希望尽可能在一套代码库中开发,通过直接调用代码中的基础功能或原Z而实现性能的优化和快速迭代。使用Monolith Architecture有很大的争议,被认为不符合“设计模式”的理念。参考文献[4],Monolithic Design主要的缺点包括:1. 缺乏美感;2. 很难重构;3. 过早优化(参见文献[6]*Optimize judiciously*); 4. 不可重用;5. 限制眼界。微服务架构是很多互联网公司的主流架构,典型的运用公司包括Amazon、美团等。Monolithic Architecture也有其忠实的粉丝,例如:Tripadvisor的全球网站就共用一套代码库;基于性能的考虑,Linux最终选择的也是Monolithic kernel的模式。

系统设计以及代码编写要遵循奥卡姆剃刀原则:Entities should not be multiplied unnecessarily。

一般而言,一个系统的代码量会随着其功能增加而变多。系统的健壮性有时候也需要通过编写异常处理代码来实现。异常考虑越周全,异常处理代码量越大。但是随着代码量的增大,引入Bug的概率也就越大,系统也就越不健壮。从另外一个角度来讲,异常流程处理代码也要考虑健壮性问题,这就形成了无限循环。所以在系统设计和代码编写过程中,奥卡姆剃刀原则要求:一个功能模块如非必要,就不要;一段代码如非必写,就不写。

奥卡姆剃刀原则和最小可用原则有所区别。最小可用原则主要运用于产品MVP阶段,本文所指的奥卡姆剃刀原则主要指系统设计和代码编写两个方面,这是完全不同的两个概念。MVP包含系统设计和代码编写,但同时,系统设计和代码编写也可以发生在成熟系统的迭代阶段。

在讲解性能优化模式之前,有必要先探讨一下性能恶化模式,因为:

很多性能优化模式的目标之一就是避免系统进入性能恶化模式;不同性能优化模式可能是避免同一种性能恶化模式;同一种性能优化模式可能在不同阶段避免不同的性能恶化模式。

在此统一阐述性能恶化模式,避免下文重复解释。为了便于读者清晰识别恶化模式和优化模式,恶化模式采用“XXX反模式”的方式进行命名。

这是一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增加,最终可能会通过如下三种方式之一恶化系统性能: 1. 线程数目变多导致线程之间CPU资源使用冲突,反过来进一步延长了单次请求时间; 2. 线程数量增多以及线程中缓存变大,内存消耗随之剧增,对于基于Java语言的服务而言,又会更频繁地full GC,反过来单次请求时间会变得更长; 3. 内存使用增多,会使操作系统内存不足,必须使用Swap,可能导致服务彻底崩溃。

典型恶化流程图如下图:

反复缓存所导致性能恶化的原因是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据完全缓存的情况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提高流量高峰时可用性,而降低平均响应时间的一个例子)。反复缓存反模式往往发生在流量高峰时候,通过线性增加机器和提高机器内存可以大大减少系统崩溃的概率。

达到当天最大量

——