系统性能有两个相关的概念:
而这两者之间又是存在着一些联系
Throughput就是我们系统的qps,而Latency可以认为是接口的处理耗时。
因此优化每个接口的性能能够提升系统的qps。
另外还有两个概念
对于表现为IO密集的进程,应该经常运行,但每次时间片不要太长。对于表现为CPU密集的进程,CPU不应该让其经常运行,但每次运行时间片要长。
简单来说,就是计算密集型的系统减少线程的使用,而io密集型可以多创建线程。
我们目前的系统(web server)几乎都是io密集型。也存在少量单个接口处理请求的过程可能是计算密集型。
先思考一个问题:
在代码中使用线程一定能优化性能吗?
这里有两个不同的例子
例子一 : 对比两个处理数据的方法
使用线程的方法:
不适用线程的方法:
使用线程的用时:31841
不使用线程的用时:20946
所以使用线程并没有优化到性能。
分析:
为什么这个时候使用线程并不能优化性能?原因很简单,计算数据的方法getNumber()使用到synchronized关键字,调用这个方法的时候会加上this锁,因此每次只能有一个线程调用这个方法,其他线程阻塞了,只能等待释放锁后才能开始执行。因此,使用了多线程也只能串行计算。相反的,多线程还会多了一些线程切换的时间(别小看这个线程切换的时间,当需要处理的请求很多而处理时间很短时,线程间切换的消耗会激增)。
因此,当核心代码中有同步代码或者锁的时候,这时使用多线程并不能优化性能。
例子二:对比两个处理逻辑中有sleep的方法
使用线程的方法:
不使用线程的方法:
使用线程的用时:1018
不使用线程的用时:100389
所以使用线程很明显极大的优化了性能。
接下来两个问题
第一个问题,因为线程在执行核心逻辑时,会执行到Thread.sleep(1000);在这里,线程会进入sleep休眠状态,当前线程会让出CPU给其他线程执行,所以这100个线程几乎同时进入休眠状态,而1000ms后,线程几乎同时执行完了,得到了结果。因此,使用线程可以优化。
第二个问题,线程优化在共享了所有线程阻塞的时间,所有线程几乎一起阻塞的,这样总处理时间近似等于最大的一个线程处理时间。而不使用线程则是所有处理时间的相加之和。
串行调用:
多线程future调用:
因此,这里使用多线程可优化100倍的时间。
最后一个计算数据例子给大家思考
一个计算数据的例子,典型的计算密集型的操作。其中有很耗时的计算,没有使用多线程的方法:
与之对比的是实现相同的功能,使用了100核心线程数的方法:
有三个问题:
接上一个问题,先给出答案
第一个方法的耗时: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思想,把运算分算出去,交给其他机器完成,主程序等待拿回结果。而这本质上是一个空间(整个计算集群)换时间的思想。
使用多线程在没有加锁且有阻塞的情况下确实可以优化性能,那么优化的实质是什么呢?
进程执行由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输入输出等。
简单的结构体系图:
计算机的核心就是由这五大部分构成,网络io的输入、输出主要是网卡、带宽等资源,这是已经确定了的,由计算机底层优化,能优化的空间不大(也可以通过改变tcp缓冲区的大小来特地优化某些场景的性能,这个以后再说)。
其他的就有三个部分,也就是两个部件:CPU和内存。
那么性能优化就有两个原则:
这两个原则本质上是从两个方面优化的。
提高CPU的使用率即是在单机的情况下尽可能多的参与程序运行和计算,在整体上减少进程阻塞、等待的时间,单位时间内执行更多的指令。
而空间换时间的本质是使用计算机的存储器资源减少CPU计算的时间,最终达到优化性能的目的。
如果你遇到一个性能不达标的方法,首先你需要知道到底是哪一块代码耗时很长,知彼知己,才能百战不殆。我们公司内部有cat监控系统。
如何cat埋点?
cat的Transaction能够统计一段代码执行的时间,所以,天生适合性能优化的埋点工作。
在需要优化的方法中,每相隔差不多行数的地方加一个Transaction,一般加5到10个Transaction,这样就可以比较清晰的看出哪个地方的代码需要优化了。
经过前面的步骤,已经知道了哪个地方的代码比较消耗性能,耗时较长了。但是并不是所有耗时长的代码都可以优化。比如上图cat上显示的,mergeclient.MTSearchPoi(searchParam)很明显是最耗时的代码块,但是通过跟调用方沟通知道了这个地方的性能消耗主要是自然结果QS和传输上的网络延迟,这两点目前都不能很好的优化,所以性能优化的重点落到了第二个耗时很长的地方poiClient上了。
分析代码的逻辑,思考这个地方能否提升cpu的利用率或者能否用空间换时间的方法优化性能。
对代码的分析后,已经得到了优化性能的策略。这时就开始着手优化了。需要注意的是,使用多线程,务必使用线程池的方法,避免创建过多的线程或阻塞队列满了造成内存溢出。另外,使用多线程还需要注意对调用服务的服务器造成的压力,可能会压垮对方的db。
而使用缓存,本地缓存注意oom和线程安全问题,分布式缓存注意缓存的实时性和更新策略,建议使用databus和ms消息实时更新。还有缓存击穿和雪崩的问题。
进行完性能优化后,我们还需要预计性能优化后达到的效果,比如使用了缓存可以把这个接口优化多少ms。
预计的效果可能与最终实际的效果不一样,如果不一样就要思考哪些地方可能疏忽了,或者没有思考完全。等到预计的效果和最终的效果基本上一致时,自己的技术就又得到了提升了。
优化完上线后,我们还需要观察优化后的结果,如果没有达到要求,我们还需要继续优化,思考更多优化的策略。
观察优化后的结果后并不代表这个过程已经完了,我们还有最后一步需要做,就是进行总结思考。
我们有三个效果需要注意:
1.需要达到的效果
2.预计达到的效果
3.最终实际达到的效果
这三个效果可能都不一样,我们需要对具体的情况进行分析,总结。
待编辑
最后归纳一下性能优化的一些常见方法
多线程同步
我们经常使用的future模式其实是开发控制的非阻塞的同步模式,这个是属于多线程同步,主线程还需要等待其他线程的返回结果
使用场景就是数据间没有相互依赖的关系(如果有依赖那就会有锁了),主线程还需要等待其他线程的结果(数据结果或运行结束的标志),适用于我们很多业务开发场景。
nio与异步
nio其实也是非阻塞的同步模式,但是是系统控制的(内核态级的),这种多见于框架层面,如rpc框架,网络通信框架。异步是一个非阻塞异步的模式,主线程不关心其他线程的结果
使用场景是对主业务流程的一些附属业务并不一定要立即得到处理结果,如预约成功后发送短信的操作,这种情况适合异步的方式处理。
批处理
批处理是将一些需要多次操作的数据放在一次处理,减少调用的次数,例如将每次去查询数据库记录的方法合并为一个,通过一条sql去查询,减少多次数据库连接查询的消耗
使用场景是可以通过一次处理完多次的任务。
MQ消息
消息队列(MQ)天生就是异步的,是系统级别的异步模式。另外,MQ极大的降低了系统间的耦合
使用场景是将一些不需要本系统处理的任务,交给其他系统去处理。
JVM调优
JVM调优是一个很大的话题,减少gc次数,合理设置机器的Load值与CPU使用率、JVM的线程数等等。
本地、分布式缓存
本地缓存(HashMap/ConcurrentHashMap、Guava Cache等),缓存服务(Redis/Tair/Memcache等),减少重复查询,提高Latency,典型的空间换时间的策略
使用场景是短时间内相同数据重复查询多次且数据更新不频繁(如果更新则同步更新缓存),先从缓存查询,查询不到再从数据库加载并回设到缓存。
数据库索引
将数据库需要条件查询的数据先预存到b+树中,真正查询的时候直接在b+树中查询,不用全表扫描,这也是牺牲空间(内存)换取时间的策略
使用场景是查询语句(如果性能不达标)的where条件后面的字段建议加上索引,这也是sql优化最常见的方式。
数据预处理
数据预处理是将你可能需要用到的数据先行处理完,保存到内存、硬盘或数据库中,等真正要使用的时候不用去实时处理原始数据,而是拿处理好的数据
使用场景是本身需要处理的数据很复杂,实时处理太慢,这里就可以使用数据预处理方案。
离线数据处理
在计算密集型的场景下,使用MapReduce思想,把运算分算出去,交给其他机器完成,主程序等待拿回结果,这种主要适用于Hadoop。
代码优化、sql优化、服务拆分……
逻辑优化层面更多的是结合具体的业务逻辑来进行优化,如for循环次数过多、作了很多无谓的条件判断、没有使用索引等。