本文发表于一年多前。旧文章可能包含过时内容。请检查页面中的信息自发布以来是否已变得不正确。

在 Kubernetes 上轻松实现 gRPC 负载均衡

许多 gRPC 新用户惊讶地发现,Kubernetes 的默认负载均衡通常无法与 gRPC 直接配合使用。例如,当您将一个简单的 gRPC Node.js 微服务应用部署到 Kubernetes 上时,就会出现以下情况:

虽然此处显示的 `voting` 服务有多个 Pod,但从 Kubernetes 的 CPU 图表可以清楚地看出,只有一个 Pod 实际在工作——因为只有一个 Pod 接收到任何流量。为什么会这样?

在这篇博客文章中,我们将描述为什么会发生这种情况,以及如何通过使用 Linkerd(一个 CNCF 服务网格和服务 Sidecar)为任何 Kubernetes 应用添加 gRPC 负载均衡来轻松解决此问题。

为什么 gRPC 需要特殊的负载均衡?

首先,让我们了解为什么我们需要为 gRPC 做一些特别的事情。

gRPC 越来越成为应用程序开发人员的常见选择。与 JSON-over-HTTP 等替代协议相比,gRPC 可以提供一些显著优势,包括显著降低(反)序列化成本、自动类型检查、规范化 API 以及更少的 TCP 管理开销。

然而,gRPC 也破坏了标准的连接级负载均衡,包括 Kubernetes 提供的负载均衡。这是因为 gRPC 基于 HTTP/2,而 HTTP/2 的设计理念是使用单个长期存在的 TCP 连接,所有请求都通过该连接进行*多路复用*——这意味着在任何时间点,同一连接上都可以有多个请求处于活动状态。通常情况下,这非常好,因为它减少了连接管理的开销。然而,这也意味着(正如你可能想象的那样)连接级均衡用处不大。一旦连接建立,就不再需要进行均衡了。所有请求都将被固定到单个目标 Pod,如下所示:

为什么这对 HTTP/1.1 没有影响?

这个问题没有发生在 HTTP/1.1 中,HTTP/1.1 也有长连接的概念,原因是 HTTP/1.1 具有一些特性,这些特性自然会导致 TCP 连接的循环。因此,连接级负载均衡“足够好”,对于大多数 HTTP/1.1 应用程序,我们不需要做更多的事情。

为了理解原因,让我们更深入地研究 HTTP/1.1。与 HTTP/2 不同,HTTP/1.1 无法多路复用请求。每个 TCP 连接在任何时候只能有一个 HTTP 请求处于活动状态。客户端发出请求(例如 `GET /foo`),然后等待服务器响应。在这个请求-响应周期发生期间,该连接上不能发出其他请求。

通常,我们希望有大量请求并行发生。因此,为了实现并发的 HTTP/1.1 请求,我们需要建立多个 HTTP/1.1 连接,并通过所有这些连接发出请求。此外,长期存在的 HTTP/1.1 连接通常会在一段时间后过期,并由客户端(或服务器)终止。这两个因素结合在一起意味着 HTTP/1.1 请求通常会在多个 TCP 连接之间循环,因此连接级负载均衡是有效的。

那么我们如何对 gRPC 进行负载均衡呢?

现在回到 gRPC。由于我们不能在连接层面进行均衡,为了实现 gRPC 负载均衡,我们需要从连接均衡转向**请求**均衡。换句话说,我们需要向每个目标打开一个 HTTP/2 连接,并在这些连接之间均衡**请求**,如下所示:

从网络术语来看,这意味着我们需要在 L5/L7 而不是 L3/L4 层面做出决策,也就是说,我们需要理解通过 TCP 连接发送的协议。

我们如何实现这一点?有几种选择。首先,我们的应用程序代码可以手动维护自己的目标负载均衡池,我们可以配置 gRPC 客户端使用此负载均衡池。这种方法提供了最大的控制权,但在 Kubernetes 这样的环境中可能会非常复杂,因为随着 Kubernetes 重新调度 Pod,池会随时间变化。我们的应用程序必须监控 Kubernetes API 并保持与 Pod 的同步。

