Yandex.Cloud 中的网络负载均衡器架构

Yandex.Cloud 中的网络负载均衡器架构
你好,我是 Sergey Elantsev,我开发 网络负载均衡器 在 Yandex.Cloud 中。 此前,我领导了 Yandex 门户的 L7 平衡器的开发——同事开玩笑说,无论我做什么,结果都是一个平衡器。 我将告诉 Habr 读者如何管理云平台中的负载,我们认为实现这一目标的理想工具是什么,以及我们如何构建这个工具。

首先,我们来介绍一些术语:

  • VIP(虚拟IP)-平衡器IP地址
  • 服务器、后端、实例——运行应用程序的虚拟机
  • RIP(真实IP)——服务器IP地址
  • Healthcheck - 检查服务器准备情况
  • 可用区,AZ - 数据中心内的隔离基础设施
  • 区域 - 不同可用区的联合

负载均衡器解决三个主要任务:它们本身执行平衡,提高服务的容错能力,并简化其扩展。 通过自动流量管理确保容错:平衡器监视应用程序的状态,并将未通过活性检查的实例排除在平衡之外。 通过在实例之间均匀分配负载以及动态更新实例列表来确保扩展。 如果平衡不够均匀,某些实例将收到超出其容量限制的负载,并且服务将变得不太可靠。

负载均衡器通常根据其运行的 OSI 模型的协议层进行分类。 Cloud Balancer 在 TCP 级别运行,对应于第四层 L4。

让我们继续概述云平衡器架构。 我们将逐步提高细节水平。 我们将平衡器组件分为三类。 配置平面类负责用户交互并存储系统的目标状态。 控制平面存储系统的当前状态并管理数据平面类的系统,数据平面类直接负责将流量从客户端传递到您的实例。

数据平面

流量最终到达称为边界路由器的昂贵设备。 为了提高容错能力,多个此类设备在一个数据中心同时运行。 接下来,流量进入平衡器,平衡器通过 BGP 向客户端宣布任播 IP 地址到所有可用区。 

Yandex.Cloud 中的网络负载均衡器架构

流量通过 ECMP 传输 - 这是一种路由策略,根据该策略,可以有多个同样好的路由到达目标(在我们的例子中,目标将是目标 IP 地址),并且数据包可以沿着其中任何一个发送。 我们还根据以下方案支持多个可用区域的工作:我们在每个区域中公布一个地址,流量流向最近的区域并且不会超出其限制。 在本文后面,我们将更详细地了解流量发生的情况。

配置平面

 
配置平面的关键组件是 API,通过它执行平衡器的基本操作:创建、删除、更改实例的组成、获取健康检查结果等。一方面,这是一个 REST API,另一方面另外,我们在云端经常使用gRPC框架,所以我们将REST“翻译”为gRPC,然后只使用gRPC。 任何请求都会导致创建一系列异步幂等任务,这些任务在 Yandex.Cloud 工作线程的公共池上执行。 任务的编写方式使得它们可以随时暂停然后重新启动。 这确保了操作的可扩展性、可重复性和日志记录。

Yandex.Cloud 中的网络负载均衡器架构

因此,来自 API 的任务将向平衡器服务控制器发出请求,该控制器是用 Go 编写的。 它可以添加和删除平衡器,更改后端和设置的组成。 

Yandex.Cloud 中的网络负载均衡器架构

该服务将其状态存储在 Yandex 数据库中,这是一个您很快就能使用的分布式托管数据库。 在 Yandex.Cloud 中,正如我们已经 告诉,狗粮概念适用:如果我们自己使用我们的服务,那么我们的客户也会乐意使用它们。 Yandex 数据库就是实现这一概念的一个示例。 我们将所有数据存储在YDB中,我们不必考虑维护和扩展数据库:这些问题都为我们解决了,我们将数据库用作服务。

让我们回到平衡器控制器。 它的任务是保存有关平衡器的信息,并向健康检查控制器发送检查虚拟机就绪情况的任务。

