在设计服务时,可以为其加入各种内置的可靠性和弹性,但为了在实践当中保持可靠性,它们还必须能够及时处理可预测的故障。因为硬件最终都会废弃,所以在 Amazon,我们将服务构建为可水平扩展且可实现冗余。任何硬盘驱动器都有最高预期使用寿命,任何软件都可能在某个时刻崩溃。服务器的运行状况似乎就应该是两种状态:要么正常工作,要么根本无法正常工作,但不会影响其他方面。遗憾的是,事实并非如此。我们发现,发生故障的服务器不仅会关闭,还可能对系统造成不可预测的损害,这种损害有时甚至与发生故障的服务器不成比例。运行状况检查可以自动检测并响应此类问题。

本文将介绍我们如何使用运行状况检查来检测和处理单服务器故障、不执行运行状况检查时会发生的情况,以及对运行状况检查故障反应过度的系统可能会如何将小问题转变成全面中断。我们还将根据我们在 Amazon 的工作经验,提供有关在各种运行状况检查实施之间平衡权衡的见解。

小故障,大影响

在我刚来到 Amazon 担任软件开发人员时,曾经负责 Amazon.com 背后的网站渲染队列的相关工作。我曾经有一次非常遗憾的经历。当时我们在进行更改来添加一些检测步骤并了解该软件的运行状况,我编写的内容里出现了错误,非常遗憾。这个错误很少触发,但是一旦触发,就会导致给定的 Web 服务器在每次请求时都会渲染空白错误页面。只有重新启动 Web 服务器进程才能解决此问题。我们检测到了这个错误并迅速回滚了更改,添加了很多测试,并改进了流程以便能在以后捕获到此类情况。但是,在这个错误进入生产环境中时,大型服务器队列中的一些服务器会进入这种故障状态。
 
导致这个错误特别棘手的一个问题在于,服务器本身并未意识到自己存在运行状况问题。而且,服务器无法将其运行状况报告给监控系统,因此它不会自动退出服务状态,也不会触发常规警报。更糟糕的是,服务器的速度变得非常快,开始产生空白错误页面,而且速度要比其同队列中“运行状况良好的服务器”渲染正常网页的速度要快得多。我们当时使用的负载均衡技术优先选择速度较快的服务器,这就导致有过多的流量定向到了运行状况不佳的服务器,进一步扩大了影响。

由于监控涉及到衡量系统中多个点的错误率和延迟,因此触发了其他一些警报。尽管这些类型的监控系统和操作进程可以用作控制问题的“防护网”,但正确的运行状况检查可以通过快速检测故障并采取相应行动,极大地降低此类错误的影响。

运行状况检查的权衡

运行状况检查是一种询问特定服务器上的服务能否成功执行工作的方法。负载均衡器会定期向每个服务器询问此问题,以确定可以安全将流量定向到哪些服务器。从队列中轮询消息的服务可能会先询问自己是否运行状况良好,然后再决定是否从队列中轮询更多工作。监控代理(在每台服务器上运行或在外部监控队列上运行)可能会询问服务器是否运行状况良好,以便它们确定是发出警报还是自动处理发生故障的服务器。

正如我的网站错误示例中所示,当运行不佳的服务器处于运行状态时,会导致服务整体可用性大幅度降低。对于一个包含十台服务器的队列,一台服务器出故障就意味着该队列的可用性为 90% 或更低。更糟糕的是,某些负载均衡算法(例如“最少请求”)会将更多工作分配给速度最快的服务器。当服务器发生故障时,它通常会开始迅速导致请求失败,因此会吸引比运行状况良好的服务器更多的请求,从而在服务队列中形成“黑洞”。在某些情况下,我们会降低失败请求的速度,使之匹配成功请求的平均延迟,通过这种方式增加额外的保护,防止出现此类“黑洞”。但在其他情况下(例如使用队列轮询器时),这种问题较难解决。例如,如果队列轮询器以最快的接收速度轮询消息,那么发生故障的服务器也会成为一个“黑洞”。在用于分配工作的环境如此多样化的情况下,我们考虑用于保护部分出故障的服务器的方式因系统而异。