或者,在 Kubernetes 中,我们可以将应用程序部署为无头服务。在这种情况下,Kubernetes 将为服务创建多个 A 记录到 DNS 条目中。如果我们的 gRPC 客户端足够高级,它可以通过这些 DNS 条目自动维护负载均衡池。但这种方法限制了我们只能使用某些 gRPC 客户端,而且通常不可能只使用无头服务。

最后,我们可以采取第三种方法:使用一个轻量级代理。

使用 Linkerd 在 Kubernetes 上实现 gRPC 负载均衡

Linkerd 是一个由 CNCF 托管的 Kubernetes *服务网格*。与我们的目的最相关的是,Linkerd 还可以作为*服务 Sidecar*,可以应用于单个服务——即使没有集群范围的权限。这意味着,当我们向服务添加 Linkerd 时,它会向每个 Pod 添加一个微小、超快的代理,这些代理会监视 Kubernetes API 并自动进行 gRPC 负载均衡。我们的部署看起来就像这样:

使用 Linkerd 有几个优点。首先,它适用于任何语言编写的服务、任何 gRPC 客户端和任何部署模型(无头或非无头)。由于 Linkerd 的代理是完全透明的,它们会自动检测 HTTP/2 和 HTTP/1.x 并进行 L7 负载均衡,并将所有其他流量作为纯 TCP 传递。这意味着一切都会**正常工作**。

其次,Linkerd 的负载均衡非常复杂。Linkerd 不仅会监视 Kubernetes API 并随着 Pod 重新调度自动更新负载均衡池,而且还会使用**指数加权移动平均**的响应延迟来自动将请求发送到最快的 Pod。如果一个 Pod 速度变慢,即使是暂时性的,Linkerd 也会将流量从它移开。这可以减少端到端尾部延迟。

最后,Linkerd 基于 Rust 的代理速度惊人且体积小巧。它们引入的 p99 延迟小于 1 毫秒,每个 Pod 需要的 RSS 小于 10MB,这意味着对系统性能的影响可以忽略不计。

60 秒内实现 gRPC 负载均衡

Linkerd 非常容易尝试。只需按照Linkerd 入门说明中的步骤操作——在您的笔记本电脑上安装 CLI,在您的集群上安装控制平面,然后“网格化”您的服务(将代理注入每个 Pod)。您将很快在您的服务上运行 Linkerd,并应立即看到正确的 gRPC 均衡。

让我们再次看看我们的示例 `voting` 服务,这次是在安装 Linkerd 之后:

正如我们所看到的,所有 Pod 的 CPU 图表都处于活动状态,这表明所有 Pod 现在都在处理流量——无需更改一行代码。瞧,gRPC 负载均衡就像魔术一样!

Linkerd 还提供了内置的流量仪表板,因此我们不再需要从 CPU 图表中猜测发生了什么。以下是 Linkerd 图表,显示了每个 Pod 的成功率、请求量和延迟百分位数:

我们可以看到每个 Pod 正在获得大约 5 RPS。我们还可以看到,虽然我们已经解决了负载均衡问题,但我们还需要在服务成功率方面做一些工作。(演示应用程序故意设置了故障——作为读者的练习,看看你是否可以通过使用 Linkerd 仪表板来找出它!)

总结

如果您有兴趣以一种简单易行的方式为您的 Kubernetes 服务添加 gRPC 负载均衡,无论它使用何种语言编写、使用何种 gRPC 客户端或如何部署,您都可以使用 Linkerd 通过几个命令添加 gRPC 负载均衡。

Linkerd 还有很多其他功能,包括安全性、可靠性、调试和诊断功能,但这些将在未来的博客文章中讨论。

想了解更多吗?我们非常欢迎您加入我们快速壮大的社区!Linkerd 是一个 CNCF 项目,托管在 GitHub 上,并在 SlackTwitter邮件列表上拥有一个活跃的社区。快来加入我们吧!