健康检查控制器

它接收更改检查规则的请求,将其保存在 YDB 中,在 healthcheck 节点之间分配任务并聚合结果,然后将结果保存到数据库并发送到负载均衡器控制器。 反过来,它会向负载均衡器节点发送更改数据平面中集群组成的请求,我将在下面讨论。

Yandex.Cloud 中的网络负载均衡器架构

让我们更多地讨论健康检查。 它们可以分为几个类别。 审计有不同的成功标准。 TCP 检查需要在固定时间内成功建立连接。 HTTP 检查需要成功的连接和带有 200 状态代码的响应。

此外,检查的作用类别也有所不同——它们是主动的和被动的。 被动检查只是监视流量发生的情况,而不采取任何特殊操作。 这在 L4 上效果不太好,因为它取决于更高级别协议的逻辑:在 L4 上,没有关于操作花费多长时间或连接完成是好还是坏的信息。 主动检查需要平衡器向每个服务器实例发送请求。

大多数负载均衡器都会自行执行活动检查。 在 Cloud,我们决定将系统的这些部分分开以提高可扩展性。 这种方法将使我们能够增加平衡器的数量,同时保持对服务的健康检查请求的数量。 检查由单独的运行状况检查节点执行,检查目标在这些节点上进行分片和复制。 您无法从一台主机执行检查,因为它可能会失败。 那么我们就得不到他检查的实例的状态。 我们从至少三个运行状况检查节点对任何实例执行检查。 我们使用一致的哈希算法来划分节点之间检查的目的。

Yandex.Cloud 中的网络负载均衡器架构

将平衡和健康检查分开可能会导致问题。 如果健康检查节点绕过平衡器(当前不提供流量)向实例发出请求,则会出现奇怪的情况:资源似乎还活着,但流量不会到达它。 我们通过这种方式解决这个问题:我们保证通过平衡器启动健康检查流量。 换句话说,移动来自客户端和健康检查的流量的数据包的方案差别很小:在这两种情况下,数据包都会到达平衡器,平衡器会将它们传送到目标资源。

不同之处在于客户端向 VIP 发出请求,而运行状况检查向每个单独的 RIP 发出请求。 这里出现了一个有趣的问题:我们为用户提供了在灰色 IP 网络中创建资源的机会。 让我们想象一下,有两个不同的云所有者将他们的服务隐藏在平衡器后面。 它们每个都在 10.0.0.1/24 子网中拥有资源,并且具有相同的地址。 您需要能够以某种方式区分它们,在这里您需要深入了解 Yandex.Cloud 虚拟网络的结构。 最好在以下位置了解更多详细信息 视频来自约:云事件,现在对我们来说很重要的是网络是多层的并且具有可以通过子网id区分的隧道。

健康检查节点使用所谓的准 IPv6 地址联系平衡器。 准地址是一个 IPv6 地址,其中嵌入了 IPv4 地址和用户子网 ID。 流量到达平衡器,平衡器从中提取 IPv4 资源地址,用 IPv6 替换 IPv4,并将数据包发送到用户网络。

反向流量以同样的方式进行:平衡器发现目的地是健康检查器的灰色网络,并将 IPv4 转换为 IPv6。

VPP——数据平面的核心

平衡器是使用矢量数据包处理 (VPP) 技术实现的,这是 Cisco 的一种用于批量处理网络流量的框架。 在我们的例子中,该框架在用户空间网络设备管理库——数据平面开发套件(DPDK)之上运行。 这确保了较高的数据包处理性能:内核中发生的中断要少得多,并且内核空间和用户空间之间没有上下文切换。 

VPP 更进一步,通过将包组合成批次,从系统中榨取更多性能。 性能提升来自于现代处理器上缓存的积极使用。 同时使用数据缓存(数据包在“向量”中处理,数据彼此接近)和指令缓存:在VPP中,数据包处理遵循图表,其节点包含执行相同任务的函数。

