性能调优方案制定模板

一、性能优化六大原则

1.三个“要”的原则 

    三个“要”的原则之间,其实是步步递进的关系。也就是首先需要查找最大性能瓶颈,然后确诊性能瓶颈产生的原因,最后针锋相对地提出最好的解决方案。而这个最优解,往往是在考虑各种情况之后提出来,并最终被选中的。

(1)要优先查最大的性能瓶颈 

    任何一个应用程序或者系统,总会有很多地方可以优化。永远都要优先从最大的性能瓶颈入手。一般来讲,如果找到最大的性能瓶颈,并且解决了它,那这个系统的性能会得到最大的提升。

(2)要确诊性能问题的根因 

    当确定了最大的性能瓶颈后,就需要对这一性能瓶颈做彻底的性能分析,找出资源不够使用的原因,也就是考察使用资源的地方。只有彻底分析了各种使用的情况,才能进一步找出最主要的,也是最可能优化的原因,对症下药。 有些资源使用的原因也许是完全合理的。对这些合理的使用,有些或许已经仔细优化过了,很难再做优化。而另外一些则有可能继续优化。对资源的不合理使用,就要尽量想办法去掉。对于需要优化的地方,就需要进一步考虑优化工作的投入产出比例,就是既考虑成本,也考虑带来的好处。因为有些情况下,虽然你可以去优化,但获得的收益并不大,所以不值得去做。 确诊性能问题的原因有时候非常困难,需要做多方面的性能测试、假设分析并验证。比如 CPU 的使用,有操作系统的原因,有应用程序的原因,但也有些 CPU 的问题,是在非常边缘的场景下才发生的。为了暴露问题,我们经常需要创造特殊的场景来重现遇到的性能问题。

(3)要考虑各种情况下的性能 

    一般说来,找一个解决方案并不难,甚至找好几个方案也不难;但是找出一个好的、最优的解决方案是不容易。因为实际生产环境很复杂,而且会出现各种各样的特殊场景。针对某个具体场景提出的一个解决方案,多半并不能适应所有的场景。 所以对提出的各种方案进行评估时,必须考虑各种情况下这个方案可能的表现。如果一个方案在某些情况下会导致其他严重的问题,这个方案或许就不是一个好的方案。            

      但同时也需要意识到,任何解决方案都有长短。如果苛求一个能在所有场景下都最优的解决方案,往往是不现实的。比如一种优化方案,可以让平均响应时间最小,但高百分位比较高。另外一种优化方案正好相反。那我们就需要考虑对自己来说哪种指标更重要。也就是说,我们经常需要在不同性能指标间权衡,以找到一个最优解能达到总体和整体最优。

2.三个“不要”的原则

(1)不要过度地反常态优化 

    性能优化的目标,是追求最合适的性价比或最高的投入产出比,在满足要求的情况下,尽量不要做过度的优化。过度的优化会增加系统复杂度和维护成本,使得开发和测试周期变长。虽然性能上带来了一定程度的提升,但是和导致的缺点来比,孰轻孰重尚不可知,需要仔细斟酌,衡量得失。 

       在设计产品时,我们对产品的性能会有一定的要求,比如吞吐量,或者客户响应时间要达到 多少。如果达不到这个既定指标,就需要去优化。反之,如果能满足这些指标,那么就不必要花费太多时间精力去优化。 

(2)不要过早的不成熟优化 

    “尽快推出产品”是最重要的。这时,过早的优化很可能优化错地方,也就是优化的地方并非真正的性能瓶颈,因此让“优化工作”成为了无用功。而且,越早的优化就越容易造成负面影响,比如影响代码的可读性和维护性。 如果一个产品已经在业界很成熟,大家非常清楚它的生产环境特点和性能瓶颈,那么优化的重要性可以适当提高。否则的话,在没有实际数据指标的基础上,为了一点点的性能提升而进行盲目优化,的确是得不偿失的。

