性能优化涉及面很广。通常而言,性能优化指下降响应时间和提升系统吞吐量两个方面,但在流量高峰时候,性能问题每每会表现为服务可用性降低,因此性能优化也能够包括提升服务可用性。在某些状况下,下降响应时间、提升系统吞吐量和提升服务可用性三者相互矛盾,不可兼得。例如:增长缓存能够下降平均响应时间,可是处理线程数量会由于缓存过大而有所限制,从而下降系统吞吐量;为了提升服务可用性,对异常请求重复调用是一个经常使用的作法,可是这会提升响应时间并下降系统吞吐量。html
对于不少像美团这样的公司,它们的系统会面临以下三个挑战: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包含系统设计和代码编写,但同时,系统设计和代码编写也能够发生在成熟系统的迭代阶段。
在讲解性能优化模式以前,有必要先探讨一下性能恶化模式,由于:
这是一种单次请求时延变长而致使系统性能恶化甚至崩溃的恶化模式。对于多线程服务,大量请求时间变长会使线程堆积、内存使用增长,最终可能会经过以下三种方式之一恶化系统性能:
长请求拥塞反模式所致使的性能恶化现象很是广泛,因此识别该模式很是重要。典型的场景以下:某复杂业务系统依赖于多个服务,其中某个服务的响应时间变长,随之系统总体响应时间变长,进而出现CPU、内存、Swap报警。系统进入长请求拥塞反模式的典型标识包括:被依赖服务可用性变低、响应时间变长、服务的某段计算逻辑时间变长等。
客户端一次用户点击行为每每会触发屡次服务端请求,这是一次请求杠杆;每一个服务端请求进而触发多个更底层服务的请求,这是第二次请求杠杆。每一层请求可能致使一次请求杠杆,请求层级越多,杠杆效应就越大。在屡次请求杠杆反模式下运行的分布式系统,处于深层次的服务须要处理大量请求,容易会成为系统瓶颈。与此同时,大量请求也会给网络带来巨大压力,特别是对于单次请求数据量很大的状况,网络可能会成为系统完全崩溃的导火索。典型恶化流程图以下图:
屡次请求杠杆所致使的性能恶化现象很是常见,例如:对于美团推荐系统,一个用户列表请求会有多个算法参与,每一个算法会召回多个列表单元(商家或者团购),每一个列表单元有多种属性和特征,而这些属性和特征数据服务又分布在不一样服务和机器上面,因此客户端的一次用户展示可能致使了成千上万的最底层服务调用。对于存在屡次请求杠杆反模式的分布式系统,性能恶化与流量之间每每遵循指数曲线关系。这意味着,在日常流量下正常运行服务系统,在流量高峰时经过线性增长机器解决不了可用性问题。因此,识别并避免系统进入屡次请求杠杆反模式对于提升系统可用性而言很是关键。
为了下降响应时间,系统每每在本地内存中缓存不少数据。缓存数据越多,命中率就越高,平均响应时间就越快。为了下降平均响应时间,有些开发者会不加限制地缓存各类数据,在正常流量状况下,系统响应时间和吞吐量都有很大改进。可是当流量高峰来临时,系统内存使用开始增多,触发了JVM进行full GC,进而致使大量缓存被释放(由于主流Java内存缓存都采用SoftReference和WeakReference所致使的),而大量请求又使得缓存被迅速填满,这就是反复缓存。反复缓存致使了频繁的full GC,而频繁full GC每每会致使系统性能急剧恶化。典型恶化流程图以下图:
反复缓存所致使性能恶化的缘由是无节制地使用缓存。缓存使用的指导原则是:工程师们在使用缓存时必须全局考虑,精细规划,确保数据彻底缓存的状况下,系统仍然不会频繁full GC。为了确保这一点,对于存在多种类型缓存以及系统流量变化很大的系统,设计者必须严格控制缓存大小,甚至废除缓存(这是典型为了提升流量高峰时可用性,而下降平均响应时间的一个例子)。反复缓存反模式每每发生在流量高峰时候,经过线性增长机器和提升机器内存能够大大减小系统崩溃的几率。
典型的服务端运行流程包含四个环节:接收请求、获取数据、处理数据、返回结果。在一次请求中,获取数据和处理数据每每屡次发生。在彻底串行运行的系统里,一次请求总响应时间知足以下公式:
一次请求总耗时=解析请求耗时 + ∑(获取数据耗时+处理数据耗时) + 组装返回结果耗时
大部分耗时长的服务主要时间都花在中间两个环节,即获取数据和处理数据环节。对于非计算密集性的系统,主要耗时都用在获取数据上面。获取数据主要有三个来源:本地缓存,远程缓存或者数据库,远程服务。三者之中,进行远程数据库访问或远程服务调用相对耗时较长,特别是对于须要进行屡次远程调用的系统,串行调用所带来的累加效应会极大地延长单次请求响应时间,这就增大了系统进入长请求拥塞反模式的几率。若是可以对不一样的业务请求并行处理,请求总耗时就会大大下降。例以下图中,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时间下降了一半:
对成熟系统进行水平切割,意味着对原系统的重大重构,工程师必须对业务和系统很是熟悉,因此要谨慎使用。水平切割主要有两方面的难点:
在以下两种状况,水平切割所带来的好处不明显:
采用水平切割的模式能够下降系统的平均响应时间和TP95响应时间,以及流量高峰时系统崩溃的几率。虽然进行代码重构比较复杂,可是水平切割模式很是容易理解,只要熟悉系统的业务,识别出能够并行处理的流程,就可以进行水平切割。有时候,即便少许的并行化也能够显著提升总体性能。对于新系统而言,若是存在可预见的性能问题,把水平分割模式做为一个重要的设计理念将会大大地提升系统的可用性、下降系统的重构风险。总的来讲,虽然存在一些具体实施的难点,水平分割模式是一个很是有效、容易识别和理解的模式。
对于移动互联网节奏的公司,新需求每每是一波接一波。基于代码复用原则,工程师们每每会在一个系统实现大量类似却彻底不相干的功能。伴随着功能的加强,系统实际上变得愈来愈脆弱。这种脆弱可能表如今系统响应时间变长、吞吐量下降或者可用性下降。致使系统脆弱缘由主要来自两方面的冲突:资源使用冲突和可用性不一致冲突。
资源使用冲突是致使系统脆弱的一个重要缘由。不一样业务功能并存于同一个运行系统里面意味着资源共享,同时也意味着资源使用冲突。可能产生冲突的资源包括:CPU、内存、网络、I/O等。例如:一种业务功能,不管其调用量多么小,都有一些内存开销。对于存在大量缓存的业务功能,业务功能数量的增长会极大地提升内存消耗,从而增大系统进入反复缓存反模式的几率。对于CPU密集型业务,当产生冲突的时候,响应时间会变慢,从而增大了系统进入长请求拥塞反模式的可能性。
不加区别地将不一样可用性要求的业务功能放入一个系统里,会致使系统总体可用性变低。当不一样业务功能糅合在同一运行系统里面的时候,在运维和机器层面对不一样业务的可用性、可靠性进行调配将会变得很困难。可是,在高峰流量致使系统濒临崩溃的时候,最有效的解决手段每每是运维,而最有效手段的失效也就意味着核心业务的可用性下降。
垂直分割思路就是将系统按照不一样的业务功能进行分割,主要有两种分割模式:部署垂直分割和代码垂直分割。部署垂直分割主要是按照可用性要求将系统进行等价分类,不一样可用性业务部署在不一样机器上,高可用业务单独部署;代码垂直分割就是让不一样业务系统不共享代码,完全解决系统资源使用冲突问题。
咱们的挑战来自于美团推荐系统,美团客户端的多个页面都有推荐列表。虽然不一样的推荐产品需求来源不一样,可是为了实现快速的接入,基于共用代码库原则,全部的推荐业务共享同一套推荐代码,同一套部署。在一段时间内,咱们发现push推荐和首页“猜你喜欢推荐”的资源消耗巨大。特别是在push推荐的高峰时刻,CPU和内存频繁报警,系统不停地full GC,形成美团用户进入客户端时,首页出现大片空白。
在对系统进行分析以后,得出两个结论:
所以咱们采起了以下措施,一方面,解决了首页“猜你喜欢”的可用性低问题,减小了将来出现可用性问题的几率,最终将其TP95响应时间下降了40%;另外一方面也提升了其余推荐产品的服务可用性和高峰吞吐量。
垂直分割主要的缺点主要有两个:
解决重复编码工做问题的一个思路就是为不一样的系统提供共享库(Shared Library),可是这种耦合反过来可能致使部署机器中引入未部署业务的开销。因此在共享库中要减小静态代码的初始化开销,并将相似缓存初始化等工做交给上层系统。总的来讲,经过共享库的方式引入的开销能够获得控制。可是对于业务密集型的系统,因为业务每每是高度定制化的,共用一套代码库的好处是开发工程师能够采用Copy-on-write的模式进行开发,须要修改的时候随时拷贝并修改。共享库中应该存放不容易变化的代码,避免使用者频繁升级,因此并不适合这种场景。所以,对于业务密集型的系统,分代码所致使的重复编码量是须要权衡的一个因素。
垂直分割是一个很是简单而又有效的性能优化模式,特别适用于系统已经出现问题而又须要快速解决的场景。部署层次的分割既安全又有效。须要说明的是部署分割和简单意义上的加机器不是一回事,在大部分状况下,即便不增长机器,仅经过部署分割,系统总体吞吐量和可用性都有可能提高。因此就短时间而言,这几乎是一个零成本方案。对于代码层次的分割,开发工程师须要在业务承接效率和系统可用性上面作一些折衷考虑。
基于性能的设计要求变化的数据和不变的数据分开,这一点和基于面向对象的设计原则相悖。在面向对象的设计中,为了便于对一个对象有总体的把握,紧密相关的数据集合每每被组装进一个类,存储在一个数据库表,即便有部分数据冗余(关于面向对象与性能冲突的讨论网上有不少文章,本文不细讲)。不少系统的主要工做是处理变化的数据,若是变化的数据和不变的数据被紧密组装在一块儿,系统对变化数据的操做将引入额外的开销。而若是易变数据占总数据比例很是小,这种额外开销将会经过杠杆效应恶化系统性能。分离易变和恒定不变的数据在对象建立、内存管理、网络传输等方面都有助于性能提升。
恒变分离模式的原理很是相似与数据库设计中的第三范式(3NF):第三范式主要解决的是静态存储中重复存储的问题,而恒变分离模式解决的是系统动态运行时候恒定数据重复建立、传输、存储和处理的问题。按照3NF,若是一个数据表的每一记录都依赖于一些非主属性集合,而这些非主属性集合大量重复出现,那么应该考虑对被依赖的非主属性集合定义一个新的实体(构建一个新的数据表),原数据库的记录依赖于新实体的ID。如此一来数据库重复存储数据量将大大下降。相似的,按照恒变分离模式,对于一个实体,若是系统处理的只是这个实体的少许变化属性,应该将不变的属性定义为一个新实体(运行时的另外一个类,数据库中的另外一个表),原来实体经过ID来引用新实体,那么原有实体在运行系统中的数据传输、建立、网络开销都会大大下降。
咱们的挑战是提供一个高性能、高一致性要求的团购服务(DealService)。系统存在一些屡次请求杠杆反模式问题,客户端一次请求会致使几十次DealService读取请求,每次获取上百个团购详情信息,服务端单机须要支持每秒万次级别的吞吐量。基于需求,系统大致框架设计以下:
每一个DealService按期从持久层同步全部发生变化的deal信息,全部的deal信息保存在内存里面。在最初的设计里面,数据库只有一个数据表DealModelTable,程序里面也只有一个实体类DealModel。因为销量、价格、用户评价等信息的频发变化,为了达到高一致性要求,服务系统每分钟须要从数据库同步几万条记录。随着美团团购数量的增多和用户活跃度的增长,系统出现了三个问题:
在对系统进行分析以后,咱们采用了以下措施,大大下降了网络传输的数据量,缓解了主从数据库同步压力,使得客户端的超时异常从高峰时候的9%下降到了小于0.01%(低于万分之一):
采用恒变分离模式,主要有三个缺点:
在以下两种场景下,恒变分离模式所带来的好处有限:
总的来讲,恒变分离模式很是容易理解,其应用每每须要知足两个条件:易变数据占总体数据比例很低(比例越低,杠杆效应越大)和易变数据所致使的操做又是系统的主要操做。在该场景下,若是系统性能已经出现问题,牺牲一些可维护性就显得物有所值。
大部分系统都是由多种类型的数据构成,大多数数据类型的都包含易变、少变和不变的属性。盲目地进行恒变分离会致使系统的复杂度指数级别的增长,系统变得很难维护,因此系统设计者必须在高性能和高维护性之间找到一个平衡点。做者的建议是:对于复杂的业务系统,尽可能按照面向对象的原则进行设计,只有在性能出现问题的时候才开始考虑恒变分离模式;而对于高性能,业务简单的基础数据服务,恒变分离模式应该是设计之初的一个重要原则。
数据局部性模式是屡次请求杠杆反模式的针对性解决方案。在大数据和强调个性化服务的时代,一个服务消费几十种不一样类型数据的现象很是常见,同时每一种类型的数据服务都有可能须要一个大的集群(多台机器)提供服务。这就意味着客户端的一次请求有可能会致使服务端成千上万次调用操做,很容易使系统进入屡次请求杠杆反模式。在具体开发过程当中,致使数据服务数量暴增的主要缘由有两个:1. 缓存滥用以及缺少规划,2. 数据量太大以致于没法在一台机器上提供全量数据服务。数据局部性模的核心思想是合理组织数据服务,减小服务调用次数。具体而言,能够从服务端和客户端两个方面进行优化。
服务端优化方案的手段是对服务进行从新规划。对于数据量太大以致于没法在一台机器上存储全量数据的场景,建议采用Bigtable或相似的解决方案提供数据服务。典型的Bigtable的实现包括Hbase、Google Cloud Bigtable等。实际上数据局部性是Bigtable的一个重要设计原则,其原理是经过Row key和Column key两个主键来对数据进行索引,并确保同一个Row key索引的全部数据都在一台服务器上面。经过这种数据组织方式,一次网络请求能够获取同一个Row key对应的多个Column key索引的数据。缺少规划也是形成服务数量剧增的一个重要缘由。不少经过统计和挖掘出来的特征数据每每是在漫长的时间里由不一样team独立产生的。而对于每种类型数据,在其产生之初,因为不肯定其实际效果以及生命周期,基于快速接入原则,服务提供者每每会用手头最容易实施的方案,例如采用Redis Cache(不加选择地使用缓存会致使缓存滥用)。数据服务之间缺少联动以及缺少标准接入规划流程就会致使数据服务数量膨胀。数据局部性原则对规划的要求,具体而言是指:1. 数据由尽量少的服务器来提供,2. 常常被一块儿使用的数据尽量放在同一台服务器上。
客户端优化有以下几个手段:
咱们的挑战来自于美团的推荐、个性化列表和个性化搜索服务。这些个性化系统须要获取各类用户、商家和团购信息。信息类型包括基本属性和统计属性。最初,不一样属性数据由不一样的服务提供,有些是RPC服务,有些是Redis服务,有些是HBase或者数据库,参见下图:
一般而言,客户端每一个用户请求都会触发多个算法。一方面,每一个算法都会召回几十甚至几百个团购或者商家ID,团购和商家基础属性被均匀地分配到几十台Redis里面(以下图),产生了大量的Redis请求,极端状况下,一次客户端请求所触发的团购基础数据请求就超过了上千次;另外一方面,用户特征属性信息有十几种,每种属性也由单独的服务提供,服务端网络调用次数暴增。在一段时间里,不少系统都进入了屡次请求杠杆反模式,Redis服务器的网卡常常被打死,屡次进行扩容,提升线程池线程数量,丝毫没有改善。
在对系统进行分析以后,按照数据局部性模式的原则,咱们采用了以下手段,完全解决了系统屡次请求杠杆反模式的问题:
数据局部性模式并不适用于系统初级阶段。在初级阶段,最小可用原则每每是主要设计原则之一,出于两方面的考虑:一方面,在初级阶段,很难预测所要提供服务的数据是否有效并且可以长期使用,以及将来的调用量;另外一方面,在初级阶段,工程师可能没法预测最终的调用模式,而不一样的调用模式会致使数据局部性方案的设计不一样。对于已经大量使用的数据服务,采用数据局部性模式进行重构必然要改变老的调用模式,这一方面会引入新的Bug,另外一方面也意味着巨大的工做量。须要特别强调的是,数据处于系统的最底层,对于结构复杂而又重要的数据,重构所带来可靠性、一致性和工做量都是须要权衡的因素。对于请求量比较小的数据服务,即便一次请求会触发严重的请求杠杆效应,可是若是原始触发请求数量在可预见的时间内没有明显变多的迹象,进行数据服务重构可能得不偿失。
数据局部性模式可以解决屡次请求杠杆反模式所致使的问题,但它并不是大数据的产物,CPU、编译器的设计理念里早就融入了该模式,因此很容易被工程师理解。虽然过分设计在系统初级阶段是一个要尽可能避免的事情,可是理解和掌握数据局部性模式对于设计出一个可扩展、可重用的系统有很大帮助。不少成熟的系统由于屡次请求杠杆反模式而致使系统频繁崩溃,理解数据局部性模式的原则有助于提升工程师分析解决问题的能力,而在确认了系统存在请求杠杆问题后,数据局部性原则是一件很是锐利的武器。
“用大炮打蚊子”原本是大材小用的意思,可是细致想想,用大炮打蚊子,成功率不高。对于开发工程师而言,一方面为了快速承接业务,按照方案复用原则,老是尽量地利用现有系统,这使得系统功能愈来愈强大;另外一方面,提升系统的通用性或可重用性也是工程师们在设计系统的一个重要目标。随着这两个过程的相互独立演化,采用通用方案解决特定问题的现象随处可见,形象地说,这就像大炮打蚊子。大炮成本很高,蚊子的数量众多,最终的结局每每是蚊子打败了大炮。
“避免蚊子大炮模式”是经济原则在运行时系统的运用,它要求采用最节省资源(CPU、内存等)的方法来解决所面临的问题,资源浪费会带来将来潜在的风险。工程师接到一个需求的时候,须要思考的不只仅是如何复用现有的系统,减小开发时间,还须要考虑现有系统为处理每一个新需求访问所需运行时成本,以及新需求的预期访问量。不然,不加辨别地利用现有系统,不只仅增大了重构风险,还有可能交叉影响,对现有系统所支持的服务形成影响。从另一个角度讲,工程师在构建一个可重用系统的时候,要明确其所不能解决和不建议解决的问题,而对于不建议解决的问题,在文档中标明潜在的风险。
咱们的挑战是为移动用户寻找其所在位置附近的商家信息。美团有很是完善的搜索系统,也有资深的搜索工程师,因此一个系统须要查找附近的商家的时候,每每第一方案就是调用搜索服务。可是在美团,太多的服务有基于LBS的查询需求,致使搜索请求量直线上升,这原本不属于搜索的主营业务,在一段时间里面反倒成了搜索的最多请求来源。而搜索引擎在如何从几十万商家里面找最近的几百商家方面的性能很是差,所以一段时间里,搜索服务频繁报警。不只仅搜索服务可用性受到了影响,全部依赖于LBS的服务的可用性都大大下降。
在对系统分析以后,咱们认为更适合解决最短直线距离的算法应该是k-d tree,在快速实现了基于k-d tree的LBS Search解决方案以后,咱们用4台服务器轻松解决了30多台搜索服务器没法解决的问题,平均响应时间从高峰时的100ms下降到300ns,性能取得了几百倍的提升。
避免蚊子大炮模式的问题和数据局部性模式相似,都与最小可用原则相冲突。在系统设计初级阶段,寻求最优方案每每意味着过分设计,整个项目在时间和成本变得不可控,而为每一个问题去找最优秀的解决方案是不现实的奢求。最优化原则的要求是全面的,不只仅要考虑的运行时资源,还须要考虑工程师资源和时间成本等,而这些点每每相互矛盾。在以下状况下,避免蚊子大炮模式所带来的好处有限:在可预见的将来,某个业务请求量很是小,这时候花大量精力去找最优技术方案效果不明显。
在设计阶段,避免蚊子大炮模式是一个须要工程师去权衡的选择,须要在开发成本和系统运行成本之间保持一个平衡点。当不少功能融入到一个通用系统里而出现性能问题的时候,要拆分出来每个功能点所形成的影响也不是件轻易的事情,因此采用分开部署而共用代码库的原则能够快速定位问题,而后有针对性地解决“蚊子大炮”问题。总的来讲,在设计阶段,避免蚊子大炮模式是工程师们进行分析和设计的一个重要准则,工程师能够暂时不解决潜在的问题,可是必定要清楚潜在的危害。构建可重用系统或方案,必定要明确其所不能解决和不建议解决的问题,避免过分使用。
本模式的极端要求是:离线服务永远不要调用实时服务。该模式比较简单也容易理解,可是,严格地讲它不是一种系统设计模式,而是一种管理规范。离线服务和在线服务从可用性、可靠性、一致性的要求上彻底不一样。原则上,工程师在编写离线服务代码的时候,应该遵循的就是离线服务编程规范,按照在线服务编程规范要求,成本就会大大提升,不符合经济原则;从另一方面讲,按照离线服务的需求去写在线服务代码,可用性、可靠性、一致性等每每得不到知足。
具体而言,实时离线分离模式建议以下几种规范:
由于违反实时离线分离模式而致使的事故很是常见。有一次,由于一个离线程序频繁的向Tair集群写数据,每一次写10M数据,使得整个Tair集群宕机。另外一次,由于Storm系统直接写MySQL数据库致使数据库链接数耗尽,从而使在线系统没法链接数据库。
为了实现实时在线分离,可能须要为在线环境和离线环境单独部署,维护多套环境所带来运维成本是工程师须要考虑的问题。另外一方面,在线环境的数据在离线环境中可能很难获取,这也是不少离线系统直接访问在线系统的缘由。可是,听从实时离线分离模式是一个很是重要的安全管理准则,任何违背这个准则的行为都意味着系统性安全漏洞,都会增大线上故障几率。
降级模式是系统性能保障的最后一道防线。理论上讲,不存在绝对没有漏洞的系统,或者说,最好的安全措施就是为处于崩溃状态的系统提供预案。从系统性能优化的角度来说,无论系统设计地多么完善,总会有一些意料以外的状况会致使系统性能恶化,最终可能致使崩溃,因此对于要求高可用性的服务,在系统设计之初,就必须作好降级设计。根据做者的经验,良好的降级方案应该包含以下措施:
典型的降级策略有三种:流量降级、效果降级和功能性降级。流量降级是指当经过主动拒绝处理部分流量的方式让系统正常服务未降级的流量,这会形成部分用户服务不可用;效果降级表现为服务质量的降级,即在流量高峰时期用相对低质量、低延时的服务来替换高质量、高延时的服务,保障全部用户的服务可用性;功能性降级也表现为服务质量的降级,指的是经过减小功能的方式来提升用户的服务可用性。效果降级和功能性降级比较接近,效果降级强调的是主功能服务质量的降低,功能性降级更多强调的是辅助性功能的缺失。作一个类好比下:计划将100个工程师从北京送到夏威夷度假,可是预算不够。采用流量降级策略,只有50工程师作头等舱去了夏威夷度假,其他工程师继续编写程序(这可很差);效果降级策略下,100个工程师都坐经济舱去夏威夷;采用功能性降级策略,100个工程师都坐头等舱去夏威夷,可是飞机上不提供食品和饮料。
咱们的系统大量使用了智能降级程序。在系统恶化的时候,智能降级程序自动降级部分流量,当系统恢复的时候,智能降级程序自动升级为正常状态。在采用智能降级程序以前,由于系统降级问题,总体系统不可用的状况偶尔发生。采用智能降级程序以后,基本上没有由于性能问题而致使的系统总体不可用。咱们的智能降级程序的主要断定策略是服务响应时间,若是出现大量长时间的响应异常或超时异常,系统就会走降级流程,若是异常数量变少,系统就会自动恢复。
为了使系统具有降级功能,须要撰写大量的代码,而降级代码每每比正常业务代码更难写,更容易出错,因此并不符合奥卡姆剃刀原则。在肯定使用降级模式的前提下,工程师须要权衡这三种降级策略的利弊。大多数面向C端的系统倾向于采用效果降级和功能性降级策略,可是有些功能性模块(好比下单功能)是不能进行效果和功能性降级的,只能采用流量降级策略。对于不能接受降级后果的系统,必需要经过其余方式来提升系统的可用性。
总的来讲,降级模式是一种设计安全准则,任何高可用性要求的服务,必需要按照降级模式的准则去设计。对于违背这条设计原则的系统,或早或晚,系统总会由于某些问题致使崩溃而下降可用性。不过,降级模式并不是不须要成本,也不符合最小可用原则,因此对于处于MVP阶段的系统,或者对于可用性要求不高的系统,降级模式并不是必须采纳的原则。
对于没法采用系统性的模式方式讲解的性能优化手段,做者也给出一些总结性的建议:
优化问题是程序员绕不开的核心问题,在这里也给你们推荐一个架构交流学习群:650385180,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,上面的性能优化知识体系图也是在群里获取。相信对于已经工做和遇到技术瓶颈的码友,在这个群里必定有你须要的内容。这些资料或许能够帮助到如下几类程序员:
1.对如今的薪资不满,想要跳槽,却对本身的技术没有信心,不知道如何面对面试官。
2.想从传统行业转行到互联网行业,但没有接触过互联网技术。
3.工做1 - 5年须要提高本身的核心竞争力,但学习没有系统化,不知道本身接下来要学什么才是正确的,踩坑后又不知道找谁,百度后依然不知因此然。
4.工做5 - 10年没法突破技术瓶颈(运用过不少技术,在公司一直写着业务代码,却依然不懂底层实现原理)
若是你如今正处于上述所说的几个阶段,那么或许能够加入进来一块儿交流学习。并且我也可以提供一些面试指导,职业规划等建议。
Christopher Alexander曾说过:"Each pattern describes a problem which occurs over and over again in our environment, and then describes the core of the solution to that problem, in such a way that you can use this solution a million times over, without ever doing it the same way twice" 。 尽管Christopher Alexander指的是建筑模式,软件设计模式适用,基于一样的缘由,性能优化模式也适用。每一个性能优化模式描述的都是工程师们平常工做中常常出现的问题,一个性能优化模式能够解决肯定场景下的某一类型的问题。因此要理解一个性能优化模式不只仅要了解性能模式的所能解决的问题以及解决手段,还须要清楚该问题所发生的场景和须要付出的代价。
最后,本文所描述的性能优化模式只是做者的工做经验总结,都是为了解决由如下三种状况所形成的性能问题:
1. 日益增加的用户数量
2. 日渐复杂的业务
3. 急剧膨胀的数据,可是这些远非该领域里面的全部模式。
对于文章中提到的其余性能优化建议,以及如今和未来可能碰到的性能问题,做者还会不断抽象,在将来总结出更多的模式。性能问题涉及领域很是普遍,而模式是一个很是好的讲解性能问题以及解决方案的方式,做者有理由相信,不管是在做者所从事的工做领域里面仍是在其余的领域里面,新的性能优化模式会不断涌现。但愿经过本文的讲述,对碰到一样问题的工程师们有所帮助,同时也抛砖引玉,期待出现更多的基于模式方式讲解性能优化的文章。
参考文献:
[1] Chang F, Dean J, Ghemawat S, et al. Bigtable: A Distributed Storage System for Structured Data
[2] Gamma E, Helm R, Johnson R, et al. Design Patterns-Elements of Reusable Object-Oriented Software. Machinery Industry, 2003
[3] Motlik F. Monolithic Core Versus Full Microservice Architecture
[4] Monolithic Design WikiWikiWeb.[5] Bovet D P, Cesati M. Understanding the Linux Kernel. 3rd ed. O'Reilly Media, 2005.[6] Bloch J. Effective Java. 2nd ed. Addison-Wesley, 2008.[7] Alexander C, Ishikawa S, Silverstein M. A Pattern Language: Towns, Buildings, Construction. Oxford University Press, 1977.