为什么服务有时会很慢?

493 阅读12分钟

你构建了一个服务,你调用了它,它做了一些事情并返回了一个结果,但是它需要多长时间,为什么有些时间会比你的用户希望的时间长?在这篇帖子中,我会从基础知识入手,逐步介绍标准化的术语和东西,让这个问题的答案变得更加复杂,同时强调需要知道的关键点。

首先,我们需要一个衡量时间的方法,了解两个根本不同的观点。如果我们以外部用户调用服务的方式来衡量体验,我们衡量的是响应时间的长短。如果我们用工具化的方式来测量请求从开始到结束的过程中,我们只测量服务。这就引出了第一个关键点,人们会马虎,往往不清楚他们的测量结果从哪里来的。

  • 要注意测量用户的响应时间服务时间

在现实世界的例子中,一个过程有很多步骤,每一步都需要时间。每个步骤的时间就是Residence时间,由一些等待时间和一些服务时间组成。举个例子,用户在iPhone上启动一个应用程序,它调用一个Web服务来验证用户。为什么有时候可能会很慢?因为每次在手机中生成请求,将请求传输到web服务,查找用户,返回结果,并显示下一步的实际服务时间的工作量应该是基本相同的。响应时间的变化是由排队等待(队列)驱动的,而这个资源也在处理其他请求。从iPhone到认证服务器的网络传输要经过多次跳转,每一次跳转之前都有一个排队等待发送数据包的队列。如果队列是空的或者很短,那么响应速度就会很快,如果队列很长,响应速度就会很慢。当请求到达服务器时,也是在队列中等待CPU开始处理该请求,如果需要数据库查询,则会有另一个队列来完成。

  • 队列中等待是导致响应时间增加的主要原因。

我们从监控工具中得到的大多数仪器仪表都是衡量某件事情完成的频率,我们称之为吞吐量。在某些情况下,我们还可以用到达率来衡量传入工作。对于一个简单的情况,比如一个web服务的稳定状态工作负载,一个请求会导致一个响应,两个都是一样的。但是重试和错误会增加到达率而不会增加吞吐量,快速变化的工作负载或者像批处理作业这样的超长请求会看到到达率和完成吞吐量之间的暂时性的不平衡,这时我们可以构建更复杂的请求模式。

  • 吞吐量是指完成的成功请求的数量。看一下到达率是否不一样,确定你知道实际测量的是什么。

虽然可以使用Zipkin或AWS X-Ray这样的追踪机制来测量流经系统的单个请求,但这次讨论的是如何思考大量请求的效果,以及它们之间的相互作用。平均行为是在一个固定的时间间隔内进行测量,这个时间间隔可以是一秒、一分钟、一小时或一天。需要有足够多的数据来进行平均,在此不谈理论,一个经验法则是平均数中至少应该有20个数据点

  • 对于不频繁的请求,选择一个时间段,平均至少20个请求加在一起,以获得有用的测量结果。

如果时间段太粗,就会掩盖了工作量的变化。例如对于视频会议系统来说,测量每小时的通话率,会忽略了大部分的通话在每小时的第一分钟左右开始,很容易出现高峰期,导致系统超负荷,所以每秒钟的测量比较合适。

  • 对于尖峰型工作负载,使用高分辨率的一秒平均测量。

监控工具各不相同,然而很少有直接测量到各种队列中的队列有多长的时间。同时也不一定能明显地看出有多少并发量可以处理队列。对于大多数网络来说,每次都是一个数据包在传输,但对于CPU来说,每个内核或vCPU都是并行工作,处理运行队列。对于数据库来说,往往有一个固定的最大连接数,这就限制了并发量

  • 对于处理请求的每一步,记录或估计用于处理请求的并发量

如果我们考虑一个系统在稳定状态下运行,平均吞吐量和响应时间稳定,那么我们可以简单地通过吞吐量和停留时间相乘来估计队列长度。这就是所谓的利特尔定律,它非常简单,经常被监控工具用来生成队列长度估计,但它只适用于随机到达工作的稳态平均数。

  • 利特尔定律:平均队列=平均吞吐量*平均居住人数

要理解为什么会起作用,以及当它不起作用时,重要的是理解工作如何到达服务,以及是什么决定了请求之间的间隙。如果你在循环中运行一个非常简单的性能测试,那么请求之间的间隙是恒定的,利特定律不适用,队列会很短,而且你的测试也不太现实,除非你试图模拟类似于传送带的情况。这是一个常见的错误,成功地做了这样的测试,然后把服务放到生产中,看着它在远低于测试工作负载的吞吐量的情况下变得很慢或者倒下,这是一个常见的错误。

  • 恒速循环测试不产生队列,而是模拟传送带。

对于现实世界的互联网流量来说,有很多独立的用户,他们并不协调,只做单次请求,请求之间的间隔是随机的。所以你需要一个负载生成器,在每个请求之间随机化一些等待时间。大多数系统的方式都是使用均匀分布的随机分布,这比传送带好,但并不正确。为了模拟网络流量,并且为了让利特尔定律适用,你需要使用负指数分布,就像Neil Gunther博士的这篇博文中描述的那样。

  • 需要适当的随机思考时间计算,才能产生更符合实际的队列。

