常见性能优化策略的总结(转)

看到一篇好文,转过来好好学习html

 

阅读目录前端

add by zhj: 我我的感受性能优化分析影响性能的因素有哪些,而后按影响力的大小进行排序,而后进行排序。java

而后进一步分析每一个因素为什么会影响性能,把这些因素再找出来,再按影响力大小进行排序。基本上,通过mysql

这两层的分析,基本就够用了。对这些因素思考解决办法。git

1. 数据库层github

    咱们的目标是减小IO访问,或者将IO访问进行负载均衡,分配到多台服务器,并行计算。redis

    1.1 数据库的数据存储在硬盘,硬盘访问速度比内存慢太多,即IO多算法

    1.2 数据量大致使扫描记录多,间接致使IO多sql

    1.3 全部数据库访问都集中在一台数据库服务器,负载重数据库

 

2. 应用层

负载均衡

 

3. 前端

 

 

 

原文:http://tech.meituan.com/performance_tunning.html

本文要感谢我职级评定过程当中的一位评委,他建议把以前所作的各类性能优化的案例和方案加以提炼、总结,以文档的形式沉淀下来,并在内部进行分享。力求达到以下效果:

1. 造成可实践、可借鉴、可参考的各类性能优化的方案以及选型考虑点,同时配合具体的真实案例,其余人遇到类似问题时,不用从零开始。

2. 有助于开阔视野,除了性能优化以外,也能提供通用的常见思路以及方案选型的考虑点,帮助你们培养在方案选型时的意识、思惟以及作各类权衡的能力。

文章在内部分享后,引发强烈分享,获得了很多同事和朋友的承认和好评,以为对平常的工做有很好的指导做用。考虑到这些经验可能对业界同行也有帮助,因此在美团点评技术团队博客公开。

常见性能优化策略分类

代码

之因此把代码放到第一位,是由于这一点最容易引发技术人员的忽视。不少技术人员拿到一个性能优化的需求之后,言必称缓存、异步、JVM等。实际上,第一步就应该是分析相关的代码,找出相应的瓶颈,再来考虑具体的优化策略。有一些性能问题,彻底是因为代码写的不合理,经过直接修改一下代码就能解决问题的,好比for循环次数过多、做了不少无谓的条件判断、相同逻辑重复屡次等。

数据库

数据库的调优,总的来讲分为如下三部分:

SQL调优

这是最经常使用、每个技术人员都应该掌握基本的SQL调优手段(包括方法、工具、辅助系统等)。这里以MySQL为例,最多见的方式是,由自带的慢查询日志或者开源的慢查询系统定位到具体的出问题的SQL,而后使用explain、profile等工具来逐步调优,最后通过测试达到效果后上线。这方面的细节,能够参考MySQL索引原理及慢查询优化

架构层面的调优

这一类调优包括读写分离、多从库负载均衡、水平和垂直分库分表等方面,通常须要的改动较大,可是频率没有SQL调优高,并且通常须要DBA来配合参与。那么何时须要作这些事情?咱们能够经过内部监控报警系统(好比Zabbix),按期跟踪一些指标数据是否达到瓶颈,一旦达到瓶颈或者警惕值,就须要考虑这些事情。一般,DBA也会按期监控这些指标值。

链接池调优

咱们的应用为了实现数据库链接的高效获取、对数据库链接的限流等目的,一般会采用链接池类的方案,即每个应用节点都管理了一个到各个数据库的链接池。随着业务访问量或者数据量的增加,原有的链接池参数可能不能很好地知足需求,这个时候就须要结合当前使用链接池的原理、具体的链接池监控数据和当前的业务量做一个综合的判断,经过反复的几回调试获得最终的调优参数。

缓存

分类

本地缓存(HashMap/ConcurrentHashMap、Ehcache、Guava Cache等),缓存服务(Redis/Tair/Memcache等)。

使用场景

