第9章 性能

身行一例,胜似千言。

—— 梅・韦斯特(Mae West)

是时候谈谈性能了。

这里所说的性能,关乎时间以及软件系统满足时间要求的能力。令人无奈的是,计算机上的操作都需要耗费时间。计算耗时以数千纳秒为单位,磁盘访问(无论是固态硬盘还是旋转磁盘)耗时以数十毫秒为单位,而网络访问耗时从同一数据中心内的数百微秒到跨洲际消息的 100 毫秒以上不等。在为系统设计性能时,必须考虑时间因素。

当事件发生时(中断、消息、来自用户或其他系统的请求,或者标记时间流逝的时钟事件),系统或系统的某个组件必须及时做出响应。描述可能发生的事件(以及何时发生),以及系统或组件对这些事件基于时间的响应,这就是讨论性能的关键所在。

基于 Web 的系统事件以用户通过诸如 Web 浏览器之类的客户端发出的请求形式出现(数量可达数万甚至数千万)。服务会从其他服务接收事件。在内燃机控制系统中,事件来自操作员的控制操作以及时间的流逝;系统必须控制气缸处于正确位置时的点火时机,以及燃料的混合比例,以实现功率和效率最大化并减少污染。

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

在软件工程发展的大部分历程中,由于早期计算机速度慢且成本高,要执行的任务远远超出了计算机的处理能力,性能一直是架构设计的驱动因素。因此,性能常常以牺牲其他所有质量属性为代价。随着硬件性价比的不断下降以及软件开发成本的持续上升,其他质量属性已成为与性能同等重要的考量因素。

但性能仍然至关重要。仍然存在(并且可能永远存在)一些重要问题,我们知道如何用计算机解决这些问题,但却无法快速解决到足以让其发挥作用的程度。

所有系统都有性能要求,即使这些要求没有明确表述出来。例如,文字处理工具可能没有任何明确的性能要求,但毫无疑问,你会认同在屏幕上看到输入字符出现之前等待一个小时(或者一分钟、一秒钟)是不可接受的。性能对于所有软件而言,始终是一个至关重要的质量属性。

性能通常与可扩展性相关联 —— 也就是说,在保持良好性能的同时,提高系统的工作处理能力。它们确实相互关联,尽管从技术上讲,可扩展性是指让系统易于以特定方式进行更改,因此它属于一种可修改性,正如 第 8 章 所讨论的那样。此外,第 17 章 会明确讨论云端服务的可扩展性。

通常,性能提升是在构建出系统版本并发现其性能不足之后才进行的。你可以通过在架构设计时就考虑性能因素来避免这种情况。例如,如果你在设计系统时采用了可扩展的资源池,随后从监测数据中确定该资源池成为了瓶颈,那么你可以轻松扩大资源池的规模。否则,你的选择就会很有限(而且大多都不理想)并且可能需要大量返工。

将大量时间花在优化仅占总时间很小比例的系统部分上是没有意义的。通过记录时间信息来监测系统,将有助于你确定实际时间花在了哪里,从而让你专注于提升系统关键部分的性能。

9.1 性能通用场景

性能场景始于一个事件到达系统。要正确响应该事件,需要消耗资源(包括时间)。在此过程中,系统可能同时在处理其他事件。

并发

并发是架构师必须理解的较为重要的概念之一,但在计算机科学课程中却鲜少讲授。并发指的是并行发生的操作。例如,假设有一个线程执行某些语句,同时另一个线程也执行相同的语句。那么两个线程都执行完这些语句后,x 的值是多少呢?可能是 2,也可能是 3。至于值为 3 是如何出现的,就留给你去思考啦 —— 或者我该说,把这个问题穿插留给你思考?

x = 1;
x++;

只要系统创建一个新线程,就会出现并发情况,因为根据定义,线程是独立的控制序列。系统中的多任务处理是由独立线程支持的。通过使用线程,系统可以同时支持多个用户。只要系统在多个处理器上执行,无论这些处理器是独立封装还是多核处理器,都会出现并发。此外,当你使用并行算法、诸如 MapReduce 这样的并行化基础架构、NoSQL 数据库,或者使用各种并发调度算法中的某一种时,都必须考虑并发问题。换句话说,并发以多种方式为你所用。