然而,情况变得更糟。事实证明,网络流量不是随机分布的,它是以突发形式出现的。这些突发的流量是成群结队的。想一想,当用户启动一个iPhone应用时,实际上会发生什么。它不是一个个请求,而是一连串的请求。此外,参加闪卖的用户会在同一时间左右同步访问他们的APP,造成流量的突发集群。这种分布被称为帕累托分布或双曲分布。此外,当网络重新配置时,流量会被延迟一段时间,队列会堆积一段时间,然后以迅雷不及掩耳之势淹没下游系统。

  • 真正的真实世界工作负载会比普通的负载测试工具所能产生的队列和长尾响应时间更多,而且会有更高的队列和长尾响应时间。

你应该预期队列和响应时间会有变化,即使在平均利用率低的时候,也会有几个极慢的请求的长尾巴。那么,当进程中的某个步骤开始忙碌时,会发生什么情况呢?对于没有可用并发量的处理步骤(比如网络传输),随着利用率的提高,请求相互争夺的概率会增加,停留时间也会增加。网络的经验法则是,在50-70%的利用率左右逐渐开始变慢

  • 计划将网络利用率控制在50%以下,以获得良好的延迟。

利用率也是一个问题,测量可能会有误导性,但它的定义是指某事忙的时间比例。对于有更多的执行并行发生的CPU来说,慢速会在更高的利用率下发生,但会更难踢到,而且会让你大吃一惊。如果你把最后一个可用的CPU作为争夺点,这就很直观了。例如,如果有16个vCPU,最后一个CPU是最后一个6.25%的容量,所以在93.75%的利用率时,停留时间会急剧增加。对于一个100个vCPU的系统,它的利用率大约为99%。对于稳定状态下随机到达的请求,近似这种行为的公式是R=S/(1-U^N)。

  • 在多处理器系统中,平均停留时间随着利用率的提高而膨胀,但在多处理器系统中,平均停留时间的膨胀率会降低,但 "撞墙 "的难度更大。

解开这个,把平均利用率作为一个比例,而不是百分比,把它上升到处理器数量的幂数。从1中减去,再除以平均服务时间,得到平均停留时间的估计值。如果平均利用率很低,除以接近1的数字,意味着平均驻留时间和平均服务时间差不多。对于一个并发数为N=1的网络,平均利用率为70%,则意味着我们除以0.3,平均停留时间比低利用率时高出3倍左右。

  • 经验法则是将整个系统的平均停留时间保持在2-3倍以下,以保持良好的平均用户可视响应时间。

对于平均利用率为95%的16vCPU系统,0.95^16=0.44,我们除以0.56,平均停留时间大约是平均停留时间的两倍。在98%的平均利用率下,0.98^16=0.72,我们除以0.28,所以平均停留时间从可接受到缓慢,而平均利用率只增加了3%。

  • 在高平均利用率下运行多处理器系统的问题是,工作负载水平的微小变化会产生越来越大的影响。

有一个标准的Unix/Linux系统的标准度量标准,叫做Load Average,这个度量标准很不容易理解,有几个问题。对于包括Solaris/AIX/HPUX在内的Unix系统,它记录的是在CPU上运行和等待运行的操作系统线程数。对于Linux系统,它还包括被阻塞的等待磁盘I/O的操作系统线程。然后,它还会保持1分钟、5分钟和15分钟三种时间衰减值。首先要理解的是,这个度量可以追溯到20世纪60年代的单CPU系统,我总是会把负载平均值除以vCPU的数量,以得到一个在不同系统之间具有可比性的度量。第二是它不像其他指标那样在固定的时间区间内进行测量,所以它不像其他指标那样,是一种平均值,而且它建立了一个延迟响应。第三是Linux的实现是一个bug,把它作为一个功能制度化了,夸大了结果。这只是一个可怕的度量,用警报或馈赠给自动缩放算法来监控,实在是太可怕了。

  • 负载平均数并不能衡量负载,也不是平均数。最好是忽略不计。

如果一个系统超时,到达的工作量超过了可以处理的工作量,我们就会达到100%的利用率,这个公式有一个除以0的除法,预言了无限的停留时间。实际情况比这更糟糕,因为当系统变慢时,首先发生的事情就是系统的上游用户重试请求,这就放大了要做的工作量,导致重试风暴。这样一来,系统就会出现长长的工作队列,进入无响应的catatonic状态。

  • 系统如果达到持续的平均利用率达到100%,就会变得毫无反应,并建立起庞大的工作队列。

我在看超时和重试的配置时,经常会发现重试次数太多,超时时间太长。这就增加了工作的放大,让重试风暴更容易发生。我在过去曾深入讨论过这个问题,最好的策略是单次重试的短暂超时,如果可能的话,最好的策略是转到不同的连接上,进入不同的服务实例。

  • 在整个系统中,超时时间的设置永远不应该是一样的,超时需要在系统的前端边缘长得多,而在系统内部深处则要短得多。

通常操作员的反应是通过重启系统来清除过载的队列,但一个设计良好的系统会通过默默地丢弃或对传入的请求产生快速失败响应来限制队列,并舍弃工作。数据库和其他有固定最大连接数的服务都是这样的。当你无法获得另一个连接来发出请求时,你会得到一个快速失败响应。如果连接数限制设置得太低,数据库会拒绝它有能力处理的工作,如果设置得太高,数据库会在拒绝更多的传入工作之前减慢速度

  • 想一想,如果系统的平均利用率达到100%时,该如何舍弃传入的工作,应该设置哪些配置限制。

在极端条件下,保持良好的响应时间的最好方法就是快速失效,甩掉传入负载。即使在正常条件下,大多数现实世界的系统都有一个长长的尾巴,反应时间很慢。然而通过正确的测量,我们可以管理和预测问题,通过精心的设计和测试,我们可以建立系统,管理其对用户请求的最大响应时间