利用错误提高并行 API 部署中的高可用性
一种更好地利用集群错误反馈的方法
引言
过去几年,互联网 API 的工作负载呈指数级增长。微服务被创建出来,将大型系统分解成小型、可管理且可通信的服务。由于这种分布式特性,高可用性 (HA) 与正确性和快速执行时间同等重要。在这篇文章中,我将描述反向代理目前如何管理 HA,并针对某些特殊情况提出一些改进建议。
高可用性包含什么
高可用性有多种形式——负载分配、并发请求/秒、硬件支持(RAM、CPU 核心)、软件(语言构造、缓存策略、连接处理)、网络带宽等等。 此外,还有导致服务长时间中断和阻碍高可用性的停机时间和部署等因素。总的来说,虽然其中大多数是可处理的,并且取决于开发人员和基础设施团队的能力,但有些因素超出了单个 API 服务的范围,例如——负载分配、停机时间和部署中断。
问题 - 服务中断的影响
任何形式的服务中断(由停机或部署中断引起),无论多么微小,都会降低高可用性指标,并破坏与该服务消费者的信任。在这种情况下,服务通常会响应错误,这些错误可能导致客户端延迟出现级联效应,或者破坏客户端工作流,或者导致客户端进行繁琐的返工。这些错误的范围仅限于单个请求-响应周期,因此任何后续的客户端请求都会导致进一步的失败或降低集群的整体吞吐量。
提出的解决方案 - 监听错误
处理服务错误的一种方法是构建一个并行服务集群,并将其隐藏在一个保护层后面。我们现在将这些服务称为节点。该层负责在故障节点和正常节点集合中选择一个可用的节点。这并非不常见。许多当今的反向代理,如 nginx、haproxy、caddy,都采用这种方法,广义上称为负载均衡。它们内部利用各种路由算法来满足不同程度的使用场景。虽然轮询通常是首选算法,并且在大多数情况下都能正常工作,但可能需要(例如)将特定客户端绑定到一组服务,这可以通过使用 IP 哈希技术来解决。这只是一个例子。还有多种此类算法(或算法组合)可用于满足特殊用例。
现有的代理仅使用服务错误来选择下一个可用服务,并在找不到任何服务时向客户端抛出错误。纯轮询的问题在于,每个请求都可能在每个节点上进行尝试,而不管其状态如何。而且,如果所有节点都宕机,代理可能会将错误直接中继给客户端,从而中断通信。我正在提出两种方法来更好地处理这种情况。
a) 将错误视为路由算法中的一个指标
考虑一种情况,集群中的三个节点有一个宕机。如果代理使用纯轮询算法,则请求有 33% 的几率会命中这个出错的服务,然后重试其他节点中的一个。这会增加这 33% 请求的响应时间,并影响集群的整体吞吐量。如果我们开始将错误作为负载均衡节点的权重进行存储,我们可以利用这些信息,将请求百分比从 33% 降低到 1% 以下,具体取决于停机时间的长短。当所有节点恢复正常时,这些权重可以被重置。这可以完全处理部分集群中断(1 - n-1 个节点宕机)的情况。
b) 在完全中断时延迟请求转发
在完全集群中断(n 个节点宕机)的情况下,现有代理会直接将错误中继给客户端,然后由客户端自行处理——停止依赖请求的流程,或运行备用逻辑,或每天批处理重新运行失败的请求,等等。这项工作可以转移到代理本身,让客户端无需运行任何辅助逻辑。考虑发送更新到集群且后续行为不依赖于响应的请求。例如,大多数异步请求,如对失败交易启动退款、发送通知、更新订单详情以提供备选配送时间等。与其因为临时集群不可用而使这些请求失败,不如让代理存储这些请求,并在至少一个节点重新可用时对其进行重新处理。
实现
为了展示其中的差异,我实现了一个小型 HTTP 负载均衡器 'ServiceQ',它同时解决了上述两点(HTTP - 因为它是互联网协议,但相同的原理也可以应用于其他任何协议,如 FTP、XMPP 等)。
关于第一点 (a),ServiceQ 维护一个节点到自上次可用以来看到的错误数量的映射,并利用它来动态分配路由算法使用的权重。节点选择的概率随着该节点错误数量的增加而降低。该算法在每个新请求和第一次尝试时运行。后续的重试则使用轮询方法选择节点。
关于第二点 (b),ServiceQ 维护一个失败请求的 FIFO 队列(当集群中的所有节点都宕机时),并在设定的时间间隔后使用上述相同的算法重试它们。这种情况会一直持续到至少一个节点重新可用为止。
影响
我使用了优秀的 Apache Bench (ab) 来测试新系统。我将向一个拥有 4 个节点(部署在 EC2 t2 medium Linux 机器上)的集群发送 2000 个并发数为 100 的 HTTPS 请求。
测试1
我们将第 (a) 点中的新算法称为 'error_probabilistic
'。基础算法是 'randomize
' 和 'round_robin
'。
Cluster: 1
Nodes: 4 (:8000, :8001, :8002, :8003)
Unavailable Nodes: :8002, :8003
No of Requests: 2000
Algorithm: randomize
No of hits and errors:
:8000 => 1460
:8001 => 542
:8002 => 513 (failed, another node chosen)
:8003 => 998 (failed, another node chosen)
Algorithm: round_robin
No of hits and errors:
:8000 => 943
:8001 => 943
:8002 => 935 (failed, another node chosen)
:8003 => 936 (failed, another node chosen)
Algorithm: error_probabilistic
No of hits and errors:
:8000 => 1024
:8001 => 978
:8002 => 57 (failed, another node chosen)
:8003 => 60 (failed, another node chosen)
正如我们所观察到的,我们的新算法在失败节点上的请求尝试次数非常少——分别为 57 和 60。另外,请注意,这里的分布几乎是相等的(因为 :8002 和 :8003 的宕机时间相同)。
另一个需要注意的点是,使用 error_probabilistic
算法完成了所有 2000 个请求总共需要 2119 次尝试,而对于其他两种算法,这个数字分别为 3513
和 4757
。反过来,这会影响集群的总响应时间和吞吐量。
测试2
Cluster: 1
Nodes: 4 (:8000, :8001, :8002, :8003)
Unavailable Nodes: :8000, :8001, :8002, :8003
No of Requests: 2000
:8000 => 2000 (failed, another node chosen)
:8001 => 2000 (failed, another node chosen)
:8002 => 2000 (failed, another node chosen)
:8003 => 2000 (failed, another node chosen)
当所有请求在尝试了每个节点后都失败时,它们会被推送到一个延迟的 FIFO 队列。ServiceQ 向客户端发送适当的响应消息,表明请求已被缓冲。当其中一个节点(:8002)重新可用时
:8000 => 99 (failed, another node chosen)
:8001 => 131 (failed, another node chosen)
:8002 => 2001
:8003 => 51 (failed, another node chosen)
这样,我们就不会丢失请求,并且客户端无需编写任何辅助错误处理逻辑。
结论
当今存在许多基于非常扎实原则的高可用性和负载均衡技术。然而,它们采用了一套静态的规则,无法很好地响应变化的集群行为。按照提议的方式监听集群错误,不仅有助于克服这些问题,还能使部署更容易,停机时间可控,服务更值得信赖。