再学一点Ribbon负载均衡

简介

spring cloud ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具.基于HTTP和TCP的客户端均衡工具
Ribbon是Netflix发布的开源项目,主要功能是提供客户端的软件负载均衡,将Netflix中间层服务连接在一起.Ribbon客户端组件提供一系列完善配置,比如连接超时,重试等等.
在这里插入图片描述
借用网络上朋友的一幅图。
当我们使用RestTemplate或者Feign的时候,通过Ribbon实现负载均衡,获取到可用的远程服务列表,最后通过TCP完成最后的调用。
比如上图中的IRule就是从服务列表中获取可用服务的计算策略。
eureka client是一个实现服务注册和发现的中间件,可用的服务列表就是从eureka中获取到,因为每个关联的服务启动的时候都会注册到注册中心,调用方只需要从注册中心拉取对应的数据配置即可。

入门

引入jar包

<dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>

只需要引入spring cloud ribbon的jar包,通过springboot的自动装配原理,我们就可以通过ribbon实现负载均衡。

    @LoadBalanced
    @Bean("restTemplate")
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        return restTemplate;
    }

相信大家在调用其他远程服务的时候,都需要使用HTTP相关的客户端,比如ApacheHttpClient,Okhttp2/3,当然还spring提供的RestTemplate。
通过上面的代码定义,添加@LoadBalanced注解,当我们使用RestTemplate调用远程服务的时候,就会通过拦截器帮助我们实现服务负载均衡的功能。
对于负载均衡我们最想知道的是,它是如何帮助我们处理的?怎么判断哪个服务是可用?怎么判断哪个服务负载比较高?

原理

上面我们解释了,springboot中仅仅引入了一个jar包,添加了一个注解,它就帮我们实现了负载均衡的功能。
大家都知道springboot要做的事情就是减少spring的xml配置,添加了很多自动装配的配置信息,同样的我们先看下ribbon相关的自动装配的配置类。

RibbonClientConfiguration

这个配置中着重看一下负载均衡器和重试Handler的默认实现

	//这里创建默认的负载均衡器,是ZoneAwareLoadBalancer,在实际的远程调用中,通过它来实现负载均衡,找到合适的server
	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
			ServerList<Server> serverList, ServerListFilter<Server> serverListFilter,
			IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
		if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
			return this.propertiesFactory.get(ILoadBalancer.class, config, name);
		}
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	}

	//ribbon的重试是通过RetryHandler实现的
	@Bean
	@ConditionalOnMissingBean
	public RetryHandler retryHandler(IClientConfig config) {
		return new DefaultLoadBalancerRetryHandler(config);
	}

继续跟踪看一下ZoneAwareLoadBalancer的构造过程

public DynamicServerListLoadBalancer(IClientConfig clientConfig, IRule rule, IPing ping,
                                         ServerList<T> serverList, ServerListFilter<T> filter,
                                         ServerListUpdater serverListUpdater) {
        //主要是一些数据的赋值操作
        super(clientConfig, rule, ping);
        this.serverListImpl = serverList;
        this.filter = filter;
        this.serverListUpdater = serverListUpdater;
        if (filter instanceof AbstractServerListFilter) {
            ((AbstractServerListFilter) filter).setLoadBalancerStats(getLoadBalancerStats());
        }
        //这里会包含服务列表的注册,包含不同的方式,在远程调用的时候,就是通过注册的服务列表,获取可用,合适的服务
        restOfInit(clientConfig);
    }

我们主要关注一下,在服务启动的时候,它是如何赋值服务列表的

@VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            servers = serverListImpl.getUpdatedListOfServers();

            //省略....
        }
        updateAllServerList(servers);
    }

在这里插入图片描述
从serverListImpl的实现来看,主要包含3种方式获取服务列表

  1. 基于配置IClientConfig配置的服务列表,也就是我们定义好的可用服务列表
  2. 基于nacos注册中心,通过api获取注册的服务列表(目前来说nacos使用的场景特别多)
  3. 静态化的服务列表
    获取到服务列表后,把服务列表值进行保存
public void setServersList(List lsrv) {
        super.setServersList(lsrv);
        List<T> serverList = (List<T>) lsrv;
        Map<String, List<Server>> serversInZones = new HashMap<String, List<Server>>();
        for (Server server : serverList) {
            // make sure ServerStats is created to avoid creating them on hot
            // path
            getLoadBalancerStats().getSingleServerStat(server);
            String zone = server.getZone();
            if (zone != null) {
                zone = zone.toLowerCase();
                List<Server> servers = serversInZones.get(zone);
                if (servers == null) {
                    servers = new ArrayList<Server>();
                    serversInZones.put(zone, servers);
                }
                servers.add(server);
            }
        }
        setServerListForZones(serversInZones);
    }

