成都麦卡网络做网站开发怎么样,微信开放平台如何注销,搜索公司信息的软件,做网站费用联系方式本文要感谢我职级评定过程中的一位评委#xff0c;他建议把之前所做的各种性能优化的案例和方案加以提炼、总结#xff0c;以文档的形式沉淀下来#xff0c;并在内部进行分享。力求达到如下效果#xff1a; 1. 形成可实践、可借鉴、可参考的各种性能优化的方案以及选型考虑… 本文要感谢我职级评定过程中的一位评委他建议把之前所做的各种性能优化的案例和方案加以提炼、总结以文档的形式沉淀下来并在内部进行分享。力求达到如下效果 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 dbif (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是否有问题以及是否需要调优。 怎么调 如果发现高峰期CPU使用率与Load值偏大这个时候可以观察一些JVM的thread count以及gc count可能主要是young gc count如果这两个值都比以往偏大也可以和一个历史经验值作对比基本上可以定位是young gc频率过高导致这个时候可以通过适当增大young区大小或者占比的方式来解决。如果发现关键接口响应时间很慢可以结合gc time以及gc log中的stop the world的时间看一下整个应用的stop the world的时间是不是比较多。如果是可能需要减少总的gc time具体可以从减小gc的次数和减小单次gc的时间这两个维度来考虑一般来说这两个因素是一对互斥因素我们需要根据实际的监控数据来调整相应的参数比如新生代与老生代比值、eden与survivor比值、MTT值、触发cms回收的old区比率阈值等来达到一个最优值。如果发生full gc或者old cms gc非常频繁通常这种情况会诱发STW的时间相应加长从而也会导致接口响应时间变慢。这种情况大概率是出现了“内存泄露”Java里的内存泄露指的是一些应该释放的对象没有被释放掉还有引用拉着它。那么这些对象是如何产生的呢为啥不会释放呢对应的代码是不是出问题了问题的关键是搞明白这个找到相应的代码然后对症下药。所以问题的关键是转化成寻找这些对象。怎么找综合使用jmap和MAT基本就能定位到具体的代码。多线程与分布式 使用场景 离线任务、异步任务、大数据任务、耗时较长任务的运行**适当地利用可达到加速的效果。 注意线上对响应时间要求较高的场合尽量少用多线程尤其是服务线程需要等待任务线程的场合很多重大事故就是和这个息息相关如果一定要用可以对服务线程设置一个最大等待时间。 常见做法 如果单机的处理能力可以满足实际业务的需求那么尽可能地使用单机多线程的处理方式减少复杂性反之则需要使用多机多线程的方式。 对于单机多线程可以引入线程池的机制作用有二 - 提高性能节省线程创建和销毁的开销 - 限流给线程池一个固定的容量达到这个容量值后再有任务进来就进入队列进行排队保障机器极限压力下的稳定处理能力在使用JDK自带的线程池时一定要仔细理解构造方法的各个参数的含义如core pool size、max pool size、keepAliveTime、worker queue等在理解的基础上通过不断地测试调整这些参数值达到最优效果。 如果单机的处理能力不能满足需求这个时候需要使用多机多线程的方式。这个时候就需要一些分布式系统的知识了。首先就必须引入一个单独的节点作为调度器其他的机器节点都作为执行器节点。调度器来负责拆分任务和分发任务到合适的执行器节点执行器节点按照多线程的方式也可能是单线程来执行任务。这个时候我们整个任务系统就由单击演变成一个集群的系统而且不同的机器节点有不同的角色各司其职各个节点之间还有交互。这个时候除了有多线程、线程池等机制像RPC、心跳等网络通信调用的机制也不可少。后续我会出一个简单的分布式调度运行的框架。 度量系统监控、报警、服务依赖管理 严格来说度量系统不属于性能优化的范畴但是这方面和性能优化息息相关可以说为性能优化提供一个强有力的数据参考和支撑。没有度量系统基本上就没有办法定位到系统的问题也没有办法有效衡量优化后的效果。很多人不重视这方面但我认为它是系统稳定性和性能保障的基石。 关键流程 如果要设计这套系统总体来说有哪些关键流程需要设计呢 ① 确定指标 ② 采集数据 ③ 计算数据存储结果 ④ 展现和分析 需要监控和报警哪些指标数据需要关注哪些 按照需求出发主要需要二方面的指标 接口性能相关包括单个接口和全部的QPS、响应时间、调用量统计时间维度越细越好最好是既能以节点为维度也可以以服务集群为维度来查看相关数据。其中还涉及到服务依赖关系的管理这个时候需要用到服务依赖管理系统单个机器节点相关包括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、全文索引、空间索引等几方面。限于篇幅我们留到未来再做介绍。