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

发布于2019-04-21 20:40:22

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

尽管选举服务显示存在多个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的全部请求会封装在相同的连接之中,如下图所示:

为什么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连接,对请求实现负载均衡,如下所示:

在网络模型中,这意味着需要实现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的负载均衡。具体部署方式如下:

使用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之后,再看下选举服务:

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

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

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