本文发布已超过一年。较旧的文章可能包含过时内容。请检查页面信息自发布以来是否仍然准确。

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

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

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

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

为什么 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 中,我们可以将应用部署为无头服务(headless services)。在这种情况下,Kubernetes 会在服务的 DNS 条目中创建多个 A 记录。如果我们的 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 重新调度时自动更新负载均衡池,Linkerd 还使用响应延迟的指数加权移动平均值来自动将请求发送到最快的 Pod。如果某个 Pod 即使只是暂时变慢,Linkerd 也会将流量从该 Pod 转移开。这可以降低端到端尾部延迟。

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

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邮件列表上拥有一个活跃的社区。快来加入我们吧!