如何做性能优化

之前也做了几次的性能优化,每次的过程好像大体上都差不多,所以就此总结一下。如果有哪里不到位的地方,欢迎及时指正。

性能优化的过程,总概括下来可以分为三个阶段:

  1. 收集监控信息
  2. 思考业务和技术可能性
  3. 确定改进措施,验证及调整

这里我们主要对前两个部分进行展开。

收集监控信息

监控信息

无论在什么领域,监控都是流程中优化的关键。我们需要从上层下层的顺序去监控,采集到我们需要的信息,然后才能进行优化。

应用层

本层需要观察到优化的方法每一步的时间,从而逐个击破地进行优化。

需要固定的监控点位置有三种:

  1. API服务性能和流量监控,包括总的指标和分调用方的指标。
  2. 资源性能监控,包括远端缓存、DB、RPC请求等。
  3. 逻辑性能监控,优先选取包含资源获取和逻辑复杂的。

一般在本层我们需要两个工具:
方法监控: 观察单个方法的性能,对于优化TP50非常有用。
链路监控: 追踪偶尔性能较差的调用,对于优化TP50之上的指标比较有用。

JVM层

本层是除了应用层之外的第二关键的信息,主要包括三类信息:

  1. 内存变化情况
  2. YGC、FGC情况
  3. 线程活跃情况

这里的工具一般是通过JRE自带的jstat/jmap/jstack等命令或者第三方工具如javamelody来观察。

容器或物理机层

容器层的指标很多,一般来说我们只会观察以下三个指标:

  1. cpu使用情况
  2. 网络情况,包括流量和TCP重传。个人经验,TCP重传在分钟达到10次之上的可以认为是网络有问题。
  3. 磁盘情况,根路径满之后会造成cpu飙升的问题

容器层一般平台都会提供监控指标,不需要额外的工具。

这部分更多的作用是帮助我们排除掉个别异常的机器或分组,减少干扰。

思考业务和技术可能性

对监控的分析是很重要的,但是在确定动手之前,我们仍然需要进行可能性的收集,这个有时候会给我们带来远超预期的价值。在这里我们关注两个方面:

  1. 业务可能性,是否业务上可以进行调整,这个往往要涉及到产品经理和业务。是对业务的重塑。这里可以参见下面的业务设计优化篇中的例子。
  2. 技术可能性,这个属于比较硬核的,在这里我们要忘掉限制,放开自己的眼界,去探索更多的可能性,比如采用redis的单片上限,pipeline效率、编译优化、lua脚本之类。

确定改进措施并验证

在第一个阶段中,我们已经隐约有了一些想法,这个阶段就是要选择并验证。在这个阶段个人的经验最重要只有一点,不要设置边界。如果站在研发的视角,我们更多的关注是系统稳定性,所以有一些边界我们不愿意去做突破,但往往是这些边界限制了性能的提升程度。

确定改进措施

业务设计优化

从上面收集的资料,最好的改进措施应该是业务设计优化,在领域驱动开发的方式中会体现的比较多,在日常中比较难以实现。如果研发能够从自己的视角提出一些建议,帮助产品和业务去做的更好,那么这就是一个双赢,既保证了系统的稳定性,也实现了业务的快速响应。

举个例子: 业务的需求是在某个一级类目上设置属性,当读取价格时,如果有这个属性,那么就需要以A的逻辑去计算返回,否则以B的逻辑去计算返回。这个问题的难点是什么? 我个人对它的理解是它突破了现有的业务模型。在当下的模型里类目属性都在四级类目上,所以对于价格系统来说很难受,商品只会给我返回末级的类目id,我还需要查询查询几次拿到一级类目,然后再去拿属性,才能进行计算。
所以我觉得理想的解法应该是末级类目设置属性,提供批量设置的功能。这样从写和读的方式都不会要求现有的业务模型结构性的改变,读和写都达到一个平衡。

为什么属性在末级类目就合理呢?
之前也有查阅很多资料,给出的原因和商品挂末级类目比较一致。我们类比类目树就像一棵树,树干只是为了细分,目的是提供一个管理的路径,而不会存储数据,树枝才是真正存储数据的地方。这个结构无论是做管理、还是用来搜索都很简单明了,所以才会被认为是一个合理的结构。归根结底,简单的设计就是最好的设计。

逻辑设计优化

这部分依赖于我们之前的方法监控和链路监控,当我们发现一个步骤比较慢的时候,就要想办法优化。根据经验,我对这里的优化方法做一个总结。包括三个类型: 并发请求的执行优化,单个请求步骤优化和存储结构变更。

并行请求的执行优化:

  1. 请求方式改批量请求。这个是我们最常用的优化手段,通常能够成倍地提高性能。之前在做库存优化的时候第一个采取的就是这个措施,减少AOP、线程开销和RTT。
  2. 线程池优化。之前我们有使用forkjoin pool,但是发现无界队列会是一个大的问题,当一个慢的请求过来时,可能会造成服务的整体崩溃,所以这里推荐使用ThreadPoolExecutor的方式去构建线程池,并有以下几个建议:
    • 线程数分类。线程池的任务最好是性能比较接近的,甚至可以创建专有线程池来隔离互相影响。
    • 线程数。建议使用自研或者工具能够监控并灵活调整线程数。因为从现在来看,几乎是不可能评估出合理的线程数。
    • 队列深度。队列深度建议设置小一些,不超过核心线程数,这样的响应时间最多是翻倍。
    • 丢弃策略。对于异步刷新的任务如果业务允许可以选用丢弃策略,如果是API接口服务可以选择拒绝策略,对于服务内部流程正常选用调用线程中执行策略。

