从Algolia成立之初,我们就决定在用户和搜索API服务器之间不放置任何负载平衡基础结构。我们做出此选择是为了使事情变得简单,消除任何潜在的单点故障,并避免监视和维护此类系统的成本。
一些DSN服务器(不是DNS)。这些是只读副本,仅用于搜索查询。它们的主要目的是为地理位置远离主群集的人们提供更快的搜索。
我们没有在搜索服务器和用户之间放置硬件或软件,而是选择依靠DNS的轮询功能在服务器之间分配负载。每个Algolia应用程序实例都与唯一的DNS记录相关联,该DNS记录以循环方式与处理给定Algolia应用程序的裸机之一进行响应。
我们认为Algolia最常见,最优化的用法是在前端实施。在这种情况下,移动设备或笔记本电脑将直接与我们的裸机服务器建立通信。在这种情况下,我们可以假设会有大量的DNS解析,每个解析都会导致几个搜索请求。这是依靠轮询DNS进行负载平衡的最佳情况:大量用户请求DNS访问Algolia服务器,然后执行一些搜索。这导致服务器负载与轮询DNS解析匹配。此外,为了实施更高的DNS解析度,我们将DNS TTL减少到一分钟。
最后,该系统很简单。它没有使用任何专用的硬件或软件来自行管理,一切进展顺利。
如前所述,我们强烈建议客户使用前端搜索实现。许多参数促使这种选择。其中之一是利用我们基于DNS的负载平衡系统。但是,这并不总是可行的:某些客户有特定的约束,例如传统设计或安全问题,这导致他们选择后端实施。这样,他们的后端服务器会将所有搜索查询中继到我们的基础架构。
现在,一小组服务器执行一些DNS解析,并将大量请求转发到所选的后端服务器。现在,我们有1个用户进行10,000个查询,而不是1,000个用户进行10个查询。
由于与我们的搜索服务器的会话寿命更长,因此后端服务器可以发送更多请求,而无需重新执行DNS解析。
有时,客户服务器甚至会覆盖我们的DNS TTL,以便他们可以使用更长的DNS缓存。
就是说,在设计基础架构时,我们主要关注的是弹性。这意味着,对于大多数客户而言,单个群集节点可以处理所有搜索负载。因此,整个群集节点上的负载不均衡不会对搜索体验产生任何影响。
最初,引入DSN的目的是通过将只读服务器放置在离主群集较近的地方,从而提高其执行搜索请求的用户的性能。但是,我们很快意识到,通过水平扩展服务器以吸收更多搜索请求,这也是在给定区域中增加搜索容量的简便方法。
我们有一个后端实施的大客户,其负载太大而无法由单个服务器处理。除了集群之外,我们已经在同一个区域中部署了许多DSN,以吸收来自其后端服务器的搜索负载。
但是,当黑色星期五到来时,他们开始遇到越来越多的搜索查询。即使我们已经在确定基础架构的大小以吸收负载,但最终还是遇到搜索查询缓慢甚至失败的情况。对于最终用户而言,这意味着在一年中您希望电子商务网站具有高性能的时候,搜索体验会大大降低,延迟也会增加。
负载是不均衡的:我们这边可以处理请求的可用服务器总数超过了可以发送请求的服务器数量。我们最终遇到了这样的情况:在最佳情况下,使用基于DNS的负载平衡,他们的每台服务器都会选择一台服务器并坚持使用几分钟,从而使服务器超载,而另一些服务器则无法使用。所有。
至少在此特定用例中,这使我们重新考虑了基于DNS的负载平衡方法,该方法将繁重的搜索负载与后端实现相结合。
为了解决黑色星期五期间的问题,我们进行了快速修复,并部署了基本的负载平衡器。我们利用了Nginx及其代理请求并将它们负载均衡到一组上游服务器(在我们的例子中是Algolia服务器)中的能力。
我们节省了一天,流量平均负载均衡。这证实了在某些情况下我们需要这样的系统。但是,在这一点上,它比实际的长期解决方案更能解决问题。整个过程主要是静态的,在Nginx配置中硬编码了特定于客户的参数。这种情况引起了许多质疑:
如何针对给定的传入请求动态定位正确的搜索API服务器组?
如何使其能够处理我们日常的基础架构操作,例如随着时间的推移更改,添加或删除服务器?
对于第二次迭代,重点是找到一种使负载均衡器通用的方法。主要挑战是动态构建能够满足传入请求的上游服务器列表。要解决此类问题,您可以考虑两种相反的方法:
我们选择了第二个解决方案,主要是因为每个请求必须处理的数据总量太大且影响太大,以致于无法保持较低的搜索请求延迟。我们实施了一个缓慢的学习流程,以尝试使一切尽可能简单,并避免管理复杂且庞大的分布式数据存储系统。
负载平衡器每次收到尚未知道的客户请求时,都会经过一个较慢的过程来获取与此客户相关联的上游服务器的列表。对于同一客户的以下所有请求都可以更快地处理,因为它们随后直接从本地缓存中获取所需的上游信息。
HAProxy为动态配置提供了Lua支持,但是从我们的测试来看,它对于我们的用例来说太有限了。
Envoy曾经(现在仍然)非常有前途,但是学习曲线相当陡峭,即使我们设法制定了可行的PoC,但它们的当前负载平衡算法对于我们的长期愿景而言过于严格。
我们尝试在Go中制作一个自定义负载平衡器。 PoC正常运行,但是仍然很难独自评估这种解决方案的安全性和性能。它也很难维护。
我们最终尝试了基于Nginx的OpenResty,它使您可以在请求处理的不同步骤中运行自定义Lua代码。它有一个相当完善的社区,有很多可用的模块,无论是官方模块还是社区驱动的模块,文档都很不错。
我们决定使用OpenResty。我们将它与Redis结合在一起用于缓存部分,因为OpenResty提供了一个方便的模块来与Redis进行交互:
通过这次迭代,我们设法找到可以从中删除任何静态配置的机制,从而使负载均衡器具有更高的可扩展性和易于维护性。然而,仍然有一些东西使它无法生产:
如何确保我们仍然可以像每天一样对基础架构进行更改?
在第三个也是最新的实现中,我们引入了一些机制来使整个系统更具故障预防能力。
除了OpenResty处理负载平衡逻辑和Redis缓存动态数据外,我们还添加了一个自定义Go守护程序lb-helper。
抽象我们的内部API。 OpenResty通过本地lb-helper了解上游服务器,该服务器定期从内部API获取数据。如果负载均衡器无法连接到我们的内部API,它仍然可以使用可能略有过时的数据进行操作。
管理失败。上游服务器每次连续发生10次以上故障时,我们都将其视为停机,并将其从活动缓存中删除。在这里,lb-helper会向下探测上游以检查是否返回。
今天,我们仍然主要依靠基于DNS的负载平衡,因为它可以满足我们99%的用例。也就是说,我们现在也意识到,这种方法在某些情况下有一定的局限性,例如将后端实现与沉重的搜索负载结合在一起的客户。在这种情况下,部署一组负载均衡器将使搜索基础结构的负载均衡。
而且,这些实验向我们展示了我们不仅构建了简单的负载平衡设备。它在我们的搜索基础架构之上带来了一个抽象层,使故障,基础架构变更或扩展对我们的客户几乎完全透明。
目前,我们正在进行第四次迭代,因此我们尝试引入一种基于延迟的算法来代替当前的轮询。 长期计划是检查我们是否可以在我们的搜索基础架构之上引入一个全球抽象层。 然而,试图以这种规模走向全球会带来一系列新的限制。 这是另一篇博客文章的主题!