我们发现,服务器会出于多种原因单独出故障,包括磁盘不可写入而导致请求立即失败、时钟突然发生偏差导致对依赖关系的调用无法通过身份验证、服务器无法检索更新的加密材料并导致解密和加密失败、关键的支持性进程因其自身的错误和内存泄漏以及冻结处理的死锁而崩溃。

服务器也会出于相关原因而失败,从而导致一个队列中的大量服务器或所有服务器一同出现故障。相关原因包括共享依赖关系中断和大规模网络问题。理想的运行状况检查将测试服务器和应用程序运行状况的各个方面,甚至可能会验证非关键支持进程是否正在运行。但是,如果运行状况检查由于非关键原因而失败,并且该失败情况在多台服务器上发生,那么就会出问题。如果在服务器仍然可以执行有用的工作时,自动化机制将其从服务中移除了,则自动化机制的弊大于利。

运行状况检查的难题在于:一方面,执行全面的运行状况检查的好处与迅速缓解单一服务器故障后果的好处之间存在矛盾关系;另一方面,误报故障会给整个队列造成损害。因此,建立良好的运行状况检查所面临的挑战之一就是要谨防误报。通常,这意味着以运行状况检查为中心的自动化机制应该停止将流量定向到单个故障服务器,但是如果整个队列都遇到问题,则应该继续允许流量通过。

衡量运行状况的方法

服务器上可能发生很多情况,我们的系统中有很多地方可以用来衡量服务器的运行状况。某些运行状况检查可以明确地报告特定的服务器已发生独立的故障,而其他运行状况检查则要更加含糊不清,如果存在关联故障,可能会出现误报。有些运行状况检查的实施难度较大。其他运行状况检查是在设置时使用 Amazon Elastic Compute Cloud (Amazon EC2) 和 Elastic Load Balancing 等服务实现的。每种类型的运行状况检查都有其自身的优势。

存活检查

存活检查会测试与服务的基本连接以及服务器进程的存在与否。它们通常由负载均衡器或外部监控代理执行,并且不了解应用程序的具体工作方式。存活检查通常包含在服务中,并且不需要应用程序作者进行任何实施。下面是 Amazon 使用的一些存活检查示例:

• 确认服务器正在侦听其预期端口并接受新的 TCP 连接的测试。
• 执行基本 HTTP 请求并确保服务器以 200 状态码做出响应的测试。
• Amazon EC2 的状态检查,测试任何系统正常运行的基本条件,例如网络可访问性。

本地运行状况检查

本地运行状况检查比存活检查要更进一步,会验证应用程序是否能够正常工作。这些运行状况检查将测试未与服务器同队列中其他服务器共享的资源。因此,这些检查不太可能同时在队列中的大量服务器上失败。这些运行状况检查将测试以下各项:

• 无法写入磁盘或从磁盘读取 – 可能会倾向于认为无状态服务不需要可写入的磁盘。但是,Amazon 的服务倾向于将其磁盘用于监控、日志记录和发布异步度量数据之类的任务。
• 关键进程崩溃或中断 – 部分服务使用服务器上的代理(类似于 NGINX)接收请求,并在另一个服务器进程中执行其业务逻辑。存活检查可能仅测试代理进程是否正在运行。本地运行状况检查过程可能会从代理传递到应用程序,以检查它们是否都正在运行并能正确响应请求。有趣的是,在本文开头的网站示例中,现有的运行状况检查足够深入,足以确保渲染进程正在运行并正常响应,但并不足以确保其正确响应。
• 缺少支持进程 – 缺少监控守护程序的主机可能会导致运营商“盲目行动”,完全不了解其服务的运行状况。其他支持进程则用于推送度量和计费使用记录或接收凭证更新。存在中断的支持进程的服务器会以不易察觉的、难以检测的方式造成无法正常工作的风险。

依赖关系运行状况检查

依赖关系运行状况检查是对应用程序与其相邻系统交互的能力执行的彻底检查。理想情况下,这些检查可以捕获服务器本地的问题(例如过期的凭证等),这些问题可能妨碍服务器与依赖关系进行交互。但是,当依赖关系本身存在问题时,这些检查也可能会误报。由于这些误报,我们必须谨慎应对依赖关系运行状况检查失败的情况。依赖关系运行状况检查可能会测试以下各项:

