【SpringCloud】03 Ribbon负载均衡原理与饿加载机制

阅读提示

本文是SpringCloud系列第三篇,请先阅读前置文章。

一、Ribbon负载均衡的流程

上一节中,我们给RestTemplate添加了@LoadBalanced注解,即可以实现负载均衡功能,这是什么原理呢?其实底层是基于Ribbon组件进行负载均衡,Ribbon通过拦截客户端请求,动态地选择服务实例,从而实现了请求分发。
Ribbon 实现负载均衡的流程图如下:
在这里插入图片描述
用户发送一个请求http://userservice/user/1,Ribbon负载均衡器对这条url进行解析,获取到服务名称为userservice,再根据这个服务名称到eureka-server注册中心去获取userservice服务实例的ip列表,微服务模式下,一个服务有多个实例,因此Ribbon根据当前的负载均衡策略,将请求分发到其中的一个实例。

也就是说,Ribbon是一个与SpringCloud集成的组件,实现了以下功能:
1.负载均衡:Ribbon可以将客户端请求均匀地分发到多个服务实例上,以实现负载均衡。它支持多种负载均衡算法,如轮询、随机、加权轮询等。

2.故障转移与容错:当某个服务实例发生故障或不可用时,Ribbon能够自动忽略故障实例,并将请求转发给其他健康的实例,以提高系统的可用性和容错能力。

3.服务发现:Ribbon可以与服务注册中心集成,通过查询服务注册中心获取可用的服务实例列表,并根据负载均衡策略选择目标实例。

二、源码分析

2.1 LoadBalancerIntercepor

Ribbon实现负载均衡是基于LoadBalancerInterceptor这个拦截器类,我们可以在IDEA中查看它的代码:

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;

	private LoadBalancerRequestFactory requestFactory;

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,
			LoadBalancerRequestFactory requestFactory) {
		this.loadBalancer = loadBalancer;
		this.requestFactory = requestFactory;
	}

	public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		// for backwards compatibility
		this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
	}

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null,
				"Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName,
				this.requestFactory.createRequest(request, body, execution));
	}

}

从名称上来看LoadBalancerInterceptor翻译过来是负载均衡拦截器,这个类实现了ClientHttpRequestInterceptor客户端http拦截器这个接口,可以看到ClientHttpRequestInterceptor接口里就只有这一个intercept方法。

@FunctionalInterface
public interface ClientHttpRequestInterceptor {
    ClientHttpResponse intercept(HttpRequest var1, byte[] var2, ClientHttpRequestExecution var3) throws IOException;
}

回到LoadBalancerInterceptor这个类的代码中,我们发现这里的intercept拦截器方法主要做了以下几件事:
1.获取请求的url,例如http://userserice/user/1
2.获取请求的host,其实就是服务名称,http://userserice/user/1的host就是userservice
3.判断服务名称不为空
4.调用this.loadBalancer.execute()方法继续处理处理服务名称和用户请求

	@Override
	public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
			final ClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();  //获取用户请求的url
		String serviceName = originalUri.getHost(); //获取请求的host 即服务名称
		Assert.state(serviceName != null, //判断服务名称不为空
				"Request URI does not contain a valid hostname: " + originalUri);
		return this.loadBalancer.execute(serviceName, //调用loadBalancer进一步处理
				this.requestFactory.createRequest(request, body, execution));
	}

2.2 LoadBalancerClient

这里的this.loadBalancer是初始化时注入的类LoadBalancerClient,译名负载均衡客户端,我们进入这个类,发现是一个接口,因此this.loadBalancer中注入的类一定是实现了LoadBalancerClient接口的具体类,从这个接口中的方法声明不难发现,该接口就是用于根据服务名称和请求来做负载均衡的。

public interface LoadBalancerClient extends ServiceInstanceChooser {

	/**
	 * Executes request using a ServiceInstance from the LoadBalancer for the specified
	 * service.
	 * @param serviceId The service ID to look up the LoadBalancer.
	 * @param request Allows implementations to execute pre and post actions, such as
	 * incrementing metrics.
	 * @param <T> type of the response
	 * @throws IOException in case of IO issues.
	 * @return The result of the LoadBalancerRequest callback on the selected
	 * ServiceInstance.
	 */
	<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;

	/**
	 * Executes request using a ServiceInstance from the LoadBalancer for the specified
	 * service.
	 * @param serviceId The service ID to look up the LoadBalancer.
	 * @param serviceInstance The service to execute the request to.
	 * @param request Allows implementations to execute pre and post actions, such as
	 * incrementing metrics.
	 * @param <T> type of the response
	 * @throws IOException in case of IO issues.
	 * @return The result of the LoadBalancerRequest callback on the selected
	 * ServiceInstance.
	 */
	<T> T execute(String serviceId, ServiceInstance serviceInstance,
			LoadBalancerRequest<T> request) throws IOException;

	/**
	 * Creates a proper URI with a real host and port for systems to utilize. Some systems
	 * use a URI with the logical service name as the host, such as
	 * http://myservice/path/to/service. This will replace the service name with the
	 * host:port from the ServiceInstance.
	 * @param instance service instance to reconstruct the URI
	 * @param original A URI with the host as a logical service name.
	 * @return A reconstructed URI.
	 */
	URI reconstructURI(ServiceInstance instance, URI original);

}

