深入理解gRPC服务发现与负载均衡
深入理解gRPC服务发现与负载均衡
在现代微服务架构中,服务发现与负载均衡是确保系统高可用性和性能的关键技术。本文将深入探讨gRPC框架中服务发现与负载均衡的实现原理,包括不同类型的负载均衡方案、具体算法以及如何在gRPC中进行定制化配置。
前言
在后台服务开发中,高可用性是构建中核心且重要的一环。服务发现(Service discovery)和负载均衡(Load Balance)一直都是我关注的话题。今天来谈一下我在实际中是如何理解及落地的。
负载均衡 && 服务发现
基础
负载均衡 ,顾名思义,是通过某种手段将流量 / 请求分配到不通的服务器上去,保证后台的每个服务收到的请求都尽可能保持平衡
服务发现 ,就是指客户端按照某种约定的方式主动去(注册中心)寻找服务,然后再连接相应的服务
关于负载均衡的构建与实现,可以看下这几篇文章:
- gRPC 服务发现 & 负载均衡
- gRPC Load Balancing
- Load Balancing in gRPC
服务发现概念
我们说的服务发现,一般理解为客户端如何发现 (并连接到) 服务,这里一般包含三个组件:
- 服务消费者:一般指客户端(可以是简单的 TCP-Client 或者是 RPC-Client )
- 服务提供者:一般指服务提供方,如传统服务,微服务等
- 服务注册中心:用来存储(Key-Value)服务提供者的服务,一般以 DNS/HTTP/RPC 等方式对外暴露接口
负载均衡概念
我们把 LB 看作一个组件,根据组件位置的不同,大致上分为三种:
- 集中式 LB(Proxy Model)
独立的 LB, 可以是硬件实现,如 F5,或者是 nginx 这种内置 Proxy-pass 或者 upstream 功能的网关,亦或是 LVS/HAPROXY,之前也使用DPDK开发过类似的专用网关。
- 进程内 LB(Balancing-aware Client)
进程内 LB(集成到客户端),此方案将 LB 的功能集成到服务消费方进程里,也被称为软负载或者客户端负载方案。服务提供方启动时,首先将服务地址注册到服务注册表,同时定期报心跳到服务注册表以表明服务的存活状态,相当于健康检查,服务消费方要访问某个服务时,它通过内置的 LB 组件向服务注册表查询,同时缓存并定期刷新目标服务地址列表,然后以某种负载均衡策略选择一个目标服务地址,最后向目标服务发起请求。LB 和服务发现能力被分散到每一个服务消费者的进程内部,同时服务消费方和服务提供方之间是直接调用,没有额外开销,性能比较好。
- 独立 LB 进程(External Load Balancing Service)
该方案是针对上一种方案的不足而提出的一种折中方案,原理和第二种方案基本类似。不同之处是将 LB 和服务发现功能从进程内移出来,变成主机上的一个独立进程。主机上的一个或者多个服务要访问目标服务时,他们都通过同一主机上的独立 LB 进程做服务发现和负载均衡。该方案也是一种分布式方案没有单点问题,一个 LB 进程挂了只影响该主机上的服务调用方,服务调用方和 LB 之间是进程内调用性能好,同时该方案还简化了服务调用方,不需要为不同语言开发客户库,LB 的升级不需要服务调用方改代码。 公司的 L5 是这种方式,每台机器上都安装了 L5 的 agent,供其他服务调用。该方案主要问题:部署较复杂,环节多,出错调试排查问题不方便。
gRPC 内置的方案
gRPC 的内置方案如下图所示:
gRPC 在官网文档中提供了实现 LB 的思路,并在不同语言的 gRPC 代码 API 中已提供了命名解析和负载均衡接口供扩展。默认提供了 DNS-resolver 的实现,接口相当规范,实现起来也不复杂,只需要实现服务注册(Registry)和服务监听 + 解析(Watcher+Resolver)的逻辑就行了,这里简单介绍其基本实现过程:
- 构建注册中心,这里注册中心一般要求具备分布式一致性(满足 CAP 定理的 AP 或 CP)的高可用的组件集群,如 Zookeeper、Consul、Etcd 等
- 构建 gRPC 服务端的注册逻辑,服务启动后定时向注册中心注册自身的关键信息(一般开启新的 groutine 来完成),至少包含 IP 和端口,其他可选信息,如自身的负载信息(CPU 和 Memory)、当前实时连接数等,这些辅助信息有助于帮助系统更好的执行 LB 算法
- gRPC 客户端向注册中心发出服务解析请求,注册中心将请求中关联的所有服务的信息返回给 gRPC 客户端,客户端与所有在线的服务建立起 HTTP2 长连接
- gRPC 客户端发起 RPC 调用,根据 LB 均衡器中实现的负载均衡策略(gRPC 中默认提供的算法是 RoundRobin),选择其中一 HTTP2 长连接进行通信,即 LB 策略决定哪个子通道 - 即哪个 gRPC 服务器将接收请求
gRPC 负载均衡的运行机制
gRPC 提供了负载均衡实现的用户侧接口,我们可以非常方便的定制化业务的负载均衡策略,为了理解 gRPC 的负载均衡的实现机制,后续博客中我会分析下
gRPC
实现负载均衡的代码。
- Resolver
- 解析器,用于从注册中心实时获取当前服务端的列表,同步发送给 Balancer
- Balancer
- 平衡器,一是接收从 Resolver 发送的服务端列表,建立并维护(长)连接状态;二是每次当 Client 发起 Rpc 调用时,按照一定算法从连接池中选择一个连接进行 Rpc 调用
- Register
- 注册,用于服务端初始化和在线时,将自己信息上报到注册中心,主要信息有 Ip,端口等
负载均衡的算法及实现
在实践中,如何选取负载均衡策略是一个很有趣的话题,例如 Nginx 的
upstream
机制中就有很多经典的 LB 策略,如带权重的轮询Weight-RoundRobin,一般常用的负载均衡方法有如下几种:
RoundRobin(轮询)
Weight-RoundRobin(加权轮询)
- 不同的后端服务器可能机器的配置和当前系统的负载并不相同,因此它们的抗压能力也不相同。给配置高、负载低的机器配置更高的权重,而配置低、负载高的机器,给其分配较低的权重,降低其系统负载,加权轮询能很好地处理这一问题,并将请求顺序且按照权重分配到后端。
Random(随机)
Weight-Random(加权随机)
- 通过系统的随机算法,根据后端服务器的列表随机选取其中的一台服务器进行访问
- 源地址哈希法
- 源地址哈希的思想是根据获取客户端的 IP 地址,通过哈希函数计算得到的一个数值,用该数值对服务器列表的大小进行取模运算,得到的结果便是客服端要访问服务器的序号。采用源地址哈希法进行负载均衡,同一 IP 地址的客户端,当后端服务器列表不变时,它每次都会映射到同一台后端服务器进行访问
- 最小连接数法
- 最小连接数算法比较灵活和智能,由于后端服务器的配置不尽相同,对于请求的处理有快有慢,它是根据后端服务器当前的连接情况,动态地选取其中当前积压连接数最少的一台服务器来处理当前的请求,尽可能地提高后端服务的利用效率,将负责合理地分流到每一台服务器
- 一致性哈希算法
- 常见的是
Ketama
算法,该算法是用来解决
cache
失效导致的缓存穿透的问题的,当然也可以适用于 gRPC 长连接的场景
gRPC 服务治理的优势
在现网环境中,后端服务就是采用了 gRPC 与 Etcd 的服务治理方案,总结下有这么几个优点;
- 采用了 gRPC 实现负载均衡策略,模块之间通信采用长连接方式,避免每次 RPC 调用时新建连接的开销,充分发挥
HTTP2
的优势
扩容和缩容都及其方便,例如扩容,只要部署上服务,运行后,服务成功注册到 Etcd 便大功告成
灵活的自定义的 LB 算法,使得后端压力更为均衡
客户端加入重试逻辑,使得网络抖动情况下,可以通过重试连接上另外一台服务
Resolver 暴露的三个接口
前文说过,gRPC 内置的服务治理功能,对开发者暴露了服务发现的
interface{}
,
resolver.Builder
和
resolver.ClientConn
和
resolver.Resolver
,相关代码。开发者在实例化这三个接口之后,就可以实现从指定的 scheme 中获取服务列表,通知 balancer 并与这些服务端建立 RPC 长连接。
resolver.Builder
resolver.ClientConn
resolver.Resolver
- resolver.Builder
Builder
用于 gRPC 内部创建
Resolver
接口的实现,但注意内部声明的
Build()
方法将接口
ClientConn
作为参数传入了,在前文的分析中,我们了解到
ClientConn
结库是非常重要的结构,其成员
conns map[*addrConn]struct{}
中维护了所有从注册中心获取到的服务端列表。
// Builder creates a resolver that will be used to watch name resolution updates.
type Builder interface {
// Build creates a new resolver for the given target.
//
// gRPC dial calls Build synchronously, and fails if the returned error is
// not nil.
Build(target Target, cc ClientConn, opts BuildOption) (Resolver, error)
// Scheme returns the scheme supported by this resolver.
// Scheme is defined at https://github.com/grpc/grpc/blob/master/doc/naming.md.
Scheme() string
}
- resolver.ClientConn
ClientConn
接口中,
UpdateState
方法需要传入
State
结构,