性能优化的方法论建设

一. 相关概念

系统性能有两个相关的概念:

  1. Throughput ,吞吐量。也就是每秒钟可以处理的请求数,任务数。
  2. Latency, 系统延迟。也就是系统在处理一个请求或一个任务时的延迟。

而这两者之间又是存在着一些联系

  • Throughput越大,Latency会越差。因为请求量过大,系统太繁忙,所以响应速度自然会低。
  • Latency越好,能支持的Throughput就会越高。因为Latency短说明处理速度快,于是就可以处理更多的请求。

Throughput就是我们系统的qps,而Latency可以认为是接口的处理耗时。

因此优化每个接口的性能能够提升系统的qps。

另外还有两个概念

  • 计算密集型主要消耗大量CPU资源,不停进行计算。由于依靠CPU性能,一直占用CPU进行计算。
  • io  密集型磁盘读取,web服务等任务,主要需要IO的读取,利用CPU的效率较低,大量时间花费在IO上。

对于表现为IO密集的进程,应该经常运行,但每次时间片不要太长。对于表现为CPU密集的进程,CPU不应该让其经常运行,但每次运行时间片要长。

简单来说,就是计算密集型的系统减少线程的使用,而io密集型可以多创建线程。

我们目前的系统(web server)几乎都是io密集型。也存在少量单个接口处理请求的过程可能是计算密集型。


二. 常见性能优化的思考

先思考一个问题:

在代码中使用线程一定能优化性能吗?

扫描二维码关注公众号,回复: 938036 查看本文章

这里有两个不同的例子

例子一 : 对比两个处理数据的方法

使用线程的方法:


不适用线程的方法:


使用线程的用时:31841

不使用线程的用时:20946

所以使用线程并没有优化到性能。

分析

为什么这个时候使用线程并不能优化性能?原因很简单,计算数据的方法getNumber()使用到synchronized关键字,调用这个方法的时候会加上this锁,因此每次只能有一个线程调用这个方法,其他线程阻塞了,只能等待释放锁后才能开始执行。因此,使用了多线程也只能串行计算。相反的,多线程还会多了一些线程切换的时间(别小看这个线程切换的时间,当需要处理的请求很多而处理时间很短时,线程间切换的消耗会激增)。

因此,当核心代码中有同步代码或者锁的时候,这时使用多线程并不能优化性能。

例子二:对比两个处理逻辑中有sleep的方法

使用线程的方法:


不使用线程的方法:


使用线程的用时:1018

不使用线程的用时:100389

所以使用线程很明显极大的优化了性能。

接下来两个问题

  • 为什么能优化?
  • 在哪里优化了这么多?

第一个问题,因为线程在执行核心逻辑时,会执行到Thread.sleep(1000);在这里,线程会进入sleep休眠状态,当前线程会让出CPU给其他线程执行,所以这100个线程几乎同时进入休眠状态,而1000ms后,线程几乎同时执行完了,得到了结果。因此,使用线程可以优化。

第二个问题,线程优化在共享了所有线程阻塞的时间,所有线程几乎一起阻塞的,这样总处理时间近似等于最大的一个线程处理时间。而不使用线程则是所有处理时间的相加之和。

串行调用:


多线程future调用:


因此,这里使用多线程可优化100倍的时间。


最后一个计算数据例子给大家思考

一个计算数据的例子,典型的计算密集型的操作。其中有很耗时的计算,没有使用多线程的方法:


与之对比的是实现相同的功能,使用了100核心线程数的方法:


有三个问题:

  • 本机的情况下,使用线程能不能优化性能?
  • 如果能优化性能,为什么可以优化?能节省多少的时间?在哪里优化了?
  • 如果不能优化,为什么不能优化?

 

三. 性能优化的基本原则

3.1 计算密集型

接上一个问题,先给出答案

第一个方法的耗时:180ms

第二个方法的耗时:51ms

多次运行后发现使用线程的耗时也是不使用线程的1/4左右。

为什么是1/4?

因为这个cpu计算密集型的方法并没有阻塞的地方,运行过程中会一直持有cpu运算。对于CPU的某一核来说,它的利用率基本上是100%了,此时提高CPU利用率就是使用其他核的CPU用来计算了,开了多线程操作系统会分配4个CPU一起进行计算,这样性能就可以优化4倍了。如果是单核的只有一个核而且已经分配给程序进行运算了,这时使用多线程是毫无意义的,只会增加线程间的切换时间,并不能优化性能。(这是我预计的,并没有单核cpu的机器来试验,如果有同学有条件可以帮我验证一下)

最后说明一下,那些说最佳线程数=CPU + 1的这种算法其实根本就不适合io密集型的场景,由于有io等待时间,很多线程会很快的进入到阻塞状态,所以线程设置太少还是会浪费资源的,tomcat的线程池本身就有上百个核心线程,就不符合这个标准(当然线程数也不要设置太大,当qps增大时,线程数太多会有频繁的线程切换)。除非你的场景是计算密集型的,而且线程数都有你自己控制,这时候可以设计最佳线程数=CPU + 1。

那么这种计算密集型的场景如何继续优化?

