首先我们的系统通常是非常复杂的。无论你的系统是一个单体应用;还是做了n多解耦、分层、拆分的工作,单元逻辑足够简单的分布式应用;但是对于一个功能视角来看,仍然非常复杂,反而分布式环境下问题要比单体应用还要复杂一个量级。 本文要说的就是:如何在复杂的系统下进行优化,让我们的硬件投入划得来,让我们的系统保障可靠的同时无比的丝滑。 性能是一个很笼统的词儿,很多时候直接性能优化三板斧,只是误打误撞的在解决问题,我们需要一个完整的方法,对于性能问题进行鉴别、分析、从而解决。本文要探讨的就是这部分“方法论”,让性能优化的ROI最大化。
要分析性能问题,首先我们得有一个鉴别标准,比如:tps承载量、qps承载量、延时、平均响应时间、每秒IO数量(IOPS)、负载、饱和度等等,甚至有些公司内会有一些内部约定的指标,不同的场景下可能直接使用的指标是不同的(这样做是合理的,更简单且直接的描述性能),但是如果要把性能优化的理论吃透,更从容的解决未知问题,成为性能优化的大师,需要找到性能描述最本质的东西,个人认为,对于不同场景进行分类,描述我们系统最恰当的最原始的指标就是吞吐量(对于容量做考量)和响应时间(对于时间做考量),我们要优化的就是这两点。
tps、qps、IOPS这些一定程度上就是在侧面描述吞吐,而平均响应时间、延时同样是吞吐量的重要影响部分,对于大部分场景,我们追求的往往是吞吐量。而某些场景会最大化追求延时降级,而非吞吐量。
看到这里可能会好奇,吞吐和响应时间不是有对应关系吗,为啥是两个指标? 由于单位资源的限定,即时相同的功能,减少响应时间就是在增大吞吐,而某些情况多牺牲部分资源消耗减少响应时间却又减少了吞吐(比如多步并发处理),围绕微批处理思路增大延时却又增大了吞吐(比如trigger buffer),具体场景具体分析哈,总之要么高吞吐、要么低延迟,更多的时候是在取折中。
话题拉回来,我们要知道什么是性能好,才能评判系统,才能进行优化,才能具体执行。总体来说:
以吞吐、延时为基准
1: 性能问题的优化是自上而下的,首先确定系统整体的性能目标及评判标准;然后整体评判
2: 其次性能问题一定是可拆分的,若干的劣化共同导致了“差”;
3: 优化的每个子环节应该有自身的目标,根据子目标进行分布评判,而不是对于整体目标的简单拆分。有的追求低延时、有的追求高吞吐、有的要兼顾。
4: 子目标以对整体赋能为标准,避免局部最优解
如何鉴别和判断: 我们拿到吞吐量、响应时间的数据之后,由N多的呈现方式,比如说平均值、标准方法、百分位数、中位数,对于性能问题来讲平均值参考意义不大,只能大致呈现系统水准,仅在系统整体性能差的情况下有帮助,而大多时候的性能分析,通常更关注极端百分位或者异常值,因为这些bad case往往代表了性能的潜在风险及劣化趋势。
要谈优化,首先避免犯错,即使不懂成体系的优化理论,常见的坑肯定是熟知的,比如说慢SQL、for-each call、大量重复运算、各种手工暴力解法。 这里不过多赘述了,大家对这些常见的性能坑。
我们的应用程序执行是分若干层级的,从应用程序(内部又可以分为业务程序、中间件程序、库函数),到系统库,再到系统调用、最后到内核、再到硬件。
就优化角度而言,越靠近工作执行的地方性能优化带来的收益就越大,直白点就是越靠近我们业务逻辑的优化,越有效。按理说越底层的东西应该对系统产生的影响越大,那为什么这么说呢?
就影响程度而言,越靠近工作执行的地方,所能对系统产生的影响就越大。比如优化10条sql的底层存储,"10条sql"-> "1条sql" -> "无同步sql"差异。
就实现角度和专业角度而言,越靠近具体工作执行的地方,实现较蠢的可能性越高。
而这些可以汇总为:对于系统而言会有“技术选型”、“不合理使用问题”等问题,就经验和概率来讲,99%的问题是因为这部分,如果把所有的问题进行汇总,然后看具体分布图,不难发现,几乎都分布在上层(应用层)。说几个问题大家感受一下,慢sql(应用层)、mysql检索复杂度过高需要优化(中间件)、磁盘IO操作不合理(系统调用)、磁盘太慢(硬件层)等问题发生的概率和分布。
总体来说,果断承认“99%的问题是因为我一时犯蠢或者不太聪明”,才能开始逐渐正视性能问题和启动性能优化工作。
所以整体的性能优化,应该是自顶而下的,由我及外的。
性能问题要求一定是 可观测的、可量化的,但是我们的系统不一定是可观测的,或者无法具体量化。这就导致面临性能低下时,就跟面对一个黑盒盲区一样,只知道差,却束手无策。
这时就需要对于我们的系统进行剖析,系统必定由n多部分构成(比如各种子服务,各种function,各种中间件)
除系统构成外,我们更应该知道系统工作时的构成(每条链路的运行视图 -- 实时运行状态),然后分析哪些地方出了问题。
首先我们对于我们的系统进行层层剖析:
尤其是对于做工程的或者做业务的,我们的整体架构大多是分布式或者微服务架构,如果进行层级、调用链路的拆分通常是如上图所示。
我们要做的就是对于系统有一个更加全面的认知,最起码要回答如下问题,可以不用非常细节,至少要知道一个概貌:
部署环境&硬件设施是什么样子
由哪些服务构成
用了哪些中间件(&版本 &适配器 &本地优化 &调参)
怎么用的中间件(比如redis做缓存,怎么用的,链路如何)
IO链路是什么样子(&对应的协议 &网络处理模型 &放大倍数)
调用链路是同步还是异步&有没有做批处理
怎么用的中间件
每个server功能是什么样子
在有了整体的概貌之后,就可以对系统进行一定的剖析了,看每一步或者关键步骤是否是支持我们观测的,并且观测手段是可以自动化监控,还是巡检机制,还是case by case。再然后就能可以根据目标对于这些步骤或者说环节进行量化分析。
至此大概率就知道我们的系统具体慢在了哪里。
先说响应时间,首先我们要对速度有一个基础的概念,数据没那么绝对,但是差不多就是这个数量级 先硬件相关:
一个CPU周期:0.3ns L1、L2、L3缓存访问:0.9ns、2.8ns、12.9ns 内存访问:120ns 固态硬盘:150us 机械硬盘:1ms 一次同机房调用:1ms 一次跨城网络传输:40ms 一次跨专线地域传输:100ms
再包装下看日常的操作耗时:
一条sql(索引合理):1-5ms 一次RPC调用:1-10ms 一次redis操作(无大key、热key):1-5ms 一次kafka send(带buffer):1ms以内 一次http请求:5ms
对于CPU的消耗程度,如果把CPU看作是稳定的(相对有点粗暴哈),消耗程度通常可以用CPU占用时间来表述(onCPU、offCPU)。比如完成一次md5需要多少时钟、执行磁盘IO需要多少时钟、一次网络IO需要多少时钟等等。 而这个东西具体的表象其实就是cpu使用率,我们不难可以看到各个进程、各个现成的占用程度,如果再挂一下火焰图不难看出哪些操作对CPU消耗更高及对应占比。
各个响应时间和CPU消耗程度是对吞吐和响应时间最本质的影响,要进行性能优化就是对于CPU占用和响应时间(处理延时)进行优化,各种方法减少CPU动作、减少延时等待,应用层的优化就是奔着这些目的去的。
应用层面 关于延时我们该如何进行优化呢?整体来看的话:减少操作、减少等待。说几个常见的策略: 能否减少操作,可以对于一次请求、一次任务的执行步骤进行梳理,分一下类,核心工作是哪些,哪些工作是可以从核心链路摘除的。 能否有复杂度更低的动作,比如检索算法是否可以优化、序列化算法是否可以优化、是否有更高效的数据结构和算法。 能否打时间差,前置处理或后置处理,比如初始化动作前置(不光是对象初始化,业务动作也行,可以提前计算、提前开户等),后置比如异步化处理,发奖券之类的 能否缓存&复用,请求处理的结果、中间过程、或者socket链接是否可以直接复用,各种池化技术。 能否并行,多步并行处理,减少整体耗时,衡量下并行带来的收益,是否比实现并行的代价高。 能否非阻塞并发处理,减少等待,避免大量的等待操作,业务处理的劣根性大多源于此。 能否提前失败,对于易失败请求,提前进行失败性检查,避免正常资源的占用,引起其他请求等待。 能否继续优化中间件操作,是否有更高效的中间件选型,操作上是否有更多的优化空间。 能否无”锁“化处理,锁是导致等待的核心因素之一,但这里是泛指可能让我们线程发生阻塞的情况,尽可能的减少同步链路中的等待机制可以让我们的响应时间更容易得到保证。
以余额支付系统交互信息流程举个例子(省了很多步骤,粗略描述):
尝试优化一下
系统层面(假设包含虚拟机) 垃圾回收中断是否影响过大,poll还是epoll、水平触发还是边缘触发,fsync还是flush,hardware 是否有特异性优化,SSD还是机械硬盘,设配新旧程度
接下来看下吞吐的相关优化,吞吐量指的是现有系统在单位时间内能够处理的请求或者任务数量。
而对于吞吐的优化通常有两点:
1: 单位资源内能够具备更大的吞吐量,提高系统资源利用率。
2: 保证吞吐量没有瓶颈,一定程度上,可以无限横向扩展。
先看第一个问题,如何让单位资源内承载更多的吞吐。 影响吞吐的因素有很多,比如前面刚刚提到的响应时间,关于响应时间和吞吐量的关系前面已经描述过了,大部分场景下适当的减少延时对于吞吐的影响是线性增长的,如何降低响应时间参照前一段的方案(大部分是正向的),唯一需要注意的是,在有限的资源下,过度的优化是会带来吞吐下降的,要做的是尽可能在不带来CPU增长的情况降低响应时间。 常用的技巧技巧批处理,比如多次IO是否可以进行合并,批处理一下,减少多次链接开销,这样对整体吞吐带来的威力是很大的。对同步批处理,一定程度上也算是对于响应时间的调整,对于整体平均响应时间是在下降的,但是对于头部请求响应时间是被劣化的。但通常的使用往往是异步化处理 + 批处理,IO 缓冲区、buffer triger、insert buffer、kafka send buffer 都是这个思路。 带来的吞吐和响应时间提升通常是炸裂的。
接下来看第二个问题,解决吞吐瓶颈。 当完成单位资源的吞吐优化之后,按理说有诉求就水平扩容即可,但是实时并非如此,经常会出现一个“热点”成为系统的吞吐瓶颈,尤其是在OLTP系统中,热点会以各种各样的方式出现,但是通常都和状态强相关,比如说全局库存、热点用户、热点机器等等。 如果要优化,这里就不得不提CAP理论了,在数据多节点分布的情况下,为了保障一致性,系统整体的可用性必然会受到影响,可用性下降,最大的表现其实就是有效吞吐的下降。突破瓶颈,持续放大吞吐,那就适当的牺牲一下数据的一致性吧。
把“热点”进行切分,让热点也能进行进行水平扩容,然后在这些短暂的不一致之上打补丁,尽快达成最终一致。
除了我们代码的实现、系统的构成会对系统有一定的影响,濒临负载极限的时候不管是吞吐量、响应时间都会受到一定程度的影响。 对吞吐而言,在机器资源一定的情况下,并不是随负载一直线性变化的,到达一定的临界值后如果持续增加负载,吞吐通常会由线性增长进入缓慢增长的,如果再持续的增加负载,会出现一定的劣化现象(比之前的吞吐量还会降低),这在JVM等虚拟机之上运行的应用,或者在池化排队机制的加持下愈发严重和明显。
对响应时间而言,在机器资源一定的情况下,也不会随始终保持耗时不变,同样在到达负载临界值时,会发成程度不一的劣化现象,有的响应时间增长迅速,有的则呈慢速下降。
这是为什么呢,因为在负载到达一定阈值时会触发程度不一的系统资源抢占问题,比如大量的排队等待处理,此时上下文切换会更加的频繁,并且对于某些JVM等虚拟机之上的运行的应用还会带来一定程度上的内存清理(分配和回收)及维护代价进而导致竞争更加激烈,也就导致了性能被进一步劣化。 这也是为什么很多中间件会限制最大链接数、最大线程数、最大排序序列的原因,而对于我们业务应用程序而言也是一样的,在进行池化处理排队机制时一定要明确合适拒绝。 并且我们应该对于我们的的服务加以保护,以此保证负载不会出现这种劣化现象,比如说限流、限并发,比如说tomcat把参数调整的合理一些,让负载,比如说CPU最大不超过60%、70%
除了到达负载临界值时会出现这个问题,在“骤发的流量”面前这个问题也非常的凸显,因为大量的初始化工作(链接建立、线程池扩容、client初始化等等),因为卡顿问题或者延迟变高等问题导致排队激化,同时机器负载也会骤然上升,导致负载临界问题或者时临界值提前到来。日常看监控,突发流量时的尖刺,90%的原因都是这个。 这一点一定要额外注意,大流量前充分预热、保持缓存热度、高性能场景放弃懒加载,极端情况下适当扩大活跃线程数、链接保活都是十分重要的。
日常应用的负载通常由服务器资源的瓶颈负载所决定,CPU争抢会劣化,内存争抢发生换页情况更糟糕。 那我们日常的负载到底应该在多少合适,这里以CPU负载为例来进行推导:
通常经验值是60%或者70%,在对稳定性要求极高的场景可能会控制的更低一些,这个值是怎么得来的?
首先CPU负载超过90%就会有概率发生劣化现象,CPU并非你想象的那么稳定性能方面会有抖动的,其次即使隔离部署,操作系统之上运行了不止你一个应用程序的进程,可能会有一些重操作的client,比如log拉取,如果log还在客户端做了grep、awk等处理那可能影响更重,这部分的buffer要留出来; 其次我们的进程的CPU消耗并不是线性不变的,仔细看看监控会发现忽高忽低,这部分buffer要留出来; 我们很难保证下游不发生抖动,如果下游抖动发生了一定程度的抖动,但还未超时,一定概率会导致主调server发生排队,进而可能发生雪崩效应,放大问题,这时候产生的劣化波动也是要能承受的,同样的也需要一定的buffer。 同样的,我们也很难保证主调方不会发生合理程度内的激增现象,骤然的CPU波动也是合理的,这部分buffer要留出来。 干掉这些buffer,通常经验值是60%上下浮动,如果单请求消耗资源极多并且可用性要求极高那可能多留点buffer,否则就少点,然后对应的平稳时对应CPU负载所能承载的负载就是我们应该设定的日常负载,再由此推出限流值、动态扩缩标准等等
语言是个有趣的问题,也是一个争论已久的问题,这里进行简单的讨论哈,通常我们的变成语言会分为编译型语言和解释型语言,还有些语言既有解释器,又有编译器,不用纠结概念,就从解释执行和编译执行的角度看问题即可。
编译型比如说C/C++,编译过的代码总体来说是高性能的,因为在CPU执行执行时不需要额外的一层映射,并且机器代码总是原始代码映射的很紧密,当然啦,很大程度取决于编译器的编译优化。 解释性如说Java(纯解释执行的话,抛开JIT不谈),需要额外的一层解释工作,会增加不少的开销,通常不会被期待有很强大的性能。 对于解释器和编译器共存的语言Java(带上JIT了),性能好坏主要看JIT的优化力度,想观测的话可以打开JIT日志,看下你的代码到底哪些进行最彻底的优化(JIT 优化层级是4),然后看哪些操作会导致退优化的发生,JIT的决策器也非常的关键。但是这种体系下,性能分析会相对费劲一些,JIT的优化策略(决策、编译)很难说对所有开发者都是白盒的,并且在真正执行的时候,和我们代码的映射已经几乎匹配不上了(还好Java的性能分析工具集够全)。
除此之外,语言的垃圾回收机制虽然带来的很多开发的便捷性,但是对于性能要求极高的场景,垃圾回收会带来额外的影响,比如说程序的停顿,内存中对象的扫描、索引、复制工作。
什么时机进行性能优化,常见的有“系统创建时”、“发生改变时”、“问题暴露时”、“摆烂-重新写”,这个业界并没有一个标准的答案,个人的处理思路是:在系统创建时满足近1年半左右的性能诉求就足够了,剩下的交给将来。 系统设计的初期肯定会进行系统的定位分析及对应的边界划分,此时系统应该具备哪些功能、应该被哪些场景所使用就已经确定了,我们要做的就是对这部分内容进行预判,这应该是属于架构设计很核心的一部分,并且在设计的过程中,如果预判有50%以上场景会发生性能要求的突变,那么我们就应该给系统留足扩展性。 架构设计思路中有种思想叫做“演进式架构”,这里推崇的就是这个观点。 但需要额外注意的是,我们可以不做过多的性能优化设计,但是要做足性能优化分析的设计,正如文章最开始所提到的,我们要保证系统是可观测、可量化的。
上面讲的是设计侧的优化问题,在系统发生变更时,如果通过分析工具和监测工具发现有性能劣化的现象,是一定要进行优化动作的,放任性能问题不管一定会阻碍我们系统的进程,技术债早晚堆成#山。
局部最优解是一个数学求解的最优化问题,我们设计实现的摸索,也可以粗暴的看作是一个最优的求解问题,那么理论上也是会存在局部最优解问题的,实践过程也确实如此,相信大家都经历过大规模的重构。因为性能优化方案选择绝对不止一条路,再有拆分到子方案岔路就变的更多了,在选择的过程中很容易发生局部最优解问题。
就整体方案选择而言 在进行系统设计的时候,因为当前的系统设计整体方案不合理,在一个相对偏差的方向上进行了极致的优化,导致性能看起是得到了解决,并且段时间内没有问题,但是在后期的发展过程中很容易导致性能优化停止不前。所以进行性能问题设计的时候,应该更多的比较各个方案的差异性,以及可发展性。多去探索未知的未知,才能让我们的方案更加合理。 方案拆分而言 架构设计应该是一个“一锅出”的过程,一定要有一个上层指导,不要求每个子模块要用何种方案,但是一定要有明确的上层指导目标,这样在选择时就避免方向上的偏差,不至于每个子模块都是自己域内最合理的方案,但拼凑在一起确发现压根解决不了问题。
首先基础架构一定提供了足够全的分析工具,所在的公司也肯定有人在搞定针对基础架构的探测分析及业务系统分析的基础支持,找到对应的人,然后问他们。 要是进行学习或者文档检索,提供一点关键词,火焰图、探针、优化分析、诊断工具、perf、profiling、Trace、netstat、iostat、vmstat、jstack、top、slabtop、ps 这种资料应该一搜一大片。
整体算是一点自己对于性能问题分析及设计优化相关的方法论总结,文章里面并没有涉及过多的细节方案,核心原因是想“以渔”。另外市面上大多的直接方案,导致小朋友们习惯性的三板斧,个人觉着这种导向性不算良好,应该更清楚为什么这么做,才能更好的解决问题吧。 除此之外,本文的内容实在是有点扯,建议通过一些系统实践来反观我所描述的这些方法和观点, 个人经历过的觉着收益匪浅的系统:支付相关-比如paycore、交易收单、推荐相关、广告相关-比如DSP、营销活动系统 内容中如有描述不当的、错误性结论,也恳请斧正,志全感谢大家了。