• 错误的配置或过时的元数据 – 如果某个进程异步寻找对元数据或配置的更新,但是服务器上的更新机制失灵,则该服务器可能会与其同队列的其他服务器严重不同步,并且会以无法预测、未经测试的方式发生行为失常。但是,如果服务器有一段时间未看到任何更新,它就无法确定究竟是更新机制失灵,还是中央更新系统停止向所有服务器发布更新。
• 无法与同队列的其他服务器或依赖关系进行通信 – 众所周知,奇怪的网络行为会影响队列中的服务器子集与依赖关系进行通信的能力,但不会影响将流量发送到该服务器的能力。软件问题(例如死锁或连接池中的错误)也可能会阻碍正常网络通信。
• 其他需要进程反弹的非正常软件错误 – 死锁、内存泄漏或状态损坏错误会导致服务器发出错误。 

异常检测

异常检测会检查队列中的所有服务器,以确定是否有任何服务器与其同队列的其他服务器相比表现异常。通过汇总各服务器的监控数据,我们可以不断地比较错误率、延迟数据或其他属性,以查找存在异常的服务器并自动将其从服务中移除。异常检测可以发现队列中某台服务器自身无法检测到的差异,例如:

• 时钟偏差 – 特别是在服务器处于高负载状态下时,已知它们的时钟会突然且急剧地发生偏差。安全度量(例如用于评估对 AWS 发出的签名请求的度量)要求客户端时钟上的时间与实际时间的偏差在 5 分钟内。如果不是,则对 AWS 服务的请求将失败。
• 旧代码 – 如果服务器断开网络连接或在长时间断电后又恢复联网,则该服务器可能正在运行危险的过时代码,而该代码与队列中的其他服务器不兼容。
• 任何意外的失败模式 – 有时服务器失败时会返回错误,但会将这些错误标识为客户端的错误,而非其自身的错误(HTTP 400 而不是 500)。服务器可能会降速但不会出故障,或者它们的响应速度可能比同队列的其他服务器快,这表明它们正在向调用方返回错误的响应。对于意外的故障模式而言,异常检测发现的故障之多令人惊讶。

实际执行异常检测时只需满足很少的几项必要条件:

• 服务器应该采取大致相同的行为方式 – 如果我们将不同类型的流量显式路由到不同类型的服务器,那么这些服务器的行为的相似度可能不够高,不足以检测到异常值。但是,在我们使用负载均衡器将流量定向到服务器的情况下,它们可能以类似的方式做出响应。
• 队列应相对同构 – 在包含不同实例类型的队列中,某些实例的速度可能比其他实例慢,这会错误地触发被动的不良服务器检测。为了解决此问题,我们按实例类型整理指标。
• 必须报告错误或行为差异 – 由于我们依靠服务器本身来报告错误,因此如果它们的监控系统也发生了问题,那么会怎样? 幸运的是,服务的客户端是添加检测的好地方。诸如 Application Load Balancer 之类的负载均衡器会发布访问日志,其中会显示每个请求都联系了哪个后端服务器、响应时间以及该请求成功还是失败。 

对运行状况检查失败做出安全的反应

在一台服务器确定其运行状况不佳时,可以采取两种措施。在最极端的情况下,它可以在本地决定不应再接受任何工作,并通过使负载均衡器运行状况检查失败或停止轮询队列来使其自身停止提供服务。服务器可以做出的另一种反应方式是通知某个中央授权机构它存在问题,然后让中央系统决定如何处理该问题。中央系统可以安全地解决问题,避免任由自动化机制导致整个队列瘫痪。

可以通过多种方法实施和响应运行状况检查。本节介绍 Amazon 使用的一些模式。

失败时开放

一些负载均衡器可以充当智能中央授权机构。当某台服务器未通过运行状况检查时,负载均衡器将停止向其发送流量。但是,如果所有服务器同时都没有通过运行状况检查,负载均衡器将实施“失败时开放”机制,允许向所有服务器发送流量。我们可以使用负载均衡器来支持依赖关系运行状况检查的安全实施,比如,查询其数据库并进行检查以确保其非关键支持进程正在运行。