当你拥有多个 CPU 或者可以利用的等待状态时,并发是有益的。允许操作并行发生可以提高性能,因为一个线程中出现的延迟能让处理器在另一个线程上继续推进。但是,由于刚才描述的交错现象(称为 “竞态条件”),并发也必须谨慎管理。

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

竞态条件是最难发现的错误类型之一;这类错误的出现具有偶然性,并且取决于(可能很微小的)时间差异。我曾经在一个操作系统中遇到过一个竞态条件,却一直无法追踪到原因。我在代码中添加了一个测试,以便下次竞态条件出现时,能触发调试过程。结果过了一年多这个错误才再次出现,从而得以确定原因。

不要因为并发相关的困难就放弃使用这一非常重要的技术。只要在使用时清楚,你必须仔细识别代码中的关键部分,并确保(或采取措施确保)这些部分不会出现竞态条件。

—LB

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

表 9.1 性能通用场景

场景部分 描述 可能的值
来源 触发可能来自一个用户(或多个用户)、外部系统,或者正在研究的系统的某个部分。 外部:
<li>用户请求
<li>来自外部系统的请求
<li>来自传感器或其他系统的数据
内部:
<li>一个组件可能会向另一个组件发出请求。
<li>计时器可能会生成通知。
触发事件 触发是事件的发生。该事件可以是服务请求,也可以是所考虑系统或外部系统某种状态的通知。 周期性、偶发性或随机性事件的发生:
<li>周期性事件按照可预测的时间间隔发生。
<li>随机性事件依据某种概率分布发生。
<li>偶发性事件按照既非周期性也非随机性的模式发生。
构件 被触发的构件可能是整个系统,也可能只是系统的一部分。例如,开机事件可能会触发整个系统。用户请求可能会到达(触发)用户界面。 <li>整个系统
<li>系统内的组件
环境 触发到来时系统或组件的状态。异常模式(如错误模式、过载模式)会影响响应。例如,设备在被锁定前允许三次不成功的登录尝试。 运行时。系统或组件可能处于以下运行模式:
<li>正常模式
<li>紧急模式
<li>纠错模式
<li>峰值负载模式
<li>过载模式
<li>降级运行模式
<li>系统的其他某种定义模式
响应 系统将处理该触发事件。处理触发事件需要花费时间。这一时间可能用于计算,也可能是因为处理过程因共享资源的竞争而受阻。由于系统过载或处理链中某个环节出现故障,请求可能无法得到满足。 <li>系统返回响应
<li>系统返回错误
<li>系统无响应
<li>若系统过载则忽略请求
<li>系统更改服务模式或级别
<li>系统处理更高优先级的事件
<li>系统消耗资源
响应度量 时间度量指标可以包括延迟或吞吐量。有时间期限要求的系统还可以衡量响应的抖动情况以及是否能够满足期限要求。统计有多少请求未得到满足,这也是一种度量方式,计算资源(如CPU、内存、线程池、缓冲区)的利用率同样属于度量范畴。 <li>响应所花费的(最长、最短、平均、中位)时间(延迟)
<li>在某个时间间隔内或在接收到的一组事件中,得到满足的请求数量或占比(吞吐量)
<li>未得到满足的请求数量或占比
<li>响应时间的变化(抖动)
<li>计算资源的使用水平