(3)不要表面的肤浅优化 

    如果对一个程序和服务没有全局的把握,没有理解底层运行机制,任何优化方案都很难达到最好的优化效果。 比如,如果你发现一个应用程序的 CPU 使用率并不高,但是吞吐率上不去,表面的优化方式可能是增大线程池来提升 CPU 使用率。这样的简单“优化”或许当时能马上看到效果,比如吞吐率也上去了,但是如果你仔细想想,就会发现如此的表面优化非常有问题。这样的情况下,线程池开多大最合适?需不需要根据底层硬件和上层请求的变化而对线程池的大小调优呢?如果需要,那么手工调整线程池大小就是一个典型的“头痛医头”的优化。 因为部署环境不会一成不变,比如以后 CPU 升级了,核数变多了,你怎么办?再次手工去调整吗?这样做很快会让人疲于奔命,难以应付,并且很容易出错。对这样的场景,正确的优化方式,是彻底了解线程的特性,以优化线程为主。至于线程池的大小,最好能够自动调整。千万别动不动就手工调优。如果这样手工调整的参数多了,就会做出一个有很多可调参数的复杂系统,很难用,也很难调优,很不可取。就比如我们都熟悉的 JVM 调优,有上千个可调参数,非常被人诟病。

二、性能优化策略

1.时空转换

(1)用时间换空间 

    用时间换空间的策略,出发点是内存和存储这样的“空间”资源,有时会成为最稀缺的资源,所以需要尽量减少占用的空间。比如,一个系统的最大性能瓶颈如果是内存使用量,那么减少内存的使用就是最重要的性能优化。这个策略具体的操作方法有几种:

  • 改变应用程序本身的数据结构或者数据格式,减少需要存储的数据的大小;

  • 想方设法压缩存在内存中的数据,比如采用某种压缩算法,真正使用时再解压缩;

  • 把一些内存数据,存放到外部的、更加便宜的存储系统里面,到需要时再取回来

    这些节省内存空间的方法,一般都需要付出时间的代价。除了内存,还有一种常见的场景是,降低数据的大小来方便网络传输和外部存储。压缩的方法和算法有很多种, 比如现在比较流行的 ZStandard(ZSTD)和 LZ4。这些算法之间有空间和时间的取舍。 衡量任何压缩算法,基本上看三个指标:压缩比例、压缩速度以及使用内存。如果系统的瓶颈在网络传输速度或者存储空间大小上,那就尽量采取高压缩比的算法,这样用时间来换空间,就能够节省时间或者其他方面的成本。

(2)用空间换时间 

    有些场景下,时间和速度更加重要,但是空间尚有富余,这时就可以考虑用空间来换时间。一般来讲,“缓存”使用的空间,和原来的空间不在同一个层次上,添加的缓存往往比原来的空间高出一个档次。而这里“空间换时间”的策略,里面的“空间”是和原来的空间相似的空间。 

      互联网的服务往往规模很大,比如全国的服务甚至是全球的服务。用户分布在各地,它们对访问时间的要求很高,这就要求被访问的数据和服务,要尽量放在离他们很近的地方。“空间换时间”就是对数据和服务进行多份拷贝,尽可能地完美覆盖大多数的用户。我们前面讲过的 CDN 内容分发网络技术就可以归类于此。 其实部署的任何大规模系统,都或多或少地采用了用空间换时间的策略,比如在集群和服务器间进行负载均衡,就是同时用很多个服务器(空间)来换取延迟的减少(时间)。

2.预先/延后处理

(1)预先 / 提前处理 

    预先 / 提前处理策略同样也表现在很多领域,比如网站页面资源的提前加载。Web 标准规定了至少两种提前加载的方式:preload 和 prefetch,分别用不同的优先级来加载资源,可以显著地提升页面下载性能。很多文件系统有预读的功能,就是提前从磁盘读取额外的数据,为下次上层应用程序读数据做准备。这个功能对顺序读取非常有效,可以明显地减少磁盘请求的数量,从而提升读数据的性能。 CPU 和内存也有相应的预取操作,就是将内存中的指令和数据,提前存放到缓存中,从而加快处理器执行速度。缓存预取可以通过硬件或者软件实现,也就是分为硬件预取和软件预取两类。硬件预取是通过处理器中的硬件来实现的。该硬件会一直监控正在执行程序中请求的指令或数据,并且根据既定规则,识别下一个程序需要的数据或指令并预取。软件预取是在程序编译的过程中,主动插入预取指令(prefetech),这个预取指令可以是编译器自己加的,也可以是我们加的代码。这样在执行过程中,在指定位置就会进行预取的操作。

