SpringCloud LoadBalancerClient 负载均衡原理

x33g5p2x  于2022-02-14 转载在 Spring  
字(5.2k)|赞(0)|评价(0)|浏览(402)

LoadBalancerClient 是 SpringCloud 提供的一种负载均衡客户端,Ribbon 负载均衡组件内部也是集成了 LoadBalancerClient 来实现负载均衡。那么 LoadBalancerClient 内部到底是如何做到的呢?我们先大概讲一下 LoadBalancerClient 的实现原理,然后再深入源码中进行解析。

LoadBalancerClient 在初始化时会通过 Eureka Client 向 Eureka 服务端获取所有服务实例的注册信息并缓存在本地,并且每10秒向 EurekaClient 发送 “ping”,来判断服务的可用性。如果服务的可用性发生了改变或者服务数量和之前的不一致,则更新或者重新拉取。最后,在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡(默认策略为轮询)。

当使用 LoadBalancerClient 进行远程调用的负载均衡时,LoadBalancerClient 先通过目标服务名在本地服务注册清单中获取服务提供方的某一个实例,比如订单服务需要访问商品服务,商品服务有3个节点,LoadBalancerClient 会通过 choose() 方法获取到3个节点中的一个服务,拿到服务的信息之后取出服务IP信息,就可以得到完整的想要访问的IP地址和接口,最后通过 RestTempate 访问商品服务。

深入解析 LoadBalancerClient 接口源码:

1、LoadBalancerClient 源码解析:

LoadBalancerClient 是 Spring Cloud 提供的一个非常重要的接口,它继承自 ServiceInstanceChooser 接口,该接口的实现类是 RibbonLoadBalanceClient,它们之间的关系如下图所示:

(1)LoadBalancerClient 接口源码:

首先我们开始追踪 LoadBalancerClient 源码:

public interface LoadBalancerClient extends ServiceInstanceChooser 
{
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;
     
    <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException;
    
    URI reconstructURI(ServiceInstance instance, URI original);
}

可以发现 LoadBalancerClient 接口继承了 ServiceInstanceChooser 接口,主要的方法为2个 execute() 方法,均是用来执行请求的。还有个 reconstructURI() 是用来重构URL的。

(2)ServiceInstanceChooser 接口源码:

继续查看 LoadBalancerClient 继承的 ServiceInstanceChooser 接口源码,具体如下:

public interface ServiceInstanceChooser 
{
	ServiceInstance choose(String serviceId);
}

ServiceInstanceChooser 接口中的主要方法为 choose(),该方法用于根据服务的名称 serviceId 来选择其中一个服务实例,即根据 serviceId 获取ServiceInstance。

(3)RibbonLoadBalanceClient 实现类源码:

接下来我们看看 LoadBalancerClient 的实现类 RibbonLoadBalanceClient,它用来执行最终的负载均衡请求。其中,RibbonLoadBalanceClient 的一个 choose() 方法用于选择具体的服务实例,其内部是通过 getServer() 方法交给 ILoadBalancer 完成的。我们先看下 RibbonLoadBalanceClient 里面几个重要实现方法的源码:

① 第一个:choose(),用来选择具体的服务实例。