例如,如果没有服务器报告运行状况良好,则 AWS 网络负载均衡器会实施“失败时开放”机制。如果一个可用区中的所有服务器都报告运行状况不佳,则网络负载均衡器还将停止向该可用区发送流量。(有关使用网络负载均衡器进行运行状况检查的更多信息,请参阅 Elastic Load Balancing 文档。) 我们的 Application Load Balancer 和 Amazon Route 53 也支持“失败时开放”机制。(有关使用 Route 53 配置运行状况检查的更多信息,请参见 Route 53 文档。)

当我们依靠“失败时开放”行为时,务必测试依赖关系运行状况检查的故障模式。例如,假设有一项服务,它将服务器连接到某个共享数据存储。如果该数据存储速度变慢或响应的错误率较低,则服务器可能偶尔会使其依赖关系运行状况检查失败。这种情况会导致服务器进入和退出服务,但不会触发“失败时开放”阈值。使用这些运行状况检查来推理和测试依赖关系的部分故障意义重大,有助于避免发生故障可能导致深度运行状况检查进而使事情更加糟糕的情况。

虽然“失败时开放”是一种有益的行为,但在 Amazon,我们倾向于对无法在所有情况下进行充分推理或测试的事情持怀疑态度。我们尚未提出一般性的证明,来证实“失败时开放”机制会按照我们的预期针对系统或系统依赖关系中的所有类型的过载、部分故障或灰色故障而触发。由于这样的限制,Amazon 团队倾向于将其快速发挥作用的负载均衡器运行状况检查限制为在本地运行状况检查,并依靠集中式系统对更深层次的依赖关系运行状况检查进行谨慎回应。这并不是说我们不使用“失败时开放”行为机制,也不能证明它可以在特定情况下正常发挥效用。但是,如果某种逻辑能迅速在大量服务器上发挥作用,我们就会持高度谨慎的态度。

没有断路机制的运行状况检查

允许服务器对自己的问题做出反应似乎是最快、最简单的恢复途径。但是,如果服务器对自己的运行状况判断有误或搞不清楚整个队列的情况,那么这种途径风险也最高。当整个队列中的所有服务器同时做出相同的错误决策时,就可能导致故障在相邻服务之间级联往复。我们需要权衡这种风险。如果运行状况检查与监控结果之间存在差异,则服务器可能会降低服务可用性,直到检测到问题为止。但是,这种方案可避免由于整个队列发生运行状况检查意外行为导致服务完全中断的不良后果。

在没有内置断路机制的情况下,可以借鉴以下实施运行状况检查的最佳实践:

• 配置作业创建器(负载均衡器、队列轮询线程)以执行存活检查和本地运行状况检查。只有在遇到完全是服务器本地出现问题(例如磁盘损坏)的情况下,负载均衡器才会自动使服务器停止服务。
• 配置其他外部监控系统,执行依赖关系运行状况检查和异常检测。这些系统可能会尝试自动终止实例,或者向操作员发出警报或提示其干预。

当我们构建系统以对依赖关系运行状况检查失败自动做出反应时,我们必须构建适当数量的阈值,以防止自动化系统意外地采取过于激烈的措施。运行有状态服务器(如 Amazon DynamoDB、Amazon S3 和 Amazon Relational Database Service (Amazon RDS))的 Amazon 团队对服务器更换具有重要的持久性要求。他们还建立了谨慎的速率限制和控制反馈回路,以便在超过阈值时自动停止并要求人为干预。在构建这种自动化机制时,我们必须确保在服务器未通过依赖关系运行状况检查时,我们能了解相关情况。对于某些指标,我们依靠服务器将其各自的状态自行报告给中央监控系统。为了弥补服务器因故障而无法报告其运行状况的情况,我们还会主动访问服务器,以检查其运行状况。 

优先考虑运行状况

对于服务器而言,优先于其常规工作执行运行状况检查非常重要,尤其是在过载情况下更是如此。在这种情况下,运行状况检查失败或对运行状况检查响应缓慢会使原本糟糕的断流结果更糟糕。 

