SpringCloud源码探析(一)-Ribbon负载均衡原理

1.概述

在SpringCloud中,负载均衡是基于组件Netflix Ribbon来实现的。Ribbon客户端组件提供了一系列完善的配置(超时、重试等),它会基于用户配置的策略(或默策略),按照策略去访问Load Balancer (简称LB)后所注册的服务,保证请求按照策略分发到不同的机器上,实现负载均衡。本文将详细分析SpringCloud中Ribbon的实现原理,以及实现负载均衡策略的原理。

2.原理分析

2.1 负载均衡概念

负载均衡(Load balancing)指的是将负载(工作任务)进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。上述解释来自于百度,应用于微服务架构中,就是当一个服务拥有多个实例时,调用方来访问这些服务,能够均匀请求到这些实例上,达到资源使用最优化、吞吐量最大化、响应时间最小化的目的,同时还需要避免服务过载

2.2 负载均衡对微服务的重要性

在微服务中,若没有负载均衡,可能会导致请求不均匀分布(一部分实例会收到大量请求而造成瘫痪,另一部分却接收到个位数请求),结果可能会导致一部分实例先宕机,进而请求又会不断涌向正常的实例,最后造成所有实例瘫痪。可以说负载均衡策略对微服务健康运行有着至关重要的作用,在没有充足的资源前提下,这种方式能够有效保证资源的充分应用和服务的健康运行。

2.3 SpringCloud中Ribbon使用

2.3.1 引入依赖

<!--添加ribbon的依赖-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

springCloud中大部分服务发现都引入了ribbon,比如feign组件、eurker-server、nacos-discovery等,若已经引入这些包,则不需要额外再引入上述依赖。

2.3.1 添加@LoadBalanced注解

@Configuration
public class RestConfig {
    
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
    
    
        return new RestTemplate();
    } 

@LoadBalanced注解则是ribbon负载均衡的具体实现,通过添加该注解,默认会以轮询方式获取所请求服务,保证负载均衡。

2.3.2 使用RestTemplate远程调用

@Service
public class OrderService {
    
    

@Autowired
private RestTemplate restTemplate;

public R  findUserByUserId(Integer id) {
    
    
    String url = "http://userService/findOrderByUserId/"+id;
    R result = restTemplate.getForObject(url,R.class);
    return result;
}
}

2.4 Ribbon的实现原理

2.4.1 Ribbon工作原理

在这里插入图片描述

在上述案例中,以订单服务根据userId查询用户信息为例。订单服务(orderService)发起查询请求:http://userService/getUserInfo,Ribbon负载均衡器会拦截该http请求,获取userService在注册中心所注册的服务列表,然后根据配置的策略(轮询、随机、哈希等),从服务列表中筛选一个userService发送请求,接收返回结果。

2.4.2 Ribbon负载均衡策略

Ribbon内置的核心负载均衡策略类图如下所示:
在这里插入图片描述
各策略的详细解释如下:

RoundRobinRule:轮询策略(Ribbon默认策略),若获取的server不可用、或者超过10次未能获取到server,则会返回一个空的server;

RandomRule:随机策略,随机选取一个server返回,若随机到的server为null或者不可用,则会不停地循环选取;
RetryRule:重试策略,一定时间内循环重试。默认使用RoundRobinRule策略,也可以传入指定策略,它会在每次选举之后,判断server是否为空、是否alive,在500ms内不停获取server;
BestAvailableRule:最小连接数策略,遍历serverList,选取可用的且连接数最小的一个server;如果server为空,则使用轮询策略重新获取;
AvailabilityFilteringRule:可用过滤策略,该策略是在轮询策略上的扩展,对所选取的server进行了超时判断、连接数超限判断等,符合条件才会返回;
ZoneAvoidanceRule:区域权衡策略,该策略是在轮询策略上的扩展,过滤了超时和连接数较多的server,还会过滤掉不符合要求的 zone 区域⾥⾯的所有节点,始终保证在⼀个区域/机房内的服务实例进行轮询;
WeightedResponseTimeRule:平均响应时间策略,该策略是在轮询策略上的扩展,它计算每个服务的平均相应时间,时间越少权重越大,若刚开始时信息不足,则使用轮询策略,待统计信息足够,会使用该策略。