单个请求的步骤优化:

  1. 合并重复请求。跨系统的合并会有成倍的收益,单个系统内部合并重复请求的收益决定于合并的步骤对应的TP50之上的指标有多高。
  2. 服务类型拆分。通常来说,我们的服务里会对应几种类型不同的服务,比如库存对应了门店仓库存和区域库存,商品的接口既有基础信息,也有详描信息。假如我们服务中的类型调用量和服务性能都有差异,那么可以进行拆分,提高性能。

存储结构变更:

  1. 使用本地缓存。这里对本地缓存有以下几个建议。
    • 对于目标数据量比较小的,变更相对频繁的,采用较短的过期时间+异步刷新策略。
    • 对于目标数据量比较小的,变更几乎不变的,采用较长的过期时间即可。
    • 对于目标数据量比较大的,建议当做热点缓存使用,不设置过大的容量。
  2. 使用远程缓存。使用redis之类的远程缓存在一些情况下会成倍的提高接口的性能。远程缓存一般来说主要是要考虑容量和一致性两个问题。容量问题可以通过精细化分类,只缓存必须业务数据和设置过期策略两种方式解决,一致性的问题通常是要主动刷新和检查才可以保证。
  3. 改变存储结构。方式上分为合并和拆分。合并是指将核心接口用到的多个分散的key合并成为一个key,从而减少IO的次数。举个例子,比如说我们查门店sku,需要查product、sku、类目、类目属性、广告词等多个key的数据之后,才能得到一个完整的门店sku信息,那么这么多次IO的效率肯定会比较低下,我们可以把这些key的合并结果给存储下来,这样就一次IO可以,相当于拿空间换时间。拆分的话通常是解决热点问题,举一个例子比如说我们的sku存储了所有的门店信息,那么访问同一个sku的不同门店请求,都会落在sku上,从而造成热key,在这样的情况就需要做拆分。

资源API优化

资源API的优化,往往能带来N倍的性能提升。这里个人目前的是Redis缓存,通过pipeline,可以比hmget带来4倍以上的提升。

这部分最主要的优化可以来自于公司中间件的建议或者官方的一些命令探索。

Redis的dump命令有做过一些尝试,但在我们的场景提升并不明显。从dump和hmget命令说明来比,主要时间复杂度一部分可以从Redis转移到应用,降低Redis的cpu压力。

JVM优化

因为个人的经验都来自于Java的优化,所以这里的VM就变成了JVM。

JVM的优化网上有很多,所以这里就结合个人的经验来简单的总结一下。

  • 年轻代和老年代的大小和比例调整。对于一个高性能的应用来说,年轻代是主要的战场,可以通过观察YGC的次数和平均时间来对年轻代进行调整。这里有一个经验是如果应用的YGC很频繁,影响到了性能,那么可以调大年轻代。原理是扫描的数量变多,但是存活对象会变少,复制的时间远远大于扫描时间。
  • GC算法的调整。目前主要采用的PS、CMS、G1这三种。PS的吞吐量最高,所以默认JDK8采用的就是PS。CMS算法能够减少STW的时间,对于性能MAX要求严格的建议采用,但会产生内存碎片,在FullGc并发清理失败时,会退化成单线程清理,所以GC开始的清理时间很关键。G1算法由于存储结构进行了改进,可以通过设置每次YGC的上限来降低延迟,但对于小内存的效果不明显,建议的分界线大小是6G。6G这个数字来源官方数据的测试,如果你希望你的应用尽可能延迟FullGC,那么也可以改成G1。
  • 编译优化。这个其实一直都存在,从JDK6中就有的JIT编译优化,到现在比较为人所知的graalvm,个人简单地拿应用单机试了一下,的确效果明显,比宣称的40%还要多。

服务设计优化

通过对服务合适的分组也可以达到性能的优化。当然服务分组会有很多考虑,这里我们只考虑性能优化的部分。主要的区分依据来自服务组合区分和调用者分组。

服务组合区分是将流量和性能近似相同的服务单独分组,这样可以对资源进行细粒度的伸缩,降低各个服务之间的互相影响。

调用者分组是根据优先级、业务用途进行区分,这里建议提供三种分组。
核心交易分组: 请求次数、批量个数、数据范围通常都会比较小,容易命中本地缓存。
worker分组: 在这个分组可以不设置本地缓存,主要是隔离一些异常的调用。
正常服务: 除上面2个之外的分组。如果数据存在两种存储介质中,比如Redis和文件,那么还可以将正常服务进行分成两个分组,从而能够对资源得到充分的利用。

验证

当我们确定了一个措施,如何验证这个措施有效呢?最主要的手段就是压测了。

首先我们需要建立我们的基准测试指标。这里包括未改进时的指标资源各种API的指标。这两个API有不同的作用,一个是我们当前的状况,另一个则是我们优化的上限。

接下来就可以使用控制变量法单机验证,逐个对优化措施叠加进行压测来,验证效果。

最后我们需要灰度集群验证和线上观察。

一些思考

在文章的最后也放出来个人的一些思考,欢迎大家讨论。

思考1: 在压测gravelvm之后我还是感触挺大的,竟然可以做到这么大的提升。虽然性能优化是永恒的课题,但优化的主导权是不是会一直在我们手中?

思考2: 对于监控工具的需求是无止尽的。

  • 链路监控的潜力很大。现在能做的是应用层的信息,如果能够深入到其他层那将为分析决策提供更重要的价值。
  • 监控的决策价值不足。尤其是现在的集群和网络监控,个人觉得得到的信息不足以决策。举例子: 为什么单机的性能到了集群之后就降了很多,如果是水桶原理,那么是哪些机器是有问题的?网络重传次数多大的时候会影响服务质量?
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值