当服务器未通过负载均衡器运行状况检查时,即要求负载均衡器立即将其从服务中移除,并且相应服务器会停止服务相当长的时间。在单台服务器出现故障时,这不是问题,但是在服务收到的流量激增的情况下,我们最不想做的事情就是缩小服务规模。在过载期间使服务器停止服务可能会导致资源呈现螺旋式减少。这会迫使其余服务器接受更多流量,导致它们更有可能发生过载,并因此而无法通过运行状况检查,以至于进一步缩小队列规模。

问题并不在于过载的服务器在过载时会返回错误。而在于服务器没有及时响应负载均衡器的 ping 请求。毕竟,负载均衡器的运行状况检查配置有超时,就像其他任何远程服务调用一样。断流的服务器响应速度缓慢的原因是多方面的,其中包括 CPU 争用较高、垃圾收集器周期长,以及工作线程耗尽。服务需要配置为预留资源以便及时响应运行状况检查,而不是接受过多的额外请求。

幸运的是,我们只要遵循一些简单的配置最佳实践,就能帮助防止这种资源螺旋式减少的情况。iptables 之类的工具,甚至某些负载均衡器都支持“最大连接数”的概念。 在这种情况下,操作系统(或负载均衡器)会限制与服务器的连接数量,以使服务器进程不会被大量并发请求所淹没,这些大量并发请求会降低服务器的速度。

当服务被支持最大连接数的代理或负载均衡器作为前端时,使 HTTP 服务器上的工作线程数与代理中的最大连接数相匹配似乎是一种合乎逻辑的做法。但是,此配置将设置服务,以备应对断流期间资源螺旋式减少的问题。代理运行状况检查也需要连接,因此,使服务器的工作线程池足够大以容纳额外的运行状况检查请求非常重要。空闲工作线程成本低,因此我们倾向于配置额外的工作线程:从少量的额外工作线程,到多达已配置代理最大连接数一倍之多的额外工作线程不等。

我们用来优先处理运行状况检查的另一种策略,是让服务器自行强制实施最大并发请求数。在这种情况下,始终允许负载均衡器运行状况检查,但是如果服务器已经达到某个阈值,则正常请求将被拒绝。Amazon 的实施范围较为广泛,从 Java 中的简单信号量到 CPU 利用率趋势的更复杂分析。

帮助确保服务及时响应运行状况检查 ping 请求的另一种方法,是在后台线程中执行依赖关系运行状况检查逻辑,并更新 ping 逻辑检查的 isHealthy 标志。在这种情况下,服务器会迅速响应运行状况检查,而依赖关系运行状况检查会其在与之交互的外部系统上产生可预测的负载。在团队采用这种做法时,他们会在检测到某个失败的运行状况检查线程时格外谨慎。如果该后台线程退出,则服务器不会检测到未来的服务器故障(也不会检测到将来的恢复情况!)。

平衡依赖关系运行状况检查与影响范围

依赖关系运行状况检查很有吸引力,因为它们可以对服务器的运行状况进行彻底的测试。遗憾的是,它们也可能很危险,因为依赖关系可能导致整个系统中出现级联故障。

通过研究 Amazon 的面向服务的架构,我们可以得出有关处理运行状况检查依赖关系的一些见解。Amazon 的每项服务都旨在执行更少的任务,不存在承担多项重任的超大型服务。我们喜欢以这种方式构建服务的原因很多,其中包括:小型团队的创新速度更快,而且如果一项服务发生问题,则这种方式能够有效减小其影响范围。这种架构设计也可以应用于运行状况检查。

当一项服务调用另一项服务时,则前者依赖于后者。如果服务仅仅是有时调用依赖关系,则我们可能会将依赖关系视为“软依赖”,因为即使服务无法与依赖关系进行通信,它仍可以执行某些类型的工作。如果没有“失败时开放”保护机制,则实施测试依赖关系的运行状况检查会将该依赖关系转化为“硬依赖”。 如果依赖关系出问题,则服务也会中断,从而导致级联故障,影响范围增大。