因为默认实现的是ZoneAwareLoadBalancer,这里保存的时候也是根据Zone保存对应的server映射关系。
那么这个server map会保存到哪里呢?
LoadBalancerStats,一个用于处理负载均衡策略的存储类,这个要记住,在调用远程服务的时候,就是使用这个实例对象的upServerListZoneMap成员变量,因为它存储了我们所有的服务列表。

RibbonAutoConfiguration

//RibbonAutoConfiguration加载前需要加载的Configuration
@AutoConfigureBefore({ LoadBalancerAutoConfiguration.class,
		AsyncLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties({ RibbonEagerLoadProperties.class,
		ServerIntrospectorProperties.class })
public class RibbonAutoConfiguration {

	@Autowired(required = false)
	private List<RibbonClientSpecification> configurations = new ArrayList<>();

    //配置是否懒加载
	@Autowired
	private RibbonEagerLoadProperties ribbonEagerLoadProperties;

	//从名字上我们可以看到,它是一个client的工厂,很多的LoadBalance的client都是要通过这个工厂获取
	@Bean
	@ConditionalOnMissingBean
	public SpringClientFactory springClientFactory() {
		SpringClientFactory factory = new SpringClientFactory();
		factory.setConfigurations(this.configurations);
		return factory;
	}
	//定义LoadBalancerClient客户端,默认实现的Ribbon的
	@Bean
	@ConditionalOnMissingBean(LoadBalancerClient.class)
	public LoadBalancerClient loadBalancerClient() {
		return new RibbonLoadBalancerClient(springClientFactory());
	}
    //LoadBalance重试工厂
	@Bean
	@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
	@ConditionalOnMissingBean
	public LoadBalancedRetryFactory loadBalancedRetryPolicyFactory(
			final SpringClientFactory clientFactory) {
		return new RibbonLoadBalancedRetryFactory(clientFactory);
	}

LoadBalancerAutoConfiguration

看一下它的主要代码

   	//所有添加了@Balanced注解的RestTemplate
    @LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
			for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
				for (RestTemplateCustomizer customizer : customizers) {
					//设置RestTemplateCustomizer, 这个对象里面封装了LoadBalancerInterceptor,
					//当使用RestTemplate的时候,就会先走拦截器中的方法
					customizer.customize(restTemplate);
				}
			}
		});
	}
	
	@Configuration(proxyBeanMethods = false)
	@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
	static class LoadBalancerInterceptorConfig {

		@Bean
		public LoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient,
				LoadBalancerRequestFactory requestFactory) {
			return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
		}

		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final LoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
				List<ClientHttpRequestInterceptor> list = new ArrayList<>(
						restTemplate.getInterceptors());
				list.add(loadBalancerInterceptor);
				restTemplate.setInterceptors(list);
			};
		}

	}

在拦截器里面就实现了负载均衡的业务逻辑

LoadBalancerInterceptor

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {
	//这里注入的就是RibbonLoadBalancerClient
	private LoadBalancerClient loadBalancer;

	private LoadBalancerRequestFactory 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 {
		.//对应服务的url
		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));
	}

}

对应的execute的方法就是RibbonLoadBalancerClient的方法实现。

public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException {
		//获取负载均衡器
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		//从可用的服务列表中选择一个服务调用
		Server server = getServer(loadBalancer, hint);
		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, Object hint) {
		if (loadBalancer == null) {
			return null;
		}
		// Use 'default' on a null hint, or just pass it on?
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

这里呢,我们已经知道它用的是默认的ZoneAwareLoadBalancer负载均衡器。

public Server chooseServer(Object key) {
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
        Server server = null;
        try {
            LoadBalancerStats lbStats = getLoadBalancerStats();
            //所有的已注册的区域服务列表
            Map<String, ZoneSnapshot> zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats);
            //省略....
            //可用的区域服务列表
            //如果当前服务列表只有一个,就返回这一个
            //如果包含多个实例,首选判断服务的实例数,如果为0,剔除掉
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
            	//随机选择区域
            	//实际的zone是从可用的zones里面获取的
            	//如果只有1个可用的,就返回这一个
            	//如果包含多个,随机选择,这里使用的是Random
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                if (zone != null) {
                    //选择负载均衡器
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    //选择服务,调用的是ZoneAwareLoadBalancer的继承类BaseLoadBalancer
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
        }
        //省略.....
    }

BaseLoadBalancer

 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;
            }
        }
    }