图9.1 给出了一个具体的性能场景示例:“在正常运行情况下,500名用户在30秒的时间间隔内发起2000个请求。系统处理所有请求的平均延迟为2秒。”

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 是一种 “系统或组件将以 Y 的响应时间处理每单位时间内到达的 X 个事件” 形式的协议。该协议既约束了系统(它必须提供相应响应),也约束了客户端(如果客户端每单位时间发出超过 X 个请求,则无法保证响应)。因此,从客户端的角度来看,如果每单位时间需要处理超过 X 个请求,就必须使用处理请求元素的多个实例。SLA 是管理基于互联网系统可扩展性的一种方法。
    • 管理采样率:在系统无法维持足够响应水平的情况下,可以降低触发事件的采样频率,例如降低从传感器接收数据的速率或每秒处理的视频帧数。当然,这样做的代价是视频流的保真度或从传感器数据中收集到的信息的准确性。不过,如果结果 “足够好”,这就是一种可行的策略。这种方法常用于信号处理系统,例如,可以选择具有不同采样率和数据格式的不同编码方式。这种设计选择旨在维持可预测的延迟水平;你必须决定较低保真度但稳定的数据流,是否优于延迟不稳定的数据流。一些系统会根据延迟测量结果或准确性需求动态管理采样率。
  • 限制事件响应:当离散事件到达系统(或组件)的速度过快而无法处理时,必须将这些事件排队,直到可以处理,或者直接丢弃。你可以选择仅以设定的最大速率处理事件,从而确保对实际处理的事件进行可预测的处理。此策略可以由队列大小或处理器利用率超过某个警告级别触发。或者,也可以由违反 SLA 的事件速率触发。如果你采用此策略,且不允许丢失任何事件,那么必须确保队列足够大,以处理最坏的情况。相反,如果你选择丢弃事件,那么需要选择一种策略:是记录丢弃的事件还是直接忽略?是否通知其他系统、用户或管理员?
  • 对事件进行优先级排序:如果并非所有事件都同等重要,可以实施一种优先级方案,根据处理事件的重要程度对事件进行排序。如果事件发生时没有足够的资源来处理它们,低优先级事件可能会被忽略。与始终处理所有事件的系统相比,忽略事件消耗的资源(包括时间)极少,从而提高了性能。例如,建筑管理系统可能会发出各种警报。诸如火灾警报等危及生命的警报应比诸如房间温度过低等信息性警报具有更高的优先级。
  • 降低计算开销:对于进入系统的事件,可以采用以下方法来减少处理每个事件所涉及的工作量:
    • 减少间接层次:正如我们在 第 8 章 中看到的,使用中介(这对可修改性非常重要)会增加处理事件流的计算开销,因此去除中介可以减少延迟。这是经典的可修改性与性能的权衡。关注点分离(可修改性的另一个关键因素),如果导致事件由一系列组件而非单个组件处理,也会增加处理事件所需的处理开销。不过,你或许能够两全其美:巧妙的代码优化可以让你在编程时使用支持封装的中介和接口(从而保持可修改性),但在运行时减少甚至在某些情况下消除代价高昂的间接层次。同样,一些代理允许客户端和服务器之间直接通信(在最初通过代理建立关系之后),从而消除所有后续请求的间接步骤。
    • 将通信资源置于同一位置:上下文切换和组件间通信成本会不断累积,尤其是当组件位于网络上的不同节点时。降低计算开销的一种策略是将资源置于同一位置。置于同一位置可能意味着将协同工作的组件托管在同一处理器上,以避免网络通信的时间延迟;也可能意味着将资源置于同一运行时软件组件中,以避免子程序调用的开销;或者意味着将多层架构的各层放置在数据中心的同一机架上。
    • 定期清理:降低计算开销的一个特殊情况是定期清理效率变低的资源。例如,哈希表和虚拟内存映射可能需要重新计算和重新初始化。许多系统管理员甚至普通计算机用户定期重启系统,正是出于这个原因。
  • 限制执行时间:可以对用于响应事件的执行时间设置限制。对于迭代的、依赖数据的算法,限制迭代次数是限制执行时间的一种方法。然而,代价通常是计算结果的准确性降低。如果你采用此策略,需要评估其对准确性的影响,看看结果是否 “足够好”。这种资源管理策略通常与管理采样率策略结合使用。
  • 提高资源使用效率:提高关键领域所使用算法的效率可以减少延迟,提高吞吐量并改善资源消耗。对于一些程序员来说,这是他们的主要性能策略。如果系统性能不佳,他们会尝试 “优化” 处理逻辑。如你所见,这种方法实际上只是众多可用策略之一。

