本文发布时间已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。
Kubernetes 上无泪的 gRPC 负载均衡
许多新的 gRPC 用户惊讶地发现 Kubernetes 的默认负载均衡通常无法直接与 gRPC 一起工作。例如,当您采用一个 简单的 gRPC Node.js 微服务应用程序并将其部署在 Kubernetes 上时,会发生以下情况
尽管此处显示的 voting
服务有多个 Pod,但从 Kubernetes 的 CPU 图中可以清楚地看到,只有一个 Pod 正在实际工作,因为只有一个 Pod 正在接收任何流量。为什么?
在这篇博客文章中,我们将描述为什么会发生这种情况,以及如何通过将 gRPC 负载均衡添加到任何带有 Linkerd(一个 CNCF 服务网格和服务边车)的 Kubernetes 应用程序中来轻松解决它。
为什么 gRPC 需要特殊的负载均衡?
首先,让我们了解为什么我们需要为 gRPC 做一些特殊的事情。
gRPC 是应用程序开发人员越来越常见的选择。与诸如基于 JSON 的 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 将在服务的 DNS 条目中创建多个 A 记录。如果我们的 gRPC 客户端足够高级,它可以自动从这些 DNS 条目中维护负载均衡池。但是,这种方法将我们限制为某些 gRPC 客户端,并且很少有可能仅使用无头服务。
最后,我们可以采用第三种方法:使用轻量级代理。
使用 Linkerd 在 Kubernetes 上进行 gRPC 负载均衡
Linkerd 是一个 CNCF 托管的 Kubernetes 服务网格。与我们的目的最相关的是,Linkerd 还充当服务边车,可以将其应用于单个服务,即使没有集群范围的权限也是如此。这意味着,当我们将 Linkerd 添加到服务中时,它会将一个微小的,超快的代理添加到每个 Pod,这些代理会监视 Kubernetes API 并自动执行 gRPC 负载均衡。然后,我们的部署将如下所示
使用 Linkerd 有几个优点。首先,它可以与以任何语言编写的服务,任何 gRPC 客户端以及任何部署模型(无头或非无头)一起使用。由于 Linkerd 的代理是完全透明的,因此它们会自动检测 HTTP/2 和 HTTP/1.x 并执行 L7 负载均衡,并且它们将所有其他流量作为纯 TCP 传递。这意味着一切都将正常工作。
其次,Linkerd 的负载均衡非常复杂。Linkerd 不仅会监视 Kubernetes API 并在重新调度 Pod 时自动更新负载均衡池,而且 Linkerd 还使用响应延迟的指数加权移动平均值来自动将请求发送到最快的 Pod。如果一个 Pod 速度减慢,即使是暂时性的,Linkerd 也会将流量从其转移出去。这可以减少端到端尾部延迟。
最后,Linkerd 基于 Rust 的代理速度非常快且很小。它们引入了小于 1 毫秒的 p99 延迟,并且每个 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 上,并在 Slack、Twitter 和邮件列表上拥有一个蓬勃发展的社区。快来加入乐趣吧!