查看原文
其他

性能,10点系统性思考

技术琐话 2021-08-08

The following article is from 喔家ArchiSelf Author 半吊子全栈工匠

作为一个半吊子全栈工匠,在20多年的职业生涯里遇到过太多关于软件性能的问题。论证或者证明性能的问题往往很关键,能否通过一次一个小而有逻辑的可证明可审核的步骤来解决性能问题呢?

曾经企图创建一种公理化的方法来优化计算机软件性能,然而能力所限,惭愧之至。退而求其次,希望能够清楚地系统思考如何优化计算机软件的性能。

1. 什么是性能?明确概念

性能——performance,有着太多概念外延,在生活中几乎随时可见,例如,职场人的performance就是中文里的绩效,performance review 就是每人都会面对的绩效考核。但是,如果在互联网上百度一下,大多数有关性能的热门文章是关于: 计算机软件执行任何您指定任务所需的时间。

如果把面向对象作为开始,那什么是任务呢?任务,基本上是一个面向业务的工作单元,任务可以嵌套。对于计算机用户来说,性能通常意味着系统执行某项任务所需的时间。响应时间是任务的执行持续时间,以每个任务的时间为单位,例如,在百度上搜索“性能” 的响应时间为0.2秒左右,在浏览器中可以有办法看到这个测量结果,这就是网页搜索的一个性能证据。

由于感受软件性能的主体是人,不同的人对于同样的软件能有不同的主观感受,而且不同的人对于软件性能关心的视角也不同。有些人眼中的性能是吞吐量,即在指定时间间隔内完成的任务执行数量,例如“每秒点击次数” 。一般来说,负责团队性能的人更担心吞吐量,因为他们要关心该系统是否能够处理所有用户需要要处理的所有数据。

那什么是性能呢?时空可能是连续的,从时空的视角看,性能是完成某项任务时所展示出来的时间及时性和空间资源有效性。对用户而言,更关注及时性,对服务或者产品提供者而言,既关注时间关注空间,是多种因素的权衡。

2 性能指标——时空纠缠

性能的指标,是指衡量性能的尺度。从时间的维度看,包括响应时间、延迟时间等,从空间的维度看,包括吞吐量,并发用户数和资源利用率等。

由于时空的内在联系,以两个重要的指标为例,吞吐量和响应时间通常相互关联,但并不完全相同,真正的关系是微妙而复杂的。

通信中的吞吐量与响应时间

假设为某个基准测试以每秒1000个任务的速度度量了吞吐量。那么,用户的平均响应时间是多少呢?人们很容易认为每个任务的平均响应时间是0.001秒,但事实并非如此。如果处理这个吞吐量的系统是有1000个并行的、独立的、同质的服务通道,在这种情况下,每个请求可能正好消耗1秒。

现在,可以知道每个任务的平均响应时间在0到1秒之间。然而,不能仅仅从吞吐量测量中推导出响应时间,必须单独测量它。当然,有数学模型可以计算给定吞吐量的响应时间,但是模型需要更多的输入,而不仅仅是吞吐量。

计算中的吞吐量与响应时间

在另一个方向上,展露了微妙之处。如果需要在单CPU计算机上编程以提供每秒100个新任务的吞吐量,假设编写的新任务在计算机系统上执行仅用0.001秒,那么是否能产生所需的吞吐量?如果能在千分之一秒内运行一次任务,那么肯定能在一整秒内至少运行100次。例如,任务请求被很好地序列化,就可以在一个循环中处理所有100个任务,一个接着一个地循序执行。

但是,如果每秒100个任务随机地出现在系统上,从100个不同的用户登录到单 CPU 计算机上,又会怎样呢?CPU 调度器和序列化资源可能会将吞吐量限制在远低于每秒100个的任务数量,从而不能完全从响应时间度量推导出吞吐量,需要单独测量。

响应时间和吞吐量不一定是相反的。要了解这两者,需要同时测量它们。哪一个更重要呢?对于给定的情况,可以从两个方向上合理地寻找答案。在许多情况下,答案是两者都是需要管理的重要指标。例如,系统可能有一个业务需求,不仅要求在99%以上的系统响应中,对给定任务的响应时间必须小于1秒,而且系统必须支持在1秒间隔内持续执行1,000个任务的吞吐量。

3. 描述性能:一切结果,都是概率

“在99%以上的系统响应”,是一种响应时间的期望限定,一些人更习惯于用“平均响应时间必须是 x 秒”来描述。不过,说明目标的百分比方法更好地现在人们经验中。