管理资源

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

  • 增加资源:更快的处理器、额外的处理器、更多的内存和更快的网络都有可能提高性能。在选择资源时,成本通常是一个需要考虑的因素,但在许多情况下,增加资源是实现即时性能提升最经济的方式。
  • 引入并发:如果请求可以并行处理,阻塞时间就可以减少。可以通过在不同线程上处理不同的事件流,或创建额外的线程来处理不同的活动集来引入并发。(引入并发后,可以使用调度资源策略选择调度策略,以实现你期望的目标。)
  • 维护计算的多个副本:这种策略减少了如果将所有服务请求都分配给单个实例时可能发生的争用。微服务架构中的复制服务或服务器池中的复制 Web 服务器就是计算副本的示例。负载均衡器是一种软件,它将新工作分配给可用的重复服务器之一;分配标准各不相同,但可以简单到采用轮询方案,或将下一个请求分配给最不繁忙的服务器。负载均衡器模式将在 9.4 节 中详细讨论。
  • 维护数据的多个副本:维护数据多个副本的两个常见示例是数据复制和缓存。“数据复制” 是指保留数据的单独副本,以减少多个同时访问造成的争用。由于复制的数据通常是现有数据的副本,因此系统必须承担保持副本一致性和同步的责任。“缓存” 也涉及保留数据副本(其中一组数据可能是另一组数据的子集),但存储在具有不同访问速度的存储设备上。不同的访问速度可能是由于内存速度与二级存储速度的差异,或者本地与远程通信速度的差异造成的。缓存的另一个任务是选择要缓存的数据。一些缓存只是简单地保留最近请求的数据副本,但也可以根据行为模式预测用户未来的请求,并在用户提出请求之前开始进行必要的计算或预取。
  • 限制队列大小:这种策略控制排队到达的最大数量,从而控制用于处理到达请求的资源。如果你采用此策略,需要制定队列溢出时的处理策略,并确定不响应丢失的事件是否可以接受。此策略通常与限制事件响应策略结合使用。
  • 调度资源:每当发生对资源的争用时,就必须对资源进行调度。处理器需要调度,缓冲区需要调度,网络也需要调度。作为架构师,你需要了解每种资源使用的特点,并选择与之相匹配的调度策略。(请参阅 “调度策略” 侧边栏。)

图 9.3 总结了性能策略。

A flowchart of the performance tactics.

图9.3 性能策略

调度策略

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

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

  • 先进先出:先进先出队列将所有资源请求平等对待,并依次满足它们。先进先出队列存在一种可能性,即一个请求可能会被排在另一个生成响应耗时较长的请求之后。只要所有请求确实同等重要,这就不是问题,但如果某些请求的优先级高于其他请求,就会带来挑战。

  • 固定优先级调度:固定优先级调度为每个资源请求源分配特定的优先级,并按照该优先级顺序分配资源。这种策略确保为高优先级请求提供更好的服务。然而,它也存在一种可能性,即低优先级但仍然重要的请求可能会等待任意长的时间才能得到处理,因为它被排在一系列高优先级请求之后。以下是三种常见的优先级确定策略:

    • 语义重要性:语义重要性根据生成任务的某些领域特性静态地分配优先级。
    • 截止期限单调调度:截止期限单调调度是一种静态优先级分配方式,为截止期限较短的流分配更高的优先级。这种调度策略用于调度具有实时截止期限的不同优先级的流。
    • 速率单调调度:速率单调调度是针对周期性流的一种静态优先级分配方式,为周期较短的流分配更高的优先级。这种调度策略是截止期限单调调度的一种特殊情况,但更为人熟知,且更有可能得到操作系统的支持。
  • 动态优先级调度:策略包括以下几种:

    • 轮询调度:轮询调度策略对请求进行排序,然后在每次有分配机会时,按照该顺序将资源分配给下一个请求。轮询调度的一种特殊形式是循环执行,即可能的分配时间以固定的时间间隔指定。
    • 最早截止期限优先调度:最早截止期限优先调度根据截止期限最早的待处理请求来分配优先级。
    • 最少松弛时间优先调度:这种策略将最高优先级分配给 “松弛时间” 最少的任务,松弛时间是指剩余执行时间与任务截止期限之间的差值。

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

  • 静态调度:循环执行调度是一种调度策略,其中抢占点和资源分配顺序是离线确定的。这样就避免了调度程序的运行时开销。

