Skip to content

Latest commit

 

History

History
366 lines (236 loc) · 39.5 KB

ch09.md

File metadata and controls

366 lines (236 loc) · 39.5 KB

第9章 性能

身行一例,胜似千言。

—Mae West

这是跟时间有关的。

性能,即:这是关于时间和软件系统满足时限要求的能力。可惜事实上,在计算机上执行操作需要时间。计算需要数千纳秒的时间,磁盘访问(无论是固态还是旋转)需要几十毫秒的时间,网络访问所需的时间从同一数据中心内的数百微秒到洲际消息的 100 毫秒以上。在设计系统性能时,必须考虑时间。

当事件发生时(中断、消息、来自用户或其他系统的请求,或标记时间流逝的时钟事件),系统或系统的某些元素必须及时响应它们。描述可能发生的事件(以及何时发生)以及系统或元素对这些事件的基于时间的响应是讨论性能的本质。

基于 Web 的系统事件以用户(数以千万计或数千万)通过其客户端(如 Web 浏览器)发出的请求的形式出现。服务从其他服务获取事件。在内燃机控制系统中,事件来自操作员的控制和时间的流逝;当气缸处于正确位置时,系统必须控制点火装置的点火和燃料混合物,以最大限度地提高功率和效率并减少污染。

对于基于 Web 的系统、以数据库为中心的系统或者处理来自其环境的输入信号的系统,所需的响应可以表示为在单位时间内可以处理的请求数。对于发动机控制系统,响应可能是点火时间允许的变化。在每种情况下,都可以表征到达的事件模式和响应模式,并且这种表征形成了用于构建性能场景的语言。

在软件工程的大部分历史中,当计算机缓慢而昂贵并且执行的任务相形见绌时,性能一直是架构的驱动因素。因此,它经常损害所有其他品质的实现。随着硬件的性价比不断下降,软件开发成本不断上升,其他质量已成为性能的重要竞争对手。

但性能仍然至关重要。仍然有(并且可能永远存在)重要的问题,我们知道如何用计算机解决,但我们没法足够快地解决它们。

所有系统都有性能要求,即使它们没有表达出来。例如,文字处理工具可能没有任何明确的性能要求,但毫无疑问,你会同意等待一小时(或一分钟或一秒钟)才能看到键入的字符出现在屏幕上是不可接受的。性能仍然是所有软件的基本重要质量属性。

性能通常与可扩展性相关,也就是说,在提高系统工作能力的同时仍能保持良好性能。它们肯定是相互关联的,尽管技术上的可扩展性使你的系统易于以特定方式进行更改,因此是一种可修改性,如 第8章 中所述。此外,云服务的可扩展性在 第17章 中有明确讨论。

通常,性能改进发生在你构建了系统版本并发现其性能不足之后。你可以通过在构建系统时考虑性能来预测这一点。例如,如果你设计了具有可伸缩资源池的系统,并且随后确定了资源池就是瓶颈(根据检测的数据),则可以轻松增加池的大小。否则,你的选择是有限的——而且大多都是糟糕的——并且可能需要大量的返工。

花费大量时间优化仅占总时间一小部分的系统部分是没有用的。通过记录计时信息来检测系统将帮助你确定实际时间花费的位置,并使你能够专注于提高系统关键部分的性能。

9.1 性能通用场景

性能方案从到达系统的事件开始。正确响应事件需要消耗资源(包括时间)。发生这种情况时,系统可能同时为其他事件提供服务。

并发

并发是架构师必须理解的更重要的概念之一,也是计算机科学课程中教授最少的主题之一。并发是指并行发生的操作。例如,假设有一个线程执行语句,另一个线程执行相同的语句。两个线程都执行了这些语句后 x 的值是多少?它可以是 2 或 3。我把它留给你弄清楚值 3 是如何发生的——或者我应该说我把它中途插入给你?

x = 1;
x++;

每当系统创建新线程时,都会发生并发性,因为根据定义,线程是独立的控制序列。系统上的多任务处理由独立线程支持。通过使用线程,你的系统同时支持多个用户。每当系统在多个处理器上执行时,无论这些处理器是单独打包还是作为多核处理器打包,都会发生并发性。此外,在使用并行算法、并行化基础设施(如 map-reduce 或 NoSQL 数据库)或使用各种并发调度算法之一时,必须考虑并发性。换句话说,并发是一种在许多方面都可供你使用的工具。