想象一下,对于每天在电脑上执行的某项任务,响应时间容忍度可能是1秒。假设,a系统90% 的平均响应时间是1秒,b系统60% 的平均响应时间是1秒,那么a系统会有10% 的用户不满意而b系统有40% 用户不满意吗?如果 a 系统中,90% 的响应时间是0.91秒; 在 b系统 中,90%的响应时间是1.07秒,那么, 这样的描述比仅仅说1.00秒的平均响应时间更有信息量。

我们尝试用可能的两个数来描述世界,一个是均值,一个是方差。客户感受到的可能是方差,而不是均值。将响应时间表示为百分数,可以产生与最终用户期望相符的性能描述,而且令人信服, 例如,”动态库加载”的任务必须在至少99.99% 的执行中在小于0.5秒的时间内完成。

我们同样用概率来描述性能,或许,一切的抽象,可能都归于数学,一切的结果,可能都归于概率。

4 问题诊断——以终为始

在曾经遇到的性能问题中,大多数是关于响应时间的: “过去做某事只需要不到一秒的时间,现在有时候需要10多秒。” 当然,一个更朴实的说法是,“整个系统太慢了,简直不能使用。” 

关于性能问题的诊断,最重要的事情是清楚地陈述问题,明确了问题的描述,才能清楚地思考问题。

以终为始,系统想要达到的目标状态是什么呢?找出一些可以用来表达目标状态的细节数据: 例如,“在许多情况下,系统的响应时间不超过2秒。如果至少有95% 的关键任务响应应时间在一秒以内,这才是我们所要的。” 

这样的描述看起来不错,但是——

如果用户没有这样一个定量目标呢? 

这个特定的目标有两个量(1s和95%) , 如果不知道其中的某一个该怎么办呢? 

更糟糕的是,如果用户确实有特定的想法,但是这些期望是不可能实现的,又该怎么办呢?

如何怎么知道什么是“可能的”或“不可能的” ?......

性能的问题诊断从问题的描述, 以终为始,循序逆推,接下来才是使用工具来应对这些问题。

时间利器——时序图

时序图是 UML中指定的一种图形,用于按照交互发生的顺序显示对象之间的交互。在可视化响应时间方面,时序图是一个非常有用的工具。

考虑一下绘制时序图的比例,每个进入的“请求”箭头和相应的“响应”箭头之间的距离与服务请求所花费的时间成正比,可以说明图中表示的组件是如何花费时间的,可以“感觉”到响应时间的相对贡献。

时序图可以帮助人们概念化响应时间在给定的系统中是如何被消耗的,还可以很好地显示同步处理线程是如何并行工作的,除了分析业务,也是性能分析的好工具。但要系统性思考性能,还需要一些其他的东西。假设,要修复任务的响应时间为2048秒,在这段时间内,运行该任务将导致应用程序服务器执行了320,000个数据库调用。图3显示了这个任务的时序图。

在应用程序和数据库层之间有太多的请求和响应箭头,以至于看不到任何细节。也就是说, 在一个很长的滚动条上打印时序图并不是一个有用的解决方案。

时序图是一个很好的工具来概念化控制流和相应的时间流,可以作为时间上的利刃,那么有空间利刃么? 

空间分析——组件描述直方图

为了处理那些需要大量调用的任务,需要一个方便的时序集合,这样就能理解时间如何花费的重要模式。概要描述是响应时间的表格分解,通常按组件响应时间贡献降序列出。

直方图一般可以确切地显示慢速任务在哪里消耗了时间。例如,可以推导出概要描述中标识的每个函数,以及函数调用响应时间所占的百分比,还可以推导出任务期间每种类型的函数调用的平均响应时间。

如果可以深入到聚合为单个调用中持续时间,就可以知道有多少这些调用对应于某个函数的其他调用,并且可以知道每个调用消耗了多少响应时间。“这个任务应该运行多长时间? ” 使用组件描述直方图,可以构造问题的答案。

老码农认为,这是问题诊断的第一个重要问题,这是解决性能问题的开端。

5 优化原则——要事优先?

性能改进与程序使用所改进东西的程度成正比。如果正在尝试改进的事情只占任务总响应时间的5% ,那么能够产生的最大影响也紧紧是总响应时间的5% 。这意味着,我们越将焦点集中在直方图的顶部(假设组件直方图按响应时间降序排列) ,整体响应时间的潜在好处就越大。

但是,这并不意味着总是按照自上而下的顺序处理组件的响应,还需要考虑执行补救措施的成本。考虑组件的响应时间直方图,添加最佳补救方法可以节省多少时间,可以看到每个补救方法的实现成本。

确立优化起点

那么,先采取什么补救措施?成本核算,寻找更好的净收益,这才是真正需要的优化点。