实际应用中的性能策略

策略是通用的设计原则。为了说明这一点,思考一下你所在地区的道路和高速公路系统的设计。交通工程师采用了一系列设计 “技巧” 来优化这些复杂系统的性能,性能有多种衡量指标,例如吞吐量(每小时有多少辆车从郊区到达足球场)、平均延迟(从你家到市中心平均需要多长时间)以及最坏情况下的延迟(急救车辆将你送到医院需要多长时间)。这些技巧是什么呢?正是我们熟知的策略。

让我们来看一些例子:

  • 管理事件速率:高速公路入口匝道的信号灯只在设定的时间间隔允许车辆驶入高速公路,车辆必须在匝道上等待(排队)轮到自己。
  • 对事件进行优先级排序:开着警灯、鸣着警笛的救护车和警车比普通市民车辆具有更高的优先级;一些高速公路设有高承载车辆(HOV)车道,优先让载有两名或更多乘客的车辆通行。
  • 维护多个副本:在现有道路上增加车道或修建平行路线。

此外,系统的使用者也可以采用他们自己的技巧:

  • 增加资源:例如购买一辆法拉利。在其他条件相同的情况下,在空旷道路上,一辆由熟练司机驾驶的最快的车能让你更快到达目的地。
  • 提高效率:找到一条比当前路线更快和 / 或更短的新路线。
  • 降低计算开销:跟紧前面的车,或者让更多人乘坐同一辆车(即拼车)。

这段讨论的意义何在呢?套用格特鲁德・斯泰因(Gertrude Stein)的话来说:性能就是性能,无论在何种场景下。几个世纪以来,工程师们一直在分析和优化复杂系统,试图提高它们的性能,并且他们一直在采用相同的设计策略来实现这一目标。所以,当你试图提高基于计算机的系统的性能时,你应该感到欣慰,因为你所应用的策略已经过充分的 “实际检验”。

—RK

9.3 基于策略的性能调查问卷

基于 9.2 节 中描述的策略,我们可以设计一组受策略启发的问题,如 表 9.2 所示。为全面了解为支持性能而做出的架构选择,分析师需提出每个问题,并将答案记录在表格中。这些问题的答案随后可成为进一步活动的重点,如文档调研、代码或其他构件分析、代码逆向工程等。

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

策略组 策略问题 支持与否(是/否) 风险 设计决策和定位 推理和假设
控制资源需求 是否制定了服务水平协议(SLA),明确规定愿意支持的最大事件到达速率?        
  能否管理到达系统的事件的采样速率        
  系统将如何限制对事件的响应(处理量)?        
  是否定义了不同类别的请求,并为每个类别定义了优先级        
  例如,你能否通过资源同置、清理资源或减少间接层次等方式来降低计算开销        
  能对算法的执行时间进行限制吗?        
  能否通过选择算法来提高计算效率        
管理资源 能否为系统或其组件分配更多资源        
  是否在使用并发?如果请求能够并行处理,阻塞时间就可以减少。        
  计算操作能否在不同处理器上进行复制        
  数据能否进行缓存(保存一份可快速访问的本地副本)或复制(以减少争用)?        
  能否对队列大小进行限制,从而为处理触发所需的资源设置上限?        
  是否已确保所采用的调度策略与对性能的考量相适配?        

9.4 性能模式

几十年来,性能问题一直困扰着软件工程师,因此,针对性能的各个方面管理开发出大量模式也就不足为奇了。在本节中,我们仅介绍其中几种。请注意,有些模式具有多种用途。例如,我们在 第 4 章 中看到了断路器模式,当时它被视为一种可用性模式,但它对性能也有益处,因为它减少了等待无响应服务的时间。

