故障可能发生在网络连接级别(进程之间的消息丢失或传递缓慢),也可能发生在进程级别(进程崩溃或运行缓慢),并且延迟始终不能与故障区分开。这意味着在错误地将活动过程怀疑为已死(产生假阳性)与延迟将无响应过程标记为已死之间进行权衡,这给了它怀疑的好处并期望它最终做出响应(产生假阴性)。
故障检测器是一个本地子系统,负责识别失败或不可达的进程,以将其从集群中排除,并在保持安全性的同时保证活性。
活性和安全性是解决特定问题的能力及其输出正确性的属性。更正式地说,活性是一种属性,可以保证必须发生特定的预期事件。例如,如果进程之一失败,则故障检测器必须检测到该故障。安全保证不会发生意外事件。例如,如果故障检测器将某个进程标记为已死,则该进程实际上必须是已死。
从实际的角度看,将故障进程排除出去可以避免不必要的工作,并防止错误传播和级联故障,同时在排除可疑活动进程时会降低可用性。
故障检测算法应表现出几个基本属性。首先,每个无故障的成员最终都应该注意到进程失败,并且算法应该能够取得进展并最终达到其最终结果。此属性称为完整性。
我们可以通过其效率来判断算法的质量:故障检测器能够多快地识别过程故障。做到这一点的另一种方法是查看算法的准确性:是否精确地检测到过程故障。换句话说,如果算法错误地认为实时进程失败或无法检测到现有的失败,则该算法是不准确的。
我们可以将效率和准确性之间的关系视为一个可调参数:效率更高的算法可能精度较低,而精度更高的算法通常效率较低。建立一个既准确又高效的故障检测器,证明是不可能的。同时,允许故障检测器产生假阳性(即,错误地将活动进程标识为失败,反之亦然)。
许多分布式系统通过使用心跳或者超时探测来实现故障检测器。这种方法因其简单性和强大的完整性而非常受欢迎。但是,在本文中,您将看到检测节点故障是多么困难。我们还将讨论一个高级架构设计,用于通过 phi accrual 检测节点故障检测。
网络延迟就像迪斯尼乐园的交通拥堵。想象一下,当您排队等候过山车时。在队列的最前面,您会看到等待时间是 10 分钟。你可能会想,10分钟并不长。因此,您排队等候了10分钟,并且到达了队列的前面 - 您忽然发现你的面前有一个更长的部分队列,您还需要额外等待 30 秒;网络延迟以类似的方式工作。
当数据包从源机器发送到目标机器时,它们会通过网络交换机,并将它们排队并一一送入目标网络链接。
一旦它到达目标机器中的网络链接,如果所有 CPU 内核当前都忙,则来自网络的传入请求将由操作系统排队,直到应用程序准备好处理它。
TCP 执行流量控制(背压),限制通过网络发送的节点数量,以减轻它包含在网络链接中的节点。因此,它在网络交换层中为数据包提供了另一层队列。
想象一下,如果您正在运行一个程序。程序没有崩溃,但它很慢。并且程序中的堆栈或者日志信息没有证明哪里出了问题。这个程序将比以前的完全失败场景更难检测到失败。这种故障就是所谓的部分故障。
如果你运行的是单个程序,如果某部分功能不工作,通常会导致整个程序崩溃。到那时,它会显示一个日志堆栈跟踪,您可以进一步检查以了解系统崩溃的原因。
部分故障更难检测,因为它们要么不起作用,要么一切正常。
由于分布式系统没有共享状态,部分故障总是发生。
如果您没有得到任何响应,这并不意味着该节点已死。
失败的原因如下所示:
如果网络调用没有得到响应,它永远不会知道远程节点的状态。除非你可以监控网络链路并发出延迟告警。
通常探针会不断发送健康检查来检查服务是否健康。当远程节点没有响应时,我们只能猜测数据包在过程中的某个地方丢失了。
下一个操作将是重试或等待一段时间,直到超时。如果操作不是幂等的,重试选项可能有点危险。因此,超时是一种更好的方法,因为如果您没有得到任何响应,则执行更多操作可能会导致不必要的副作用,例如双重计费。
如果我们想做超时的方法,超时应该是多长时间?
如果时间太长,您可能会让客户等待。因此,在网络上的体验很糟糕。
如果您将超时设置得太短,您可能会得到误报,将完全健康的节点标记为死亡。例如,如果节点是活动的,它有更长的时间来处理某些动作。过早宣布节点死亡并让其他节点接管可能会导致操作执行两次,如果使用这种方式,要在业务层面保证服务的幂等。
此外,一旦节点被宣布为死亡,它需要将其所有任务委托给其他节点,从而将更多负载放在其他节点上,如果其他节点已经有很多负载,则会导致级联故障。
正确的超时时间基于应用程序逻辑和业务用例。
如果用户容忍该时间,服务可以在 x 时间后声明操作超时。例如,如果 7 分钟不会给用户带来不好的体验,支付服务可以设置 7 分钟作为超时时间。许多团队通过反复试验来检测超时时间。在这种情况下,我们设置的超时时间通常是恒定的。例如,在 7 分钟内,或 5 分钟内……等等。
然而,一个更聪明的检测超时的方法是不将超时视为一个常数值,而是由一个分布的方差组成。如果我们测量网络往返时间在很长一段时间内和许多机器上的分布,我们可以确定延迟的预期可变性。
我们可以收集平均响应时间和一些可变性(抖动)因素的所有数据。监控系统可以根据观察到的响应时间分布自动调整超时。这种故障检测算法的方法是通过 Akka 和 Cassandra 使用的 Phi Accrual 故障检测器完成的。
Phi Accrual 故障检测器使用每个心跳的固定窗口大小采样来估计信号的分布。每次一个向远程节点调用心跳,它都会将响应时间写入固定窗口。该算法将使用这个固定窗口来获得响应时间的均值、方差和标准差。如果您有兴趣,这里有一个检测 phi 的公式https://doc.akka.io/docs/akka/current/typed/failure-detector.html
。
在下面我们将简要介绍节点故障检测的高级设计。
使用由两部分组成的节点故障检测组件:解释器和监视器。
解释器的工作是解释节点的可疑程度。监视器的工作是接收每个节点的心跳并将心跳时间委托给解释器。
监视器将不断地对每个远程节点进行心跳。每次向远程节点发送健康检查时,都会在一段时间内收到响应。然后它将响应时间发送给解释器以检测节点的怀疑级别。
有两种放置解释器的方式:集中式和分布式。
集中式
集中的方式是将解释器和监视器作为自己的服务,系统对每个节点进行解释并将信号发送给其他节点以进行进一步的操作。结果将是一个布尔值,无论是否怀疑。
分布式
分布式方式是将解释器放置在每个应用程序层中 - 让应用程序可以自由配置怀疑级别以及它应该对每个怀疑级别采取的操作。
集中方式的优点是更容易管理节点。然而,分布式方法可以微调或优化每个节点以根据不同的怀疑级别表现不同。
我们可以将 Phi Accrual Failure 算法用于我们在上一节中讨论的解释器。我们设置了什么 phi 的阈值——如果 phi 结果高于阈值,我们宣布远程节点死亡。如果 phi 结果低于阈值,则远程节点可用。
当监视器将请求发送到远程节点时,解释器开始计时响应时间。如果远程节点响应时间超过阈值,解释器可以停止请求并将节点声明为可疑节点。总之不把节点故障作为二元问题(该进程只能处于运行或者宕机状态),而是连续捕获受检视进程崩溃的可能性。
在设计应用程序时,检测节点并不是一件容易的事。原因之一是分布式系统中的非共享状态模型。工程师需要在不可靠的网络中设计可靠的系统。
大多数时候,公司都会反复试验来检测节点故障。然而,我们可以用可变性来处理节点是否死亡,而不是用布尔值来处理它们——当节点发生故障时的分布式方差,使用 Phi Accrual 故障检测器并设置超时阈值水平。
最后,节点检测故障的高级抽象设计可以由监控组件和解释器组成。监控将不断向远程节点发送心跳并将响应时间委托给解释器以分析怀疑级别。如果一个节点达到某个可疑级别阈值,解释器会向调用它们的服务返回一个布尔值,以指示需要的额外操作。