本文将帮助您了解负载均衡在 Kubernetes 中的工作原理、扩展长期连接时会发生什么,以及为什么在使用 HTTP/2、gRPC、RSockets、AMQP 或其他长期协议时应考虑客户端平衡。
关于 Kubernetes 中流量如何重新分配的一些信息
Kubernetes 为部署应用程序提供了两个方便的抽象:服务和部署。
部署描述了在任何给定时间应用程序的运行方式和数量。 每个应用程序都部署为 Pod 并分配一个 IP 地址。
服务在功能上与负载均衡器类似。 它们旨在跨多个 Pod 分配流量。
让我们看看它是什么样子.
- 在下图中,您可以看到同一应用程序的三个实例和一个负载均衡器:
- 负载均衡器称为服务并分配有 IP 地址。 任何传入请求都会重定向到其中一个 Pod:
- 部署场景决定了应用程序的实例数量。 您几乎永远不需要直接在以下位置展开:
- 每个 Pod 都分配有自己的 IP 地址:
将服务视为 IP 地址的集合很有用。 每次访问该服务时,都会从列表中选择一个 IP 地址并用作目标地址。
看起来像这样.
- 服务收到一个curl 10.96.45.152请求:
- 该服务选择三个 pod 地址之一作为目标:
- 流量被重定向到特定的 Pod:
如果您的应用程序由前端和后端组成,那么您将为每个应用程序提供一个服务和一个部署。
当前端向后端发出请求时,它不需要确切知道后端服务了多少个 Pod:可能有 XNUMX 个、XNUMX 个或 XNUMX 个。
此外,前端不知道为后端提供服务的 Pod 的地址。
当前端向后端发出请求时,使用后端服务的IP地址,该IP地址不会改变。
下面是它的外观.
- 1下请求内部后端组件。 它不是为后端选择特定的后端,而是向服务发出请求:
- 该服务选择后端 Pod 之一作为目标地址:
- 流量从 Pod 1 流向 Pod 5,由服务选择:
- Under 1 并不确切知道有多少像 under 5 这样的 pod 隐藏在服务后面:
但该服务到底如何分发请求呢? 好像是用了循环平衡? 让我们弄清楚一下。
Kubernetes 服务中的平衡
Kubernetes 服务不存在。 没有为服务分配 IP 地址和端口的进程。
您可以通过登录集群中的任何节点并运行 netstat -ntlp 命令来验证这一点。
您甚至无法找到分配给该服务的 IP 地址。
服务的IP地址位于控制层,在控制器中,并记录在数据库-etcd中。 另一个组件 - kube-proxy 使用相同的地址。
Kube-proxy 接收所有服务的 IP 地址列表,并在集群中的每个节点上生成一组 iptables 规则。
这些规则规定:“如果我们看到服务的 IP 地址,我们需要修改请求的目标地址并将其发送到其中一个 Pod。”
服务 IP 地址仅用作入口点,侦听该 IP 地址和端口的任何进程都不会提供服务。
让我们看看这个.
- 考虑一个包含三个节点的集群。 每个节点都有 pod:
- 涂成米色的捆绑豆荚是服务的一部分。 由于该服务不作为进程存在,因此显示为灰色:
- 第一个 Pod 请求服务,并且必须转到关联的 Pod 之一:
- 但服务不存在,进程不存在。 它是如何工作的?
- 在请求离开节点之前,它会经过 iptables 规则:
- iptables 规则知道该服务不存在,并将其 IP 地址替换为与该服务关联的 Pod 的 IP 地址之一:
- 该请求收到一个有效的IP地址作为目的地址并正常处理:
- 根据网络拓扑,请求最终到达 pod:
iptables 可以负载均衡吗?
不,iptables 用于过滤,并不是为了平衡而设计的。
但是,可以编写一组像这样工作的规则
这正是 Kubernetes 中实现的。
如果你有三个 pod,kube-proxy 将编写以下规则:
- 以33%的概率选择第一个子,否则进入下一条规则。
- 选择第二个的概率为 50%,否则进入下一条规则。
- 选择下面的第三个。
该系统导致每个 pod 被选中的概率为 33%。
并且不能保证 Pod 2 将在 Pod 1 之后被选中。
注意:iptables使用随机分布的统计模块。 因此,平衡算法是基于随机选择的。
现在您了解了服务的工作原理,让我们看看更有趣的服务场景。
Kubernetes 中的长连接默认情况下不会扩展
从前端到后端的每个 HTTP 请求都由一个单独的 TCP 连接提供服务,该连接可以打开和关闭。
如果前端每秒向后端发送 100 个请求,则打开和关闭 100 个不同的 TCP 连接。
您可以通过打开一个 TCP 连接并将其用于所有后续 HTTP 请求来减少请求处理时间和负载。
HTTP 协议有一个称为 HTTP keep-alive 或连接重用的功能。 在这种情况下,单个 TCP 连接用于发送和接收多个 HTTP 请求和响应:
默认情况下不启用此功能:服务器和客户端都必须进行相应配置。
设置本身很简单,并且适合大多数编程语言和环境。
以下是一些不同语言示例的链接:
如果我们在 Kubernetes 服务中使用 keep-alive 会发生什么?
假设前端和后端都支持 keep-alive。
我们有一份前端副本和三份后端副本。 前端发出第一个请求并打开到后端的 TCP 连接。 请求到达服务,选择后端 Pod 之一作为目标地址。 后端发送响应,前端接收响应。
与 TCP 连接在收到响应后关闭的通常情况不同,它现在保持打开状态以接受进一步的 HTTP 请求。
如果前端向后端发送更多请求会发生什么?
为了转发这些请求,将使用开放的 TCP 连接,所有请求都将发送到第一个请求所在的同一后端。
iptables 不应该重新分配流量吗?
不是在这种情况下。
创建 TCP 连接时,它会通过 iptables 规则,选择流量将前往的特定后端。
由于所有后续请求都位于已打开的 TCP 连接上,因此不再调用 iptables 规则。
让我们看看它是什么样子.
- 第一个 pod 向服务发送请求:
- 你已经知道接下来会发生什么。 该服务不存在,但有 iptables 规则将处理该请求:
- 将选择后端 Pod 之一作为目标地址:
- 请求到达 Pod。 此时,两个 pod 之间将建立持久的 TCP 连接:
- 来自第一个 pod 的任何后续请求都将通过已建立的连接:
结果是更快的响应时间和更高的吞吐量,但您失去了扩展后端的能力。
即使后端有两个 pod,且连接持续,流量也将始终流向其中之一。
这可以解决吗?
由于 Kubernetes 不知道如何平衡持久连接,所以这个任务就落到了你的身上。
服务是称为端点的 IP 地址和端口的集合。
您的应用程序可以从服务获取端点列表,并决定如何在它们之间分发请求。 您可以打开与每个 Pod 的持久连接,并使用循环法平衡这些连接之间的请求。
或者申请更多
负责平衡的客户端代码应遵循以下逻辑:
- 从服务获取端点列表。
- 为每个端点打开持久连接。
- 当需要发出请求时,使用打开的连接之一。
- 定期更新端点列表,创建新端点或在列表发生变化时关闭旧的持久连接。
这就是它的样子.
- 您可以在客户端平衡请求,而不是第一个 pod 将请求发送到服务:
- 您需要编写代码来询问哪些 pod 是服务的一部分:
- 获得列表后,将其保存在客户端并使用它连接到 Pod:
- 您负责负载平衡算法:
现在问题来了:这个问题只适用于HTTP keep-alive吗?
客户端负载均衡
HTTP 不是唯一可以使用持久 TCP 连接的协议。
如果您的应用程序使用数据库,则每次您需要发出请求或从数据库检索文档时,不会打开 TCP 连接。
相反,会打开并使用与数据库的持久 TCP 连接。
如果您的数据库部署在 Kubernetes 上,并且访问作为服务提供,那么您将遇到上一节中描述的相同问题。
一个数据库副本将比其他数据库副本负载更多。 Kube-proxy 和 Kubernetes 不会帮助平衡连接。 您必须注意平衡对数据库的查询。
根据您用来连接数据库的库,您可能有不同的选项来解决此问题。
下面是从 Node.js 访问 MySQL 数据库集群的示例:
var mysql = require('mysql');
var poolCluster = mysql.createPoolCluster();
var endpoints = /* retrieve endpoints from the Service */
for (var [index, endpoint] of endpoints) {
poolCluster.add(`mysql-replica-${index}`, endpoint);
}
// Make queries to the clustered MySQL database
还有许多其他协议使用持久 TCP 连接:
- WebSocket 和安全 WebSocket
- HTTP / 2的
- 远程过程调用
- RSockets
- 空气质量计划
您应该已经熟悉其中的大多数协议。
但如果这些协议如此受欢迎,为什么没有标准化的平衡解决方案呢? 为什么客户端逻辑需要改变? 有原生的 Kubernetes 解决方案吗?
Kube-proxy 和 iptables 旨在涵盖部署到 Kubernetes 时的最常见用例。 这是为了方便。
如果您使用的是公开 REST API 的 Web 服务,那么您很幸运 - 在这种情况下,不使用持久 TCP 连接,您可以使用任何 Kubernetes 服务。
但是一旦开始使用持久 TCP 连接,您就必须弄清楚如何在后端之间均匀分配负载。 Kubernetes 不包含针对这种情况的现成解决方案。
然而,当然有一些选择可以提供帮助。
平衡 Kubernetes 中的长期连接
Kubernetes 中有四种类型的服务:
- 集群IP
- 节点端口
- 负载均衡器
- 无头
前三个服务基于虚拟 IP 地址运行,kube-proxy 使用该地址来构建 iptables 规则。 但所有服务的根本基础是无头服务。
无头服务没有任何与其关联的 IP 地址,仅提供一种用于检索与其关联的 Pod(端点)的 IP 地址和端口列表的机制。
所有服务均基于无头服务。
ClusterIP 服务是一个无头服务,并添加了一些内容:
- 管理层为其分配一个IP地址。
- Kube-proxy 生成必要的 iptables 规则。
这样您就可以忽略 kube-proxy 并直接使用从无头服务获取的端点列表来平衡您的应用程序的负载。
但是我们如何为集群中部署的所有应用程序添加类似的逻辑呢?
如果您的应用程序已经部署,那么此任务似乎是不可能的。 然而,还有一个替代选择。
服务网格将为您提供帮助
您可能已经注意到客户端负载平衡策略是相当标准的。
当应用程序启动时,它:
- 从服务获取 IP 地址列表。
- 打开并维护连接池。
- 通过添加或删除端点定期更新池。
一旦应用程序想要发出请求,它:
- 使用某种逻辑(例如循环)选择可用连接。
- 执行请求。
这些步骤适用于 WebSocket、gRPC 和 AMQP 连接。
您可以将此逻辑分离到一个单独的库中并在您的应用程序中使用它。
但是,您可以使用 Istio 或 Linkerd 等服务网格来代替。
Service Mesh 通过以下流程增强您的应用程序:
- 自动搜索服务IP地址。
- 测试 WebSockets 和 gRPC 等连接。
- 使用正确的协议平衡请求。
Service Mesh 有助于管理集群内的流量,但它非常消耗资源。 其他选项包括使用 Netflix Ribbon 等第三方库或 Envoy 等可编程代理。
如果忽略平衡问题会发生什么?
您可以选择不使用负载平衡,但仍然不会注意到任何变化。 让我们看几个工作场景。
如果您的客户端多于服务器,那么这不是一个大问题。
假设有五个客户端连接到两个服务器。 即使没有平衡,两台服务器都会被使用:
连接可能不均匀分布:可能有四个客户端连接到同一服务器,但很有可能两台服务器都会被使用。
更成问题的是相反的情况。
如果您的客户端较少而服务器较多,您的资源可能未得到充分利用,并且会出现潜在的瓶颈。
假设有两个客户端和五个服务器。 在最好的情况下,五台服务器中的两台将有两个永久连接。
其余服务器将处于空闲状态:
如果这两台服务器无法处理客户端请求,则水平扩展将无济于事。
结论
Kubernetes 服务旨在在大多数标准 Web 应用程序场景中工作。
但是,一旦您开始使用使用持久 TCP 连接的应用程序协议(例如数据库、gRPC 或 WebSocket),服务就不再适合。 Kubernetes 不提供平衡持久 TCP 连接的内部机制。
这意味着您在编写应用程序时必须考虑客户端平衡。
团队准备的翻译
关于该主题还可以阅读什么:
来源: habr.com