即使我们将功能分为不同的服务,每个服务也可能服务于多个 API。有时,服务上的 API 具有自己的依赖关系。如果一个 API 受到影响,我们希望该服务能继续运行其他 API。例如,服务既可以是控制平面(例如在长期存在的资源上偶尔称为 CRUD API),也可以是数据平面(高吞吐量超关键业务 API)。我们希望数据平面 API 能够继续运行,即使控制平面 API 无法与其依赖关系进行通信时也是如此。

同样,根据数据的输入或状态,即使同一个 API 的行为也可能有所不同。一种常见的模式是读取 API,该 API 查询数据库,但会将响应缓存在本地一段时间。即便数据库关闭,该服务仍可以提供缓存的读取,直到数据库重新联机。如果只有一个代码路径运行状况不佳,则无法通过运行状况检查会扩大与依赖关系间通信问题的影响范围。

关于需要对哪些依赖关系执行运行状况检查的讨论提出了一个有趣的问题,即微服务与相对整体服务之间的权衡。很少有明确的规则可将服务划分为多少个可部署的单元或终端节点,但是像“需要对哪些依赖关系执行运行状况检查?”和“故障随后会扩大影响范围吗?”这样的问题不失为确定如何从微观或宏观角度上提供服务的有趣视角。 

运行状况检查中真正的错误

从理论上讲,所有这些都是有意义的,但在实际操作中,如果没有正确进行运行状况检查,系统会怎样? 我们从 AWS 客户和 Amazon 各区域的案例中寻找模式来了解全局。我们还研究了补偿因素,即团队为了防止运行状况检查中的弱点引起广泛问题而实施的“双保险”。

部署

运行状况检查问题的一种模式涉及部署。诸如 AWS CodeDeploy 之类的部署系统一次将新代码推送到队列的一个子集中,等待一轮部署完成,然后再进行下一轮部署。此过程依靠服务器在使用新代码启动并运行之后回报给部署系统。如果它们不回报信息,则部署系统认为新代码有问题,并会回滚部署。

最基本的服务启动部署脚本会简单地复刻 (fork) 服务器进程,并立即向部署系统发出“已完成部署”的响应。但这种做法非常危险,因为新代码可能会出各种错:新代码可能在启动后立即崩溃、挂起并且无法开始监听服务器套接字、无法加载成功处理请求所需的配置或遇到错误。如果未将部署系统配置为针对依赖关系运行状况检查进行测试,则它不会意识到推送了存在问题的部署。结果会导致服务器接连发生故障。

幸运的是,在实践中,Amazon 团队实施了多种缓解机制,以防止这种情况造成整个队列瘫痪。其中一项缓解机制就是配置警报,这些警报将在总队列规模过小、运行负载过高、高延迟或高错误率时触发。如果触发了这些警报中的任何一个,则部署系统将停止部署并回滚。

另一种缓解机制是使用阶段式部署方法。您不必在一次部署中完成整个队列的部署,而是可将服务配置为首先部署队列的一个子集(比如一个可用区),然后暂停部署,并针对该可用区运行全套集成测试。这种按可用性部署的协调方式非常方便,因为服务已经被设计为能够在单个可用区出现问题时保持运行。

当然,在部署到生产环境之前,Amazon 团队会通过测试环境来推送这些变更,并运行自动集成测试以捕获此类故障。但是,生产环境与测试环境之间可能存在细微且不可避免的差异,因此,在对生产造成影响之前,结合运用多层部署安全机制以捕获各种问题非常重要。尽管运行状况检查对于保护服务避免出现存在问题的部署很重要,但我们不能止步于此。我们考虑使用“双保险”方法来保护队列免受这些错误和其他错误的影响。

异步处理器

另一种失败模式与异步消息处理有关,例如通过轮询 SQS 队列或 Amazon Kinesis Stream 来接收其作业的服务。与在系统中接收来自负载均衡器的请求的系统不同,这种系统不会自动执行运行状况检查并将服务器从服务中移除。