2.5 Ribbon在SpringCloud中源码解读

Ribbon在SpringCloud核心代码的加载顺序如下:
在这里插入图片描述
LoadBalancerAutoConfiguration配置类中,给添加了@LoadBalanced注解的RestTemplate类,增加了一个拦截器LoadBalancerInterceptor,该拦截器拦截到请求后,会调用RibbonLoadBalancerClient中getLoadBalancer()方法,获取可用server,该方法调用IRule的choose()方法,获取策略,根据策略选择server。

2.5.1 LoadBalancerInterceptor

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 {
    
    
		//获取请求路径uri
		final URI originalUri = request.getURI();
		//获取服务名称
		String serviceName = originalUri.getHost();
		Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
		//此处真正执行的方法是:RibbonLoadBalancerClient.execute()方法
		return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
	}
}

上述代码中的核心方法就是intercept()方法,debug案例如下,获取服务名称之后,会携带服务名称去查询并获取可用server。
在这里插入图片描述

2.5.2 RibbonLoadBalancerClient

@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
    
    
	//获取服务列表
	ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
	//进行负载均衡操作,获取server
	Server server = getServer(loadBalancer);
	if (server == null) {
    
    
		throw new IllegalStateException("No instances available for " + serviceId);
	}
	RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
			serviceId), serverIntrospector(serviceId).getMetadata(server));

	return execute(serviceId, ribbonServer, request);
}


protected Server getServer(ILoadBalancer loadBalancer) {
    
    
		if (loadBalancer == null) {
    
    
			return null;
		}
		//默认采用轮询策略获取server
		return loadBalancer.chooseServer("default"); // TODO: better handling of key
	}

// BaseLoadBalancer类中使用chooseServer()方法
 public Server chooseServer(Object key) {
    
    
        if (counter == null) {
    
    
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
    
    
            return null;
        } else {
    
    
            try {
    
    
            //根据传入的服务id,从负载均衡器中为指定的服务选择一个服务实例
                return rule.choose(key);
            } catch (Exception e) {
    
    
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }



//根据传入的服务id,指定的负载均衡器中的服务实例执行请求
@Override
	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);
		}

		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 and rethrow so RestTemplate behaves correctly
		catch (IOException ex) {
    
    
			statsRecorder.recordStats(ex);
			throw ex;
		}
		catch (Exception ex) {
    
                                          
			statsRecorder.recordStats(ex);
			ReflectionUtils.rethrowRuntimeException(ex);
		}
		return null;
	}


2.6 Ribbon饥饿加载机制

Ribbon默认是采用懒加载,第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
在这里插入图片描述
饥饿加载则会在项目启动时创建,降低第一次访问的耗时通过下面配置开启饥饿加载:

ribbon:
  eager-load:
    enabled: true # 开启饥饿加载
    clients: userservice # 指定服务名为userservice的服务饥饿加载

在这里插入图片描述
由上图可知,配置完饥饿加载后,服务启动后会直接生成LoadBalanceClient。

2.7 自定义负载均衡策略

2.7.1 bean注入指定IRule实现类

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

2.7.1 通过配置文件配置

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule # 负载均衡规则

3.小结

1.本文详细介绍了负载均衡的原理、实现流程以及源码分析等;
2.Ribbon饥饿加载机制,只在使用时才创建LoadBalanceClient;
3.配置饥饿加载时,要指定服务名称;
4.可以通过bean注入和配置文件配置的方式来进行负载均衡策略配置。

4.参考文献

1.https://www.bilibili.com/video/BV1LQ4y127n4
2.https://www.cnblogs.com/WarBlog/p/15397657.html
3.https://zhuanlan.zhihu.com/p/88431514

猜你喜欢

转载自blog.csdn.net/qq_33479841/article/details/128860785