使用MapReduce思想,把运算分算出去,交给其他机器完成,主程序等待拿回结果。而这本质上是一个空间(整个计算集群)换时间的思想。

3.2 多线程优化的实质

使用多线程在没有加锁且有阻塞的情况下确实可以优化性能,那么优化的实质是什么呢?

进程执行由CPU执行周期和I/O等待周期组成。进程在这两个状态之间切换(CPU burst—I/O bust)。

进程执行从CPU区间(CPU burst)开始,在这之后是I/O区间(I/O burst)。接着另外一个CPU区间,然后是另外一个I/O区间,如此进行下去,最终,最后的CPU区间通过系统请求中止执行。 

单线程与多线程的对比:


分析这两种情况的运行过程,原始的方法在每一个处理单元(CPU计算 + 线程、IO阻塞)都会让主线程(处理本次请求的线程)进入阻塞状态,这样整个线程的执行就是走走停停,这里新加一个概念叫CPU处理时间的占比。

  • CPU处理时间的占比 = 单个CPU计算时间 / 单个CPU计算时间 + 单个线程、IO阻塞时间 = CPU计算总时间 / CPU计算总时间 + 线程、IO阻塞总时间

而使用多线程的方法,由于每个处理单元都由一个线程来执行,虽然每个新建的线程会进入阻塞状态,但是在不同的线程中,其他线程都可以在等待cpu计算完后同时进入阻塞状态。这样就共享了阻塞时间。而主线程只用等待最长阻塞的线程返回就可以处理完这个过程了。那么在这种情况下

  • CPU处理时间的占比 = CPU计算总时间 / CPU计算总时间 + 最长的一个线程、IO阻塞时间 ≈ CPU计算总时间 / CPU计算总时间 + 一个线程、IO阻塞时间

这样本质上的区别就是在消耗少量资源(线程)的情况下,极大的提高了CPU的利用率。

3.3 nio的思考

nginx采用nio来处理请求,先来看一下nginx的处理逻辑。
首先假如不使用nio的话,nginx可以怎么处理?
nginx会让一个server使用一个进程(或线程)负责一个request的方式,那么进程数就是并发数。那么显而易见的,就是会有很多进程在等待中。等什么?最多的应该是等待网络传输。

简单来说:同样的4个进程,如果采用一个进程负责一个request的方式,那么,同时进来4个request之后,每个进程就负责其中一个,直至会话关闭。期间,如果有第5个request进来了。就无法及时反应了,因为4个进程都没干完活呢,因此,一般有个调度进程,每当新进来了一个request,就新开个进程来处理。

而nginx 的异步非阻塞工作方式正是利用了这点等待的时间。在需要等待的时候,这些进程就空闲出来待命了。因此表现为少数几个进程就解决了大量的并发问题。

nginx每进来一个request,会有一个worker进程去处理。但不是全程的处理,处理到什么程度呢?处理到可能发生阻塞的地方,比如向上游(后端)服务器转发request,并等待请求返回。那么,这个处理的worker不会这么傻等着,他会在发送完请求后,注册一个事件:“如果upstream返回了,告诉我一声,我再接着干”。于是他就休息去了。此时,如果再有request 进来,他就可以很快再按这种方式处理。而一旦上游服务器返回了,就会触发这个事件,worker才会来接手,这个request才会接着往下走。

由于web server的工作性质决定了每个request的大部份生命都是在网络传输中,实际上花费在server机器上的时间片不多。

3.4 性能优化的两大原则

冯·诺依曼结构的计算机是由五大部分组成的:

  • 控制器
  • 运算器
  • 存储器
  • 输入设备
  • 输出设备

控制器和运算器共同组成了CPU,提供程序的执行和运算,存储器分为内存和外部存储器,用于提供存放数据。输入、输出设备其实并不单单指常见的显示器、鼠标等,是计算机与外界进行信息交换的桥梁,在服务中更多的是指网络IO输入输出等。

简单的结构体系图:


计算机的核心就是由这五大部分构成,网络io的输入、输出主要是网卡、带宽等资源,这是已经确定了的,由计算机底层优化,能优化的空间不大(也可以通过改变tcp缓冲区的大小来特地优化某些场景的性能,这个以后再说)。

其他的就有三个部分,也就是两个部件:CPU和内存。

那么性能优化就有两个原则:

  • 提高CPU的使用率
  • 空间换时间

这两个原则本质上是从两个方面优化的。

提高CPU的使用率即是在单机的情况下尽可能多的参与程序运行和计算,在整体上减少进程阻塞、等待的时间,单位时间内执行更多的指令。

而空间换时间的本质是使用计算机的存储器资源减少CPU计算的时间,最终达到优化性能的目的。


四. 性能优化的步骤

4.1 CAT埋点

如果你遇到一个性能不达标的方法,首先你需要知道到底是哪一块代码耗时很长,知彼知己,才能百战不殆。我们公司内部有cat监控系统。

如何cat埋点?

cat的Transaction能够统计一段代码执行的时间,所以,天生适合性能优化的埋点工作。

在需要优化的方法中,每相隔差不多行数的地方加一个Transaction,一般加5到10个Transaction,这样就可以比较清晰的看出哪个地方的代码需要优化了。

