基于 Kubernets 简单实现 gRPC 负载均衡

阅读数:1437 2018 年 12 月 10 日 17:26

基于Kubernets简单实现gRPC负载均衡

很多刚刚接触 gRPC 的用户,通常会惊讶于 Kubernetes 默认提供的负载均衡对于 gRPC 来说无法实现开箱即用的效果。比如,将一个简单的基于 Node.js 实现的 gRPC 微服务部署在 Kubernetes 后,如下图所示:

基于Kubernets简单实现gRPC负载均衡

尽管选举服务显示存在多个 pod,但是从 Kubernetes 的 CPU 图表可以清晰的看出,只有一个 pod 能够接收到流量,处于工作状态。这是为什么?

在本文中,会解释该现象对应的原因,以及如何在任意 Kubernetes 应用上修复 gRPC 负载均衡存在的问题。这需要增加名为 Linkerd 的服务网格(service mesh),同时也是一种服务 sidecar。

为什么 gRPC 需要额外的负载均衡设置?

首先需要一起来了解下,为什么 gRPC 需要额外的设置。

gRPC 正逐渐成为应用开发者的常见选择。相比于其他服务协议,如基于 HTTP 的 JSON,gRPC 能够提供更好的特性,包括更低的(反)序列化开销、自动类型检查、格式化 API,以及更小的 TCP 管理开销。

但是,gRPC 也打破了标准的连接层负载均衡约定,也就是 Kubernetes 中默认提供的负载均衡方式。这是因为 gRPC 是基于 HTTP/2 构建,而 HTTP/2 是面向单个 TCP 长连接进行设计的,全部的请求都会复用这一个 TCP 连接。通常情况下,这种方式很棒,因为这样可以减少连接管理的开销。但是,这也意味着(读者也可以想象到)连接层的负载均衡会失效。一旦连接创建之后,就不会再次触发负载均衡。指向某个 pod 的全部请求会封装在相同的连接之中,如下图所示:

基于Kubernets简单实现gRPC负载均衡

为什么 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。因为不能在连接层实现负载均衡,所以需要在应用层来完成。换句话说,需要为每个目标地址创建一个 HTTP/2 连接,对请求实现负载均衡,如下所示:

基于Kubernets简单实现gRPC负载均衡

在网络模型中,这意味着需要实现 L5/L7 层的负载均衡,而不是 L3/L4。这样就需要感知 TCP 连接上传输的协议格式。

如何实现?有这么几种选择。第一种,可以在应用之中手工创建并维护目标地址的连接池,通过配置 gRPC 客户端使用该连接池来实现。这种方式控制起来最灵活,但是在 Kubernetes 这种环境中实现起来非常复杂,因为 Kubernetes 每次进行 pod 层面的调度,连接池都需要进行相应变更。应用需要监控 Kubernetes 的 API,并与 pod 信息保持实时同步。

在 Kubernetes 中,还有另外一种方式,是将服务按照无状态方式进行部署。在该情况下,Kubernetes 会在 DNS 中为服务创建多条记录。如果所使用的 gRPC 客户端足够先进,就能通过上述多个 DNS 记录来实现负载均衡。但是这种实现方式依赖于使用特定的 gRPC 客户端,并且只能使用无状态模式部署服务。

最后,还有第三种方式:使用一个轻量级代理。

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

Linkerd 是一种基于 CNCF 的 Kubernetes 服务网格。Linkerd 通过 sidecar 的方式来实现负载均衡,可以单独部署到某个服务,甚至不需要集群许可。这意味着为服务添加 Linkerd,等价于为每个 pod 添加了一个很小并且很高效的代理,由这些代理来监控 Kubernetes 的 API,并自动实现 gRPC 的负载均衡。具体部署方式如下:

基于Kubernets简单实现gRPC负载均衡

使用 Linkerd 有如下几个好处。首先,Linkerd 支持任何语言下的 gRPC 客户端,也支持每种部署模式(无状态部署或者有状态部署)。这是因为 Linkerd 代理是在传输层完成,会自动对 HTTP/2 和 HTTP/1.x 进行检测,并实现 L7 层的负载均衡,而对其他的流量不做任何处理。这意味着不会产生额外的影响。

其次,Linkerd 负载均衡是非常复杂的。除了需要监听 Kubernetes API,并且在 pod 发生调度后自动更新负载均衡的连接池之外,Linkerd 还会根据响应的延迟,使用指数级权重对请求进行调整,优先将请求发送到相应延迟低的 pod。如果某个 pod 响应很慢,甚至只是偶然抖动,Linkerd 都会将其上的流量摘掉。这样做可以减少端到端的延迟。

最后,Linkerd 基于 rust 实现的代理体积非常小,并且速度快的难以置信。Linkerd 声称百分之 99 的请求开销小于 1ms,并且对于 pod 上物理内存的占用小于 10mb。这意味着 Linkerd 对于系统性能的影响可以忽略不计。

60s 实现 gRPC 负载均衡

测试 Linkerd 非常简单。只需要按照 Linkerd 入门介绍即可。在笔记本上安装 CLI,在集群上安装控制层,然后对服务“网格化”(将代理注入每个 pod)。Linkerd 立刻就能在服务中生效,并实现合理的 gRPC 路由。

安装 Linkerd 之后,再看下选举服务:

基于Kubernets简单实现gRPC负载均衡

可以看到,CPU 图表中每个 pod 都被激活,表示每个 pod 都开始接受流量。而实现这些无需改动任何一行代码。哈哈,Linkerd 就像有魔力一般,实现了 gRPC 负载均衡。

Linkerd 也提供了内置的流量大盘,所以也无需猜测 CPU 图表中的变化究竟代表着什么。下面就是 Linkerd 的大盘,包括每个 pod 的成功率,请求大小,以及延迟。

基于Kubernets简单实现gRPC负载均衡

可以看到每个 pod 每秒大概处理 5 个请求。同时通过大盘还能发现,服务成功率还存在一些问题需要解决。(在示例应用中,特意构造了一些异常,方便为读者演示可以通过 Linkerd 大盘来观察服务质量!)

评论

发布