当你有多个 CPU 或可以利用它的等待状态时,并发是一件好事。允许并行执行操作可以提高性能,因为在一个线程中引入的延迟允许处理器在另一个线程上继续前进。但由于刚才描述的交错现象(称为竞争条件),并发也必须仔细管理。

如我们的示例所示,当存在两个控制线程并且存在共享状态时,可能会发生竞争条件。并发管理通常归结为管理状态的共享方式。防止竞争条件的一种技术是使用锁来强制对状态进行顺序访问。另一种技术是根据执行部分代码的线程对状态进行分区。也就是说,如果我们有两个 x 实例,则 x 不会被两个线程共享,并且不会发生竞争条件。

竞争条件是最难发现的bug类型之一;这种bug的发生是零星的,取决于(可能是分钟)时间差异。我曾经在操作系统中遇到过无法追踪的竞争条件。我在代码中进行了测试,以便下次发生竞争条件时,触发调试过程。该bug花了一年多的时间才复现,以便我们确定原因。

不要让与并发相关的困难阻止你使用这种非常重要的技术。只需知道你必须仔细识别代码中的关键部分,并确保(或采取措施确保)这些部分中不会出现竞争条件。

—LB

表9.1 总结了性能的通用场景。

表 9.1 性能的通用场景

场景部分 描述 可能的值
来源 触发可以来自一个用户(或多个用户)、外部系统或所考虑的系统的某些部分。 外部:
  • 用户请求
  • 来自外部系统的请求
  • 来自传感器或其他系统的数据
    内部:
  • 一个组件可能会请求另一个组件。
  • 计时器可能会生成通知。
  • 触发事件 触发是事件的到来。该事件可以是服务请求,也可以是所考虑系统或外部系统的某些状态的通知。 周期性、零星的或随机事件的到来:
  • 定期事件以可预测的时间间隔到达。
  • 随机事件根据某种概率分布到达。
  • 零星事件根据既不是周期性也不是随机性的模式到达。
  • 工件 被触发的工件可能是整个系统,也可能只是系统的一部分。例如,通电事件可能会触发整个系统。用户请求可能会到达(触发)用户界面。
  • 整个系统
  • 系统内的组件
  • 环境 触发事件到达时系统或组件的状态。异常模式(错误模式、过载模式)将影响响应。例如,在锁定设备之前,允许三次不成功的登录尝试。 运行时。系统或组件可以在以下环境中运行:
  • 正常模式
  • 紧急模式
  • 纠错模式
  • 峰值负载
  • 过载模式
  • 降级的操作模式
  • 其他一些已定义的系统模式
  • 响应 系统将处理触发事件。处理触发事件需要时间。时间可能是用在计算上了,也可能因为等待被阻塞的竞争资源。由于系统过载或处理链中某处的故障,请求也可能无法满足。
  • 系统返回响应
  • 系统返回错误
  • 系统未生成响应
  • 如果过载,系统会忽略请求
  • 系统更改服务模式或级别
  • 系统服务优先级更高的事件
  • 系统消耗资源
  • 响应测量 时间的度量可以包括延迟或吞吐量。具有截止时间的系统还可以测量响应抖动和满足截止时间的能力。测量有多少请求未得到满足也是一种度量,就像使用了多少计算资源(例如,CPU,内存,线程池,缓冲区)一样。
  • 响应所花费的(最大、最小、平均、中位数)时间(延迟)
  • 在某个时间间隔(吞吐量)或收到的事件集内满足的请求数或百分比
  • 未满足的请求的数量或百分比
  • 响应时间的变化(抖动)
  • 计算资源的使用级别
  • 图9.1 给出了一个具体的性能场景示例:在正常操作下,500 个用户在 30 秒的间隔内发起 2,000 个请求。系统以两秒的平均延迟处理所有请求

    A sample performance scenario is presented.

    图 9.1 性能场景示例

    9.2 性能策略

    性能策略的目标是生成对在某些基于时间或基于资源的约束下到达系统的事件的响应。事件可以是单个事件或流,并且触发计算的执行。性能策略对生成响应的时间或资源进行控制,如 图9.2 所示。

    The goal of performance tactics to control response diagram is presented. The stimulus is the events that arrive. The response is the events processed within time and resource budgets.

    图9.2 性能策略的目标

    在事件到达之后但在系统完成对事件的响应之前的任何时刻,系统要么正在响应该事件,要么由于某种原因阻止处理。这导致了响应时间和资源使用情况的两个基本因素:处理时间(当系统工作响应并主动消耗资源时)和阻塞时间(当系统无法响应时)。

    • 处理时间和资源使用。 处理会消耗资源,也需要时间。事件由一个或多个组件的执行来处理,这些组件花费的时间是资源。硬件资源包括 CPU、数据存储、网络通信带宽和内存。软件资源包括由设计中的系统定义的实体。例如,必须管理线程池和缓冲区,并且必须按顺序访问关键部分。

      例如,假设消息由一个组件生成。它可能被放置在网络上,然后到达另一个组件。然后将其放置在缓冲区中;以某种方式转换;根据某种算法进行处理;转换输出;放置在输出缓冲区中;并发送到某个组件、另一个系统或某个参与者。其中每个步骤都会增加该事件处理的总体延迟和资源消耗。

      不同的资源在其利用率接近其容量时(即当它们饱和时)的行为会有所不同。例如,随着 CPU 负载的增加,性能通常会相当稳定地下降。相反,当你开始耗尽内存时,在某些时候,页面交换变得势不可挡,性能就会突然崩溃。

    • 阻塞的时间和资源竞争。 计算可能会因为竞争某些所需资源、资源不可用或计算依赖于其他尚不可用的计算的结果而被阻止:

      • 资源竞争。 许多资源一次只能由一个客户端使用。因此,其他客户端必须等待访问这些资源。图9.2 显示到达系统的事件。这些事件可能位于单个流或多个流中。竞争同一资源的多个流、或者同一流中的不同事件竞争同一资源会导致延迟。对资源的竞争越多,延迟就越长。

      • 资源的可用性。即使没有竞争,如果资源不可用,计算也无法继续。不可用可能是由于资源脱机或组件因任何原因出现故障而导致的。

      • 对其他计算的依赖性。 计算可能必须等待,因为它必须与另一个计算的结果同步,或者因为它正在等待它启动的计算的结果。如果一个组件调用另一个组件并且必须等待该组件响应,而当被调用的组件位于网络的另一端(而不是位于同一处理器上)或被调用组件负载过重时,时间可能会很长。

    无论原因是什么,都必须确定架构中资源限制可能严重导致整体延迟的位置。

    在这种背景下,我们转向我们的策略类别。我们可以减少对资源的需求(控制资源需求),或者让我们可用的资源更有效地处理需求(管理资源)。

    控制资源需求

    提高性能的一种方法是仔细管理对资源的需求。这可以通过减少处理的事件数或限制系统响应事件的速率来完成。此外,还可以应用多种技术来确保明智地使用所拥有的资源:

    • 管理工作请求。 减少工作的一种方法是减少进入系统以执行工作的请求数量。执行此操作的方法包括以下内容:

      • 管理事件到达。管理来自外部系统的事件到达的常用方法是制定服务级别协议 (SLA),该协议指定你愿意支持的最大事件到达率。SLA 是以下形式的协议:“系统或组件将处理每单位时间到达的 X 个事件,响应时间为 Y”。此协议同时约束系统(它必须提供该响应)和客户端(如果它每单位时间发出超过 X 个请求,则不保证响应。因此,从客户端的角度来看,如果它每单位时间需要处理 X 个以上的请求,则必须利用处理请求的元素的多个实例。SLA 是管理基于 Internet 的系统可伸缩性的一种方法。

      • 管理采样率。 在系统无法保持足够的响应级别的情况下,你可以降低触发事件的采样频率,例如,从传感器接收数据的速率或每秒处理的视频帧数。当然,这里付出的代价是视频流的保真度或从传感器数据中收集的信息。尽管如此,如果结果“足够好”,这是一个可行的策略。这种方法通常用于信号处理系统,例如,不同的副本可以选择具有不同采样率和数据格式。此设计选择旨在保持可预测的延迟级别;你必须确定具有较低保真度但一致的数据流是否比具有不稳定的延迟更可取。某些系统动态管理采样率,以响应延迟测量或精度需求。

    • 限制事件响应。当离散事件到达系统(或组件)的速度太快而无法处理时,必须对事件进行排队,直到可以处理它们,或者干脆丢弃它们。你可以选择仅处理最多设置的最大速率的事件,从而确保实际处理的事件的可预测处理。此策略可能由队列大小或处理器利用率超过某个警告级别触发。或者,它可能由违反 SLA 的事件速率触发。如果你采用此策略并且丢失任何事件是不可接受的,那么你必须确保你的队列足够大以处理最坏的情况。相反,如果你选择删除事件,则需要选择一个策略:是记录丢弃的事件还是干脆忽略它们?你是否通知其他系统、用户或管理员?

    • 确定事件的优先级。如果并非所有事件都同等重要,则可以强制实施优先级方案,根据为事件提供服务的重要性对事件进行排名。如果出现低优先级事件时没有足够的资源来为它们提供服务,则可能会忽略低优先级事件。忽略事件消耗的资源(包括时间)最少,因此相对于始终为所有事件提供服务的系统,这样性能更高。例如,楼宇管理系统可能会发出各种警报。危及生命的警报(如火灾警报)应比信息警报(如房间太冷)具有更高的优先级。

    • 减少计算开销。 对于确实进入系统的事件,可以实现以下方法来减少处理每个事件所涉及的工作量:

      • 减少间接。中介的使用(对于可修改性非常重要,正如我们在 第8章 中看到的那样)增加了处理事件流的计算开销,因此删除它们可以改善延迟。这是一个经典的可修改性/性能权衡。关注点分离(可修改性的另一个关键)也会增加为事件提供服务所需的处理开销,如果它导致事件由组件链而不是单个组件提供服务。但是,你可能能够实现两全其美:聪明的代码优化可以让你使用支持封装(从而保持可修改性)的中介和接口进行编程,但减少或在某些情况下消除运行时代价高昂的间接寻址。类似地,一些代理允许客户端和服务器之间的直接通信(在最初通过代理建立关系之后),从而消除了所有后续请求的间接步骤。

      • 共置通信资源。上下文切换和组件间通信成本加起来,尤其是当组件位于网络上的不同节点上时。减少计算开销的一种策略是共置资源。共置可能意味着在同一处理器上托管协作组件,以避免网络通信的时间延迟;这可能意味着将资源放在同一个运行时软件组件中,以避免子例程调用的费用;或者,这可能意味着将多层架构的层放置在数据中心的同一机架上。

      • 定期清理。 减少计算开销的一个特殊情况是对效率低下的资源执行定期清理。例如,哈希表和虚拟内存映射可能需要重新计算和重新初始化。正是出于这个原因,许多系统管理员甚至普通计算机用户都会定期重新启动他们的系统。

    • 限定执行时间。你可以限制用于响应事件的执行时间。对于迭代、依赖于数据的算法,限制迭代次数是一种限制执行时间的方法。然而,成本通常是一个不太准确的计算。如果你采用这种策略,你将需要评估它对准确性的影响,看看结果是否“足够好”。此资源管理策略经常与管理采样率策略配合。

    • 提高资源使用效率。 提高关键领域使用的算法的效率可以减少延迟,提高吞吐量和资源消耗。对于一些程序员来说,这是他们的“主要”性能策略。如果系统性能不佳,他们会尝试“调整”其处理逻辑。如你所见,这种方法实际上只是众多可用策略之一。

    管理资源

    即使对资源的需求是不可控的,对这些资源的管理也是可以的。有时一种资源可以换取另一种资源。例如,中间数据可以保存在缓存中,也可以根据哪些资源更关键(时间、空间或网络带宽)重新生成。以下是一些资源管理策略:

    • 增加资源。 更快的处理器、额外的处理器、额外的内存和更快的网络都有可能提高性能。成本通常是资源选择的一个考虑因素,但在许多情况下,增加资源是立即获得改进的最便宜的方法。

    • 引入并发。如果可以并行处理请求,则可以减少阻塞时间。可以通过处理不同线程上的不同事件流或创建其他线程来处理不同的活动集来引入并发性。(引入并发后,你可以选择调度策略,使用调度资源策略来实现你认为所需的目标。

    • 维护计算的多个副本。此策略可减少将所有服务请求分配给单个实例时发生的争用。微服务体系结构中的复制服务或服务器池中的复制 Web 服务器是计算副本的示例。负载均衡器是一种将新工作分配给可用重复服务器之一的软件;分配条件各不相同,但可以像轮询方案或将下一个请求分配给最不繁忙的服务器一样简单。负载均衡器模式在 第9.4节 中有详细讨论。

    • 维护数据的多个副本。 维护数据的多个副本的两个常见示例是数据复制和缓存。数据复制涉及保留数据的单独副本,以减少多个同时访问的竞争。由于要复制的数据通常是现有数据的副本,因此保持副本的一致性和同步成为系统必须承担的责任。缓存还涉及保留数据副本(一组数据可能是另一组数据的子集),但存储在具有不同访问速度的存储上。不同的访问速度可能是由于内存速度与辅助存储速度,或本地通信与远程通信速度。缓存的另一个职责是选择要缓存的数据。一些缓存仅通过保留最近请求的任何内容的副本来运行,但也可以根据行为模式预测用户的未来请求,并在用户发出请求之前开始必要的计算或预取以遵守这些请求。

    • 绑定队列大小。此策略控制排队到达的最大数量,从而控制用于处理到达请求的资源。如果采用此策略,则需要为队列溢出时发生的情况建立一个策略,并确定是否可以接受不响应丢失的事件。这种策略经常与极限事件响应策略配合。

    • 调度资源。每当发生资源竞争时,都必须调度该资源。调度处理器、调度缓冲区和网络。作为架构师,你关心的是了解每个资源的使用特征,并选择与之兼容的调度策略。(请参阅“调度策略”侧栏。

    图9.3总结了性能策略。

    A flowchart of the performance tactics.

    图9.3 性能策略

    调度策略

    调度策略在概念上有两个部分:优先级分配和调度。所有计划策略都分配优先级。在某些情况下,分配就像先进先出(或FIFO)一样简单。在其他情况下,它可以与请求的截止日期或其语义重要性相关联。调度的竞争标准包括最佳资源使用、请求重要性、最小化使用的资源数、最小化延迟、最大化吞吐量、防止饥饿以确保公平性等。你需要了解这些可能冲突的条件,以及所选调度策略对系统满足这些条件的能力的影响。

    仅当高优先级事件流可用时,才能调度该资源(分配给该资源)。有时,这取决于抢占资源的当前用户。可能的抢占选项如下:可以随时发生、只能在特定的抢占点发生、或者无法抢占正在执行的进程。一些常见的调度策略如下:

    • 先进先出。 FIFO 队列将所有资源请求视为平等,并依次满足它们。FIFO队列的一种可能性是,一个请求将卡在另一个需要很长时间才能生成响应的请求后面。只要所有请求真正相等,这不是问题,但如果某些请求的优先级高于其他请求,就会带来挑战。

    • 固定优先级调度。 固定优先级调度为每个资源请求源分配特定优先级,并按该优先级顺序分配资源。此策略可确保为更高优先级的请求提供更好的服务。但是,它也承认优先级较低但仍然重要的请求可能需要任意长时间才能得到服务,因为它被困在一系列优先级较高的请求后面。三种常见的优先级策略如下:

      • 语义重要性。 语义重要性根据生成优先级的任务的某些域特征静态分配优先级。

      • 截止时间单调性。 截止时间单调性是一种静态优先级分配,它为截止时间较短的流分配更高的优先级。此调度策略用于调度具有实时截止时间的不同优先级的流。

      • 速率单调。 速率单调是周期流的静态优先级分配,它为周期较短的流分配更高的优先级。此调度策略是截止时间单调的特例,但更广为人知,并且更有可能受到操作系统的支持。

    • 动态优先级调度。策略包括:

      • 循环赛。 循环赛调度策略对请求进行排序,然后在每次分配可能性时,按该顺序将资源分配给下一个请求。循环赛的一种特殊形式是循环执行,其中可能的分配时间以固定的时间间隔指定。

      • 最早截止日期优先。 最早截止日期优先根据具有最早截止日期的待处理请求分配优先级。

      • 最少松弛时间优先。 此策略将最高优先级分配给具有最少“松弛时间”的作业,即作业的剩余执行时间与作业截止时间之间的差值。

      对于可抢占的单个处理器和进程,最早截止时间优先和最不松弛优先的调度策略都是最佳选择。也就是说,如果可以调度这组流程,以便满足所有截止日期,那么这些策略将能够成功调度该集。

    • 静态调度。循环执行调度是一种调度策略,其中抢占点和对资源的分配顺序是脱机确定的。从而避免了调度程序的运行时开销。

    路上的性能策略

    策略是通用的设计原则。要练习这一点,请考虑你居住的道路和高速公路系统的设计。交通工程师采用一系列设计“技巧”来优化这些复杂系统的性能,其中性能有许多衡量标准,例如吞吐量(每小时有多少辆汽车从郊区到足球场),平均延迟(平均需要多长时间,从你的房子到市中心)和最坏情况的延迟(急救车需要多长时间才能把你送到医院)。这些技巧是什么?无非是我们的好老朋友——策略。

    让我们考虑一些例子:

    • 管理事件率。 高速公路入口匝道上的灯只允许汽车在设定的时间间隔内进入高速公路,汽车必须在匝道上等待(排队)才能轮到他们。

    • 优先事件。 救护车和警察,随着他们的灯光和警笛声响起,比普通公民具有更高的优先级;一些高速公路有高占用率车辆 (HOV) 车道,优先考虑有两个或更多乘员的车辆。

    • 维护多个副本。 为现有道路添加行车道或建立平行路线。

    此外,系统用户可以使用自己的技巧:

    • 增加资源。 例如,购买法拉利。在所有其他条件相同的情况下,在开阔的道路上拥有称职的驾驶员的最快汽车将使你更快地到达目的地。

    • 提高效率。 找到一条比你当前路线更快和/或更短的新路线。

    • 减少计算开销。 靠近你前面的汽车,或将更多人装载到同一辆车中(即拼车)。

    这个讨论的意义何在?套用格特鲁德·斯坦(Gertrude Stein)的话:性能就是表现得更有效率(Performance is performance is performance)。几个世纪以来,工程师一直在分析和优化复杂系统,试图提高其性能,并且他们一直在采用相同的设计策略来做到这一点。因此,当你尝试提高基于计算机的系统的性能时,你应该感到一些安慰,你正在应用经过彻底“道路测试”的策略。

    —RK

    9.3 基于策略的性能的问卷

    根据 第9.2节 中描述的策略,我们可以创建一组受策略启发的问题,如 表9.2 所示。为了大致了解为支持性能而做出的架构选择,分析师会询问每个问题并将答案记录在表中。然后,可以将这些问题的答案作为进一步活动的重点:文档调查、代码或其他工件分析、代码逆向工程等。

    表9.2 基于策略的性能调查表

    策略组 策略问题 支持与否(是/否) 风险 设计决策和位置 原因和假设
    控制资源需求 你是否制定了服务级别协议 (SLA) 来指定你愿意支持的最大事件到达率?
    你能否管理到达系统的事件采样的速率
    系统将如何限制事件的响应(处理量)?
    你是否定义了不同类别的请求并为每个类别定义了优先级
    你能否通过共置、清理资源或减少间接寻址来减少计算开销
    你能限定算法的执行时间吗?
    你能通过你选择的算法来提高计算效率吗?
    管理资源 是否可以为系统或其组件分配更多资源
    你是否正在使用并发?如果可以并行处理请求,则可以减少阻塞时间。
    是否可以在不同的处理器上复制计算
    管理资源 是否可以对数据进行缓存(以维护可快速访问的本地副本)或复制(以减少竞争)?
    是否可以限制队列大小以设置处理刺激所需的资源的上限?
    你是否确保你使用的调度策略适合你关注的性能?

    9.4 性能模式

    几十年来,性能问题一直困扰着软件工程师,因此开发一组丰富的模式来管理性能的各个方面也就不足为奇了。在本节中,我们仅对其中的几个进行讨论。请注意,某些模式有多种用途。例如,我们在 第4章 中看到了断路器模式,它被标识为可用性模式,但它对性能也有帮助,因为它减少了等待无响应服务的时间。

    我们将在此处介绍的模式是服务网格、负载均衡器、限制和映射-归约(Map-Reduce)。

    服务网格

    服务网格模式用于微服务架构。网格的主要功能是挎斗,这是一种伴随每个微服务的代理,它提供了广泛有用的功能来解决与应用无关的问题,例如服务间通信、监视和信息安全性。挎斗与每个微服务一起执行,并处理所有服务间通信和协调。(正如我们将在第16章中描述的那样,这些元素通常被打包到pod中。它们一起部署,从而减少了由于网络而导致的延迟,从而提高了性能。

    此方法允许开发人员将微服务的功能(核心业务逻辑)与横切关注点(如身份验证和授权、服务发现、负载平衡、加密和可观测性)的实现、管理和维护分开。

    好处:

    • 管理关注跨域的软件可以购买现成的,也可以由不执行任何其他操作的专家团队实施和维护,从而使业务逻辑开发人员能够只关注业务问题。

    • 服务网格强制将通用功能部署到与使用这些通用功能的服务相同的处理器上。这减少了服务与其通用功能程序之间的通信时间,因为通信不需要使用网络消息。

    • 可以将服务网格配置为使通信依赖于上下文,从而简化 第3章 中描述的 Canary 和 A/B 测试等功能。

    权衡:

    • 挎斗引入了更多的执行进程,每个进程都会消耗一些处理能力,从而增加系统的开销。

    • 挎斗通常包含多个函数,并非每个服务或每次调用服务都需要所有这些函数。

    负载均衡

    负载均衡器是一种中介,用于处理来自一组客户端的消息,并确定哪个服务实例应响应这些消息。此模式的关键是负载均衡器作为传入消息的单一联系点(例如,单个 IP 地址),但随后它会将请求分发给可以响应请求的提供者池(服务器或服务)。通过这种方式,可以在提供者池中平衡负载。负载均衡器实现某种形式的计划资源策略。调度算法可能非常简单,例如轮询,或者它可以考虑每个提供者上的负载,或每个提供者等待服务的请求数。

    好处:

    • 服务器的任何失效对客户端都是不可见的(假设仍有一些剩余的处理资源)。

    • 通过在多个提供者之间共享负载,可以保持较低的延迟,并且客户端的延迟更可预测。

    • 向负载均衡器可用的池中添加更多资源(更多服务器、更快的服务器)相对简单,并且没有客户端需要注意这一点。

    权衡:

    • 负载平衡算法必须非常快;否则,它本身可能会导致性能问题。

    • 负载均衡器是潜在的瓶颈或单点故障,因此它本身经常被复制(甚至负载平衡)。

    负载均衡器在 第17章 中有更详细的讨论。

    限流

    限流模式是管理工作请求策略的打包。它用于限制对某些重要资源或服务的访问。在此模式中,通常有一个中介(限流器),用于监视(请求)服务并确定是否可以为传入请求提供服务。

    好处:

    • 通过限制传入请求,你可以正常处理需求的变化。这样做,服务永远不会过载;它们可以保持在性能“最佳位置”,在那里它们可以有效地处理请求。

    权衡:

    • 限流逻辑必须非常快;否则,它本身可能会导致性能问题。

    • 如果客户端需求经常超过容量,则缓冲区需要非常大,否则存在丢失请求的风险。

    • 此模式可能很难添加到客户端和服务器紧密耦合的现有系统中。

    Map-Reduce

    Map-reduce模式有效地执行大型数据集的分布式和并行排序,并为程序员提供了一种简单的方法来指定要完成的分析。与我们独立于任何应用程序的其他性能模式不同,map-reduce 模式专门设计用于为特定类型的重复出现的问题提供高性能:对大型数据集进行排序和分析。任何处理海量数据的组织都会遇到这个问题——想想谷歌、Facebook、雅虎和Netflix——所有这些组织实际上都使用map-reduce。

    Map-reduce 模式包含三个部分:

    • 第一个是专门的基础设施,负责将软件分配给大规模并行计算环境中的硬件节点,并根据需要对数据进行排序。节点可以是虚拟机、独立处理器或多核芯片中的某个核。

    • 第二个和第三个是两个由码农开发的功能叫做:可以预见的是,mapreduce

      • 映射(map) 功能将键和数据集作为输入。它使用密钥将数据哈希到一组存储桶中。例如,如果我们的数据集由扑克牌组成,那么关键可能是花色。map 函数还用于筛选数据,即确定数据记录是参与进一步处理还是丢弃。继续我们的卡牌示例,我们可能会选择丢弃小丑或字母卡牌(A,K,Q,J),只保留数字卡牌,然后我们可以根据其花色将每张牌映射到一个桶中。map-reduce 模式的映射阶段的性能通过具有多个映射实例得到增强,每个映射实例处理数据集的不同部分。输入文件被划分为多个部分,并创建多个映射实例来处理每个部分。继续我们的例子,让我们考虑我们有10亿张扑克牌,而不仅仅是一副牌。由于每张卡牌都可以单独检查,因此映射过程可以由数万或数十万个实例并行执行,而无需在它们之间的通信。映射完所有输入数据后,这些存储桶将由 map-reduce 基础设施进行洗牌,然后分配给新的处理节点(可能重用映射阶段中使用的节点)以进行化简阶段。例如,可以将所有梅花分配给一个集群实例,将所有方块分配给另一个集群,依此类推。

      • 所有繁重的分析都在*化简(reduce)*功能中进行。化简实例数与映射功能输出的桶数相对应。化简阶段执行一些码农指定的分析,然后发出该分析的结果。例如,我们可以计算梅花、方块、红桃和黑桃的数量,或者我们可以对每个桶中所有牌的数值求和。输出集几乎总是比输入集小得多,因此得名“化简(reduce)”。

    映射实例是无状态的,并且彼此之间不通信。映射实例和化简实例之间的唯一通信是从映射实例以<键、值>对的形式发出的数据。

    好处:

    • 通过使用并行性,可以有效地分析非常大的未排序数据集。

    • 任何实例的失败对处理的影响都很小,因为map-reduce通常会将大型输入数据集分解为许多较小的数据集进行处理,并将每个数据集分配给自己的实例。

    权衡:

    • 如果你没有大型数据集,则map-reduce模式产生的开销是没必要的。

    • 如果无法将数据集划分为大小相似的子集,则并行性的优势将丧失。

    • 需要多次化简(reduce)的操作很难编排。

    9.5 扩展阅读

    性能这个主题有丰富文献。以下是我们推荐的一些书籍,作为性能的一般概述:

    • 软件和系统性能工程基础:过程、性能建模、需求、测试、可扩展性和实践 [Bondi 14]。本书提供了性能工程的全面概述,从技术实践到组织实践。

    • 软件性能和可扩展性:一种定量方法 [Liu 09]。本书涵盖了面向企业应用程序的性能,重点是排队理论和测量。

    • 性能解决方案:创建响应式可扩展软件的实用指南 [Smith 01]。本书介绍了在设计时考虑性能,重点是构建(并填充真实数据)实用的预测性能模型。

    要获得许多性能模式中的一些概述,请参阅实时设计模式:实时系统的健壮可扩展架构 [Douglass 99]面向模式的软件架构第 3 卷:资源管理模式 [Kircher 03]。此外,Microsoft还发布了基于云的应用程序的性能和可伸缩性模式目录:https://docs.microsoft.com/en-us/azure/architecture/patterns/category/performance-scalability。

    9.6 问题讨论

    1. “每个系统都有实时性能限制。讨论。你能举个反例吗?

    2. 编写一个具体的性能场景,描述航空公司的平均准时航班到达性能。

    3. 为在线拍卖网站编写多个性能场景。考虑一下你主要关注的是最坏情况延迟、平均情况延迟、吞吐量还是其他一些响应度量。你将使用哪些策略来满足你的方案?

    4. 基于 Web 的系统通常使用 代理服务器,这是系统中从客户端(例如浏览器)接收请求的第一个元素。代理服务器能够提供经常请求的网页,例如公司的主页,而不会打扰执行事务的实际应用程序服务器。一个系统可能包含许多代理服务器,并且它们通常位于靠近大型用户社区的地理位置,以减少例行请求的响应时间。你在这里看到了哪些性能策略?

    5. 交互机制之间的根本区别在于交互是同步的还是异步的。讨论每种性能响应的优缺点:延迟、截止时间、吞吐量、抖动、未命中率、数据丢失或你可能习惯的任何其他与性能相关的响应。

    6. 查找物理世界中(即非软件)应用每种管理资源策略的示例。例如,假设你正在管理一家大型实体零售店。使用这些策略,你将如何让人们更快地通过结账队伍?

    7. 用户界面框架通常是单线程的。这是为什么呢?性能影响是什么?(提示:考虑竞争条件)。