我们在此介绍的模式有服务网格、负载均衡器、限流和 Map-Reduce。

服务网格

服务网格模式用于微服务架构。网格的主要特点是边车,这是一种伴随每个微服务的代理,它提供广泛有用的功能,以解决与应用程序无关的问题,如服务间通信、监控和安全。边车与每个微服务并行执行,处理所有服务间的通信与协调。(正如我们将在 第 16 章 中所述,这些元素通常被打包到 “容器组” 中。)它们一起部署,减少了网络带来的延迟,从而提升性能。

这种方法使开发人员能够将微服务的功能(核心业务逻辑)与诸如身份验证和授权、服务发现、负载均衡、加密和可观测性等跨领域问题的实现、管理及维护分离开来。

Benefits:

优点

  • 用于管理跨领域问题的软件可以购买现成的,或者由专门的团队来实现和维护,这样业务逻辑的开发人员就可以只专注于业务逻辑。
  • 服务网格强制将实用功能部署到与使用这些实用功能的服务相同的处理器上。由于通信无需使用网络消息,这减少了服务与其实用功能之间的通信时间。
  • 服务网格可以配置为使通信依赖于上下文,从而简化了如 第 3 章 中所述的金丝雀(Canary)测试和 A/B 测试等功能。

权衡:

  • 边车引入了更多的执行进程,每个进程都会消耗一定的处理能力,增加了系统开销。
  • 边车通常包含多种功能,但并非每个服务或每次服务调用都需要所有这些功能。

负载均衡

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

优点

  • 服务器的任何失效对客户端来说都是不可见的(假设仍有一些剩余处理资源)。
  • 通过在多个提供者之间分担负载,可以为客户端保持更低且更可预测的延迟。
  • 相对容易向负载均衡器可用的池中添加更多资源(更多服务器、更快的服务器),且无需客户端知晓。

权衡:

  • 负载均衡算法必须非常快,否则它本身可能会导致性能问题。
  • 负载均衡器是一个潜在的瓶颈或单点故障,因此它本身通常也会被复制(甚至进行负载均衡)。

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

限流

限流模式是管理工作请求策略的一种封装。它用于限制对某些重要资源或服务的访问。在这种模式中,通常有一个中介(限流器),它监控(对服务的)请求,并确定传入的请求是否可以得到处理。

优点

  • 通过限制传入请求,你可以优雅地处理需求的变化。这样,服务永远不会过载,可以保持在一个性能 “最佳点”,高效地处理请求。

权衡:

  • 限流逻辑必须非常快,否则它本身可能会导致性能问题。
  • 如果客户端需求经常超过容量,缓冲区就需要非常大,否则就有丢失请求的风险。
  • 在客户端和服务器紧密耦合的现有系统中,添加此模式可能会很困难。

Map-Reduce

Map-Reduce 模式可高效地对大型数据集进行分布式并行排序,并为程序员提供一种简单的方式来指定要进行的分析。与我们介绍的其他与应用程序无关的性能模式不同,Map-Reduce 模式是专门为解决一类特定的常见问题(对大型数据集进行排序和分析)而设计,以实现高性能。任何处理海量数据的组织,如谷歌、脸书、雅虎和网飞,都会遇到这个问题,而这些组织实际上都在使用 Map-Reduce。