@Override
	public ServiceInstance choose(String serviceId) {
		return choose(serviceId, null);
	}

	/**
	 * New: Select a server using a 'key'.
	 * @param serviceId of the service to choose an instance for
	 * @param hint to specify the service instance
	 * @return the selected {@link ServiceInstance}
	 */
	public ServiceInstance choose(String serviceId, Object hint) {
		Server server = getServer(getLoadBalancer(serviceId), hint);
		if (server == null) {
			return null;
		}
		return new RibbonServer(serviceId, server, isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
	}

② 第二个:getServer(),获取实例。

protected Server getServer(ILoadBalancer loadBalancer) {
		return getServer(loadBalancer, null);
	}

	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
        // 最终通过 loadBalancer 去做服务实例的选择。
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

可以看到最终通过 loadBalancer 去做服务实例的选择。那我们下面就看下 loadBalancer 是怎么怎么实现服务实例的选择的?

(4)BaseLoadBalancer 源码:

我们从上面能看到 ILoadBalancer 中的 chooseServer 方法里面默认值为 default,进入ILoadBalancer 实现类 BaseLoadBalancer 的 chooseServer() 看下:

我们的 key 的值为“default”,那么这个 key 代表的是什么意思呢?进去 rule 对象里面看下:

可以看到这个 rule 是 IRule 接口声明出来的,且默认定义的实现类是 RoundRobinRule(),也就是轮询策略。那我们接下来看下 IRule 接口:

(5)IRule 接口源码:

public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

我们可以看到 IRule 接口定义了3个方法,choose() 是用来选择实例的,setLoadBalancer() 和 getLoadBalance() 用来设置和获取 ILoadBalancer 的。那么接下来 IRule 接口有多少个实现类:

(1)随机策略 RandomRule:随机数选择服务列表中的服务节点Server,如果当前节点不可用,则进入下一轮随机策略,直到选到可用服务节点为止

(2)轮询策略 RoundRobinRule:按照接收的请求顺序,逐一分配到不同的后端服务器

(3)重试策略 RetryRule:在选定的负载均衡策略机上重试机制,在一个配置时间段内当选择Server不成功,则一直尝试使用 subRule 的方式选择一个可用的server;

(4)可用过滤策略 PredicateBaseRule:过滤掉连接失败 和 高并发连接 的服务节点,然后从健康的服务节点中以线性轮询的方式选出一个节点返回

(5)响应时间权重策略 WeightedRespinseTimeRule:根据服务器的响应时间分配一个权重weight,响应时间越长,weight越小,被选中的可能性越低。主要通过后台线程定期地从 status 里面读取平均响应时间,为每个 server 计算一个 weight

(6)并发量最小可用策略 BestAvailableRule:选择一个并发量最小的服务节点 server。ServerStats 的 activeRequestCount 属性记录了 server 的并发量,轮询所有的server,选择其中 activeRequestCount 最小的那个server,就是并发量最小的服务节点。该策略的优点是可以充分考虑每台服务节点的负载,把请求打到负载压力最小的服务节点上。但是缺点是需要轮询所有的服务节点,如果集群数量太大,那么就会比较耗时。

(7)区域权重策略 ZoneAvoidanceRule:综合判断 server 所在区域的性能 和 server 的可用性,使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 来判断是否选择某个server,前一个判断判定一个zone的运行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate 用于过滤掉连接数过多的Server。

同样的,如果我们也可以通过实现 IRule 接口来自定义一个负载均衡策略。

2、ILoadBalancer 源码解析:

ILoadBalancer 是一个接口,该接口定义了一系列实现负载均衡的方法,LoadBalancerClient 的实现类 RibbonLoadBalanceClient 也将负载均衡的具体实现交给了 ILoadBalancer 来处理,ILoadBalancer 通过配置 IRule、IPing 等,向 EurekaClient 获取注册列表信息,默认每10秒向 EurekaClient 发送一次 “ping”,进而检查是否需要更新服务的注册列表信息。最后,在得到服务注册列表信息后,ILoadBalancer 根据 IRule 的策略进行负载均衡。ILoadBalancer 接口的实现类结果如下图所示:

查看 BaseLoadBalancer 和 DynamicServerListLoadBalancer 源码,默认情况下实现了以下配置:

(1)IClientConfig clientConfig:用于配置负载均衡客户端,默认实现类是 DefaultClientConfigImpl。

(2)IRule rule:用于配置负载均衡的策略,默认使用的是 RoundRobinRule 轮询策略。

(3)IPing ping:用于检查当前服务是否有响应,从而判断当前服务是否可用,默认实现类是 DummyPing,该实现类的 isAlive() 方法返回值是 true,默认所有服务实例都是可用的。

(4)ServerList serverList:用于获取所有 Server 注册列表信息。通过跟踪源码会发现,ServerList 的实现类是 DiscoveryEnabledNIWSServerList,该类定义的 obtainServersViaDiscovery() 方法是根据 eurekaClientProvider.get() 方法获取 EurekaClient,再根据 EurekaClient 获取服务注册列表信息。EurekaClient 的实现类是DiscoveryClient,DiscoveryClient 具有服务注册、获取服务注册列表等功能。

(5)ServerListFilter filter:定义了根据配置过滤或者动态获取符合条件的服务列表,默认实现类是 ZonePreferenceServerListFilter,该策略能够优先过滤出与请求调用方处于同区域的服务实例。

相关文章