例如,VPP中IP数据包的处理按照以下顺序进行:首先,在解析节点中解析数据包头,然后将它们发送到该节点,该节点根据路由表进一步转发数据包。

有点硬核。 VPP 的作者不容忍处理器缓存使用方面的妥协,因此处理数据包向量的典型代码包含手动向量化:存在一个处理循环,其中处理“队列中有四个数据包”的情况,然后两个相同,然后-一个。 预取指令通常用于将数据加载到缓存中,以加快后续迭代中对数据的访问速度。

n_left_from = frame->n_vectors;
while (n_left_from > 0)
{
    vlib_get_next_frame (vm, node, next_index, to_next, n_left_to_next);
    // ...
    while (n_left_from >= 4 && n_left_to_next >= 2)
    {
        // processing multiple packets at once
        u32 next0 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        u32 next1 = SAMPLE_NEXT_INTERFACE_OUTPUT;
        // ...
        /* Prefetch next iteration. */
        {
            vlib_buffer_t *p2, *p3;

            p2 = vlib_get_buffer (vm, from[2]);
            p3 = vlib_get_buffer (vm, from[3]);

            vlib_prefetch_buffer_header (p2, LOAD);
            vlib_prefetch_buffer_header (p3, LOAD);

            CLIB_PREFETCH (p2->data, CLIB_CACHE_LINE_BYTES, STORE);
            CLIB_PREFETCH (p3->data, CLIB_CACHE_LINE_BYTES, STORE);
        }
        // actually process data
        /* verify speculative enqueues, maybe switch current next frame */
        vlib_validate_buffer_enqueue_x2 (vm, node, next_index,
                to_next, n_left_to_next,
                bi0, bi1, next0, next1);
    }

    while (n_left_from > 0 && n_left_to_next > 0)
    {
        // processing packets by one
    }

    // processed batch
    vlib_put_next_frame (vm, node, next_index, n_left_to_next);
}

因此,健康检查通过 IPv6 与 VPP 进行通信,VPP 将其转换为 IPv4。 这是由图中的节点完成的,我们称之为算法 NAT。 对于反向流量(以及从 IPv6 到 IPv4 的转换),存在相同的算法 NAT 节点。

Yandex.Cloud 中的网络负载均衡器架构

来自平衡器客户端的直接流量通过图节点,这些节点本身执行平衡。 

Yandex.Cloud 中的网络负载均衡器架构

第一个节点是粘性会话。 它存储的哈希值 二元组 对于已建立的会话。 五元组包括传输信息的客户端的地址和端口、可用于接收流量的资源的地址和端口以及网络协议。 

五元组哈希帮助我们在后续一致性哈希节点中执行更少的计算,以及更好地处理平衡器后面的资源列表变化。 当没有会话的数据包到达平衡器时,它会被发送到一致性哈希节点。 这是使用一致性哈希进行平衡的地方:我们从可用的“实时”资源列表中选择一个资源。 接下来,数据包被发送到 NAT 节点,该节点实际上会替换目标地址并重新计算校验和。 正如你所看到的,我们遵循 VPP 的规则 - like to like,将相似的计算分组以提高处理器缓存的效率。

一致的散列

我们为什么选择它?它到底是什么? 首先,让我们考虑前面的任务 - 从列表中选择资源。 

Yandex.Cloud 中的网络负载均衡器架构

通过不一致散列,计算传入数据包的散列,并通过该散列除以资源数量的余数从列表中选择资源。 只要列表保持不变,这个方案就可以正常工作:我们总是将具有相同 5 元组的数据包发送到同一个实例。 例如,如果某些资源停止响应运行状况检查,那么对于哈希的很大一部分,选择将发生变化。 客户端的 TCP 连接将被中断:先前到达实例 A 的数据包可能开始到达实例 B,而实例 B 不熟悉该数据包的会话。

