我们知道负载均衡可以分为服务端负载均衡和客户端负载均衡,而服务端负载均衡又按照实现方式的不同可以划分为软件负载均衡和硬件负载均衡,nginx 便是典型的软件负载均衡。
RPC的负载均衡一般不会采用硬件负载均衡或者基于四层代理软件负载均衡,原因如下
- 搭建负载均衡设备或 TCP/IP 四层代理,需要额外成本
- 请求流量都经过负载均衡设备,多经过一次网络传输,会额外浪费一些性能
- 负载均衡添加节点和摘除节点,一般都要手动添加,当大批量扩容和下线时,会有大量的人工操作,“服务发现”在操作上是个问题
- 我们在服务治理的时候,针对不同接口服务、服务的不同分组,我们的负载均衡策略是需要可配的,如果大家都经过这一个负载均衡设备,就不容易根据不同的场景来配置不同的负载均衡策略了。
1. 负载均衡的方法
1.1. 代理模型
客户端并不知道服务端的存在,它所有的请求都打到代理服务,由代理服务去分发到服务端,并且实现公平的负载算法。 客户机可能不可信,这种情况通过用户面向用户的服务,类似于我们的nginx将请求分发到后端机器。
缺点: 客户端不知道后端的存在,且客户端不可信,此模型还好增加RPC的延迟,而且代理服务会影响服务本身的吞吐量
优点: 客户端不需要过多的改造,在中间层做监控等拦截操作非常容易。
1.2. 客户端负载均衡方式(进程内LB(Balancing-aware Client))
客户端知道有多个后端服务,由客户端去选择服务端,并且客户端可以从后端服务器中自己总结出一份负载的信息,实现负载均衡算法。例如,客户端可以包含许多用于从列表中选择服务器的负载均衡策略(循环,随机等)。在此模型中,服务器列表将在客户端中静态配置,由名称解析系统提供,外部负载均衡器等。在任何情况下,客户端都负责从列表中选择首选服务器。
优点:高性能,因为消除了第三方的交互
缺点:会使客户端的代码大大复杂化:客户端需要跟踪服务器负载和健康情况,维护负载均衡策略,这些策略可能相当复杂。有些时候我们还需要以多种语言实现和维护负载均衡策略,而且客户端需要被信任得是靠谱的客户端。
2. 代理模式的实现方式
代理负载均衡可以是 L3/L4(传输级别) 或 L7(应用程序级别)。
在 L3/L4 中,服务器终止TCP连接并打开另一个连接到所选的后端。
L7 只是在客户端连接到服务端端连接之间搞一个应用来做中间人。
L3/L4 级别的负载均衡按设计只做很少的处理,与L7级别的负载均衡相比的延迟会更少,而且更便宜,因为它消耗更少的资源。
在L7(应用程序级)负载平衡中,负载均衡服务终止并解析协议。负载均衡服务可以检查每个请求并根据请求内容分配后端。这就意味监控拦截等操作可以非常方便快捷的做在这里。
L3/L4 vs L7
- 这些连接之间的RPC负载变化很大: 建议使用L7.
- 存储或计算相关性很重要 :建议使用L7,并使用cookie或类似的路由请求来纠正服务端.
- 设备资源少(缺钱): 建议使用 L3/L4.
- 对延迟要求很严格(就是要快): L3/L4.
3. 客户端负载均衡的实现方式
客户端负载均衡的实现有两种:进程内LB(Balancing-aware Client)和 独立 LB 进程(External Load Balancing Service)
3.1. 进程内LB(Balancing-aware Client)
这个较重的客户端将更多的负载均衡逻辑放在客户端中。例如,客户端可以包含许多用于从列表中选择服务端的负载均衡策略(循环,随机等)。客户端还负责跟踪可用的服务器。客户端通常需要集成其他基础设施(如服务发现、名称解析、配额管理)。
这种方法的缺点之一是以多种语言和/或客户端版本编写和维护负载均衡策略。这些策略可能相当复杂。一些算法还需要客户端到服务器通信,因此除了为用户请求发送 RPC 之外,客户端还需要更重,以支持其他 RPC 来获取运行状况或负载信息。
3.2. 独立 LB 进程(External Load Balancing Service)
也叫Lookaside 负载均衡(旁观式)
客户端负载均衡代码保持简单和可移植,实现用于服务端选择的众所周知的算法(例如,循环)。 复杂的负载均衡算法和功能再一个单独的负载均衡服务中实现。客户端只需要查下这个旁观式的负载均衡服务,这个服务就能给你最佳的服务器信息,然后客户端再拿着这个数据去请求相应的服务端。 负载均衡器可以与后端服务器通信以收集负载和健康信息。
4. 负载均衡策略
RPC 负载均衡策略一般包括随机、轮询、权重、Hash等方式。其中的轮询策略应该是我们最常用的一种了,通过随机算法,我们基本可以保证每个节点接收到的请求流量是均匀的;同时我们还可以通过控制节点权重的方式,来进行流量控制。比如我们默认每个节点的权重都是 100,但当我们把其中的一个节点的权重设置成 50 时,它接收到的流量就是其他节点的 1/2。
由于负载均衡机制完全是由 RPC 框架自身实现的,所以它不再需要依赖任何负载均衡设备,自然也不会发生负载均衡设备的单点问题,服务调用方的负载均衡策略也完全可配,同时我们可以通过控制权重的方式,对负载均衡进行治理。
然而,服务端的服务器有可能硬件条件不同
- 如果按低配置的服务器的能力均匀分摊负载,高配置的服务器利用率可能不足
- 如果按高配置的服务器的能力均匀分摊负载,低配置的服务器可能会扛不住
所以我们需要设计一个自适应的负载均衡策略:能根据下游服务器的处理能力来动态、自适应的进行负载均衡。
静态权重可以将流量按比例分摊到不同服务器,但是很多时候服务器的处理能力很难用一个固定的数值量化,所以我们需要自适应的动态权重
一个服务端的处理能力应该由调用方说了算,那么调用方如何判定一个服务端的处理能力呢?
一个简单的方法如下:
- 默认初始处理能力相同,及每次分配给每个服务端的概率相同。
- 每当服务端按时处理一个请求,则认为服务端的处理能力足够,权重动态加上一个值,例如1
- 每当服务端超时处理一个请求,则认为服务端的处理能力可能不足,权重动态减去一个值,这个值应该比权重上升的值要大(例如10)
为了方便权重的处理,可以把权重的范围限定在[0,100]之间,初始值可以设为60,随着时间的推移,处理能力强的服务端成功处理的请求越来越多,就会被分配到更多的流量。
复杂一点的方法:
调用方收集与之建立长连接的每个服务端的指标数据,如服务端的负载指标、CPU 核数、内存大小、请求处理的耗时指标(如请求平均耗时、TP99、TP999)、服务节点的状态指标(如正常、亚健康)。通过这些指标,计算出一个分数,比如总分 10 分,如果 CPU 负载达到 70%,就减去3分,CPU的分数为7分(这个分数只是个例子,需要根据实际情况制定策略)
我们可以为每个指标都设置一个指标权重占比,然后再根据这些指标数据,计算一个最终的分数。我们再通过最终的分数修改服务节点最终的权重。例如给一个服务节点综合打分是 8 分(满分 10 分),服务节点的权重是 100,那么计算后最终权重就是 80(100*80%)。
参考资料
https://www.jianshu.com/p/dd89ef1a645e
https://grpc.io/blog/grpc-load-balancing/
https://www.codercto.com/a/26074.html
《RPC实战与核心原理》