如果服务没有进行足够深入的运行状况检查,则单个队列工作线程服务器可能会发生故障,例如磁盘空间占满或文件描述符用尽。这种问题不会阻止服务器从队列中拉取作业,但会导致服务器无法成功处理消息。此问题会导致消息处理延迟,存在问题的服务器会迅速从队列中拉取作业,但无法处理这些作业。

在这种情况下,通常会有几种补偿因素来帮助控制相关影响。例如,如果服务器无法处理它从 SQS 拉取的消息,则在配置的消息可见性超时之后,SQS 会将消息重新传送给另一台服务器。端到端延迟会增加,但消息不会被丢弃。另一个补偿因素是警报。在处理消息时出现过多错误时,服务器会发出警报,提醒操作人员开展调查。

磁盘空间占满

我们看到的另一类故障是服务器上的磁盘空间占满时,导致处理和日志记录均失败。由于服务器可能无法将其故障报告给监控系统,因此此故障会导致监控可见性出现差异。

同样,一些缓解措施可以防止服务“盲目行动”,并且可以迅速减轻影响。被 Application Load Balancer 或 API Gateway 等代理作为前端的系统的错误率和延迟指标由这些代理产生。在这种情况下,即使服务器未报告故障,也会触发警报。对于基于队列的系统,诸如 Amazon Simple Queue Service (Amazon SQS) 之类的服务会报告指标,指示某些消息的处理已延迟。

这些解决方案的共同点就是存在多层监控。不仅服务器自己会报告错误,外部系统也会报告错误。运行状况检查遵循同样的原则很重要。外部系统可以测试给定系统的运行状况,而且比该给定系统自行测试更准确。因此,团队可以使用 AWS Auto Scaling 配置负载均衡器来执行外部 ping 运行状况检查。

团队也会编写自己的自定义运行状况检查系统,以定期询问每台服务器其运行状况是否良好,并在服务器运行状况不佳时报告给 AWS Auto Scaling。该系统的一种常见实现方式涉及到每分钟运行一次的 Lambda 函数,可以测试每台服务器的运行状况。这些运行状况检查甚至可以将每次运行之间的状态保存在 DynamoDB 之类的数据库中,这样它们就不会立即将过多的服务器意外标记为运行状况不佳。

僵尸

另一种问题模式包括僵尸服务器。服务器可能会在一段时间内与网络断开连接,但仍保持运行状态,也有可能在长时间关闭后重新启动。

在僵尸服务器恢复工作时,它们可能与队列中的其他服务器明显不同步,而这可能会导致严重的问题。例如,如果僵尸服务器运行的是较旧的、不兼容的软件版本,则当它尝试与使用不同 schema 的数据库进行交互时可能会导致失败,或者导致使用错误的配置。

为了解决僵尸问题,系统通常会使用其当前正在运行的软件版本作为对运行状况检查的回应。随后,中央监控代理会比较整个队列的响应,以查找运行意外过时软件版本的所有服务器,并阻止这些服务器恢复服务。

结论

服务器及其运行的软件会出于各种奇怪的原因而出故障。硬件总有一天会发生物理故障。作为软件开发人员,我们编写的代码总归会出现类似于上述错误的问题,导致软件陷入崩溃状态。从轻量级存活检查到每项服务器指标的被动监控,都需要实施多层检查来捕获所有类型的意外故障模式。

当这些故障发生时,重要的是要检测到它们,并快速停用受影响的服务器。但是,与任何队列自动化一样,我们添加了限速、阈值和断路机制,它们可以关闭自动化机制,并在存在不确定性或极端情况下要求人为干预。采用“失败时开放”机制和构建集中式系统,都是充分利用深度运行状况检查的优势以及获得限速自动化机制安全性的有效策略。

动手实验室

通过动手实验室练习您在这里学到的原理。


关于作者

David Yanacek 是 AWS Lambda 的高级首席工程师。自 2006 年以来,David 一直在 Amazon 从事软件开发工作,之前致力于 Amazon DynamoDB 和 AWS IoT 以及内部 Web 服务框架和队列运营自动化系统的开发。在工作中,David 最喜欢做的就是执行日志分析并筛选操作指标,进而找到逐步提升系统运行流畅性的方法。

超时、重试和抖动回退