带有改进成本的组件响应时间直方图打开了一扇大门,让我们可以就首先实施哪些补救措施做出更好的决定,为预测改进后的性能指标提供了一个尺度。进一步,可以找到比预期更有效的方法,以低于预期的成本缩短响应时间。

首先采取什么补救措施取决于对成本估算的信任程度。“非常便宜”是否真的考虑到了所提议的改进可能对系统造成的风险呢?例如,改变这个参数或者删除那个索引看起来非常经济,但是这个改变是否有潜在的破坏性?改变了一些现在甚至没有想到的组件的良好性能呢?可靠的成本估算是技术能力得到体现的另一个领域。

循序渐进中的信誉

另一个值得考虑的因素是可以通过创造小的胜利来获得的信誉。也许低成本、低风险的改进不会带来总体响应时间的改进,但是它建立一个小改进的跟踪记录,完全符合对于为缓慢的任务节省多少响应时间的预测,也是有价值的。在软件性能领域,预测和最终实现的跟踪记录能够带来必要的可信度,以影响我们的同事甚至经理、客户等等,他们会支持你采取越来越昂贵的补救措施,为企业带来更大的回报。

需要注意的是,当提出更大而昂贵、高风险的补救方案且获得支持时,要小心谨慎。信誉是脆弱的,建立很难,但推倒只需要一瞬。

减少相干风险

在实践中,常常会出现修复一个任务的性能后,结果损害了另一个任务的性能。那么,在性能优化的时候,应该注意些什么呢?

这里,可以类比一个这样的问题:“为了感觉凉快,是该打开窗子还是脱掉厚衣服呢?”

这就是性能优化的最小化风险原则,确保自己本地的东西是有秩序的,尽量缩小故障域的范围。如果除了使用一两个程序之外,所有程序都处理得很好,那么最安全的解决方案就是将范围本地化在这一两个程序的修改上。

6 性能中的时空因素

在具体的性能优化过程中,会遇到各种各样的情况,常见要素包括数据倾斜、执行效率、负载和延迟。

数据倾斜

当处理处理组件响应时间直方图的时候,可能反复遇到这样的问题: x个数据库调用占用了y秒的响应时间。如果能消除一半的调用,能消除多少不必要的响应时间呢?答案往往出人意料,几乎从来不是“一半的响应时间”, 取决于我们可以消除的单个调用的响应时间。不能假设每个调用的持续时间是平均y/x秒,语句没有告诉我们调用持续时间是一致的。

数据倾斜是具体调用中的不一致性,出现倾斜的可能性使得无法对组件响应时间提供准确的答案。在不了解任何有关数据倾斜信息的条件下,可以提供的答案是,“在0到y秒之间的某个位置。但是,假设有具体的附加信息。就可以制定出更精确的最佳情况和最差情况估计。在数据库应用中,读写分离也只是大粒度分隔数据倾斜的一种方式。

运行效率

即使整个系统中的每个人都很痛苦,仍然应该首先关注业务需要修复的程序。起点是确保程序尽可能高效地工作。在不增加容量和不牺牲业务功能的情况下, 效率与可消除多少任务执行的总服务时间成反比。换句话说,效率与浪费成反比。 

以下是数据库应用程序中经常出现的2个有关浪费例子:

  • 中间层程序为每一行数据库插入创建了一个独立的 SQL 语句。它执行了1000个数据库prepare调用也就是1000个网络IO调用 ,而本可以通过一个调用从而减少999个网络IO调用来完成这项工作。

  • 一条 SQL 语句涉及了数据库缓冲上万次,以返回一个几百行的结果集。而一个额外的过滤语句可以返回终端用户真正想要看到的6行,只对数据库缓冲区访问进行几十次次触摸。

当然,如果一个系统存在某些全局性问题,例如,考虑不周的索引、设置糟糕的参数、配置糟糕的硬件等等,会导致整个系统的大量任务效率低下,那么应该修复它。但是,不要为了适应效率低下的程序而调整系统,不要用权宜之计作为永久的解决方案。

解决效率低下的问题往往在解决程序本身效率低下的问题上。即使某些程序是商业化的现成应用程序,从长远来看,要与软件供应商合作使程序更有效,而不是试图优化系统,使其尽可能高效地处理固有的低效率程序。

使程序更高效可以为系统中的每个人带来巨大的好处,很容易看出减少浪费是如何帮助修复任务的响应时间的。

工作负载

许多人也不明白的是,让一个程序变得更有效率,会给系统中其他程序带来性能改进,而这些程序与正在修复的程序没有明显的关系。这是由于负载对系统的影响。