什么状况适合用缓存?考虑如下两种场景:

  • 短期内相同数据重复查询屡次且数据更新不频繁,这个时候能够选择先从缓存查询,查询不到再从数据库加载并回设到缓存的方式。此种场景较适合用单机缓存。
  • 高并发查询热点数据,后端数据库不堪重负,能够用缓存来扛。

选型考虑

  • 若是数据量小,而且不会频繁地增加又清空(这会致使频繁地垃圾回收),那么能够选择本地缓存。具体的话,若是须要一些策略的支持(好比缓存满的逐出策略),能够考虑Ehcache;如不须要,能够考虑HashMap;如须要考虑多线程并发的场景,能够考虑ConcurentHashMap。
  • 其余状况,能够考虑缓存服务。目前从资源的投入度、可运维性、是否能动态扩容以及配套设施来考虑,咱们优先考虑Tair。除非目前Tair还不能支持的场合(好比分布式锁、Hash类型的value),咱们考虑用Redis。

设计关键点

何时更新缓存?如何保障更新的可靠性和实时性?

更新缓存的策略,须要具体问题具体分析。这里以门店POI的缓存数据为例,来讲明一下缓存服务型的缓存更新策略是怎样的?目前约10万个POI数据采用了Tair做为缓存服务,具体更新的策略有两个:

  • 接收门店变动的消息,准实时更新。
  • 给每个POI缓存数据设置5分钟的过时时间,过时后从DB加载再回设到DB。这个策略是对第一个策略的有力补充,解决了手动变动DB不发消息、接消息更新程序临时出错等问题致使的第一个策略失效的问题。经过这种双保险机制,有效地保证了POI缓存数据的可靠性和实时性。

缓存是否会满,缓存满了怎么办?

对于一个缓存服务,理论上来讲,随着缓存数据的日益增多,在容量有限的状况下,缓存确定有一天会满的。如何应对?
① 给缓存服务,选择合适的缓存逐出算法,好比最多见的LRU。
② 针对当前设置的容量,设置适当的警惕值,好比10G的缓存,当缓存数据达到8G的时候,就开始发出报警,提早排查问题或者扩容。
③ 给一些没有必要长期保存的key,尽可能设置过时时间。

缓存是否容许丢失?丢失了怎么办?

根据业务场景判断,是否容许丢失。若是不容许,就须要带持久化功能的缓存服务来支持,好比Redis或者Tair。更细节的话,能够根据业务对丢失时间的容忍度,还能够选择更具体的持久化策略,好比Redis的RDB或者AOF。

缓存被“击穿”问题