Map-Reduce 模式有三个部分:

  • 首先是一个专门的基础设施,它负责在大规模并行计算环境中将软件分配到硬件节点,并根据需要处理数据排序。一个节点可以是虚拟机、独立处理器或多核芯片中的一个核心。
  • 第二和第三部分是两个由程序员编写的函数,显然,分别称为 “映射(map)” 和 “归约(reduce)”。
    • 映射” 函数以一个键和一个数据集作为输入。它使用该键将数据哈希到一组桶中。例如,如果我们的数据集由扑克牌组成,键可以是花色。映射函数还用于过滤数据,即确定一条数据记录是参与进一步处理还是被丢弃。继续以扑克牌为例,我们可能选择丢弃大小王或字母牌(A、K、Q、J),只保留数字牌,然后根据花色将每张牌映射到一个桶中。通过拥有多个映射实例,每个实例处理数据集的不同部分,可提高 Map-Reduce 模式映射阶段的性能。一个输入文件被分成多个部分,并创建多个映射实例来处理每个部分。继续我们的例子,假设我们有 10 亿张扑克牌,而不只是一副牌。由于每张牌可以单独检查,映射过程可以由成千上万的实例并行执行,它们之间无需通信。一旦所有输入数据都被映射,这些桶将由 Map-Reduce 基础设施进行混洗,然后分配给新的处理节点(可能会重用映射阶段使用的节点)进行归约阶段。例如,所有梅花牌可以分配给一个实例集群,所有方块牌分配给另一个集群,依此类推。
    • 所有繁重的分析都在 “归约” 函数中进行。归约实例的数量与映射函数输出的桶的数量相对应。归约阶段进行一些由程序员指定的分析,然后输出分析结果。例如,我们可以统计梅花、方块、红桃和黑桃的数量,或者我们可以对每个桶中所有牌的数值进行求和。输出集几乎总是比输入集小得多,这就是 “归约” 这个名字的由来。

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

优点

  • 通过利用并行性,可以高效地分析极其庞大的未排序数据集。
  • 任何实例的故障对处理的影响都很小,因为 Map-Reduce 通常会将大型输入数据集分解为许多较小的数据集进行处理,每个数据集分配给其自己的实例。

权衡:

  • 如果没有大型数据集,Map-Reduce 模式带来的开销就不合理。
  • 如果不能将数据集划分为大小相似的子集,并行性的优势就会丧失。
  • 需要多次归约的操作编排起来很复杂。

9.5 扩展阅读

性能是众多文献探讨的主题。以下是我们推荐的几本关于性能的综合性概述书籍:

  • 《软件与系统性能工程基础:流程、性能建模、需求、测试、可扩展性及实践》[Bondi 14]。本书全面概述了性能工程,涵盖从技术实践到组织层面的内容。
  • 《软件性能与可扩展性:量化方法》[Liu 09]。本书聚焦于面向企业应用的性能,重点讲解排队论和度量。
  • 《性能解决方案:创建响应迅速、可扩展软件的实用指南》[Smith 01]。本书介绍了以性能为导向的设计,重点在于构建(并用真实数据填充)实用的预测性性能模型。

若要全面了解众多性能模式,可参阅《实时设计模式:实时系统的稳健可扩展架构》[Douglass 99] 以及《面向模式的软件架构 第 3 卷:资源管理模式》[Kircher 03]。此外,微软发布了基于云应用的性能和可扩展性模式目录:https://docs.microsoft.com/en-us/azure/architecture/patterns/category/performance-scalability 。

9.6 问题讨论

1. “每个系统都有实时性能约束。” 对此展开讨论。你能给出反例吗?

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

3. 为一个在线拍卖网站编写几个性能场景。思考你主要关注的是最坏情况延迟、平均情况延迟、吞吐量,还是其他某种响应指标。你会使用哪些策略来满足这些场景?

4. 基于 Web 的系统常使用代理服务器,它是系统中第一个接收来自客户端(比如你的浏览器)请求的组件。代理服务器能够提供经常被请求的网页,如公司主页,而无需打扰执行交易的实际应用服务器。一个系统可能包含多个代理服务器,并且它们通常设置在地理位置上靠近大型用户群体的地方,以减少常规请求的响应时间。你认为这里运用了哪些性能策略?

5. 交互机制的一个根本区别在于交互是同步还是异步。针对延迟、截止期限、吞吐量、抖动、失误率、数据丢失或你熟悉的其他任何与性能相关的响应,分别讨论同步和异步交互的优缺点。

6. 找出应用每种管理资源策略的现实世界(非软件)示例。例如,假设你在经营一家实体大型零售商店。你会如何运用这些策略让顾客更快地通过收银台?

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