负载是由并发任务执行引起的资源竞争。这就是为什么我们的性能测试不能捕捉到生产后期出现的所有性能问题的原因。

负载的一个度量是利用率,即资源使用除以指定时间间隔内的资源容量。随着资源利用率的提高,用户从该资源请求服务时的响应时间也会增加。任何一个在高峰时间在北京开过车的人都经历过这种现象,当交通非常拥挤时,必须在红绿灯等候更长的时间。

软件慢下来和汽车是不一样的,汽车在繁忙的交通中时速30英里而在开阔的道路上时速60英里。由于CPU的每个时钟周期有固定的指令数量,计算机软件总是以同样的速度运行,但是响应时间肯定会随着系统资源的使用增加而减少。

还是时空的纠缠,随着负载的增加,系统变慢的原因有两个: 排队延迟和一致性延迟。

排队延迟

负载和响应时间之间的数学关系是众所周知的。一个称为 M/M/m 的排队模型将响应时间与满足一组特定需求的系统负载联系了起来。M/M/m 有一个假设,即系统具有“理论上完美的可伸缩性” ,尽管有一些过分,但 M/M/m 模型在性能方面还是有很多值得我们学习的地方。下图显示了m=8时该模型的响应时间和负载之间的关系。

在上图中,可以从数学上看出在不同负载条件下使用系统时的感受。在低负载时,响应时间基本上与空负载时的响应时间相同。随着负载的增加,可以感觉到响应时间出现了轻微的、逐渐的降低。这种逐渐的退化并没有造成太大的危害,但是随着负载持续上升,响应时间开始以一种既不轻微也渐变的方式退化。相反,这种退化令人不爽,而且实际上是双曲线的。

在完美的可伸缩性M/M/m 模型中,响应时间(r)由两个部分组成: 服务时间(s)和排队延迟(q)。服务时间是任务消耗给定资源的时间,以每个任务执行的时间为单位。排队延迟是指任务在排队等待使用给定资源的时间。排队延迟也以每个任务执行的时间来度量,是指给定任务的响应时间与否则就会卸载系统上同一任务的响应时间之间的差异(不要忘记我们完美的可伸缩性假设)。

一致性延迟

相干延迟是由于任务的有序性执行造成的时间延迟,不能使用M/M/m那样的排队模型。这是因为 M/M/m 假定所有 m 的服务通道都是并行、同构而且独立的,意味着模型中假设了当你在先进先出队列中等待足够长的时间,并且前面排队的所有请求都已经退出服务队列后,会轮到你接受服务。然而,相干性延迟并不是这样工作的。

假设有一个 HTML 数据输入表单,其中一个“ Update”的按钮执行 SQL Update 语句,另一个“ Save”的按钮执行 SQL commit 语句,这样构建的应用程序几乎可以确认性能的糟糕程度。这对于希望更新同一行数据的其他任务来说,影响可能是毁灭性的。每个任务都必须等待该行上的锁定(或者,在某些系统上,是更糟糕的页锁定) ,直到锁定用户决定继续并单击 “save”,或者直到数据库管理员终止用户的会话。

在这种情况下,任务等待释放锁的时间长短与系统有多忙无关,取决于系统各种资源利用之外的随机因素。这就是为什么永远不能假设在单元测试环境中执行的性能测试足以决定是否将新代码插入生产系统。

7 理解性能拐点

回归到有关性能的两个最重要的指标:

  1. 最佳响应时间: 用户不想为了完成任务而等待太长时间。

  2. 最佳吞吐量: 希望尽可能多的人能够同时运行他们的任务。

如前所述,这两个目标是矛盾的。优化第一个目标需要最小化系统的负载; 优化第二个目标需要最大化负载。介于两者之间的某个负载级别可能是系统的最佳负载。

发生这种最佳平衡的资源利用值称为性能拐点。此时,吞吐量最大化,对响应时间的负面影响最小。在数学上,拐点是响应时间除以利用率达到最小值。拐点的一个很好的属性是,它发生在通过原点的一条直线与响应时间曲线相切的某一点上。 

为什么拐点如此重要?对于具有随机服务请求的系统,允许持续的资源负载超过拐点会导致响应时间和吞吐量随着负载的微小变化而剧烈波动。因此,对于具有随机请求到达的系统,管理负载以使其不超过拐点是至关重要的。

即使系统可以完美地伸缩,一旦平均负载超过拐点,仍然会遇到大量的性能问题。更何况,实践的系统远不如模型中的假设。因此,性能拐点的利用率值更具约束性。

总结一下:

  • 系统中的每一个资源都有一个拐点。

  • 在一个随机请求的系统中,如果允许系统中任何资源的持续利用率超过拐点值,就会遇到性能问题。

