不考虑对上游服务的调用可能失败的所有方式,通常更容易考虑成功请求是什么。它应该是及时的,在期望的格式,并且包含预期的数据。如果我们遵循这个定义,那么其他一切都是某种失败,无论是:
在规划失败应对方案时,我们应该努力能够处理这些错误,就像我们应该试图阻止我们的服务发出它们一样。因此,让我们开始研究解决这些错误的不同技术。 (注意:本文中提到的所有示例和工具都在Go中。但是,不需要事先了解Go) 介绍断路器 电气箱中保护您的设备称为断路器。软件断路器以相同的方式工作。软件断路器是一种位于两段代码之间的机制,用于监控流经它的所有内容的健康状况。但是,它不是在发生故障时停电,而是阻止请求。 当服务被请求淹没时,服务可能会中断。一旦服务超载,进行任何进一步的请求可能会导致两个问题。首先,发出请求可能毫无意义,因为我们不会得到有效和/或及时的响应;其次,因为创建了更多请求,就无法让上游服务从不堪重负中恢复,事实上,很可能更多地重创它。 断路器不仅仅是保护我们的上游服务。它们对我们的服务也有好处,我们将在下一节中看到。 回退 断路器,如Hystrix,包括定义回退的能力。 假设您正在编写需要两个位置之间的道路行驶距离的服务。 如果事情按预期工作,我们会称之为“距离计算器服务”,为其提供起点和终点位置,并返回距离。但是,该服务目前正在宕机。因此,在这种情况下合理的回退可能是通过使用一些三角法来估计距离。当然,以这种方式计算距离将是不准确的,但是使用允许我们继续处理用户请求的不准确值远比完全失败请求好得多。 在后备处理中,使用估计值而不是实际值而不是唯一选项,其他常见选项包括:
当然,有一些情况没有合理的后备。但即使在这些情况下,使用断路器仍然是有益的。 考虑发出和等待最终失败的请求的成本。有CPU,内存和网络资源,都被用于发出请求并等待响应。然后是对用户的延迟响应,这些资源都处于等待之中。 当断路打开时,所有这些成本都被避免,因为没有提出请求,而是立即失败。虽然向用户返回错误并不理想,但返回最快的错误是也是一种选择,不过只是最糟糕的。 断路器应该跟踪所有错误吗? 最简洁的答案是不。我们不应该跟踪由用户引起的错误(即HTTP错误代码400和401),而是跟踪网络或基础设施(即HTTP错误代码503和500)。 如果我们跟踪用户造成的错误,那么一个恶意用户就有可能发送大量错误请求,导致我们的断路打开并造成服务的中断。 断路恢复 我们已经讨论了当出现太多错误时断路器如何打开电路并切断请求。我们还应该知道断路如何再次关闭。 与上面使用的电气示例不同,使用软件断路器,您无需在黑暗中找到保险丝盒并手动关闭断路。软件断路器可以自行闭合断路。 在断路器断开电路后,它将等待一个可配置的周期,称为睡眠窗口,之后它将通过允许一些请求来测试断路。如果服务已恢复,它将关闭断路并恢复正常操作。如果请求仍然返回错误,那么它将重复睡眠/尝试过程直到恢复。 Bulwark堡垒 在Grab,我们使用Hystrix-Go断路器,这个实现包括一个壁垒bulwark。bulwark是一个软件进程,它监视并发请求的数量,并且能够防止超过配置的最大并发请求数。这是一种非常便宜的限速形式。 在我们的例子中,通过打开断路来实现防止太多请求(如上所述)。此过程不计入错误,也不会直接影响其他断路计算。 那为什么这很重要?正如我们之前谈到的那样,当服务收到太多并发请求时,服务可能会变得无响应(甚至崩溃)。 请考虑以下情形:黑客已决定使用DDOS攻击攻击您的服务。突然间,您的服务正在接收通常数量的请求的100倍。然后,您的服务可以向上游提供100倍的请求数量。 如果您的上游没有实现某种形式的速率限制,有了这么多请求,它就会崩溃。通过在服务和上游之间引入一个舷墙,您可以实现两件事:
断路器设置 Hystrix-Go 具有五种设置,它们分别是: 1. 超时: 此持续时间是在被视为错误之前允许请求的最长时间。这考虑到并非所有对上游资源的调用都会立即失败。 有了这个,我们可以通过定义我们愿意等待上游的时间来限制我们处理请求所需的总时间。 2.最大并发请求 这是堡垒设置(如上所述)。 考虑默认值(10)表示同时发出请求而不是“每秒”。因此,如果请求通常很快(在几毫秒内完成),则不需要允许更多。 此外,将此值设置得过高可能会导致您的服务缺少发出请求所需的资源(内存,CPU,端口)。 3.请求阈值 这是在打开断路之前必须在评估(滚动窗口)期间内进行的最小请求数。 此设置用于确保低请求量期间的少量错误不会打开断路。 4.睡眠窗口 这是电路在断路器试图检查请求的健康状况之前等待的持续时间(如上所述)。 将此设置得太低会限制断路器的有效性,因为它经常打开/检查。但是,将此持续时间设置得太高会限制恢复时间。 5.错误百分比阈值 这是在断路打开之前必须失败的请求的百分比。 设置此值时应考虑许多因素,包括:
断路配置 在接下来的几节中,我们将讨论与断路配置相关的一些不同选项,特别是每个主机和每个服务配置,以及我们作为程序员如何定义断路。 在Hystrix-Go中,典型的使用模式如下所示:
hystrix.Go("my_command", func() error {
// talk to other services
return nil
}, func(err error) error {
// do this when services are down
return nil
})
func List() {
hystrix.Go("my_upstream_list", func() error {
// call list endpoint
return nil
}, nil)
}
func Create() {
hystrix.Go("my_upstream_create", func() error {
// call create endpoint
return nil
}, nil)
}
func Update() {
hystrix.Go("my_upstream_update", func() error {
// call update endpoint
return nil
}, nil)
}
func Delete() {
hystrix.Go("my_upstream_delete", func() error {
// call delete endpoint
return nil
}, nil)
}
func List() {
hystrix.Go("my_upstream", func() error {
// call list endpoint
return nil
}, nil)
}
func Create() {
hystrix.Go("my_upstream", func() error {
// call create endpoint
return nil
}, nil)
}
func Update() {
hystrix.Go("my_upstream", func() error {
// call update endpoint
return nil
}, nil)
}
func Delete() {
hystrix.Go("my_upstream", func() error {
// call delete endpoint
return nil
}, nil)
}
每个主机一个断路 如上所述,一个坏主机可能会影响您的断路,因此您可能会考虑为每个上游目标主机使用一个断路。 但是,要实现这一点,我们的服务必须了解上游主机的数量和身份。在前面的示例中,它只知道负载均衡器的存在。因此,如果我们从前面的示例中删除负载均衡器,我们将留下7台主机。 使用此配置,我们的一个坏主机不能影响跟踪其他主机的电路。感觉就像一场胜利。 但是,在删除负载均衡器后,我们的服务现在需要承担其职责并执行客户端负载平衡。 为了能够执行客户端负载平衡,我们的服务必须跟踪上游服务中所有主机的存在和运行状况,并在主机之间平衡请求。在Grab,我们的许多基于gRPC的服务都是以这种方式配置的。(banq注:SpringCloud中是基于erueka注册服务器) 使用我们的新配置,我们遇到了一些额外的复杂性,与客户端负载平衡有关,我们也从1个断路器变为6个。这些额外的5个断路也会产生一些资源(即内存)成本。在这个例子中,它可能看起来不是很多,但随着我们采用额外的上游服务并且这些上游主机的数量增加,成本确实成倍增加。 我们应该考虑的最后一件事是这种配置将如何影响我们满足请求的能力。当主机首次出现故障时,我们的请求错误率将与之前相同:1个坏主机/ 6个主机总数= 16.66%错误率 但是,在将断路打开直到坏主机之后发生了足够的错误,将能够避免向该主机再次发出请求,然后会恢复,重新开始只有0%的错误率。 每个服务与每个主机的最终想法 根据上面的讨论,您可能希望将所有断路转换为每个主机。但是,这样做的额外复杂性不应低估。(banq注:微服务是每个服务一个主机,如果是SOA,需要服务和主机的区分)。 此外,我们还应该考虑当坏主机发生故障时,每个服务负载均衡器可能会有什么响应。如果我们的每个服务示例中的负载均衡器配置为监视在每个主机上运行的服务的运行状况(而不仅仅是主机本身的运行状况),那么它能够从负载均衡器中检测并删除该主机并可能替换它有一个新的主机。 可以同时使用每个服务和每个主机(虽然我从未尝试过)。在此配置中,每个服务电路应仅在几乎没有机会存在任何有效主机时打开,并且通过这样做可以节省在重试周期中运行的请求处理时间。其配置必须是: 断路器(每个服务)→重试→断路器(每个主机)。 我的建议是考虑上游服务失败的方式和原因,然后根据您的情况使用最简单的配置。