“性能优化“相信是每个程序员都关心的问题,在实际工作中也常会对系统做优化。关于性能优化有两个问题很值得探讨:
- HOW:影响性能的因素很多,该从何入手?找到需要优化的点后,如何进行优化?
- WHY:优化后为什么能提升性能?为什么有些优化点更值得做?
至于WHAT,我认为是不言而喻的,性能优化的终极目标只有两点:QPS和RT,可以说所有优化最终都是为了这两个指标。
本文讨论的核心内容是几个与性能相关的公式,比如QPS公式、RT公式、线程数公式等。熟悉这几个公式能让我们抓住系统性能的底层逻辑,有助于在实践过程中对症下药。这些公式在一些性能相关的书籍里都有出现,公式提供了很好的理论支持,但关于如何实践的文章并不多。
本文先对公式进行简单推导和验证,熟悉的同学可以跳过这部分。
- 针对HOW:本文会通过一个线上应用的优化案例,尝试探讨出一套可参照的实践流程。
- 针对WHY:在公式验证和实践中,对实际优化结果结合公式进行定量计算,解释类似如下问题——为什么做了一个优化,QPS能提升100或者50%?
PS:个人水平有限,难免出现纰漏,欢迎指正。
公式推导
本小节对几个核心公式进行简单推导,帮助尚不太了解的同学弄清来龙去脉。
QPS和RT
前文提到,QPS和RT是性能优化的终极目标。其中QPS(Query per second)描述了单位时间内系统的吞吐量,而RT长短则反应了接口响应速度。提升QPS能帮助我们利用更少的机器资源扛住更多的流量,而降低RT能提升用户体验。
单线程QPS公式
在单线程下,这个公式永远正确。而我们的系统都是多线程的,所以我们需要知道多线程的QPS如何计算。
多线程QPS公式
很简单,就是单线程的QPS * 线程数:
可以看到,多线程下的QPS和两个因素相关:RT和线程数,接下里分别讨论下RT和线程数。
RT公式
RT一般可分为客户端RT和服务端RT,客户端RT包含浏览器发出请求—》服务器处理—》请求报文返回三个阶段。
两者关系如下:
客户端RT = 服务端RT + 2*网络耗时
网络耗时可以通过CDN、专线等方式减小,我们重点关注下服务端RT。
一个请求打到服务器上,会由一个线程来承载,同步模型下,而线程从创建到退出的过程,就是一次请求的处理过程。而线程从创建到退出的状态流转,涉及到多线程的调度,这里简单说明下线程的调度。
线程调度
在Linux/Windows上,从JDK1.2开始,JVM线程直接绑定一个内核线程(1:1模型),由系统内核的调度器来调度,在内核看来,内核线程和进程没有区别。对于java这类非实时进程,Linux的调度策略是基于优先级的抢占式调度。
系统将CPU时间切分成多个时间片,从就绪队列中,选取一个就绪的进程,为其分配时间片执行,可能有三种结果:
- 时间片耗尽前,进程执行完毕,任务退出。
- 时间片耗尽,进程仍未执行完毕,返回就绪队列,等待调度。
- 进程执行过中遇到阻塞事件,放弃时间片,进入阻塞队列,等待事件返回,再进入就绪队列,等待调度。
根据上述过程,进程有以下几种基本状态:创建、运行、等待(阻塞)、就绪、销毁。转换关系如下:
讲了这么多,根据上述状态可知,服务器RT由运行态耗时和非运行态耗时(创建、阻塞、就绪、销毁)组成,使用线程池的情况下,可以忽略创建和销毁的时间,得到公式如下:
CPUTime描述了需要CPU时间片的运行时间,WaitTime描述了阻塞的实际,而ReadyTime描述了就绪队列中等待调度的时间。
公式意义:RT和三个变量有关,分别是优化其中任意一项,都能减小RT。
最佳线程数公式
前面讲述了RT公式,接下来推导线程数该设置为多少?
- 以单核单线程为例,假设线程(Thread A)执行过程中,有部分时间在做阻塞IO,那么从线程和CPU的视角来看一个时间段的线程状态和CPU使用情况,如下图,可以发现,CPU有相当一段时间,处于闲置状态,未能充分利用。
- 同样单核的情况下,尝试增加一个线程B,做同样的事,根据前面线程的调度模型与DMA,线程和CPU情况如图。在CPU未达到满负荷情况下,线程数增加一倍,理论上QPS和CPU利用率提升了一倍。
- 继续增加线程数,可以发现,当达到某个临界值之后,CPU等待阻塞的空闲时间能被完全利用。
根据前面的推导,在同步模型下,最佳线程数定义:刚好消耗完线程阻塞时间的线程数临界值
在多核的情况下,在《Programming Concurrency on the JVM》一书中,描述最佳线程数=核心数 / (1-阻塞系数),其中阻塞系数=阻塞时间/(阻塞时间+线程CPU时间),代入可得:
其中UseRatio代表CPU使用率,在系统有其他瓶颈(IO、网卡、内存、锁等)时,利用率很难达到90%以上,这需要先解决瓶颈问题。
公式意义:线程不够,无法充分利用CPU的空闲资源,导致实际QPS上不去,线程过多会引起就绪队列变长,CPU频繁调度导致线程等待cpu时间片变长,进而影响RT,线程数存在一个最佳的线程数临界值。
最大QPS公式
根据多线程的QPS公式可知,最佳线程数下,系统具有最大QPS。分别代入RT公式和最佳线程数公式:
这里假设最佳线程数下,线程几乎不需要等待调度,即ReadyTime=0。
公式意义:理论上,系统的最大QPS和线程CPU计算时间成反比,和线程阻塞时间无关,提升QPS需要减小CPU计算时间。另外,考虑上下文切换开销、CPU利用率、STW等因素,实际QPS会小于理论值。
公式因子
CPU Time由算法和数据结构决定,会影响RT和最大QPS值,反应线程拿到时间片后,需要执行的时间。
Wait Time在应用中主要表现为线程wait(park,sleep)或者IO耗时,很大程度决定了RT和需要的线程数。
Ready Time主要由线程数与核心数决定,在线程数<最佳线程数时,可以忽略不计。当线程数超过最佳线程数,随线程数的增加而增加,影响线程RT。
ThreadNum由阻塞系数决定。当线程数过大,会导致一些问题,比如大量上下文切换导致CPU资源浪费、虚拟机开销,对于JVM,线程RT变长后也会带来更多的GC,GC伴随的STW会降低线程效率。
公式验证
前面进行了公式的简单推导,停留在理论阶段的知识还不算知识,下面通过一个简单实验来模拟下CPU+Wait(IO)场景进行测试,来验证下公式在实际环境下的适用性。
实验概述
A(模拟目标应用):内部通过循环模拟一段时间CPU计算,再http调用B提供的服务模拟远程IO,然后返回
B(模拟远程服务):服务内部休眠一定时间后返回
C(压测机):通过Apache BenchMarking或者amazon对A进程压测,形成报告
机器配置:8C16G VM(JVM opts -Xms10g -Xmx10g -Xmn4g -XX:SurvivorRatio=5)
实验场景
设计了三组实验场景:
- 参照组:CPU Time=100ms , Wait Time=400ms , 线程数从1增至150
- IO优化组:CPU Time=100ms , Wait Time=200ms , 线程数从1增至150
- CPU优化组:CPU Time=50ms , Wait Time=400ms , 线程数从1增至150
其中参照组用于纵向比较线程数、QPS、CPUTime、WaitTime、CPU使用率等因素之间的关系。
另外IO优化组和CPU优化组用于模拟分别对IO和CPUTime进行优化,三组横行比较优化后的效果
参照组结果
根据公式计算可得:
- 最佳线程数=(100ms+400ms)/100ms*8=40
- 理论最大QPS=1000ms/100ms*8=80
实验结果如下
这里主要记录了不同线程数下,QPS、RT、CPU-user使用率、上下文切换(CS)、load几个数值。其中在150线程时,CS最大值接近7000/s,带来的系统开销基本可以忽略。对于其他几个下面我对数据绘制了几个线图,方便查看趋势:
线程数-QPS-RT图(红线部分是理论最佳线程数)
上图所示:
- 当线程数小于40(最佳线程数)时,随线程数增大,QPS呈线性上升,RT缓慢上涨。
- 当线程数大于40时,随线程数增大,QPS提升不大,RT呈线性上升。
线程数-QPS-CPU使用率图
上图所示:
- 随着线程数增大,CPU(user)使用率线性上升,同时QPS也呈线性上升
- 当线程数达到40时,CPU使用率接近90%,CPU和QPS缓慢提升。
线程数-RT-load图
上图所示:
- 线程数在接近40前,load和RT缓慢上涨
- 超过40后,load和RT成线性上涨
实验小结
通过压测的现象,结合公式我们可以得到几个结论:
- 线程数小于最佳线程数时,CPU尚未充分利用,调度良好,增加线程数可以提升CPU使用率,进而提升QPS,RT基本不变。
- 线程数达到最佳线程数时,CPU充分利用,此时QPS接近最大值。RT和QPS处于最佳平衡点。
- 线程数超过最佳线程数时,CPU已经饱和,QPS基本不再提升,因为CPU调度加剧,RT拉长。实际RT=实际线程数/最佳线程数*理论RT
- QPS实际和CPU使用率成正相关,RT增长和load成正相关。
优化组横向对比
分别对IO和CPU time优化后,对QPS影响如何呢?由公式计算可得:
- 参照组参数:CPU Time=100ms , Wait Time=400ms , RT=500ms , 推算最佳线程数=40,理论最大QPS=80
- IO优化组参数:CPU Time=100ms , Wait Time=200ms , RT=250ms , 推算最佳线程数=24,理论最大QPS=80
- CPU优化组参数:CPU Time=50ms , Wait Time=400ms , RT=450ms , 推算最佳线程数=72,理论最大QPS=160
线程数-QPS三组对比图
阅读全文请点击