对于一些设置了过时时间的key,若是这些key可能会在某些时间点被超高并发地访问,是一种很是“热点”的数据。这个时候,须要考虑另一个问题:缓存被“击穿”的问题。

  • 概念:缓存在某个时间点过时的时候,刚好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过时通常都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
  • 如何解决:业界比较经常使用的作法,是使用mutex。简单地来讲,就是在缓存失效的时候(判断拿出来的值为空),不是当即去load db,而是先使用缓存工具的某些带成功操做返回值的操做(好比Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操做返回成功时,再进行load db的操做并回设缓存;不然,就重试整个get缓存的方法。相似下面的代码:

    public String get(key) { String value = redis.get(key); if (value == null) { //表明缓存值过时 //设置3min的超时,防止del操做失败的时候,下次缓存过时一直不能load db if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //表明设置成功 value = db.get(key); redis.set(key, value, expire_secs); redis.del(key_mutex); } else { //这个时候表明同时候的其余线程已经load db并回设到缓存了,这时候重试获取缓存值便可 sleep(50); get(key); //重试 } } else { return value; } } 

异步

使用场景

针对某些客户端的请求,在服务端可能须要针对这些请求作一些附属的事情,这些事情其实用户并不关心或者用户不须要当即拿到这些事情的处理结果,这种状况就比较适合用异步的方式处理这些事情。

做用

  • 缩短接口响应时间,使用户的请求快速返回,用户体验更好。
  • 避免线程长时间处于运行状态,这样会引发服务线程池的可用线程长时间不够用,进而引发线程池任务队列长度增大,从而阻塞更多请求任务,使得更多请求得不到技术处理。
  • 线程长时间处于运行状态,可能还会引发系统Load、CPU使用率、机器总体性能降低等一系列问题,甚至引起雪崩。异步的思路能够在不增长机器数和CPU数的状况下,有效解决这个问题。

常见作法

一种作法,是额外开辟线程,这里能够采用额外开辟一个线程或者使用线程池的作法,在IO线程(处理请求响应)以外的线程来处理相应的任务,在IO线程中让response先返回。

若是异步线程处理的任务设计的数据量很是巨大,那么能够引入阻塞队列BlockingQueue做进一步的优化。具体作法是让一批异步线程不断地往阻塞队列里扔数据,而后额外起一个处理线程,循环批量从队列里拿预设大小的一批数据,来进行批处理(好比发一个批量的远程服务请求),这样进一步提升了性能。

另外一种作法,是使用消息队列(MQ)中间件服务,MQ天生就是异步的。一些额外的任务,可能不须要我这个系统来处理,可是须要其余系统来处理。这个时候能够先把它封装成一个消息,扔到消息队列里面,经过消息中间件的可靠性保证把消息投递到关心它的系统,而后让这个系统来作相应的处理。

好比C端在完成一个提单动做之后,可能须要其它端作一系列的事情,可是这些事情的结果不会马上对C端用户产生影响,那么就能够先把C端下单的请求响应先返回给用户,返回以前往MQ中发一个消息便可。并且这些事情理应不是C端的负责范围,因此这个时候用MQ的方式,来解决这个问题最合适。

NoSQL

和缓存的区别

先说明一下,这里介绍的和缓存那一节不同,虽然可能会使用同样的数据存储方案(好比Redis或者Tair),可是使用的方式不同,这一节介绍的是把它做为DB来用。若是看成DB来用,须要有效保证数据存储方案的可用性、可靠性。

使用场景

须要结合具体的业务场景,看这块业务涉及的数据是否适合用NoSQL来存储,对数据的操做方式是否适合用NoSQL的方式来操做,或者是否须要用到NoSQL的一些额外特性(好比原子加减等)。

若是业务数据不须要和其余数据做关联,不须要事务或者外键之类的支持,并且有可能写入会异常频繁,这个时候就比较适合用NoSQL(好比HBase)。

好比,美团点评内部有一个对exception作的监控系统,若是在应用系统发生严重故障的时候,可能会短期产生大量exception数据,这个时候若是选用MySQL,会形成MySQL的瞬间写压力飙升,容易致使MySQL服务器的性能急剧恶化以及主从同步延迟之类的问题,这种场景就比较适合用Hbase相似的NoSQL来存储。

JVM调优

何时调?

经过监控系统(如没有现成的系统,本身作一个简单的上报监控的系统也很容易)上对一些机器关键指标(gc time、gc count、各个分代的内存大小变化、机器的Load值与CPU使用率、JVM的线程数等)的监控报警,也能够看gc log和jstat等命令的输出,再结合线上JVM进程服务的一些关键接口的性能数据和请求体验,基本上就能定位出当前的JVM是否有问题,以及是否须要调优。

怎么调?

  1. 若是发现高峰期CPU使用率与Load值偏大,这个时候能够观察一些JVM的thread count以及gc count(可能主要是young gc count),若是这两个值都比以往偏大(也能够和一个历史经验值做对比),基本上能够定位是young gc频率太高致使,这个时候能够经过适当增大young区大小或者占比的方式来解决。
  2. 若是发现关键接口响应时间很慢,能够结合gc time以及gc log中的stop the world的时间,看一下整个应用的stop the world的时间是否是比较多。若是是,可能须要减小总的gc time,具体能够从减少gc的次数和减少单次gc的时间这两个维度来考虑,通常来讲,这两个因素是一对互斥因素,咱们须要根据实际的监控数据来调整相应的参数(好比新生代与老生代比值、eden与survivor比值、MTT值、触发cms回收的old区比率阈值等)来达到一个最优值。
  3. 若是发生full gc或者old cms gc很是频繁,一般这种状况会诱发STW的时间相应加长,从而也会致使接口响应时间变慢。这种状况,大几率是出现了“内存泄露”,Java里的内存泄露指的是一些应该释放的对象没有被释放掉(还有引用拉着它)。那么这些对象是如何产生的呢?为啥不会释放呢?对应的代码是否是出问题了?问题的关键是搞明白这个,找到相应的代码,而后对症下药。因此问题的关键是转化成寻找这些对象。怎么找?综合使用jmap和MAT,基本就能定位到具体的代码。

多线程与分布式

使用场景

离线任务、异步任务、大数据任务、耗时较长任务的运行**,适当地利用,可达到加速的效果。

注意:线上对响应时间要求较高的场合,尽可能少用多线程,尤为是服务线程须要等待任务线程的场合(不少重大事故就是和这个息息相关),若是必定要用,能够对服务线程设置一个最大等待时间。

常见作法

若是单机的处理能力能够知足实际业务的需求,那么尽量地使用单机多线程的处理方式,减小复杂性;反之,则须要使用多机多线程的方式。

对于单机多线程,能够引入线程池的机制,做用有二:

  • 提升性能,节省线程建立和销毁的开销
  • 限流,给线程池一个固定的容量,达到这个容量值后再有任务进来,就进入队列进行排队,保障机器极限压力下的稳定处理能力在使用JDK自带的线程池时,必定要仔细理解构造方法的各个参数的含义,如core pool size、max pool size、keepAliveTime、worker queue等,在理解的基础上经过不断地测试调整这些参数值达到最优效果。

若是单机的处理能力不能知足需求,这个时候须要使用多机多线程的方式。这个时候就须要一些分布式系统的知识了。首先就必须引入一个单独的节点,做为调度器,其余的机器节点都做为执行器节点。调度器来负责拆分任务,和分发任务到合适的执行器节点;执行器节点按照多线程的方式(也多是单线程)来执行任务。这个时候,咱们整个任务系统就由单击演变成一个集群的系统,并且不一样的机器节点有不一样的角色,各司其职,各个节点之间还有交互。这个时候除了有多线程、线程池等机制,像RPC、心跳等网络通讯调用的机制也不可少。后续我会出一个简单的分布式调度运行的框架。

度量系统(监控、报警、服务依赖管理)

严格来讲,度量系统不属于性能优化的范畴,可是这方面和性能优化息息相关,能够说为性能优化提供一个强有力的数据参考和支撑。没有度量系统,基本上就没有办法定位到系统的问题,也没有办法有效衡量优化后的效果。不少人不重视这方面,但我认为它是系统稳定性和性能保障的基石。

关键流程

若是要设计这套系统,整体来讲有哪些关键流程须要设计呢?
① 肯定指标
② 采集数据
③ 计算数据,存储结果
④ 展示和分析

须要监控和报警哪些指标数据?须要关注哪些?

按照需求出发,主要须要二方面的指标:

  1. 接口性能相关,包括单个接口和所有的QPS、响应时间、调用量(统计时间维度越细越好;最好是,既能以节点为维度,也能够以服务集群为维度,来查看相关数据)。其中还涉及到服务依赖关系的管理,这个时候须要用到服务依赖管理系统
  2. 单个机器节点相关,包括CPU使用率、Load值、内存占用率、网卡流量等。若是节点是一些特殊类型的服务(好比MySQL、Redis、Tair),还能够监控这些服务特有的一些关键指标。

数据采集方式

一般采用异步上报的方式,具体作法有两种:第一种,发到本地的Flume端口,由Flume进程收集到远程的Hadoop集群或者Storm集群来进行运算;第二种,直接在本地运算好之后,使用异步和本地队列的方式,发送到监控服务器。

数据计算

能够采用离线运算(MapReduce/Hive)或者实时/准实时运算(Storm/Spark)的方式,运算后的结果存入MySQL或者HBase;某些状况,也能够不计算,直接采集发往监控服务器。

展示和分析

提供统一的展示分析平台,须要带报表(列表/图表)监控和报警的功能。

真实案例分析

案例一:商家与控制区关系的刷新job

背景

这是一个每小时按期运行一次的job,做用是用来刷新商家与控制区的关系。具体规则就是根据商家的配送范围(多个)与控制区是否有交集,若是有交集,就把这个商家划到这个控制区的范围内。

业务需求

须要这个过程越短越好,最好保持在20分钟内。

优化过程

原有代码的主要处理流程是:

  1. 拿到全部门店的配送范围列表和控制区列表。
  2. 遍历控制区列表,针对每个控制区:
    a. 遍历商家的配送范围列表,找到和这个控制区相交的配送范围列表。
    b. 遍历上述商家配送范围列表,对里面的商家ID去重,保存到一个集合里。
    c. 批量根据上述商家ID集合,取到对应的商家集合。
    d. 遍历上述商家集合,从中拿到每个商家对象,进行相应的处理(根据是否已经是热门商家、自营、在线支付等条件来判断是否须要插入或者更新以前的商家和控制区的关系)。
    e. 删除这个控制区当前已有的,可是不该该存在的商家关系列表。

分析代码,发现第2步的a步骤和b步骤,找出和某控制区相交的配送范围集合并对商家ID去重,能够采用R树空间索引的方式来优化。具体作法是:

  • 任务开始先更新R树,而后利用R树的结构和匹配算法来拿到和控制区相交的配送范围ID列表。
  • 再批量根据配送范围ID列表,拿到配送范围列表。
  • 而后针对这一批配送范围列表(数量很小),用原始多边形相交匹配的方法作进一步过滤,而且对过滤后的商家ID去重。

这个优化已经在第一期优化中上线,整个过程耗时由40多分钟缩短到20分钟之内

第一期优化改成R树之后,运行了一段时间,随着数据量增大,性能又开始逐渐恶化,一个月后已经恶化到50多分钟。因而继续深刻代码分析,寻找了两个优化点,安排第二期优化并上线。

这两个优化点是:

  • 第2步的c步骤,原来是根据门店ID列表从DB批量获取门店,如今能够改为mget的方式从缓存批量获取(此时商家数据已被缓存);
  • 第2步的d步骤,根据是否已经是热门商家、自营、在线支付等条件来判断是否须要插入或者更新以前的商家和控制区的关系。

上线后效果

经过日志观察,执行时间由50多分钟缩短到15分钟之内,下图是截取了一天的4台机器的日志时间(单位:毫秒):

 


能够看到,效果仍是很是明显的。

案例二:POI缓存设计与实现

背景

2014年Q4,数据库中关于POI(这里能够简单理解为外卖的门店)相关的数据的读流量急剧上升,虽说加入从库节点能够解决一部分问题,可是毕竟节点的增长是会达到极限的,达到极限后主从复制会达到瓶颈,可能会形成数据不一致。因此此时,急需引入一种新的技术方案来分担数据库的压力,下降数据库POI相关数据的读流量。另外,任何场景都考虑加DB从库的作法,会对资源形成必定的浪费。

实现方案

基于已有的通过考验的技术方案,我选择Tair来做为缓存的存储方案,来帮DB分担来自于各应用端的POI数据的读流量的压力。理由主要是从可用性、高性能、可扩展性、是否通过线上大规模数据和高并发流量的考验、是否有专业运维团队、是否有成熟工具等几个方面综合考量决定。

详细设计

初版设计

缓存的更新策略,根据业务的特色、已有的技术方案和实现成本,选择了用MQ来接收POI改变的消息来触发缓存的更新,可是这个过程有可能失败;同时启用了key的过时策略,而且调用端会先判断是否过时,如过时,会从后端DB加载数据并回设到缓存,再返回。经过两个方面双保险确保了缓存数据的可用。

第二版设计

初版设计运行到一段时间之后,咱们发现了两个问题:

  1. 某些状况下不能保证数据的实时一致(好比技术人员手动改动DB数据、利用MQ更新缓存失败),这个时候只能等待5分钟的过时时间,有的业务是不容许的。
  2. 加入了过时时间致使另一个问题:Tair在缓存不命中的那一刻,会尝试从硬盘中Load数据,若是硬盘没有再去DB中Load数据。这无疑会进一步延长Tair的响应时间,这样不只使得业务的超时比率加大,并且会致使Tair的性能进一步变差。

为了解决上述问题,咱们从美团点评负责基础架构的同事那里了解到Databus能够解决缓存数据在某些状况下不一致的问题,而且能够去掉过时时间机制,从而提升查询效率,避免tair在内存不命中时查询硬盘。并且为了防止DataBus单点出现故障影响咱们的业务,咱们保留了以前接MQ消息更新缓存的方案,做了切换开关,利用这个方案做容错,总体架构以下:

 

上线后效果

上线后,经过持续地监控数据发现,随着调用量的上升,到DB的流量有了明显地减小,极大地减轻了DB的压力。同时这些数据接口的响应时间也有了明显地减小。缓存更新的双重保障机制,也基本保证了缓存数据的可用。见下图:

 



案例三:业务运营后台相关页面的性能优化

背景

随着业务的快速发展,带来的访问量和数据量的急剧上升,经过咱们相应的监控系统能够发现,系统的某些页面的性能开始出现恶化。 从用户方的反馈,也证实了这点。此时此刻,有必要迅速排期,敏捷开发,对这些页面进行调优。

欢迎页

  • 需求背景:欢迎页是地推人员乃至总部各类角色人员进入外卖运营后台的首页,会显示地推人员最想看到最关心的一些核心数据,其重要性不言而喻,因此该页面的性能恶化会严重影响到用户体验。所以,首先须要优化的就是欢迎页。经过相应定位和分析,发现致使性能恶化的主要缘由有两个:数据接口层和计算展示层。
  • 解决方案:对症下药,分而治之。通过仔细排查、分析定位,数据接口层采用接口调用批量化、异步RPC调用的方式来进行有效优化,计算展示层决定采用预先计算、再把计算好的结果缓存的方式来提升查询速度。其中,缓存方案根据业务场景和技术特色,选用Redis。定好方案后,快速开发上线。
  • 上线效果:上线后性能对比图,以下:

     

组织架构页

  • 需求背景:组织架构页,采用了四层树形结构图,一块儿呈现加载,初版上线后发现性能很是差。用户迫切但愿对这个页面的性能进行调优。
  • 解决方案:通过分析代码,定位到一个比较经典的问题:里面执行了太屡次小数据量的SQL查询。因而采用多个SQL合并成大SQL的方式,而后使用本地缓存来缓存这些数据,合理预估数据量和性能,充分测试后上线。
  • 上线效果:上线后性能对比图,以下:

     

订单关联楼宇页

  • 需求背景:随着订单量日益增大,订单表积累的数据日益增多,订单关联楼宇页的性能也日益变差(响应时间线性上升)。而这个页面和地推人员的业绩息息相关,因此地推人员使用该页面的频率很是高,性能日益恶化极大地影响了地推人员的用户体验。
  • 解决方案:通过分析与设计,决定采用当时已有的订单二级索引月分表来代替原始的订单表来供前端的查询请求;而且限制住筛选的时间条件,使得筛选的开始时间和结束时间不能跨月(事先和用户沟经过,能够接受,能知足用户的基本需求),这样就只需一个月分索引表便可,经过适当的功能限制来达到性能的调优。这样从二级索引月分表中根据各类查询条件查到最终的分页的订单ID集合,而后再根据订单ID从订单库来查出相应的订单数据集合。
  • 上线效果:上线后发如今调用量几乎没怎么变的状况下,性能提高明显,以下图:

     

其余

除了上面介绍的以外,优化还涉及前端、分布式文件系统、CDN、全文索引、空间索引等几方面。限于篇幅,咱们留到将来再作介绍。

相关文章
相关标签/搜索