如何在Kubernetes实现gRPC的负载平衡?


许多新的gRPC用户惊讶地发现Kubernetes的默认负载平衡通常不能与gRPC一起使用。

gRPC需要特殊的负载平衡!

让我们理解为什么我们需要为gRPC做一些特别的事情。
gRPC是应用程序开发人员日益普遍的选择。与诸如JSON-over-HTTP之类的替代协议相比,gRPC可以提供一些显着的好处,包括显着降低(de)序列化成本,自动类型检查,形式化API以及更少的TCP管理开销。
但是,gRPC还打破了标准的连接级负载平衡,包括Kubernetes提供的功能。这是因为gRPC构建在HTTP / 2上,而HTTP / 2被设计为具有单个长期TCP连接,所有请求都被多路复用 - 意味着多个请求可以在任何时间点在同一连接上处于活动状态。通常,这很好,因为它减少了连接管理的开销。但是,它也意味着(正如您可能想象的)连接级平衡不是很有用。一旦建立连接,就不再需要进行平衡。所有请求都将固定到单个目标窗格。

为什么这不会影响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负载平衡
LinkerdKubernetes的CNCF主机服务网格。与我们的目的最相关的是,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的代理非常快且小巧。他们引入<1ms的p99延迟,每个pod需要<10mb的RSS,这意味着对系统性能的影响可以忽略不计。