有两个类实现了该接口:
第一个是SpringCloud自带的负载均衡器,第二个是Ribbon
在这里插入图片描述
我们进入第二个类RibbonLoadBalancerClient,与execute()方法有关的代码如下:

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
        return this.execute(serviceId, (LoadBalancerRequest)request, (Object)null);
    }

    public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        Server server = this.getServer(loadBalancer, hint);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonServer ribbonServer = new RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
        }
    }

    public <T> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<T> request) throws IOException {
        Server server = null;
        if (serviceInstance instanceof RibbonServer) {
            server = ((RibbonServer)serviceInstance).getServer();
        }

        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonLoadBalancerContext context = this.clientFactory.getLoadBalancerContext(serviceId);
            RibbonStatsRecorder statsRecorder = new RibbonStatsRecorder(context, server);

            try {
                T returnVal = request.apply(serviceInstance);
                statsRecorder.recordStats(returnVal);
                return returnVal;
            } catch (IOException var8) {
                statsRecorder.recordStats(var8);
                throw var8;
            } catch (Exception var9) {
                statsRecorder.recordStats(var9);
                ReflectionUtils.rethrowRuntimeException(var9);
                return null;
            }
        }
    }

很有趣的事情,这里竟然一下子重载了三个execute方法,我们的拦截器intercept方法调用的是第一个execute方法,而这个execute方法会直接调用第二个execute方法,并多给出一个Object hint参数,这个参数看字面意思是用于后续指示选择服务实例的提示信息,也就是负载均衡的规则。
(但是我奇怪的是这里为什么要这样设计,拦截器调用execute方法时只需要传入请求名和request,而Object hint参数每次都传null值,好像没啥意义呀,这里留个悬念。)
第二个execute方法中,getLoadBalancer(serviceId)是用服务名称去eureka获取服务列表;getServer(loadBalancer, hint)则是根据负载均衡规则,去服务列表中选取一个服务实例的地址。例如127.0.0.1:8081,接下来就是调用第三个execute方法来真正完全分发请求的步骤。

2.3 负载均衡策略IRule

进入getServer(loadBalancer, hint)方法:

    protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }

发现loadBalancer.chooseServer()这个方法传入了hint参数,即负载均衡规则。
继续跟进loadBalancer.chooseServer()方法,是一个接口,有三个实现方法,经排查是第一个,因为其他两个都返回server为空,很显然server为空的时候,第二个execute方法会抛异常,如果不放心可以前端发一条指令,全程debug模式。
在这里插入图片描述
进入BaseLoadBalancer的chooseServer()方法中:

    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }

这里是根据rule的choose方法,来选择服务实例,点击rule:

    private final static IRule DEFAULT_RULE = new RoundRobinRule();
    private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
    private static final String DEFAULT_NAME = "default";
    private static final String PREFIX = "LoadBalancer_";

    protected IRule rule = DEFAULT_RULE;

可以看到,rule的模型类型是RoundRobinRule类,进入这个类:

/**
 * The most well known and basic load balancing strategy, i.e. Round Robin Rule.
 */
public class RoundRobinRule extends AbstractLoadBalancerRule {
//略
}

通过注释就可以发现,rule是负载均衡的方法,而默认的rule规则是轮询Round Robin Rule。
而轮询负载均衡的具体实现则是在RoundRobinRule类的public Server choose()方法当中,这里的key就是之前execute方法中构造的hint参数,居然完全没有使用到,那么我猜测只有一种可能性,旧版本是使用Object hint这个参数来指定负载均衡方法的,后来为了方便用户调用和修改负载均衡方法,这个参数就不用了默认传null,转而使用IRule接口,同时修改这么多代码的接口过于麻烦,就一直保留了这个参数。

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }

        Server server = null;
        int count = 0;
        while (server == null && count++ < 10) {
            List<Server> reachableServers = lb.getReachableServers();
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();

            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }

            int nextServerIndex = incrementAndGetModulo(serverCount);
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }

            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

总结:
在这里插入图片描述
我们总结一下Ribbon组件的负载均衡执行流程,首先负载均衡拦截器拦截请求,获取服务为名称,接着RibbonLoadBalancerClient中的execute方法会调用getLoadBalancer()根据user-service到eureka拉取服务列表;获取到服务列表后,根据负载均衡规则IRule进行请求分发,把请求发到一个实例中。

三、自定义负载均衡策略

负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类:
在这里插入图片描述
不同规则的含义如下:

内置负载均衡规则类规则描述
RoundRobinRule简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。
AvailabilityFilteringRule对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。
WeightedResponseTimeRule为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。
ZoneAvoidanceRule(默认)以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询
BestAvailableRule忽略那些短路的服务器,并选择并发数较低的服务器。
RandomRule随机选择一个可用的服务器。
RetryRule重试机制的选择逻

默认的实现就是ZoneAvoidanceRule,是一种高级一点的轮询方案。
我们有两种方式自定义负载均衡规则:
1.代码方式
在order-service中的OrderApplication类中,定义一个新的IRule:

@Bean
public IRule randomRule(){
    return new RandomRule();
}

这种方式定义的规则是全局的,在order-service中调用任何其他微服务时,都使用该负载均衡规则。
2.配置文件方式
在order-service的application.yml文件中,添加新的配置也可以修改规则:

userservice: # 给某个微服务配置负载均衡规则,这里是userservice服务
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则 

这种方式可以对不同的服务指定不同的负载均衡规则,更加灵活。

四、饥饿加载

Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
加载的过程包括有拉取服务列表,创建负载均衡器。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,在order-service中配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true
    clients: userservice

重新启动服务测试,可以发现第一次请求耗时更低了。

  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值