为什么服务有时会变慢?
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
你构建了一个服务,调用它,它执行某些操作并返回结果,但这个过程需要多长时间?为什么有时它所需的时间比用户预期的要长?在本文中,我将从基础知识入手,逐步介绍标准化的术语以及使这个问题的答案更加复杂的因素,同时重点强调需要了解的关键点。
首先,我们需要一种方法来衡量所需时间,并理解两种截然不同的视角。如果我们以外部用户的身份调用服务来衡量体验,那么我们衡量的是响应时间。如果我们对代码进行插桩,从头到尾测量请求的运行时间,那么我们衡量的仅仅是服务本身。这就引出了第一个关键点:人们在使用术语时常常不够严谨,而且通常不清楚他们的测量数据来源。
注意要分别测量用户端的响应时间和服务端的服务时间。
以实际应用为例,一个流程包含许多步骤,每个步骤都需要时间。每个步骤所需的时间称为驻留时间,它由等待时间和处理时间组成。例如,用户在 iPhone 上启动一个应用,该应用会调用一个 Web 服务来验证用户身份。为什么有时速度会很慢呢?在手机上生成请求、将其发送到 Web 服务、查找用户、返回结果并显示下一步所需的实际处理时间应该每次都大致相同。响应时间的变化是由于用户需要排队等待资源(即排队)造成的,因为该资源也在处理其他请求。从 iPhone 到身份验证服务器的网络传输需要经过多个跃点,在每个跃点之前都有一个数据包队列等待发送。如果队列为空或队列较短,则响应速度会很快;如果队列很长,则响应速度会很慢。当请求到达服务器时,它也会进入一个队列,等待 CPU 开始处理该请求;如果需要进行数据库查询,则还有另一个队列来完成该查询。
排队等待是导致响应时间增加的主要原因。
我们从监控工具中获取的大部分数据都是衡量任务完成频率的指标,我们称之为吞吐量。在某些情况下,我们还会用到达率来衡量传入的工作量。对于像 Web 服务这样具有稳定状态工作负载的简单情况(即一个请求对应一个响应),两者是相同的。然而,重试和错误会增加到达次数,但不会提高吞吐量;快速变化的工作负载或像批处理作业这样耗时很长的请求会导致到达次数和完成吞吐量之间出现暂时的不平衡;此外,还可以构建更复杂的请求模式。
吞吐量是指已成功完成的请求数量。请查看到达率是否有所不同,并确保您了解实际测量的是什么。
虽然可以使用Zipkin或AWS X-Ray等追踪机制来测量流经系统的单个请求,但本文讨论的重点在于如何思考大量请求的影响以及它们之间的相互作用。平均行为是在固定的时间间隔内测量的,该时间间隔可以是秒、分钟、小时或天。需要有足够的数据来进行平均,这里暂且不深入探讨理论,一个经验法则是,平均值至少需要 20 个数据点。
对于不频繁的请求,选择一个平均至少包含 20 个请求的时间段,以获得有用的测量结果。
如果时间段过于粗略,就会掩盖工作负载的变化。例如,对于视频会议系统,按小时测量通话率会忽略这样一个事实:大多数通话都发生在每小时的第一分钟左右,很容易出现峰值导致系统过载,因此按秒测量更为合适。
对于峰值工作负载,请使用高分辨率的一秒平均测量值。
监控工具种类繁多,但很难直接测量各个队列的等待时间。此外,也很难确定有多少并发能力可用于处理这些队列。大多数网络一次只传输一个数据包,但对于 CPU 而言,每个核心或虚拟 CPU 都会并行处理运行队列。对于数据库,通常会限制与客户端的连接数上限,从而限制并发能力。
对于请求处理的每个步骤,记录或估计用于处理该请求的并发性。
如果我们考虑一个处于稳定状态的系统,其平均吞吐量和响应时间都比较稳定,那么我们可以通过将吞吐量乘以停留时间来估算队列长度。这就是所谓的利特尔法则,它非常简单,监控工具经常使用它来生成队列长度的估算值,但它仅适用于随机到达的工作负载的稳定平均状态。
利特尔定律:平均排队长度 = 平均吞吐量 * 平均停留时间
要理解这种方法为何有效以及何时无效,关键在于理解工作如何到达服务以及请求之间的间隔由什么决定。如果你在一个循环中运行一个非常简单的性能测试,那么请求之间的间隔是恒定的,利特尔法则不适用,队列会很短,你的测试也不太真实,除非你想模拟类似传送带的情况。一个常见的错误是,成功完成这类测试后,将服务部署到生产环境,却发现它在远低于测试负载的吞吐量下就会变慢甚至崩溃。
恒定速率循环测试不会产生队列,它们模拟的是传送带。
对于真实的互联网流量,由于许多独立用户互不协调地发出单个请求,请求之间的间隔是随机的。因此,你需要一个负载生成器,能够随机化每个请求之间的等待时间。大多数系统采用均匀分布的随机数,这比传送带式的随机数分配要好,但并不准确。为了模拟网络流量,并使利特尔定律成立,你需要使用负指数分布,正如尼尔·冈瑟博士在这篇博文中描述的那样。
需要进行正确的随机思考时间计算,以生成更真实的队列。
然而,情况会变得更糟。事实证明,网络流量并非随机分布,而是以突发形式出现。这些突发流量往往成群出现。想想用户启动 iPhone 应用时实际发生的情况。它并非只发出一个请求,而是发出一系列请求。此外,参与限时抢购的用户会在同一时间访问应用,从而导致流量集中爆发。这种分布被称为帕累托分布或双曲线分布。另外,当网络重新配置时,流量会延迟一段时间,形成一个队列,然后向下游系统倾泻大量流量。更新: Jim Brady 和 Neil Gunther在如何配置负载测试工具使其更贴近实际应用方面做了一些有益的研究, Jim Brady 还在 CMG2019 会议上发表了一篇论文,探讨如何衡量测试负载的性能。
真正的现实世界工作负载更具突发性,并且会比普通负载测试工具默认生成的工作负载具有更高的队列长度和更长的响应时间。
即使在平均利用率较低的最佳情况下,您也应该预料到队列和响应时间会有所波动,并且会出现少数几个速度极慢的请求。那么,当流程中的某个步骤开始繁忙时会发生什么呢?对于没有可用并发的处理步骤(例如网络传输),随着利用率的增加,请求之间相互竞争的概率也会增加,因此请求的停留时间也会延长。对于网络而言,经验法则是,当利用率达到 50% 到 70% 左右时,网络速度会逐渐开始下降。
为了获得良好的延迟效果,请计划将网络利用率保持在 50% 以下。
利用率也存在问题,测量结果可能具有误导性,但它的定义是某个资源处于繁忙状态的时间比例。对于并行执行任务较多的 CPU,利用率越高,性能下降就越明显,而且下降速度更快,可能会出乎意料。如果将最后一个可用的 CPU 视为资源争用点,这一点就很容易理解了。例如,如果有 16 个 vCPU,最后一个 CPU 占用剩余的 6.25% 容量,因此在利用率达到 93.75% 左右时,CPU 的驻留时间会急剧增加。对于一个拥有 100 个 vCPU 的系统,驻留时间会在利用率达到 99% 左右时急剧增加。对于稳定状态下随机到达的请求(条件与利特尔定律相同),近似描述这种行为的公式是 R=S/(1-U^N)。
随着利用率的提高,平均停留时间的膨胀在多处理器系统中有所减少,但“遇到瓶颈”的程度更大。
进一步分析,将平均利用率视为比例而非百分比,并将其取处理器数量的幂。用 1 减去该比例,再除以平均服务时间,即可估算出平均驻留时间。如果平均利用率较低,除以接近 1 的数值意味着平均驻留时间与平均服务时间几乎相同。对于并发数为 N=1 的网络,70% 的平均利用率意味着除以 0.3,此时平均驻留时间约为低利用率时的三倍。
经验法则是,为了保持良好的平均用户可见响应时间,应将整个系统中的平均停留时间膨胀控制在 2-3 倍以下。
对于一个拥有 16 个虚拟 CPU、平均利用率为 95% 的系统,0.95^16 = 0.44,除以 0.56,平均驻留时间大约翻了一番。当平均利用率为 98% 时,0.98^16 = 0.72,除以 0.28,因此平均驻留时间仅提升 3%,就会从可接受的水平变为较慢的水平。
多处理器系统在高平均利用率下运行的问题在于,工作负载水平的微小变化都会产生越来越大的影响。
有一个名为“负载平均值”的标准 Unix/Linux 指标,但它鲜为人知且存在诸多问题。对于包括 Solaris/AIX/HPUX 在内的 Unix 系统,它记录 CPU 上运行和等待运行的操作系统线程数。对于Linux,它还包括因等待磁盘 I/O 而阻塞的操作系统线程数。然后,它会维护三个随时间衰减的值,分别对应1 分钟、5 分钟和 15 分钟。首先需要理解的是,该指标可以追溯到 20 世纪 60 年代的单 CPU 系统,我通常会将负载平均值除以虚拟 CPU (vCPU) 的数量,以获得跨系统可比的指标。其次,它不像其他指标那样在固定时间间隔内进行测量,因此它并非真正意义上的平均值,并且存在响应延迟。第三,Linux 的实现本身就是一个缺陷,却被当作特性而存在,导致结果偏高。因此,它不适合用于警报监控或作为自动扩缩容算法的输入。
负载平均值并不能衡量负载,也不是平均值,最好忽略。
如果系统不堪重负,接收到的工作量超过了处理能力,就会达到 100% 的利用率,此时公式中除以零的运算会导致无限长的等待时间。但实际情况比这更糟,因为当系统运行缓慢时,首先发生的是上游用户会重试请求,这会进一步增加待处理的工作量,导致重试风暴。系统会积压大量工作,最终进入无响应的“死机”状态。
系统持续达到平均 100% 的利用率后,就会变得无响应,并积压大量工作。
当我查看超时和重试的配置时,我经常发现重试次数过多,超时时间也过长。这会增加工作量放大效应,并更容易导致重试风暴。我之前已经深入讨论过这个问题,并且专门为此撰写了一篇新文章。最佳策略是设置较短的超时时间,并尽可能只进行一次重试,最好是连接到不同的服务实例。
系统中的超时时间绝不应该设置得相同,前端的超时时间需要设置得更长,而系统深处的超时时间则需要设置得更短。
通常情况下,操作员会通过重启系统来清理过载的队列,但设计良好的系统会限制队列大小,并通过静默丢弃或对传入请求生成快速失败响应来减少工作量。数据库和其他具有固定最大连接数的服务就是这样运作的。当无法建立新的连接来发出请求时,就会收到快速失败响应。如果连接数限制设置得太低,数据库会拒绝它有能力处理的工作;如果设置得太高,数据库在拒绝更多传入工作之前速度就会变得非常慢。
考虑一下如果系统平均利用率达到 100%,该如何减少传入的工作量,以及应该设置哪些配置限制。
在极端条件下保持良好响应时间的最佳方法是快速故障转移并卸载新增负载。即使在正常情况下,大多数实际系统也存在响应时间较长的时期。然而,通过正确的测量,我们可以管理和预测问题;通过精心设计和测试,我们可以构建能够控制其对用户请求最大响应时间的系统。
想要深入了解这个话题,尼尔·冈瑟的博客、书籍和培训课程中有很多优质信息。上世纪90年代末,我曾与尼尔合作开发了一门暑期绩效培训课程,该课程在斯坦福大学举办。此后,他一直举办相关活动。对我而言,与尼尔共同授课是一次意义非凡的经历,它让我深入学习了排队论,真正巩固了我对系统运行方式的理解。
照片由 Adrian 在波尔多的一个酒窖拍摄,画面中是一排排等待装瓶的酒桶。
文章来源:https://dev.to/aws/why-are-services-slow-sometimes-mn3