一致性哈希解决了所描述的问题。 解释这个概念的最简单方法是:假设您有一个环,您可以通过哈希(例如,通过 IP:端口)向其分配资源。 选择资源就是转动轮子一个角度,该角度由数据包的哈希值决定。

Yandex.Cloud 中的网络负载均衡器架构

当资源组成发生变化时,这可以最大限度地减少流量重新分配。 删除资源只会影响该资源所在的一致性哈希环部分。 添加资源也会改变分布,但是我们有一个粘性会话节点,它允许我们不将已经建立的会话切换到新资源。

我们研究了平衡器和资源之间引导流量时会发生什么情况。 现在让我们看看返回流量。 它遵循与检查流量相同的模式 - 通过算法 NAT,即通过反向 NAT 44 处理客户端流量,通过 NAT 46 处理运行状况检查流量。 我们坚持自己的方案:我们统一健康检查流量和真实用户流量。

负载均衡器节点和组装组件

VPP 中均衡器和资源的组成由本地服务 - loadbalancer-node 报告。 它订阅来自负载均衡器控制器的事件流,并能够绘制当前 VPP 状态与从控制器接收的目标状态之间的差异。 我们得到了一个封闭的系统:来自 API 的事件到达平衡器控制器,平衡器控制器将任务分配给健康检查控制器以检查资源的“活跃度”。 反过来,将任务分配给运行状况检查节点并聚合结果,然后将它们发送回平衡器控制器。 Loadbalancer-node 订阅来自控制器的事件并更改 VPP 的状态。 在这样的系统中,每个服务只知道邻近服务所需要的内容。 连接数量有限,我们有能力独立运营和扩展不同的细分市场。

Yandex.Cloud 中的网络负载均衡器架构

避免了哪些问题?

我们控制平面中的所有服务都是用 Go 编写的,具有良好的可扩展性和可靠性特征。 Go 有许多用于构建分布式系统的开源库。 我们积极使用 GRPC,所有组件都包含服务发现的开源实现 - 我们的服务监视彼此的性能,可以动态更改其组成,并且我们将其与 GRPC 平衡联系起来。 对于指标,我们还使用开源解决方案。 在数据层面,我们获得了不错的性能和大量的资源储备:事实证明,组装一个可以依靠VPP而不是铁网卡的性能的支架是非常困难的。

问题和解决方法

是什么效果不太好? Go有自动内存管理,但内存泄漏仍然会发生。 处理它们的最简单方法是运行 goroutine 并记住终止它们。 要点:注意 Go 程序的内存消耗。 通常一个好的指标是 goroutine 的数量。 这个故事有一个优点:在 Go 中,很容易获取运行时数据 - 内存消耗、运行的 goroutine 数量以及许多其他参数。

此外,Go 可能不是功能测试的最佳选择。 它们相当冗长,“批量运行 CI 中的所有内容”的标准方法不太适合它们。 事实上,功能测试对资源的要求更高,并且会导致真正的超时。 因此,测试可能会失败,因为 CPU 正忙于单元测试。 结论:如果可能,将“繁重”测试与单元测试分开进行。 

微服务事件架构比单体架构更复杂:收集数十台不同机器上的日志并不是很方便。 结论:如果你制作微服务,请立即考虑跟踪。

我们的计划

我们将推出内部平衡器、IPv6 平衡器,添加对 Kubernetes 脚本的支持,继续对我们的服务进行分片(目前仅对 healthcheck-node 和 healthcheck-ctrl 进行分片),添加新的健康检查,并实现检查的智能聚合。 我们正在考虑使我们的服务更加独立的可能性 - 以便它们不直接相互通信,而是使用消息队列。 云中最近出现了与 SQS 兼容的服务 Yandex 消息队列.

最近,Yandex 负载均衡器公开发布。 探索 文件 到服务中,以方便您的方式管理平衡器并提高项目的容错能力!

来源: habr.com

添加评论