因此,负载管理是至关重要的,这样的资源利用率就不会超过系统的拐点。

8 容量规划保性能

容量规划是一个复杂一些的技术,有如下的目标约束:

•对给定资源的目标容量是在高峰时间可以流畅地完成任务,而不需超过拐点。

•如果利用率低于拐点,系统性能大致呈线性。

•如果系统运行的任何资源超出了它们的拐点范围,无论是否意识到这些问题,都会存在性能问题。

•如果存在性能问题,不需要花时间在数学模型上,而是通过重新安排负载、减少负载或增加容量来尽快修复这些问题。

你可能已经注意到,我多次使用随机到达这个术语。为什么这很重要?

有些系统可能没有完全确定的作业计划。如果访问者可以完全确定地进入系统,这意味着可以准确地知道下一个服务请求什么时候到达,那么就可以使资源利用率临时超过阈值,而不一定会造成性能问题。在一个具有确定性到达的系统上,目标是100% 的资源利用率,而不是将如此多的工作负载进行排队。

拐点之所以在随机访问的系统中如此重要,是因为它们倾向于聚集并导致短暂的利用率峰值。这些峰值需要足够的空闲容量来消耗,这样才能使用户不必忍受每次峰值发生时明显的队列延迟(这会导致响应时间的明显波动)。

对于给定的资源,只要持续时间不超过几秒钟,利用率的暂时上升超过拐点是可以的。那么,多少秒是多呢?如果无法满足基于百分比的响应时间承诺或者吞吐量承诺,那么峰值持续时间就太长了。根据经验,应该至少确保峰值持续时间不超过8秒。 

9 性能测试

关于排队延迟和一致性延迟的讨论导致了一个非常困难的问题: 如何才能对一个新应用程序进行足够的测试,以确保不会因为性能问题而破坏生产环境呢?

一切模型都不会是完美的, 性能测试可能是困难的,在这些模型和性能测试中,很可能在实际生产中遇到这些问题之前预见问题。

有些人认为这种性能测试是徒劳的,因此完全有理由不进行测试。千万不要陷入这种心态, 因为:

如果试图在生产环境上线前发现问题,会发现更多的问题,而不是不去尝试。•在性能测试中,尽管永远不会发现所有的问题, 但这才是为什您需要一个可靠而高效的方法来解决上线前测试过程中的泄漏问题。

不要跳过性能测试。至少,当解决上线操作过程中不可避免会出现的性能问题时,性能测试计划将使我们成为更有能力的诊断专家和更清晰的思考者。

再次回归到吞吐量和响应时间,吞吐量通常容易测量,响应时间则要困难得多。用秒表计算终端用户操作的时间可能并不困难,但是要得到真正需要的东西可能非常困难,这就是为什么要深入研究响应时间的细节。

不幸的是,人们倾向于测量容易测量的东西,而不一定是他们应该测量的东西。在这里,糟糕并不意味着永远不能工作。实际上,如果替代措施从来都不起作用,情况会更好。这样就没人会用了。问题在于,代理测量有时会起作用。这激发了人们的信心,他们使用的措施应该一直工作,然后他们没有。代理测量有两大问题。

当需要评估一个真实系统的细节时,取决于系统允许获得的测量数据有多好。

10 性能是一个功能

最后,希望性能被看作是一个软件应用的功能,就像在 bug 跟踪系统中所展示的那样。然而,像许多其他特性一样,在编写、学习、设计和创建应用程序的时候,无法确切地知道系统性能是怎样的。对于许多应用程序 ,直到软件进入生产阶段,性能仍然是完全未知的。 

既然不知道应用程序在生产环境中的性能如何,那么在编写程序的时候,要考虑如果在生产环境中轻松地修复性能问题。编写一个在生产环境中容易修复的应用程序,一般是从一个在生产环境中容易测量的应用程序开始的。

通常,当提到生产环境性能度量的时候,人们会对性能度量的侵入效应感到担忧。有额外代码路径来度量计时的软件不会比没有额外代码路径的软件慢吗?过早的优化是一切罪恶的根源么?!将性能度量整合到产品中更有可能创建一个快速的应用程序,更重要的是,一个随着时间推移会变得更快的应用程序。

性能,就像任何其他特性一样,不会自然而然地发生,必须经过设计和构建。要做好性能,必须考虑它,研究它,为它编写额外的代码,测试它,并最终得到它。

  往期推荐:


技术琐话 

以分布式设计、架构、体系思想为基础,兼论研发相关的点点滴滴,不限于代码、质量体系和研发管理。


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存