4.2 思考能应用上的策略

经过前面的步骤,已经知道了哪个地方的代码比较消耗性能,耗时较长了。但是并不是所有耗时长的代码都可以优化。比如上图cat上显示的,mergeclient.MTSearchPoi(searchParam)很明显是最耗时的代码块,但是通过跟调用方沟通知道了这个地方的性能消耗主要是自然结果QS和传输上的网络延迟,这两点目前都不能很好的优化,所以性能优化的重点落到了第二个耗时很长的地方poiClient上了。

分析代码的逻辑,思考这个地方能否提升cpu的利用率或者能否用空间换时间的方法优化性能。

4.3 进行性能优化

对代码的分析后,已经得到了优化性能的策略。这时就开始着手优化了。需要注意的是,使用多线程,务必使用线程池的方法,避免创建过多的线程或阻塞队列满了造成内存溢出。另外,使用多线程还需要注意对调用服务的服务器造成的压力,可能会压垮对方的db。

而使用缓存,本地缓存注意oom和线程安全问题,分布式缓存注意缓存的实时性和更新策略,建议使用databus和ms消息实时更新。还有缓存击穿和雪崩的问题。

4.4 预计优化后能达到的效果

进行完性能优化后,我们还需要预计性能优化后达到的效果,比如使用了缓存可以把这个接口优化多少ms。

预计的效果可能与最终实际的效果不一样,如果不一样就要思考哪些地方可能疏忽了,或者没有思考完全。等到预计的效果和最终的效果基本上一致时,自己的技术就又得到了提升了。

4.5 观察优化后的结果

优化完上线后,我们还需要观察优化后的结果,如果没有达到要求,我们还需要继续优化,思考更多优化的策略。

4.6 思考总结

观察优化后的结果后并不代表这个过程已经完了,我们还有最后一步需要做,就是进行总结思考。

我们有三个效果需要注意:

1.需要达到的效果

2.预计达到的效果

3.最终实际达到的效果

这三个效果可能都不一样,我们需要对具体的情况进行分析,总结。


五. 性能优化的案例

待编辑

 

六. 性能优化的常用方法

最后归纳一下性能优化的一些常见方法


6.1 提高CPU利用率策略

多线程同步

我们经常使用的future模式其实是开发控制的非阻塞的同步模式,这个是属于多线程同步,主线程还需要等待其他线程的返回结果

使用场景就是数据间没有相互依赖的关系(如果有依赖那就会有锁了),主线程还需要等待其他线程的结果(数据结果或运行结束的标志),适用于我们很多业务开发场景。

nio与异步

nio其实也是非阻塞的同步模式,但是是系统控制的(内核态级的),这种多见于框架层面,如rpc框架,网络通信框架。异步是一个非阻塞异步的模式,主线程不关心其他线程的结果

使用场景是对主业务流程的一些附属业务并不一定要立即得到处理结果,如预约成功后发送短信的操作,这种情况适合异步的方式处理。

批处理

批处理是将一些需要多次操作的数据放在一次处理,减少调用的次数,例如将每次去查询数据库记录的方法合并为一个,通过一条sql去查询,减少多次数据库连接查询的消耗

使用场景是可以通过一次处理完多次的任务。

MQ消息

消息队列(MQ)天生就是异步的,是系统级别的异步模式。另外,MQ极大的降低了系统间的耦合

使用场景是将一些不需要本系统处理的任务,交给其他系统去处理。

JVM调优

JVM调优是一个很大的话题,减少gc次数,合理设置机器的Load值与CPU使用率、JVM的线程数等等。

6.2 空间换时间策略

本地、分布式缓存

本地缓存(HashMap/ConcurrentHashMap、Guava Cache等),缓存服务(Redis/Tair/Memcache等),减少重复查询,提高Latency,典型的空间换时间的策略

使用场景是短时间内相同数据重复查询多次且数据更新不频繁(如果更新则同步更新缓存),先从缓存查询,查询不到再从数据库加载并回设到缓存。

数据库索引

将数据库需要条件查询的数据先预存到b+树中,真正查询的时候直接在b+树中查询,不用全表扫描,这也是牺牲空间(内存)换取时间的策略

使用场景是查询语句(如果性能不达标)的where条件后面的字段建议加上索引,这也是sql优化最常见的方式。

数据预处理

数据预处理是将你可能需要用到的数据先行处理完,保存到内存、硬盘或数据库中,等真正要使用的时候不用去实时处理原始数据,而是拿处理好的数据

使用场景是本身需要处理的数据很复杂,实时处理太慢,这里就可以使用数据预处理方案。

离线数据处理

在计算密集型的场景下,使用MapReduce思想,把运算分算出去,交给其他机器完成,主程序等待拿回结果,这种主要适用于Hadoop。

6.3 逻辑优化

代码优化、sql优化、服务拆分……

逻辑优化层面更多的是结合具体的业务逻辑来进行优化,如for循环次数过多、作了很多无谓的条件判断、没有使用索引等。


猜你喜欢

转载自blog.csdn.net/ningdunquan/article/details/79908880