IRule

public interface IRule{
    /*
     从所有的服务列表或者可用的服务列表选择还存活的服务
     */
	//根据指定的key选择服务
    public Server choose(Object key);
    
    //设置或者获取负载均衡器
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

在这里插入图片描述
从实现上我们可以看到,IRule的实现有很多种。

  1. BestAvaiableRule

each client ill get a random list of servers which avoids the problem that one server with the lowest concurrent requests is chosen by a large number of clients and immediately gets overwhelmed

选择最低并发的服务

  1. NacosRule

Supports preferentially calling the ribbon load balancing rules of the same cluster instance.

同集群优先调用

  1. RandomRule
    随机选择一个服务
  2. RetryRule
    重试策略
  3. AvailabilityFilteringRule
    可用性过滤策略 。 过滤掉一直连接失败的被标记为circuit tripped的后端Server,并过滤掉那些高并发的后端Server或者使用一个AvailabilityPredicate来包含过滤server的逻辑。
  4. RoundRobingRule
    轮询策略 。 轮询index,选择index对应位置的Server。

如果我们想定义自己的选择策略,只需要实现IRule接口,在Configuration中配置我们自己实现的规则。
上面这一部分内容,主要是通过RestTemplate进行远程服务调用的时候,调用的链路过程,大家注意一下。

整合Feign

Feign的基本原理,有一篇博文已经解释过了,大家可前去查看。Feign基本原理
在Feign的MethodHandler中,选择不同的Client实现调用流程,与Ribbon的整合实际调用的就是LoadBalancerFeignClient类。

public Response execute(Request request, Request.Options options) throws IOException {
		try {
			URI asUri = URI.create(request.url());
			String clientName = asUri.getHost();
			URI uriWithoutHost = cleanUrl(request.url(), clientName);
			//创建Ribbon Request
			FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
					this.delegate, request, uriWithoutHost);

			IClientConfig requestConfig = getClientConfig(options, clientName);
			//通过负载均衡调用远程服务
			return lbClient(clientName)
					.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
		}
		//省略....
	}
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
            	//Function函数,提供一个Server参数,这里的server就是负载均衡后的可server
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                        //执行调用
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } 
        //省略...
        
    }

着重看下command.submit()方法的内部逻辑,返回的是一个Observable实例。
这个代码是真的长,看的头疼,作者写的时候也不拆分一下,哎。

public Observable<T> submit(final ServerOperation<T> operation) {
        final ExecutionInfoContext context = new ExecutionInfoContext();
        
        if (listenerInvoker != null) {
            try {
                listenerInvoker.onExecutionStart();
            } catch (AbortExecutionException e) {
                return Observable.error(e);
            }
        }

        final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
        final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

        // Use the load balancer
        //selectServer()根据ribbon定义的IRule规则,选择一个可用的服务节点
        Observable<T> o = 
                (server == null ? selectServer() : Observable.just(server))
                .concatMap(new Func1<Server, Observable<T>>() {
                    @Override
                    // Called for each server being selected
                    public Observable<T> call(Server server) {
                        context.setServer(server);
                        final ServerStats stats = loadBalancerContext.getServerStats(server);
                        
                        // Called for each attempt and retry
                        Observable<T> o = Observable
                                .just(server)
                                .concatMap(new Func1<Server, Observable<T>>() {
                                    @Override
                                    public Observable<T> call(final Server server) {
                                
                                        
                                        final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start();
                                        
                                        return operation.call(server).doOnEach(new Observer<T>() {
                                        //执行完的回调
                                        //省略....                           
                                            
                                            private void recordStats(Stopwatch tracer, ServerStats stats, Object entity, Throwable exception) {
                                                tracer.stop();
                                                loadBalancerContext.noteRequestCompletion(stats, entity, exception, tracer.getDuration(TimeUnit.MILLISECONDS), retryHandler);
                                            }
                                        });
                                    }
                                });
                        //重试重试处理机制
                        if (maxRetrysSame > 0) 
                            o = o.retry(retryPolicy(maxRetrysSame, true));
                        return o;
                    }
                });
            
        if (maxRetrysNext > 0 && server == null) 
            o = o.retry(retryPolicy(maxRetrysNext, false));
        
        //省略....

到这里基本上就是ribbon的负载均衡策略,它主要是客户端层面的负载均衡,服务端的话,比如说Nginx,有些源码写的不是很详细,想具体了解的朋友,建议自己去看一遍源码,加深一下印象,ribbon里面确实有很多的代码嵌套太深。
这里还有一个重试机制没写,篇幅太长了,太多了,看着不舒服,等有时间再搞一波。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值