随着分布式系统的普及,高可用性(High Availability, HA)成为了衡量系统成功的关键因素之一。本文将探讨负载均衡技术如何管理高可用性,并提出一些针对特定情况的改进措施。
高可用性涉及多种形式,包括负载分配、并发请求/秒、硬件支持(如内存、CPU核心)、软件支持(如语言结构、缓存策略、连接处理)、网络带宽等。此外,还有停机时间和部署等可能导致长时间服务中断的因素,这些都可能影响HA。虽然大多数问题都是可以处理的,并且取决于开发人员和基础设施团队的能力,但有些问题超出了单个API服务的范围,如负载分配、停机时间和部署中断。
任何服务的中断(无论是由于停机还是部署中断造成的),无论多么短暂,都会降低HA指标,并破坏服务消费者的信任。在这种情况下,服务通常会返回错误,这些错误可能会对客户端延迟产生级联效应,或者破坏客户端的工作流程,或者迫使客户端进行繁琐的重做。这些错误的范围限制在单个请求-响应周期内,因此任何随后的客户端请求都会导致进一步的失败或降低集群的整体吞吐量。
处理服务错误的一个解决方案是构建一个并行服务集群,并在其后面放置一个保护层。现在让将这些服务称为节点。这一层负责在一组有故障和正常工作的节点中选择一个可用的节点。这不是不常见的。许多现代的反向代理,如nginx、haproxy、caddy,都使用这种方法,这在广义上被称为负载均衡。它们内部使用各种路由算法来满足不同程度的用例。虽然轮询通常是首选算法,并且在大多数情况下都能工作,但可能需要(比如说)将某些客户端绑定到一组服务,这可以通过使用IP哈希技术来解决。这只是其中一个例子。有多种这样的算法(或算法组合)可以用于特殊用例。
现有的代理仅使用服务错误来选择下一个可用的服务,如果它们找不到任何服务,就将错误传递给客户端。使用纯轮询的问题是,每个请求都可能在每个节点上尝试,而不管其状态如何。此外,如果所有节点都宕机了,代理可能只是简单地将错误回传给客户端,这结束了通信。提出了两种更好的处理这种情况的方法。
考虑一个场景,一个集群中的三个节点中有一个宕机了。如果代理使用纯轮询算法,那么有33%的机会请求会击中这个错误的服务,然后在其他节点之一上重试。这增加了这33%请求的响应时间,并影响了集群的整体吞吐量。如果开始将错误存储为负载均衡节点的权重,可以使用这些信息将请求的百分比从33%降低到不到1%,这取决于停机时间的长度。当所有节点都恢复时,可以重置权重。这可以完全处理部分集群中断(1-n-1个节点宕机)。
在总集群中断(n个节点宕机)的情况下,现有的代理只是简单地将错误回传给客户端,然后客户端根据需要处理它 - 停止依赖请求的流程,或者运行替代逻辑,或者每天批量重新运行失败的请求,等等。这项工作可以卸载到代理本身,让客户端免于运行任何辅助逻辑。考虑发送更新到集群的请求,并且进一步的行为不依赖于响应。例子可能包括大多数异步请求,如在交易失败时启动退款,发送通知,更新订单详情以替代交货时间等。与其因为临时集群不可用而失败这些请求,代理可以存储并查看在至少有一个节点重新可用时重新处理它们。
为了展示差异,实现了一个小型的HTTP负载均衡器'ServiceQ',它涵盖了上述两点(HTTP - 因为它是互联网的协议,但同样的原则可以应用于任何其他协议,如FTP、XMPP等)。
对于点(a),ServiceQ维护一个节点到自上次可用以来看到的错误数量的映射,并使用它动态分配节点的权重,路由算法利用这些权重。随着节点上的错误增加,节点被选择的概率降低。这个算法在每个新请求和第一次尝试时运行。随后的重试通过轮询方法选择节点。
对于点(b),ServiceQ维护一个失败请求的FIFO队列(当集群中的所有节点都宕机时),并在设定的时间间隔后使用上述算法重试它们。这一直持续到至少有一个节点重新可用。
使用了出色的Apache Bench(ab)来测试新系统。将发送2000个HTTPS请求,每个请求的并发性为100,发送到一个有4个节点的集群(部署在EC2 t2中型Linux机器上)。
让将点(a)中的新算法称为'error_probabilistic'。基础算法是'randomize'和'round_robin'。
集群: 1
节点: 4 (:8000, :8001, :8002, :8003)
不可用节点: :8002, :8003
请求数量: 2000
算法: randomize
命中和错误数量:
:8000 => 1460
:8001 => 542
:8002 => 513 (失败,选择了另一个节点)
:8003 => 998 (失败,选择了另一个节点)
算法: round_robin
命中和错误数量:
:8000 => 943
:8001 => 943
:8002 => 935 (失败,选择了另一个节点)
:8003 => 936 (失败,选择了另一个节点)
算法: error_probabilistic
命中和错误数量:
:8000 => 1024
:8001 => 978
:8002 => 57 (失败,选择了另一个节点)
:8003 => 60 (失败,选择了另一个节点)
正如所观察到的,新算法在失败的节点上尝试请求的次数非常少 - 57和60。还要注意的是,分配几乎是相等的(因为:8002和:8003停机的时间相同)。另一个要注意的点是,使用error_probabilistic算法完成所有2000个请求需要总共2119次尝试,而其他两个算法的数量是3513和4757。这反过来又影响了集群的总响应时间和吞吐量。
当所有请求在每个节点上重试失败后,它们被推送到一个延迟的FIFO队列中。ServiceQ向客户端发送适当的响应消息,表明请求已缓冲。当其中一个节点(:8002)重新可用时:
:8000 => 99 (失败,选择了另一个节点)
:8001 => 131 (失败,选择了另一个节点)
:8002 => 2001
:8003 => 51 (失败,选择了另一个节点)