您现在的位置是:网站首页> 编程资料编程资料
美团点评对于网站性能优化的经验总结_网站优化_网站运营_
2023-04-18
856人已围观
简介 美团点评对于网站性能优化的经验总结_网站优化_网站运营_
性能优化涉及面很广。一般而言,性能优化指降低响应时间和提高系统吞吐量两个方面,但在流量高峰时候,性能问题往往会表现为服务可用性下降,所以性能优化也可以包括提高服务可用性。在某些情况下,降低响应时间、提高系统吞吐量和提高服务可用性三者相互矛盾,不可兼得。例如:增加缓存可以降低平均响应时间,但是处理线程数量会因为缓存过大而有所限制,从而降低系统吞吐量;为了提高服务可用性,对异常请求重复调用是一个常用的做法,但是这会提高响应时间并降低系统吞吐量。
对于很多像美团这样的公司,它们的系统会面临如下三个挑战:1. 日益增长的用户数量,2. 日渐复杂的业务,3. 急剧膨胀的数据。这些挑战对于性能优化而言表现为:在保持和降低系统TP95响应时间(指的是将一段时间内的请求响应时间从低到高排序,高于95%请求响应时间的下确界)的前提下,不断提高系统吞吐量,提升流量高峰时期的服务可用性。这种场景下,三者的目标和改进方法取得了比较好的一致。本文主要目标是为类似的场景提供优化方案,确保系统在流量高峰时期的快速响应和高可用。
文章第一部分是介绍,包括采用模式方式讲解的优点,文章所采用案例的说明,以及后面部分用到的一些设计原则;第二部分介绍几种典型的“性能恶化模式”,阐述导致系统性能恶化,服务可用性降低的典型场景以及形成恶化循环的过程;第三部分是文章重点,阐述典型的“性能优化模式”,这些模式或者可以使服务远离“恶化模式”,或者直接对服务性能进行优化;文章最后一部分进行总结,并对未来可能出现的新模式进行展望。
介绍
模式讲解方式
关于性能优化的文章和图书已有很多,但就我所知,还没有采用模式的方式去讲解的。本文借鉴《设计模式》("Design Patterns-Elements of Reusable Object-Oriented Software")对设计模式的阐述方式,首先为每一种性能优化模式取一个贴切的名字,便于读者快速理解和深刻记忆,接着讲解该模式的动机和原理,然后结合作者在美团的具体工作案例进行深度剖析,最后总结采用该模式的优点以及需要付出的代价。简而言之,本文采用“命名-->原理和动机-->具体案例-->缺点和优点”的四阶段方式进行性能优化模式讲解。与其他方式相比,采用模式进行讲解有两个方面的优点:一方面,读者不仅仅能够掌握优化手段,而且能够了解采用该手段进行性能优化的场景以及所需付出的代价,这有利于读者全面理解和灵活应用;另一方面,模式解决的是特定应用场景下的一类问题,所以应用场景描述贯穿于模式讲解之中。如此,即使读者对原理不太了解,只要碰到的问题符合某个特定模式的应用场景(这往往比理解原理要简单),就可以采用对应的手段进行优化,进一步促进读者对模式的理解和掌握。
案例说明
文章的所有案例都来自于美团的真实项目。出于两方面的考虑,作者做了一定的简化和抽象:一方面,系统可以优化的问题众多,而一个特定的模式只能解决几类问题,所以在案例分析过程中会突出与模式相关的问题;另一方面,任何一类问题都需要多维度数据去描述,而应用性能优化模式的前提是多维度数据的组合值超过了某个临界点,但是精确定义每个维度数值的临界点是一件很难的事情,更别说多维度数据组合之后临界点。因此有必要对案例做一些简化,确保相关取值范围得到满足。基于以上以及其他原因,作者所给出的解决方案只是可行性方案,并不保证其是所碰到问题的最佳解决方案。
案例涉及的所有项目都是基于Java语言开发的,严格地讲,所有模式适用的场景是基于Java语言搭建的服务。从另外一方面讲,Java和C++的主要区别在于垃圾回收机制,所以,除去和垃圾回收机制紧密相关的模式之外,文章所描述的模式也适用于采用C++语言搭建的服务。对于基于其他语言开发的服务,读者在阅读以及实践的过程中需要考虑语言之间的差别。
设计原则
必须说明,本文中各种模式所要解决的问题之所以会出现,部分是因为工程师运用了某些深层次的设计原则。有些设计原则看上去和优秀的设计理念相悖,模式所解决的问题似乎完全可以避免,但是它们却被广泛使用。“存在即合理”,世界上没有完美的设计方案,任何方案都是一系列设计原则的妥协结果,所以本文主要关注点是解决所碰到的问题而不是如何绕过这些设计原则。下面对文中重要的设计原则进行详细阐述,在后面需要运用该原则时将不再解释。
最小可用原则
最小可用原则(快速接入原则)有两个关注点:1. 强调快速接入,快速完成;2. 实现核心功能可用。这是一个被普遍运用的原则,其目标是缩短测试周期,增加试错机会,避免过度设计。为了快速接入就必须最大限度地利用已有的解决方案或系统。从另外一个角度讲,一个解决方案或系统只要能够满足基本需求,就满足最小可用原则的应用需求。过度强调快速接入原则会导致重构风险的增加,原则上讲,基于该原则去设计系统需要为重构做好准备。
经济原则
经济原则关注的是成本问题,看起来很像最小可用原则,但是它们之间关注点不同。最小可用原则的目标是通过降低开发周期,快速接入而实现风险可控,而快速接入并不意味着成本降低,有时候为了实现快速接入可能需要付出巨大的成本。软件项目的生命周期包括:预研、设计、开发、测试、运行、维护等阶段。最小可用原则主要运用在预研阶段,而经济原则可以运用在整个软件生命周期里,也可以只关注某一个或者几个阶段。例如:运行时经济原则需要考虑的系统成本包括单次请求的CPU、内存、网络、磁盘消耗等;设计阶段的经济原则要求避免过度设计;开发阶段的经济原则可能关注代码复用,工程师资源复用等。
代码复用原则
代码复用原则分为两个层次:第一个层次使用已有的解决方案或调用已存在的共享库(Shared Library),也称为方案复用;第二个层次是直接在现有的代码库中开发,也称之为共用代码库。
方案复用是一个非常实用主义的原则,它的出发点就是最大限度地利用手头已有的解决方案,即使这个方案并不好。方案的形式可以是共享库,也可以是已存在的服务。方案复用的例子参见避免蚊子大炮模式的具体案例。用搜索引擎服务来解决查找附近商家的问题是一个性能很差的方案,但仍被很多工程师使用。方案复用原则的一个显著优点就是提高生产效率,例如:Java之所以能够得到如此广泛应用,原因之一就是有大量可以重复利用的开源库。实际上“Write once, run anywhere”是Java语言最核心的设计理念之一。基于Java语言开发的代码库因此得以在不同硬件平台、不同操作系统上更广泛地使用。
共用代码库要求在同一套代码库中完成所有功能开发。采用这个原则,代码库中的所有功能编译时可见,新功能代码可以无边界的调用老代码。另外,原代码库已存在的各种运行、编译、测试、配置环境可复用。主要有两个方面地好处:1. 充分利用代码库中已有的基础设施,快速接入新业务;2. 直接调用原代码中的基础功能或原語,避免网络或进程间调用开销,性能更佳。共用代码库的例子参见垂直分割模式的具体案例。
从设计的角度上讲,方案复用类似于微服务架构(Microservice Architecture,有些观点认为这是一种形式的SOA),而共用代码库和Monolithic Architecture很接近。总的来说,微服务倾向于面向接口编程,要求设计出可重用性的组件(Library或Service),通过分层组织各层组件来实现良好的架构。与之相对应,Monolith Architecture则希望尽可能在一套代码库中开发,通过直接调用代码中的基础功能或原語而实现性能的优化和快速迭代。使用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反模式”的方式进行命名。
长请求拥塞反模式(High Latency Invocating AntiPattern)
这是一种单次请求时延变长而导致系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增加,最终可能会通过如下三种方式之一恶化系统性能:
线程数目变多导致线程之间CPU资源使用冲突,反过来进一步延长了单次请求时间;
线程数量增多以及线程中缓存变大,内存消耗随之剧增,对于基于Java语言的服务而言,又会更频繁地full GC,反过来单次请求时间会变得更长;
内存使用增多,会使操作系统内存不足,必须使用Swap,可能导致服务彻底崩溃。
典型恶化流程图如下图:
长请求拥塞反模式所导致的性能恶化现象非常普遍,所以识别该模式非常重要。典型的场景如下:某复杂业务系统依赖于多个服务,其中某个服务的响应时间变长,随之系统整体响应时间变长,进而出现CPU、内存、Swap报警。系统进入长请求拥塞反模式的典型标识包括:被依赖服务可用性变低、响应时间变长、服务的某段计算逻辑时间变长等。
多次请求杠杆反模式(Levered Multilayer Invocating AntiPattern)
客户端一次用户点击行为往往会触发多次服务端请求,这是一次请求杠杆;每个服务端请求进而触发多个更底层服务的请求,这是第二次请求杠杆。每一层请求可能导致一次请求杠杆,请求层级越多,杠杆效应就越大。在多次请求杠杆反模式下运行的分布式系统,处于深层次的服务需要处理大量请求,容易会成为系统瓶颈。与此同时,大量请求也会给网络带来巨大压力,特别是对于单次请求数据量很大的情况,网络可能会成为系统彻底崩溃的导火索。典型恶化流程图如下图:
多次请求杠杆所导致的性能恶化现象非常常见,例如:对于美团推荐系统,一个用户列表请求会有多个算法参与,每个算法会召回多个列表单元(商家或者团购),每个列表单元有多种属性和特征,而这些属性和特征数据服务又分布在不同服务和机器上面,所以客户端的一次用户展现可能导致了成千上万的最底层服务调用。对于存在多次请求杠杆反模式的分布式系统,性能恶化与流量之间往往遵循指数曲线关系。这意味着,在平常流量下正常运行服务系统,在流量高峰时通过线性增加机器解决不了可用性问题。所以,识别并避免系统进入多次请求杠杆反模式对于提高系统可用性而言非常关键。
反复缓存反模式(Recurrent Caching AntiPattern)
为了降低响应时间,系统往往在本地内存中缓存很多数据。缓存数据越多,命中率就越高,平均响应时间就越快。为了降低平均响应时间,有些开发者会不加限制地缓存各种数据,在正常流量情况下,系统响应时间和吞吐量都有很大改进。但是当流量高峰来临时,系统内存使用开始增多,触发了JVM进行full GC,进而导致大量缓存被释放(因为主流Java内存缓存都采用SoftReference和WeakReference所导致的),而大量请求又使得缓存被迅速填满,这就是反复缓存。反复缓存导致了频繁的full GC,而频繁full GC往往会导致系统性能急剧恶化。典型恶化流程图如下图:
反复缓存所导致性能恶化的原因是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据完全缓存的情况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提高流量高峰时可用性,而降低平均响应时间的一个例子)。反复缓存反模式往往发生在流量高峰时候,通过线性增加机器和提高机器内存可以大大减少系统崩溃的概率。
性能优化模式
水平分割模式(Horizontal partitioning Pattern)
原理和动机
典型的服务端运行流程包含四个环节:接收请求、获取数据、处理数据、返回结果。在一次请求中,获取数据和处理数据往往多次发生。在完全串行运行的系统里,一次请求总响应时间满足如下公式:
一次请求总耗时=解析请求耗时 + ∑(获取数据耗时+处理数据耗时) + 组装返回结果耗时
大部分耗时长的服务主要时间都花在中间两个环节,即获取数据和处理数据环节。对于非计算密集性的系统,主要耗时都用在获取数据上面。获取数据主要有三个来源:本地缓存,远程缓存或者数据库,远程服务。三者之中,进行远程数据库访问或远程服务调用相对耗时较长,特别是对于需要进行多次远程调用的系统,串行调用所带来的累加效应会极大地延长单次请求响应时间,这就增大了系统进入长请求拥塞反模式的概率。如果能够对不同的业务请求并行处理,请求总耗时就会大大降低。例如下图中,Client需要对三个服务进行调用,如果采用顺序调用模式,系统的响应时间为18ms,而采用并行调用只需要7ms。
水平分割模式首先将整个请求流程切分为必须相互依赖的多个Stage,而每个Stage包含相互独立的多种业务处理(包括计算和数据获取)。完成切分之后,水平分割模式串行处理多个Stage,但是在Stage内部并行处理。如此,一次请求总耗时等于各个Stage耗时总和,每个Stage所耗时间等于该Stage内部最长的业务处理时间。
水平分割模式有两个关键优化点:减少Stage数量和降低每个Stage耗时。为了减少Stage数量,需要对一个请求中不同业务之间的依赖关系进行深入分析并进行解耦,将能够并行处理的业务尽可能地放在同一个Stage中,最终将流程分解成无法独立运行的多个Stage。降低单个Stage耗时一般有两种思路:1. 在Stage内部再尝试水平分割(即递归水平分割),2. 对于一些可以放在任意Stage中进行并行处理的流程,将其放在耗时最长的Stage内部进行并行处理,避免耗时较短的Stage被拉长。
水平分割模式不仅可以降低系统平均响应时间,而且可以降低TP95响应时间(这两者有时候相互矛盾,不可兼得)。通过降低平均响应时间和TP95响应时间,水平分割模式往往能够大幅度提高系统吞吐量以及高峰时期系统可用性,并大大降低系统进入长请求拥塞反模式的概率。
具体案例:
我们的挑战来自为用户提供高性能的优质个性化列表服务,每一次列表服务请求会有多个算法参与,而每个算法基本上都采用“召回->特征获取->计算”的模式。 在进行性能优化之前,算法之间采用顺序执行的方式。伴随着算法工程师的持续迭代,算法数量越来越多,随之而来的结果就是客户端响应时间越来越长,系统很容易进入长请求拥塞反模式。曾经有一段时间,一旦流量高峰来临,出现整条服务链路的机器CPU、内存报警。在对系统进行分析之后,我们采取了如下三个优化措施,最终使得系统TP95时间降低了一半:
算法之间并行计算;
每个算法内部,多次特征获取进行了并行处理;
在调度线程对工作线程进行调度的时候,耗时最长的线程最先调度,最后处理。
缺点和优点
对成熟系统进行水平切割,意味着对原系统的重大重构,工程师必须对业务和系统非常熟悉,所以要谨慎使用。水平切割主要有两方面的难点:
并行计算将原本单一线程的工作分配给多线程处理,提高了系统的复杂度。而多线程所引入的安全问题让系统变得脆弱。与此同时,多线程程序测试很难,因此重构后系统很难与原系统在业务上保持一致。
对于一开始就基于单线程处理模式编写的系统,有些流程在逻辑上能够并行处理,但是在代码层次上由于相互引用已经难以分解。所以并行重构意味着对共用代码进行重复撰写,增大系统的整体代码量,违背奥卡姆剃刀原则。
对于上面提到的第二点,举例如下:A和B是逻辑可以并行处理的两个流程,基于单线程设计的代码,假定处理完A后再处理B。在编写处理B逻辑代码时候,如果B需要的资源已经在处理A的过程中产生,工程师往往会直接使用A所产生的数据,A和B之间因此出现了紧耦合。并行化需要对它们之间的公共代码进行拆解,这往往需要引入新的抽象,更改原数据结构的可见域。
在如下两种情况,水平切割所带来的好处不明显:
一个请求中每个处理流程需要获取和缓存的数据量很大,而不同流程之间存在大量共享的数据,但是请求之间数据共享却很少。在这种情况下,流程处理完之后,数据和缓存都会清空。采用顺序处理模式,数据可以被缓存在线程局部存储(ThreadLocal)中而减少重复获取数据的成本;如果采用水平切割的模式,在一次请求中,不同流程会多次获取并缓存的同一类型数据,对于内存原本就很紧张的系统,可能会导致频繁full GC,进入反复缓存反模式。
某一个处理流程所需时间远远大于其他所有流程所需时间的总和。这种情况下,水平切割不能实质性地降低请求响应时间。
采用水平切割的模式可以降低系统的平均响应时间和TP95响应时间,以及流量高峰时系统崩溃的概率。虽然进行代码重构比较复杂,但是水平切割模式非常容易理解,只要熟悉系统的业务,识别出可以并行处理的流程,就能够进行水平切割。有时候,即使少量的并行化也可以显著提高整体性能。对于新系统而言,如果存在可预见的性能问题,把水平分割模式作为一个重要的设计理念将会大大地提高系统的可用性、降低系统的重构风险。总的来说,虽然存在一些具体实施的难点,水平分割模式是一个非常有效、容易识别和理解的模式。
垂直分割模式(Vertical partitioning Pattern)
原理和动机:
对于移动互联网节奏的公司,新需求往往是一波接一波。基于代码复用原则,工程师们往往会在一个系统实现大量相似却完全不相干的功能。伴随着功能的增强,系统实际上变得越来越脆弱。这种脆弱可能表现在系统响应时间变长、吞吐量降低或者可用性降低。导致系统脆弱原因主要来自两方面的冲突:资源使用冲突和可用性不一致冲突。
资源使用冲突是导致系统脆弱的一个重要原因。不同业务功能并存于同一个运行系统里面意味着资源共享,同时也意味着资源使用冲突。可能产生冲突的资源包括:CPU、内存、网络、I/O等。例如:一种业务功能,无论其调用量多么小,都有一些内存开销。对于存在大量缓存的业务功能,业务功能数量的增加会极大地提高内存消耗,从而增大系统进入反复缓存反模式的概率。对于CPU密集型业务,当产生冲突的时候,响应时间会变慢,从而增大了系统进入长请求拥塞反模式的可能性。
不加区别地将不同可用性要求的业务功能放入一个系统里,会导致系统整体可用性变低。当不同业务功能糅合在同一运行系统里面的时候,在运维和机器层面对不同业务的可用性、可靠性进行调配将会变得很困难。但是,在高峰流量导致系统濒临崩溃的时候,最有效的解决手段往往是运维,而最有效手段的失效也就意味着核心业务的可用性降低。
垂直分割思路就是将系统按照不同的业务功能进行分割,主要有两种分割模式:部署垂直分割和代码垂直分割。部署垂直分割主要是按照可用性要求将系统进行等价分类,不同可用性业务部署在不同机器上,高可用业务单独部署;代码垂直分割就是让不同业务系统不共享代码,彻底解决系统资源使用冲突问题。
具体案例:
我们的挑战来自于美团推荐系统,美团客户端的多个页面都有推荐列表。虽然不同的推荐产品需求来源不同,但是为了实现快速的接入,基于共用代码库原则,所有的推荐业务共享同一套推荐代码,同一套部署。在一段时间内,我们发现push推荐和首页“猜你喜欢推荐”的资源消耗巨大。特别是在push推荐的高峰时刻,CPU和内存频繁报警,系统不停地full GC,造成美团用户进入客户端时,首页出现大片空白。
在对系统进行分析之后,得出两个结论:
首页“猜你喜欢”对用户体验影响更大,应该给予最高可用性保障,而push推荐给予较低可用性保障;
首页“猜你喜欢”和push推荐都需要很大的本地缓存,有较大的内存使用冲突,并且响应时间都很长,有严重的CPU使用冲突。
因此我们采取了如下措施,一方面,解决了首页“猜你喜欢”的可用性低问题,减少了未来出现可用性问题的概率,最终将其TP95响应时间降低了40%;另一方面也提高了其他推荐产品的服务可用性和高峰吞吐量。
将首页“猜你喜欢”推荐进行单独部署,而将push推荐和其他对系统资源要
相关内容
- 新手必读:怎样写出成功的伪原创?_网站优化_网站运营_
- 如何有效的优化网页的加载速度?优化网页速度的7种方法_网站优化_网站运营_
- KeyCDN的免费CDN加速服务使用全攻略_网站优化_网站运营_
- 干货分享:一张图让你快速了解收录与索引的区别_网站优化_网站运营_
- 针对Google的SEO优化中可利用的官方工具使用总结_网站优化_网站运营_
- 网站导航的布局需要注意什么?如何做好网站导航的布局优化?_网站优化_网站运营_
- 内链如何优化?网站内链优化细节及核心点分析_网站优化_网站运营_
- 网站页面标题如何优化?网站页面标题优化策略汇总_网站优化_网站运营_
- 如何做用户需求分析?SEOer、产品经理必懂的用户需求分析方法_网站优化_网站运营_
- 十条服务器端优化Web性能的技巧总结_网站优化_网站运营_