(2)延后 / 惰性处理 

    尽量将操作(比如计算),推迟到必需执行的时刻,这样很可能避免多余的操作,甚至根本不用操作。运用这一策略最有名的例子,就是 COW(Copy On Write,写时复制)。假设多个线程都想操作一份数据,一般情况下,每个线程可以自己拷贝一份,放到自己的空间里面。但是拷贝的操作很费时间。系统如果采用惰性处理,就会将拷贝的操作推迟。如果多个线程对这份数据只有读的请求,那么同一个数据资源是可以共享的,因为“读”的操作不会改变这份数据。当某个线程需要修改这一数据时(写操作),系统就将资源拷贝一份给该线程使用,允许改写,这样就不会影响别的线程。 COW 最广为人知的应用场景有两个。一个是 Unix 系统 fork 调用产生的子进程共享父进程的地址空间,只有到某个子进程需要进行写操作才会拷贝一份。另一个是高级语言的类和容器,比如 Java 中的 CopyOnWrite 容器,用于多线程并发情况下的高效访问。C++ 里面经常使用的 STL 标准模板库中的很多类,比如 string 类,也是具有写时才拷贝技术的类。

3.并行 / 异步操作

(1)并行操作 

    并行操作是一种物理上把一条流水线分成好几条的策略。直观上说,一个人干不完的活,那就多找几个人来干。并行操作既增加了系统的吞吐量,又减少了用户的平均等待时间。比如现代的 CPU 都有很多核,每个核上都可以独立地运行线程,这就是并行操作。并行操作需要我们的程序有扩展性,不能扩展的程序,就无法进行并行处理。这里的并行概念有不同的粒度,比如是在服务器的粒度(所谓的横向扩展),还是在多线程的粒度,甚至是在指令级别的粒度。 绝大多数互联网服务器,要么使用多进程,要么使用多线程来处理用户的请求,以充分利用多核 CPU。另外一种情况就是在有 IO 阻塞的地方,也是非常适合使用多线程并行操作的,因为这种情况 CPU 基本上是空闲状态,多线程可以让 CPU 多干点活。

(2)异步操作 

      异步操作这一策略和并行操作不同,这是一种逻辑上把一条流水线分成几条的策略。 

4.缓存 / 批量合并

(1)缓存数据 

    缓存的本质是加速访问。这是一个用得非常普遍的策略,几乎体现在计算机系统里面每一个模块和领域,CPU、内存、文件系统、存储系统、内容分布、数据库等等,都会遵循这样的策略。 程序设计中,对于可能重复创建和销毁,且创建销毁代价很大的对象(比如套接字和线程),也可以缓存,对应的缓存形式,就是连接池和线程池等。对于消耗较大的计算,也可以将计算结果缓存起来,下次可以直接读取结果。比如对递归代码的一个有效优化手段,就是缓存中间结果。

(2)批量合并处理 

在有 IO(比如网络 IO 和磁盘 IO)的时候,合并操作和批量操作往往能提升吞吐量,提高性能。

  • 最常见的是批量 IO 读写。就是在有多次 IO 的时候,可以把它们合并成一次读写数据。这样可以减少读写时间和协议负担。比如,GFS 写文件的时候,尽量批量写,以减少IO 开销。

  • 对数据库的读写操作,也可以尽量合并。比如,对键值数据库的查询,最好一次查询多个键,而不要分成多次。

  • 涉及到网络请求的时候,网络传输的时间可能远大于请求的处理时间,因此合并网络请求也很有必要。上层协议呢,端到端对话次数尽量不要太频繁(Chatty),否则的话,总的应用层吞吐量不会很高。

5.更先进算法和数据结构

(1)先进的算法 

    同一个问题,肯定会有不同的算法实现,进而就会有不同的性能。比如各种排序算法,就是各有千秋。有的实现可能是时间换空间,有的实现可能是空间换时间,那么就需要根据你自己的实际情况做权衡。对每一种具体的场景(包括输入集合大小、时间空间的要求、数据的大小分布等),总会有一种算法是最适合的。我们需要考虑实际情况,来选择这一最优的算法。

(2)高效的数据结构 

    没有一个数据结构是在所有情况下都是最好的,比如你可能经常用到的 Java 里面列表的各种实现,包括各种口味的 List、Vector、LinkedList,它们孰优孰劣,取决于很多个指标:添加元素、删除元素、查询元素、遍历耗时等等。我们同样要权衡取舍,找出实际场合下最适合的高效的数据结构。

四、制定调优方案

1.应用调优

  • 提高系统的处理核心数(并发,并发太多会抢资源)

  • 减少单次任务响应时间

(1)优化代码

    应用层的问题代码往往会因为耗尽系统资源而暴露出来。例如某段代码导致内存溢出,往往是将 JVM 中的内存用完了,这个时候系统的内存资源消耗殆尽了,同时也会引发 JVM 频繁地发生垃圾回收,导致 CPU 100% 以上居高不下,这个时候又消耗了系统的 CPU 资源。

还有一些是非问题代码导致的性能问题,这种往往是比较难发现的,需要依靠经验来优化。例如经常使用的 LinkedList 集合,大数据量下如果使用 for 循环遍历该容器,将大大降低读的效率,但这种效率的降低很难导致系统性能参数异常。

(2)优化设计

面向对象有很多设计模式,可以帮助我们优化业务层以及中间件层的代码设计。优化后,不仅可以精简代码,还能提高整体性能。例如,单例模式在频繁调用创建对象的场景中,可以共享一个创建对象,这样可以减少频繁地创建和销毁对象所带来的性能消耗。

(3)优化算法

好的算法可以帮助我们大大地提升系统性能。例如,在不同的场景中,使用合适的查找算法可以降低时间复杂度。

(4)并行和异步

(5)批量

多次单一操作 → 合并为单次批量操作。

2.系统参数调优

JVM、Web 容器以及操作系统的优化也是非常关键的。

根据自己的业务场景,合理地设置 JVM 的内存空间以及垃圾回收算法可以提升系统性能。例如,如果我们业务中会创建大量的大对象,我们可以通过设置,将这些大对象直接放进老年代。这样可以减少年轻代频繁发生小的垃圾回收(Minor GC),减少 CPU 占用时间,提升系统性能。

Web 容器线程池的设置以及 Linux 操作系统的内核参数设置不合理也有可能导致系统性能瓶颈,根据自己的业务场景优化这两部分,可以提升系统性能。

3.调优策略

(1)时间换空间

有时候系统对查询时的速度并没有很高的要求,反而对存储空间要求苛刻,这个时候我们可以考虑用时间来换取空间。

(2)空间换时间

这种方法是使用存储空间来提升访问速度。现在很多系统都是使用的 MySQL 数据库,较为常见的分表分库是典型的使用空间换时间的案例。

因为 MySQL 单表在存储千万数据以上时,读写性能会明显下降,这个时候我们需要将表数据通过某个字段 Hash 值或者其他方式分拆,系统查询数据时,会根据条件的 Hash 值判断找到对应的表,因为表数据量减小了,查询性能也就提升了。

4.升级软件

相对旧版本,新发布的稳定版本明显性能更高。坚持升级,也可以保证在调优、问题修复和安全警报方面与时俱进。

5.代码预热

对一些高并发服务进行的预热,其实并不是期望 JVM 能对热点代码做 JIT 等优化,对线程池、连接池和本地缓存的预热才是重点。

6.简化

  • 业务层面:流程精简、需求简化。

  • 编码层面:循环内减少高开销操作。

  • 架构层面:减少没必要的抽象/分层。

  • 数据层面:数据清洗、提取、聚合。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值