Spring Cloud从入门到精通(三):负载均衡 Ribbon

Ribbon

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,封装了Netflix Ribbon实现。Spring Cloud Ribbon是一个工具类框架,不需要独立部署,但是在微服务系统中有着极其重要的作用。

客户端负载均衡

负载均衡是对系统的高可用,网络压力的缓解的重要手段之一。我们通常使用的例如Nginx一般都是服务端负载均衡,服务端负载均衡会在服务器维护一个可用的服务清单,通过心跳检测来剔除有故障的服务节点,当客户端发送请求到达负载均衡设备时,该设备按照某种算法从其维护的可用服务清单中取出一台服务的地址,进行转发。

而客户端负载均衡与服务端负载均衡最大的不同在于服务清单的存储位置。在客户端负载均衡中,所有客户端维护着自己要访问的服务清单,而这些服务清单来自于服务注册中心。

入门案例

接下来先通过一个例子来学习一下Ribbon的使用方式,然后我们一步步地深入。

(1)打开服务注册中心和服务提供者

打开服务注册中心,然后打开服务提供者的两个实例,端口号分别为9998,9999.
然后服务提供者提供一个接口。

@RequestMapping("/hello/{msg}")
    public String hello(@PathVariable String msg){
        return "hello,SpringCloud"+msg+":"+ request.getServerPort();
    }

(2)构建服务消费者

首先需要创建一个服务消费者项目,pom文件中需要引入Ribbon依赖。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>eureka</artifactId>
        <groupId>com.springcloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-consumer</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.0.0.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
            <version>2.0.3.RELEASE</version>
        </dependency>
    </dependencies>
</project>

编写配置文件,指定注册中心地址

spring:
  application:
    name: eureka-consumer
server:
  port: 8888
eureka:
  client:
    service-url:
            defaultZone:
                http://server1:8001/eureka/

编写,并且启动类,注入一个RestTemplate的Bean,一定要加上@LoadBalanced注解,此时RestTemplate就结合Ribbon开启了负载均衡功能

@EnableEurekaClient
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

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

编写一个接口,调用远程的服务,此时URI地址就不再需要硬编码,而是直接提供服务名字,Ribbon会自动帮我们拉取服务清单,并且使用默认的负载均衡策略(轮询)去调用服务

@RestController
public class ConsumerController {

    @Autowired
     RestTemplate restTemplate;

    @RequestMapping("/test/{msg}")
    public  String  test(@PathVariable String msg){
        return restTemplate.getForObject("http://eureka-client01/hello/"+msg,String.class);
    }
}

我们多次访问地址http://localhost:8888/test/loadbanlance,测试一下
可以看到会轮流访问两个服务。

在这里插入图片描述

在这里插入图片描述

Ribbon源码分析

我们只在RestTemplate的Bean上加入了@LoadBalanced的注解,就实现了服务端的负载均衡。看起来简洁而又神奇,接下来我们从源码来深入分析看一下其中的过程。
过程挺绕的,最好还是用笔记记一下每步做了什么。

我们就从@LoadBalanced注解起手,跟进去源码看看。

/**
 * Annotation to mark a RestTemplate bean to be configured to use a LoadBalancerClient
 * @author Spencer Gibb
 */
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {
}

可以看到该注解的注释上写,注解用来标记一个RestTemplate Bean,以使用一个LoadBalancerClient(负载均衡客户端)来配置它。那么什么是负载均衡客户端呢?那么我们搜索LoadBalancerClient看一下,发现了是一个接口。

/**
 * Represents a client side load balancer
 * @author Spencer Gibb
 */
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里面两定义了两个execute方法和一个reconstructURI方法。
两个execute方法均为使用从负载均衡器中挑选出来的实例执行请求内容。
reconstructURI作用为系统重构一个合适的host:port形式的URI。我们在请求时,使用了服务名称,例如http://eureka-service/hello,这个方法会将我们的服务名称拼接成合适的host:port形式。

LoadBalancerClient继承了ServiceInstanceChooser 接口,我们进去看看继承了它的哪些内容。

/**
 * Implemented by classes which use a load balancer to choose a server to
 * send a request to.
 *
 * @author Ryan Baxter
 */
public interface ServiceInstanceChooser {

    /**
     * Choose a ServiceInstance from the LoadBalancer for the specified service
     * @param serviceId the service id to look up the LoadBalancer
     * @return a ServiceInstance that matches the serviceId
     */
    ServiceInstance choose(String serviceId);
}

可以看到ServiceInstanceChooser接口只有一个抽象方法,通过serviceId挑选一个服务实例返回。

那么到底是如何通过一个注解,就能让普通的RestTemplate具有客户端负载均衡的功能呢?LoadBalancerAutoConfiguration这个类为实现负载均衡器的自动化配置类,这个类会对加了@LoadBalanced注解的RestTemplate自动地配置一些东西。我们跟进去看看。

@Configuration
//RestTemplate类必须存在当前环境中 
@ConditionalOnClass(RestTemplate.class)

//在Spring的Bean中必须有LoadBalancerClient的实现Bean
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {

	@LoadBalanced
	@Autowired(required = false)
	private List<RestTemplate> restTemplates = Collections.emptyList();

	
	//初始化方法,通过RestTemplateCustomizer来给RestTemplate添加拦截器
	@Bean
	public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(
			final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
		return () -> restTemplateCustomizers.ifAvailable(customizers -> {
            for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer : customizers) {
                    customizer.customize(restTemplate);
                }
            }
        });
	}

	@Autowired(required = false)
	private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

	@Bean
	@ConditionalOnMissingBean
	public LoadBalancerRequestFactory loadBalancerRequestFactory(
			LoadBalancerClient loadBalancerClient) {
		return new LoadBalancerRequestFactory(loadBalancerClient, transformers);
	}

	@Configuration
	@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);
            };
		}
	}

	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryAutoConfiguration {

		@Bean
		@ConditionalOnMissingBean
		public LoadBalancedRetryFactory loadBalancedRetryFactory() {
			return new LoadBalancedRetryFactory() {};
		}
	}

	//内部类
	@Configuration
	@ConditionalOnClass(RetryTemplate.class)
	public static class RetryInterceptorAutoConfiguration {
		@Bean
		@ConditionalOnMissingBean
		public RetryLoadBalancerInterceptor ribbonInterceptor(
				LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties,
				LoadBalancerRequestFactory requestFactory,
				LoadBalancedRetryFactory loadBalancedRetryFactory) {
			return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,
					requestFactory, loadBalancedRetryFactory);
		}

		//给RestTemplate增加loadBalancerInterceptor拦截器
		@Bean
		@ConditionalOnMissingBean
		public RestTemplateCustomizer restTemplateCustomizer(
				final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
			return restTemplate -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                        restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
		}
	}
}

在该类中,首先维护了一个被@LoadBalanced修饰的RestTemplate集合。
loadBalancedRestTemplateInitializerDeprecated为初始化方法,该方法主要通过调用 customizer.customize(restTemplate)方法来给 RestTemplate 增加拦截器 LoadBalancerlnterceptor。 而这个拦截器就是将RestTemplate变为客户端负载均衡的关键所在。我们跟进去LoadBalancerlnterceptor看看

public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;
	private LoadBalancerRequestFactory requestFactory;

	//构造方法,注入一个LoadBalancerClient实现
	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, requestFactory.createRequest(request, body, execution));
	}
}

我们看到LoadBalancerInterceptor 中首先在构造方法中需要传入LoadBalancerClient的实现。当一个被@LoadBalanced注解注释的RestTemplate对象发起HTTP请求时,会被LoadBalancerInterceptor拦截器的intercept方法所拦截,由于我们在使用RestTemplate时采用了服务名称作为host,所以拦截器中的方法通过getHost获取的就是我们传递的服务名称。然后通过调用LoadBalancerClient的实现类的execute方法来选择服务实例发送请求。

到现在为止LoadBalancerClient还只是一个接口,我们找到它的实现类看一下。LoadBalancerClient的实现类为RibbonLoadBalancerClient。RibbonLoadBalancerClient是非常重要的一个类,最终负载均衡的请求处理由它来完成,我们来看一下它的源码。

首先在拦截器中最后调用的是execute(String serviceId, LoadBalancerRequest< T > request)方法,我们来看一下这个方法如何处理请求。

@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		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);
	}

我们可以通过方法名字知道,它首先通过了getServer方法来获取具体的服务实例。我们看一下getServer的源码

protected Server getServer(ILoadBalancer loadBalancer) {
		if (loadBalancer == null) {
			return null;
		}
		return loadBalancer.chooseServer("default"); // TODO: better handling of key
	}

我们发现它根本没有使用LoadBalancerClient定义的choose方法,而是使用了Netflix包中的ILoadBalancer接口定义的chooseServer来选择具体的实例。
ILoadBalancer是Netflix中的一个接口,定义了一系列实现负载均衡的方法。我们来看一下源码

public interface ILoadBalancer {

    //向负载均衡器维护的服务列表中添加服务实例
	public void addServers(List<Server> newServers);
	
	//通过某种策略,从负载均衡器中挑选出一个服务实例
	public Server chooseServer(Object key);
	
	//用来通知和标识某个服务实例已经停止服务
	public void markServerDown(Server server);

	//获取当前正常服务的服务列表
	public List<Server> getServerList(boolean availableOnly);

	//获取所有已知的服务,包括停止的和正常的
	public List<Server> getAllServers();
}

该接口中有一个Server对象,定义存储的是一些服务端节点的元数据信息,包括host、port以及一些部署信息等。

我们通过IDEA整理了一下该接口的实现关系图。

在这里插入图片描述

通过命名可以知道,BaseLoadBalancer实现了基础的负载均衡,而DynamicServerListLoadBalancer和ZoneAwareLoadBalancer做了一些功能的扩展。

那么在整合Ribbon时Spring Cloud 使用了哪个实现呢?我们查找org.springframework.cloud.netflix.ribbon下的RibbonClientConfiguration配置类,可以知道在整合时默认使用了ZoneAwareLoadBalancer类。

@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的ILoadBalancer某个实现类实现的。接下来我们回到RibbonLoadBalancerClient类中的execute方法(为了不让大家来回翻,我再粘贴一次),我们getServer获取到具体的服务实例后,将其包装成RibbonServer对象,该对象除了存储了服务实例的信息之外,还存储了服务id等信息。

@Override
	public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		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);
	}

然后再调用三个参数的execute方法。

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

可以看到该方法调用了拦截器传过来的LoadBalancerRequest接口中的apply方法。这个方法呢就是用来向实际的具体服务实例发送请求,并且实现服务名称转换为host:port形式的访问地址。该方法接收一个ServiceInstance对象,这个是一个接口,定义了每个服务实例需要的一些基本信息,如serviceId,host,port等,上面提到的RibbonServer就实现了该接口。我们看下apply是如何实现的。

public ListenableFuture<ClientHttpResponse> apply(ServiceInstance instance) throws Exception {
                HttpRequest serviceRequest = new ServiceRequestWrapper(request, instance, AsyncLoadBalancerInterceptor.this.loadBalancer);
                return execution.executeAsync(serviceRequest, body);
 }

apply实现中调用了execution.executeAsync方法,并且传入了ServiceRequestWrapper对象,该对象继承了HttpRequestWrapper并重写了getURI方法。我们来看一下

public class ServiceRequestWrapper extends HttpRequestWrapper {
	private final ServiceInstance instance;
	private final LoadBalancerClient loadBalancer;

	public ServiceRequestWrapper(HttpRequest request, ServiceInstance instance,
								 LoadBalancerClient loadBalancer) {
		super(request);
		this.instance = instance;
		this.loadBalancer = loadBalancer;
	}

	@Override
	public URI getURI() {
		URI uri = this.loadBalancer.reconstructURI(
				this.instance, getRequest().getURI());
		return uri;
	}
}

可以看到重写后的方法通过调用LoadBalancerClient 的reconstructURI方法,然后传入服务实例和当前的URI来构建host:port形式的URI并进行返回。

在执行request.apply()方法时,这个request是从哪儿来的呢?我们追溯一下发现是拦截器传给我们的。那么拦截器又是从哪儿获取的呢?,可以看到调用了requestFactory.createRequest(request, body, execution)。
这儿有个createRequest方法,名字是创建请求,我们去看一下。跟踪一下,发现这个方法最后会调用InterceptingClientHttpRequest下的execute方法。

@Override
		public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
			if (this.iterator.hasNext()) {
				ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
				return nextInterceptor.intercept(request, body, this);
			}
			else {
				HttpMethod method = request.getMethod();
				Assert.state(method != null, "No standard HTTP method");
				ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
				for (Map.Entry<String, List<String>> entry : request.getHeaders().entrySet()) {
					delegate.getHeaders().addAll(entry.getKey(), entry.getValue());
				}
				if (body.length > 0) {
					StreamUtils.copy(body, delegate.getBody());
				}
				return delegate.execute();
			}
		}

可以看到在创建请求时,这里的request.getURI()会调用传入的Request对象的getURI方法。那么这个Request对象是谁呢?我们往前追溯一下,发现是ServiceRequestWrapper这个对象。到此,一切就豁然开朗了。此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的实例地址。

@Override
	public URI reconstructURI(ServiceInstance instance, URI original) {
		Assert.notNull(instance, "instance can not be null");
		String serviceId = instance.getServiceId();
		RibbonLoadBalancerContext context = this.clientFactory
				.getLoadBalancerContext(serviceId);

		URI uri;
		Server server;
		if (instance instanceof RibbonServer) {
			RibbonServer ribbonServer = (RibbonServer) instance;
			server = ribbonServer.getServer();
			uri = updateToSecureConnectionIfNeeded(original, ribbonServer);
		} else {
			server = new Server(instance.getScheme(), instance.getHost(), instance.getPort());
			IClientConfig clientConfig = clientFactory.getClientConfig(serviceId);
			ServerIntrospector serverIntrospector = serverIntrospector(serviceId);
			uri = updateToSecureConnectionIfNeeded(original, clientConfig,
					serverIntrospector, server);
		}
		return context.reconstructURIWithServer(server, uri);
	}

从该方法中看到,通过ServiceInstance的serviceId,从SpringClientFactory类的clientFactory对象中获取对应serviceId的负载均衡器的上下文RibbonLoadBalancerContext对象,然后根据ServiceInstance中的信息来构建具体服务实例信息的server 对象,并使用RibbonLoadBalancerContext的reconstructURIWithServer方法来构建服务实例的URI。

SpringClientFactory类是一个用来创建客户端负载均衡器的工厂类,该类会为每一个不同名的Ribbon客户端生成不同的Spring上下文。
RibbonLoadBalancerContext类是LoadBalancerContext的字类,用于存储一些被负载均衡器使用的上下文内容和api操作。

看reconstructURIWithServer的实现可以看到,根据Server对象的host和port信息,然后根据服务名为host的original对象获取其他信息,最后拼接成最终要访问的服务实例地址。

public URI reconstructURIWithServer(Server server, URI original) {
        String host = server.getHost();
        int port = server.getPort();
        String scheme = server.getScheme();
        
        if (host.equals(original.getHost()) 
                && port == original.getPort()
                && scheme == original.getScheme()) {
            return original;
        }
        if (scheme == null) {
            scheme = original.getScheme();
        }
        if (scheme == null) {
            scheme = deriveSchemeAndPortFromPartialUri(original).first();
        }

        try {
            StringBuilder sb = new StringBuilder();
            sb.append(scheme).append("://");
            if (!Strings.isNullOrEmpty(original.getRawUserInfo())) {
                sb.append(original.getRawUserInfo()).append("@");
            }
            sb.append(host);
            if (port >= 0) {
                sb.append(":").append(port);
            }
            sb.append(original.getRawPath());
            if (!Strings.isNullOrEmpty(original.getRawQuery())) {
                sb.append("?").append(original.getRawQuery());
            }
            if (!Strings.isNullOrEmpty(original.getRawFragment())) {
                sb.append("#").append(original.getRawFragment());
            }
            URI newURI = new URI(sb.toString());
            return newURI;            
        } catch (URISyntaxException e) {
            throw new RuntimeException(e);
        }
    }

我们大致的理了下Ribbon实现负载均衡的流程,我们来汇总一下。
首先,在RestTemplate上加注解(@LoadBalanced),加这个注解会让负载均衡的客户端(LoadBalancerClient)来配置它。
LoadBalancerAutoConfiguration这个类是会给RestTemplate自动化配置的,主要用来给RestTemplate增加一个拦截器LoadBalancerInterceptor。
当然,这个拦截器中注入了LoadBalancerClient的实现
然后当RestTemplate发送请求时,会被这个拦截器的intercept拦截方法所拦截,因为我们在请求URI中使用了服务名,所以我们可以在拦截的时候拿到服务名。
然后拦截器调用LoadBalancerClient的execute方法去根据服务名选择具体实例发起请求。
那么这个execute方法是怎么实现的呢?我们发现这个方法第一步是根据我们传入的服务名去获取具体的服务实例(getServer)
但是这个方法其实根本就不是它实现的,它只是使用了ILoadBalancer接口的某个实现,ILoadBalancer是Ribbon中的原生接口,即为负载均衡器接口,这个接口有大量的实现,它默认使用了ZoneAwareLoadBalancer实现
然后呢我们getServer就获取到了具体的服务实例,这个服务实例里面存储了host、port等等信息。
我们调用了拦截器传过来的一个对象的apply方法,apply方法接收到具体服务实例之后呢,会调用RibbonLoadBalancerClient的重构URI的方法来重构成合适的host:port进行请求。

流程理清楚了,接下来我们去看下负载均衡器到底是如何实现选择服务实例的。

前面我们已经简单的介绍了Spring Cloud定义了LoadBalancerClient作为通用接口,并且提供了RibbonLoadBalancerClient实现。但是在具体实现客户端负载均衡时,还是通过Ribbon的ILoadBalancer接口实现,在前面我们已经对这个接口作过一些简单介绍。接下来我们对它的一些实现类来分析一下源码看看是如何实现客户端负载均衡的。

(1)AbstractLoadBalancer

首先来看AbstractLoadBalancer,它是接口的一个抽象类实现。

public abstract class AbstractLoadBalancer implements ILoadBalancer {
    
    public enum ServerGroup{
        ALL,
        STATUS_UP,
        STATUS_NOT_UP        
    }
        
    /**
     * delegate to {@link #chooseServer(Object)} with parameter null.
     */
    public Server chooseServer() {
    	return chooseServer(null);
    }

    
    /**
     * List of servers that this Loadbalancer knows about
     * 
     * @param serverGroup Servers grouped by status, e.g., {@link ServerGroup#STATUS_UP}
     */
    public abstract List<Server> getServerList(ServerGroup serverGroup);
    
    /**
     * Obtain LoadBalancer related Statistics
     */
    public abstract LoadBalancerStats getLoadBalancerStats();    
}

可以看到AbstractLoadBalancer 首先定义了一个枚举ServerGroup,里面包含三种不同类型。

  • ALL:所有服务实例
  • STATUS_UP:正常服务的实例
  • STATUS_NOT_UP:停止服务的实例

其中定义了两个抽象方法

  • List< Server > getServerList(ServerGroup serverGroup):根据枚举类型来获取不同的服务实例的列表。

  • LoadBalancerStats getLoadBalancerStats():获取LoadBalancerStats 的方法。LoadBalancerStats对象用来存储负载均衡器中各个服务实例当前的属性和统计信息。

(2)BaseLoadBalancer
接着来看BaseLoadBalancer,它是Ribbon负载均衡器的基础实现类,定义了很多负载均衡器的基础内容。

首先定义了两个存储服务实例Server对象的列表,一个用于存储所有服务实例,一个用于存储正常服务的实例。

	@Monitor(name = PREFIX + "AllServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> allServerList = Collections
            .synchronizedList(new ArrayList<Server>());
    @Monitor(name = PREFIX + "UpServerList", type = DataSourceType.INFORMATIONAL)
    protected volatile List<Server> upServerList = Collections
            .synchronizedList(new ArrayList<Server>());

定义了之前提到的用来存储负载均衡器各服务实例属性和统计信息的LoadBalancerStats 对象。

protected LoadBalancerStats lbStats;

定义了检查服务实例是否正常的IPing对象,在BaseLoadBalancer默认为空。

protected IPing ping = null;

定义了检查服务实例操作的执行策略对象IPingStrategy,在BaseLoadBalancer默认使用了该类中的静态内部类SerialPingStrategy实现,根据源码,我们可以看到该策略使用线性遍历ping服务实例的方式进行检查。

private static class SerialPingStrategy implements IPingStrategy {

        @Override
        public boolean[] pingServers(IPing ping, Server[] servers) {
            int numCandidates = servers.length;
            boolean[] results = new boolean[numCandidates];

            logger.debug("LoadBalancer:  PingTask executing [{}] servers configured", numCandidates);

            for (int i = 0; i < numCandidates; i++) {
                results[i] = false; /* Default answer is DEAD. */
                try {
                    // NOTE: IFF we were doing a real ping
                    // assuming we had a large set of servers (say 15)
                    // the logic below will run them serially
                    // hence taking 15 times the amount of time it takes
                    // to ping each server
                    // A better method would be to put this in an executor
                    // pool
                    // But, at the time of this writing, we dont REALLY
                    // use a Real Ping (its mostly in memory eureka call)
                    // hence we can afford to simplify this design and run
                    // this
                    // serially
                    if (ping != null) {
                        results[i] = ping.isAlive(servers[i]);
                    }
                } catch (Exception e) {
                    logger.error("Exception while pinging Server: '{}'", servers[i], e);
                }
            }
            return results;
        }
    }

定义了负载均衡的处理规则IRule对象,从BaseLoadBalancer中的chooseServer方法知道,负载均衡器将服务实例选择交给了IRule的choose方法来实现。默认使用了RoundRobinRule类实现最常用最基本的轮询负载均衡规则。

启动ping任务,在BaseLoadBalancer的默认构造函数中,会直接启动一个定时检查Server是否健康的任务,默认执行间隔为10s。

void setupPingTask() {
        if (canSkipPing()) {
            return;
        }
        if (lbTimer != null) {
            lbTimer.cancel();
        }
        lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + name,
                true);
        lbTimer.schedule(new PingTask(), 0, pingIntervalSeconds * 1000);
        forceQuickPing();
    }

实现了ILoadBalancer接口中定义的方法。

//向负载均衡器中添加新的服务实例。
@Override
    public void addServers(List<Server> newServers) {
        if (newServers != null && newServers.size() > 0) {
            try {
                ArrayList<Server> newList = new ArrayList<Server>();
                newList.addAll(allServerList);
                newList.addAll(newServers);
                setServersList(newList);
            } catch (Exception e) {
                logger.error("LoadBalancer [{}]: Exception while adding Servers", name, e);
            }
        }
    }

//挑选具体的服务实例
public String choose(Object key) {
        if (rule == null) {
            return null;
        } else {
            try {
                Server svr = rule.choose(key);
                return ((svr == null) ? null : svr.getId());
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server", name, e);
                return null;
            }
        }
    }

//标记某个服务停止
 public void markServerDown(Server server) {
        if (server == null || !server.isAlive()) {
            return;
        }

        logger.error("LoadBalancer [{}]:  markServerDown called on [{}]", name, server.getId());
        server.setAlive(false);
        // forceQuickPing();

        notifyServerStatusChangeListener(singleton(server));
    }

//获取正常的服务实例列表
@Override
    public List<Server> getReachableServers() {
        return Collections.unmodifiableList(upServerList);
    }

//获取所有的服务实例列表
@Override
    public List<Server> getAllServers() {
        return Collections.unmodifiableList(allServerList);
    }

(3)DynamicServerListLoadBalancer

DynamicServerListLoadBalancer继承BaseLoadBalancer,是对BaseLoadBalancer的扩展,我们来看看其中增加了一些什么功能。

首先我们发现了一个关于服务列表的对象。

volatile ServerList<T> serverListImpl;

通过类名泛型T extends Server可知它是Server的一个子类,这个对象的源码如下。

public interface ServerList<T extends Server> {

    public List<T> getInitialListOfServers();
    
    /**
     * Return updated list of servers. This is called say every 30 secs
     * (configurable) by the Loadbalancer's Ping cycle
     * 
     */
    public List<T> getUpdatedListOfServers();   

}

该接口定义了两个抽象方法,getInitialListOfServers用于获取初始化的服务实例清单,而getUpdatedListOfServers用来获取更新的服务实例清单。

ServerList有多个实现类,那么在DynamicServerListLoadBalancer中采用了哪个实现呢?因为负载均衡器需要实现服务实例的动态更新,所以我们在Spring Cloud整个Ribbon与Eureka的包下可以找到EurekaRibbonClientConfiguration,在该类中可以找到如下创建ServerList实例的内容

@Bean
	@ConditionalOnMissingBean
	public ServerList<?> ribbonServerList(IClientConfig config, Provider<EurekaClient> eurekaClientProvider) {
		if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
			return this.propertiesFactory.get(ServerList.class, config, serviceId);
		}
		DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
				config, eurekaClientProvider);
		DomainExtractingServerList serverList = new DomainExtractingServerList(
				discoveryServerList, config, this.approximateZoneFromHostname);
		return serverList;
	}

可以看到这里创建了一个DomainExtractingServerList 。打开源码发现,其内部定义了一个ServerList,并且给出了getInitialListOfServers和getUpdatedListOfServers的具体实现。

public class DomainExtractingServerList implements ServerList<DiscoveryEnabledServer> {

	private ServerList<DiscoveryEnabledServer> list;
	private final RibbonProperties ribbon;

	private boolean approximateZoneFromHostname;

	public DomainExtractingServerList(ServerList<DiscoveryEnabledServer> list,
			IClientConfig clientConfig, boolean approximateZoneFromHostname) {
		this.list = list;
		this.ribbon = RibbonProperties.from(clientConfig);
		this.approximateZoneFromHostname = approximateZoneFromHostname;
	}

	@Override
	public List<DiscoveryEnabledServer> getInitialListOfServers() {
		List<DiscoveryEnabledServer> servers = setZones(this.list
				.getInitialListOfServers());
		return servers;
	}

	@Override
	public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
		List<DiscoveryEnabledServer> servers = setZones(this.list
				.getUpdatedListOfServers());
		return servers;
	}

	private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
		List<DiscoveryEnabledServer> result = new ArrayList<>();
		boolean isSecure = this.ribbon.isSecure(true);
		boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
		for (DiscoveryEnabledServer server : servers) {
			result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr,
					this.approximateZoneFromHostname));
		}
		return result;
	}

}

但是这个ServerList对象是构造函数中传过来的,看前面的代码,知道是由DiscoveryEnabledNIWSServerList实现的。那么DiscoveryEnabledNIWSServerList是如何获取两个服务实例的呢?

  @Override
    public List<DiscoveryEnabledServer> getInitialListOfServers(){
        return obtainServersViaDiscovery();
    }

    @Override
    public List<DiscoveryEnabledServer> getUpdatedListOfServers(){
        return obtainServersViaDiscovery();
    }

从源码中可以看到是通过该类的一个方法实现服务实例的获取。我们看一下这个方法

private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList<DiscoveryEnabledServer>();

        if (eurekaClientProvider == null || eurekaClientProvider.get() == null) {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList<DiscoveryEnabledServer>();
        }

        EurekaClient eurekaClient = eurekaClientProvider.get();
        if (vipAddresses!=null){
            for (String vipAddress : vipAddresses.split(",")) {
                // if targetRegion is null, it will be interpreted as the same region of client
                List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, isSecure, targetRegion);
                for (InstanceInfo ii : listOfInstanceInfo) {
                    if (ii.getStatus().equals(InstanceStatus.UP)) {

                        if(shouldUseOverridePort){
                            if(logger.isDebugEnabled()){
                                logger.debug("Overriding port on client name: " + clientName + " to " + overridePort);
                            }

                            // copy is necessary since the InstanceInfo builder just uses the original reference,
                            // and we don't want to corrupt the global eureka copy of the object which may be
                            // used by other clients in our system
                            InstanceInfo copy = new InstanceInfo(ii);

                            if(isSecure){
                                ii = new InstanceInfo.Builder(copy).setSecurePort(overridePort).build();
                            }else{
                                ii = new InstanceInfo.Builder(copy).setPort(overridePort).build();
                            }
                        }

                        DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, isSecure, shouldUseIpAddr);
                        des.setZone(DiscoveryClient.getZone(ii));
                        serverList.add(des);
                    }
                }
                if (serverList.size()>0 && prioritizeVipAddressBasedServers){
                    break; // if the current vipAddress has servers, we dont use subsequent vipAddress based servers
                }
            }
        }
        return serverList;
    }

这个方法主要依靠EurekaClient从服务注册中心获取到具体的服务实例列表listOfInstanceInfo,这里传入的vipAddress是服务名称,比如User-Service。然后对这些对象进行遍历,将状态为UP的实例转换为DiscoveryEnabledServer对象,最后将这些实例放到列表里返回。

在获取到最新的服务清单之后,返回list传入到DomainExtractingServerList类中,然后getInitialListOfServers和getUpdatedListOfServers方法会调用setZones方法继续处理。

private List<DiscoveryEnabledServer> setZones(List<DiscoveryEnabledServer> servers) {
		List<DiscoveryEnabledServer> result = new ArrayList<>();
		boolean isSecure = this.ribbon.isSecure(true);
		boolean shouldUseIpAddr = this.ribbon.isUseIPAddrForServer();
		for (DiscoveryEnabledServer server : servers) {
			result.add(new DomainExtractingServer(server, isSecure, shouldUseIpAddr,
					this.approximateZoneFromHostname));
		}
		return result;
	}

这里主要是将返回的List列表中的元素,转换为DomainExtractingServer对象,在该对象的构造函数中将为服务实例对象设置一些必要的属性,例如id,zone,isAlive等等。

通过上面的分析我们已经知道,Ribbon与Eureka整合后,如何获取服务实例清单。那么它又是如何触发向Eureka Server去获取服务实例清单以及如何更新本地的服务实例清单呢?继续来看DynamicServerListLoadBalancer,我们可以找到这个类

protected final ServerListUpdater.UpdateAction updateAction = new ServerListUpdater.UpdateAction() {
        @Override
        public void doUpdate() {
            updateListOfServers();
        }
    };

根据命名我们大概能猜到,这个类负责对ServerList的更新,所以我们称它叫服务更新器。从源码可以看到,ServerListUpdater内部定义了一个UpdateAction接口,上面的updateAction 对象就是以匿名内部类的形式创建了一个具体实现,其中doUpdate的实现就是对ServerList的具体更新操作。

public interface ServerListUpdater {

  
    public interface UpdateAction {
        void doUpdate();
    }


	//启动服务更新器,传入的UpdateAction 为UpdateAction 接口的具体实现
    void start(UpdateAction updateAction);

  	//停止服务更新器
    void stop();

    //获取最近的更新时间
    String getLastUpdate();

    //获取上次更新到实现的时间间隔,单位为毫秒
    long getDurationSinceLastUpdateMs();

   //获取错过的更新周期数
    int getNumberMissedCycles();

   //获取核心线程数
    int getCoreThreads();
}

ServerListUpdater有两个实现类。
PollingServerListUpdater:动态服务列表更新的默认策略,DynamicServerListLoadBalancer负载均衡器的默认实现就是它,它通过定时任务的方式去动态更新服务列表。

EurekaNotificationServerListUpdater:DynamicServerListLoadBalancer负载均衡器也可以使用它。它需要利用Eureka的事件监听器来驱动服务列表的更新操作。

我们先来看PollingServerListUpdater。

 @Override
    public synchronized void start(final UpdateAction updateAction) {
        if (isActive.compareAndSet(false, true)) {
            final Runnable wrapperRunnable = new Runnable() {
                @Override
                public void run() {
                    if (!isActive.get()) {
                        if (scheduledFuture != null) {
                            scheduledFuture.cancel(true);
                        }
                        return;
                    }
                    try {
                        updateAction.doUpdate();
                        lastUpdated = System.currentTimeMillis();
                    } catch (Exception e) {
                        logger.warn("Failed one update cycle", e);
                    }
                }
            };

            scheduledFuture = getRefreshExecutor().scheduleWithFixedDelay(
                    wrapperRunnable,
                    initialDelayMs,
                    refreshIntervalMs,
                    TimeUnit.MILLISECONDS
            );
        } else {
            logger.info("Already active, no-op");
        }
    }

可以看到start方法中创建了一个Runnable 县城实现,然后调用了doUpdate方法,最后又为该线程创建了一个定时任务。

我们还可以找到这两个参数

	private static long LISTOFSERVERS_CACHE_UPDATE_DELAY = 1000; // msecs;
    private static int LISTOFSERVERS_CACHE_REPEAT_INTERVAL = 30 * 1000; // msecs;

这两个参数说明,更新服务实例在初始化一秒后开始更新,然后每隔30秒更新一次。我们回到DynamicServerListLoadBalancer中调用doUpdate的具体位置,可以看到它最后调用了updateListOfServers();方法。

@VisibleForTesting
    public void updateListOfServers() {
        List<T> servers = new ArrayList<T>();
        if (serverListImpl != null) {
            servers = serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
                    getIdentifier(), servers);

            if (filter != null) {
                servers = filter.getFilteredListOfServers(servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
                        getIdentifier(), servers);
            }
        }
        updateAllServerList(servers);
    }

可以看到这里用到了之前说的getUpdatedListOfServers方法,这个方法用于获取服务可用实例列表。这里使用了filter这个对象,可以发现它是ServerListFilter类的对象。我们看下这个接口,这个接口中只有一个方法,主要用来对服务实例列表的过滤。

public interface ServerListFilter<T extends Server> {

    public List<T> getFilteredListOfServers(List<T> servers);

}

该接口有一些实现类。
AbstractServerListFilter是一个抽象的过滤器,这里定义了一个过滤时需要的一个重要对象LoadBalancerStats,前面说过,这个对象里面存放了负载均衡器的属性和一些统计信息。

public abstract class AbstractServerListFilter<T extends Server> implements ServerListFilter<T> {

    private volatile LoadBalancerStats stats;
    
    public void setLoadBalancerStats(LoadBalancerStats stats) {
        this.stats = stats;
    }
    
    public LoadBalancerStats getLoadBalancerStats() {
        return stats;
    }

}

ZoneAffinityServerListFilter:该过滤器基于“区域感知”的方式实现服务实例的过滤。它会根据提供服务实例所处的区域和消费者自身所处的区域进行比较,过滤掉不是同处一个区域的实例。

   @Override
    public List<T> getFilteredListOfServers(List<T> servers) {
        if (zone != null && (zoneAffinity || zoneExclusive) && servers !=null && servers.size() > 0){
            List<T> filteredServers = Lists.newArrayList(Iterables.filter(
                    servers, this.zoneAffinityPredicate.getServerOnlyPredicate()));
            if (shouldEnableZoneAffinity(filteredServers)) {
                return filteredServers;
            } else if (zoneAffinity) {
                overrideCounter.increment();
            }
        }
        return servers;
    }

看源码可知,对于服务实例列表的过滤是通过Iterables.filter(servers, this.zoneAffinityPredicate.getServerOnlyPredicate())实现的,判断依据由zoneAffinityPredicate实现服务实例与消费者区域的比较。在过滤之后,并不会马上返回结果。而是通过shouldEnableZoneAffinity方法判断是否要启用区域感知的功能。

private boolean shouldEnableZoneAffinity(List<T> filtered) {    
        if (!zoneAffinity && !zoneExclusive) {
            return false;
        }
        if (zoneExclusive) {
            return true;
        }
        LoadBalancerStats stats = getLoadBalancerStats();
        if (stats == null) {
            return zoneAffinity;
        } else {
            logger.debug("Determining if zone affinity should be enabled with given server list: {}", filtered);
            ZoneSnapshot snapshot = stats.getZoneSnapshot(filtered);
            double loadPerServer = snapshot.getLoadPerServer();
            int instanceCount = snapshot.getInstanceCount();            
            int circuitBreakerTrippedCount = snapshot.getCircuitTrippedCount();
            if (((double) circuitBreakerTrippedCount) / instanceCount >= blackOutServerPercentageThreshold.get() 
                    || loadPerServer >= activeReqeustsPerServerThreshold.get()
                    || (instanceCount - circuitBreakerTrippedCount) < availableServersThreshold.get()) {
                logger.debug("zoneAffinity is overriden. blackOutServerPercentage: {}, activeReqeustsPerServer: {}, availableServers: {}", 
                        new Object[] {(double) circuitBreakerTrippedCount / instanceCount,  loadPerServer, instanceCount - circuitBreakerTrippedCount});
                return false;
            } else {
                return true;
            }
            
        }
    }

我们看这个方法的实现,可以看到使用了LoadBalancerStats的getZoneSnapshot来获取这些过滤后的同区域实例的基础指标(包含实例数量、断路器断开数、活动请求数、实例平均负载等),根据一系列算法求出下面的几个平均值并与设置的阀值对比,若有一个符合,就不启用区域感知过滤的服务实例清单。这一算法实现为集群出现区域故障时,依然可以依靠其他区域的实例进行正常服务。

blackOutServerPercentage:故障实例百分百(断路器断开数/实例数量)>=0.8
activeReqeustsPerServer:实例平均负载>=0.6
availableServers:可用实例数(实例数量-断路器断开数)<2

DefaultNIWSServerListFilter:完全继承ZoneAffinityServerListFilter,是默认的Netflix Internal Web Service过滤器。

ZonePreferenceServerListFilter:Spring Cloud整合时新增的过滤器,使用Spring Cloud整合Ribbon和Eureka会默认使用该过滤器。实现了通过配置获取Eureka实例元数据的所属区域(Zone)来过滤出同区域的服务实例。

@Override
	public List<Server> getFilteredListOfServers(List<Server> servers) {
		List<Server> output = super.getFilteredListOfServers(servers);
		if (this.zone != null && output.size() == servers.size()) {
			List<Server> local = new ArrayList<>();
			for (Server server : output) {
				if (this.zone.equalsIgnoreCase(server.getZone())) {
					local.add(server);
				}
			}
			if (!local.isEmpty()) {
				return local;
			}
		}
		return output;
	}

方法很简单,通过父类ZoneAffinityServerListFilter的过滤器获得区域感知的服务实例列表,然后遍历结果,取出根据消费者配置的区域来进行过滤。如果是空就直接返回父类区域感知的结果,不为空则返回通过消费者配置的Zone过滤的结果。

(4)ZoneAwareLoadBalancer

ZoneAwareLoadBalancer是对DynamicServerListLoadBalancer的扩展,在DynamicServerListLoadBalancer中我们发现它没有重写chooseServer方法,所以它依然采用BaseLoadBalancer中的实现,以轮询的方式选择服务实例,该算法简单没有区域Zone的概念,所以会产生跨区域访问的情况,造成高延迟。而ZoneAwareLoadBalancer解决了这个问题。

首先,在ZoneAwareLoadBalancer中并没有重写setServersList方法,说明实现服务实例清单的更新主逻辑没变。但是它重写了setServerListForZones方法。

protected void setServerListForZones(Map<String, List<Server>> zoneServersMap) {
        super.setServerListForZones(zoneServersMap);
        if (balancers == null) {
            balancers = new ConcurrentHashMap<String, BaseLoadBalancer>();
        }
        for (Map.Entry<String, List<Server>> entry: zoneServersMap.entrySet()) {
        	String zone = entry.getKey().toLowerCase();
            getLoadBalancer(zone).setServersList(entry.getValue());
        }
        // check if there is any zone that no longer has a server
        // and set the list to empty so that the zone related metrics does not
        // contain stale data
        for (Map.Entry<String, BaseLoadBalancer> existingLBEntry: balancers.entrySet()) {
            if (!zoneServersMap.keySet().contains(existingLBEntry.getKey())) {
                existingLBEntry.getValue().setServersList(Collections.emptyList());
            }
        }
    }  

可以看到它创建了一个ConcurrentHashMap类型的balancers对象,它将用来存储每个区域Zone对应的负载均衡器,而具体的负载均衡器的创建是通过下面第一个循环中的getLoadBalancer方法实现,同时在创建负载均衡器的时候创建它的规则(如果当前实现中没有IRule的实例,就创建一个AvailabilityFilteringRule规则,如果已经有具体实例,就克隆一个)。在创建完负载均衡器以后又调用setServersList方法设置对应Zone区域的实例清单。第二个循环是对Zone区域中实例清单的检查,看看是否有Zone区域下已经没实例了。是的话就将balancers中对应Zone区域的实例列表清空,该操作是为了后续选择节点时,防止过期的Zone干扰选择算法。

了解完负载均衡器如何扩展服务实例清单后,我们看看它是如何挑选具体的服务实例的。

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);
            logger.debug("Zone snapshots: {}", zoneSnapshot);
            if (triggeringLoad == null) {
                triggeringLoad = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".triggeringLoadPerServerThreshold", 0.2d);
            }

            if (triggeringBlackoutPercentage == null) {
                triggeringBlackoutPercentage = DynamicPropertyFactory.getInstance().getDoubleProperty(
                        "ZoneAwareNIWSDiscoveryLoadBalancer." + this.getName() + ".avoidZoneWithBlackoutPercetage", 0.99999d);
            }
            Set<String> availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get());
            logger.debug("Available zones: {}", availableZones);
            if (availableZones != null &&  availableZones.size() < zoneSnapshot.keySet().size()) {
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                logger.debug("Zone chosen: {}", zone);
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
            }
        } catch (Exception e) {
            logger.error("Error choosing server using zone aware logic for load balancer={}", name, e);
        }
        if (server != null) {
            return server;
        } else {
            logger.debug("Zone avoidance logic is not invoked.");
            return super.chooseServer(key);
        }
    }

源码一开始就看到,只有负载均衡器中维护的实例所属的区域大于1时才会执行这里的策略,否则还是执行父类的。
当大于1时:
调用ZoneAvoidanceRule中的静态方法createSnapshot,为当前负载均衡器中所有的Zone区域创建快照,保存在Map中。

调用ZoneAvoidanceRule中的静态方法getAvailableZones来获取可用的Zone区域集合,在该函数中会通过Zone区域快照中的统计数据来实现可用区的挑选。
首先它会剔除符合这些规则的Zone区域:所属实例数为0的区域。Zone区域内实例的平均负载小于0.实力故障率(断路器断开次数/实例数)大于等于阀值(默认0.9999)

然后根据Zone区域的实例平均负载计算出最差的Zone区域,这里的最差指的是实力平均负载最高的Zone区域。

如果在上面的过程中没有符合剔除要求的区域,同时实例最大平均负载小于阀值(默认20%),就直接返回所有的Zone为可用区域。否则,从最坏Zone区域合集中随机选择一个,将它从可用Zone区域中剔除。

当获得的可用Zone区域集合不为空,并且个数小于Zone区域总数,就随机选择一个Zone区域。
在确定了某个Zone区域后,则获取了对应的Zone区域的负载均衡器,并调用chooseServer选择具体的服务实例。

Ribbon中实现了很多的负载均衡策略,我们来详细解读一下。整理出了IRule接口的一些实现图

在这里插入图片描述

(1)AbstractLoadBalancerRule

AbstractLoadBalancerRule:负载均衡策略的抽象类,该类中定义了负载均衡器ILoadBalancer对象,该对象能够在具体实现选择服务策略时,获取到一些负载均衡器中维护的信息来作为分配依据。

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {

    private ILoadBalancer lb;
        
    @Override
    public void setLoadBalancer(ILoadBalancer lb){
        this.lb = lb;
    }
    
    @Override
    public ILoadBalancer getLoadBalancer(){
        return lb;
    }      
}

(2)RandomRule
该策略实现了随机选择一个服务实例的功能。具体实现为通过传入的负载均衡器获取可用服务实例列表和所有服务实例列表,并通过rand.nextInt获取一个随机数,然后作为upList的索引返回具体事例。

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> upList = lb.getReachableServers();
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();
            if (serverCount == 0) {
                /*
                 * No servers. End regardless of pass, because subsequent passes
                 * only get more restrictive.
                 */
                return null;
            }

            int index = rand.nextInt(serverCount);
            server = upList.get(index);

            if (server == null) {
                /*
                 * The only time this should happen is if the server list were
                 * somehow trimmed. This is a transient condition. Retry after
                 * yielding.
                 */
                Thread.yield();
                continue;
            }

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

            // Shouldn't actually happen.. but must be transient or a bug.
            server = null;
            Thread.yield();
        }

        return server;

    }

(3)RoundRobinRule

该策略实现了轮询选择每个服务实例的功能。先从负载均衡器中获取可用和所有的服务实例列表,然后增加一个count变量,该变量会在每次循环后累加,如果一直选不到Server超过十次,就会尝试结束并打印警告信息。轮询的实现是通过构造函数里的nextServerCyclicCounter对象实现。

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

(4)RetryRule
该策略实现了一个具有重试机制的实例选择功能。我们看到其内部定义了一个IRule对象,默认使用了RoundRobinRule实现。choose方法中实现了反复重试的策略,若期间能够选择到具体的服务实例就返回,选择不到就根据设置的尝试结束时间为阀值(maxRetryMillis+choose方法开始的时间),当超过该阀值返回null。

public Server choose(ILoadBalancer lb, Object key) {
		long requestTime = System.currentTimeMillis();
		long deadline = requestTime + maxRetryMillis;

		Server answer = null;

		answer = subRule.choose(key);

		if (((answer == null) || (!answer.isAlive()))
				&& (System.currentTimeMillis() < deadline)) {

			InterruptTask task = new InterruptTask(deadline
					- System.currentTimeMillis());

			while (!Thread.interrupted()) {
				answer = subRule.choose(key);

				if (((answer == null) || (!answer.isAlive()))
						&& (System.currentTimeMillis() < deadline)) {
					/* pause and retry hoping it's transient */
					Thread.yield();
				} else {
					break;
				}
			}

			task.cancel();
		}

		if ((answer == null) || (!answer.isAlive())) {
			return null;
		} else {
			return answer;
		}
	}

(5)WeightedResponseTimeRule
该策略是对RoundRobinRule的扩展。增加了根据实例的运行情况来计算权重,根据权重来挑选实例。它的实现有三个核心内容。

定时任务:WeightedResponseTimeRule在初始化的时候会通过serverWeightTimer启动一个定时任务,用来为每个服务实例计算权重,默认30秒执行一次。

class DynamicServerWeightTask extends TimerTask {
        public void run() {
            ServerWeight serverWeight = new ServerWeight();
            try {
                serverWeight.maintainWeights();
            } catch (Exception e) {
                logger.error("Error running DynamicServerWeightTask for {}", name, e);
            }
        }
    }

权重计算:

public void maintainWeights() {
            ILoadBalancer lb = getLoadBalancer();
            if (lb == null) {
                return;
            }
            
            if (!serverWeightAssignmentInProgress.compareAndSet(false,  true))  {
                return; 
            }
            
            try {
                logger.info("Weight adjusting job started");
                AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb;
                LoadBalancerStats stats = nlb.getLoadBalancerStats();
                if (stats == null) {
                    // no statistics, nothing to do
                    return;
                }
                //计算所有的实例平均响应时间总和
                double totalResponseTime = 0;
                // find maximal 95% response time
                for (Server server : nlb.getAllServers()) {
                	//如果服务实例的状态快照不在缓存中,自动加载
                    // this will automatically load the stats if not in cache
                    ServerStats ss = stats.getSingleServerStat(server);
                    totalResponseTime += ss.getResponseTimeAvg();
                }
                // weight for each server is (sum of responseTime of all servers - responseTime)
                // so that the longer the response time, the less the weight and the less likely to be chosen
                //计算每个实例的权重weightSoFar +totalResponseTime -实例的平均响应时间
                Double weightSoFar = 0.0;
                
                // create new list and hot swap the reference
                List<Double> finalWeights = new ArrayList<Double>();
                for (Server server : nlb.getAllServers()) {
                    ServerStats ss = stats.getSingleServerStat(server);
                    double weight = totalResponseTime - ss.getResponseTimeAvg();
                    weightSoFar += weight;
                    finalWeights.add(weightSoFar);   
                }
                setWeights(finalWeights);
            } catch (Exception e) {
                logger.error("Error calculating server weights", e);
            } finally {
                serverWeightAssignmentInProgress.set(false);
            }

        }

该方法首先根据LoadBalancerStats 中记录的每个实例的统计信息,累加所有实例的平均响应时间,得到总平均响应时间totalResponseTime。
为负载均衡器中维护的实例清单逐个计算权重,计算规则为weightSoFar +totalResponseTime -实例的平均响应时间。其中weightSoFar 初始化为0,并且每计算好一个权重需要累加到weightSoFar 上供下一个使用。

例如有四个实例ABCD,它们的平均响应时间为10、40、80、100,所以总响应时间为230。
A的权重为:0+230-10=220
B的权重为:220+230-40=410
C的权重为:410+230-80=560
D的权重为:560+230-100=690
需要注意的是,这里的权重值只是表示了各个实例权重区间的上限,并不是实例的优先级,所以不是数值越大被选中的几率越高。那么什么是权重区间呢?每个实例的区间下限是上一个实例的区间上限。
所以上面四个实例的区间是
A[0,220],B(220,410],C(410,560],D(560,690)
实际上每个区间的宽度就是,总的平均响应时间-实例的平均响应时间,所以实例的平均响应时间越短,权重区间宽度越大,被选中的几率就越高。那么这些区间的开闭是如何决定的呢?我们从源码入手:

public Server choose(ILoadBalancer lb, Object key) {
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            // get hold of the current reference in case it is changed from the other thread
            List<Double> currentWeights = accumulatedWeights;
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> allList = lb.getAllServers();

            int serverCount = allList.size();

            if (serverCount == 0) {
                return null;
            }

            int serverIndex = 0;
			
			//获取最后一个实例的权重
            // last one in the list is the sum of all weights
            double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); 
            // No server has been hit yet and total weight is not initialized
            // fallback to use round robin
            //如果最后一个实例的权重小于0.001,则采用父类的轮询策略
            if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) {
                server =  super.choose(getLoadBalancer(), key);
                if(server == null) {
                    return server;
                }
            } else {
            	//如果最后一个实例权重大于0.001,就产生一个[0,maxTotalWeight )随机数
                // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive)
                double randomWeight = random.nextDouble() * maxTotalWeight;
                // pick the server index based on the randomIndex
                int n = 0;
                for (Double d : currentWeights) {
                	//遍历维护的权重清单,若权重大于等于随机得到的数值,就选择这个实例
                    if (d >= randomWeight) {
                        serverIndex = n;
                        break;
                    } else {
                        n++;
                    }
                }

                server = allList.get(serverIndex);
            }

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

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

            // Next.
            server = null;
        }
        return server;
    }

可以看到核心过程,第一先获取[0,最大权重值)的一个随机数,遍历权重列表,比较权重值和随机数的大小,如果权重值大于等于随机数,就拿当前权重列表的索引值去服务实例列表中选择具体的服务实例。而权重区间的开闭原则根据算法,正常每个区间为(x,y]的形式,但是第一个和最后一个不同。是因为随机数最小取值可以为0,但是随机数最大是取不到最大权重值的。
例如上面的从[0,690)选择一个随机数,230,由于该值位于第二个区间,所以选择B来进行请求。

(6)ClientConfigEnabledRoundRobinRule
该策略比较特殊,我们一般不直接使用。它本身没有什么特殊的处理逻辑,它内部定义了一个RoundRobinRule,所以choose也是采用了RoundRobinRule的轮询策略。所以它功能实际上和RoundRobinRule相同。

我们一般通过继承这个策略,然后自己做一些高级的策略,然后可以用父类的策略作为备选。

(7)BestAvailableRule
它继承了ClientConfigEnabledRoundRobinRule,它注入了负载均衡器的统计对象LoadBalancerStats,在choose方法中利用LoadBalancerStats保存的实例统计信息来选择满足要求的实例。源码中看到,它通过遍历负载均衡器中维护的所有服务实例,过滤掉故障的实例。找出并发请求数最小的一个,所以该策略是选出最空闲的实例。

public Server choose(Object key) {
        if (loadBalancerStats == null) {
            return super.choose(key);
        }
        List<Server> serverList = getLoadBalancer().getAllServers();
        int minimalConcurrentConnections = Integer.MAX_VALUE;
        long currentTime = System.currentTimeMillis();
        Server chosen = null;
        for (Server server: serverList) {
            ServerStats serverStats = loadBalancerStats.getSingleServerStat(server);
            if (!serverStats.isCircuitBreakerTripped(currentTime)) {
                int concurrentConnections = serverStats.getActiveRequestsCount(currentTime);
                if (concurrentConnections < minimalConcurrentConnections) {
                    minimalConcurrentConnections = concurrentConnections;
                    chosen = server;
                }
            }
        }
        if (chosen == null) {
            return super.choose(key);
        } else {
            return chosen;
        }
    }

该策略的核心是LoadBalancerStats,当其为空的时候,该策略是无法执行的,所以当LoadBalancerStats为空时,会采用父类的轮询策略。

(8)PredicateBasedRule
PredicateBasedRule是一个抽象类,继承了ClientConfigEnabledRoundRobinRule,从其命名中可以知道是一个基于Predicate实现的策略。Predicate是Googel Guava Collecation工具对集合进行过滤的条件接口。

public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
   
    /**
     * Method that provides an instance of {@link AbstractServerPredicate} to be used by this class.
     * 
     */
    public abstract AbstractServerPredicate getPredicate();
        
    /**
     * Get a server by calling {@link AbstractServerPredicate#chooseRandomlyAfterFiltering(java.util.List, Object)}.
     * The performance for this method is O(n) where n is number of servers to be filtered.
     */
    @Override
    public Server choose(Object key) {
        ILoadBalancer lb = getLoadBalancer();
        Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        if (server.isPresent()) {
            return server.get();
        } else {
            return null;
        }       
    }
}

首先定义了一个抽象方法getPredicate获取AbstractServerPredicate 对象,而在choose方法中,通过AbstractServerPredicate 的chooseRoundRobinAfterFiltering方法选出具体的服务实例。我们来看下该方法

  public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
        List<Server> eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
        return Optional.of(eligible.get(incrementAndGetModulo(eligible.size())));
    }

....

  public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
        if (loadBalancerKey == null) {
            return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate()));            
        } else {
            List<Server> results = Lists.newArrayList();
            for (Server server: servers) {
                if (this.apply(new PredicateKey(loadBalancerKey, server))) {
                    results.add(server);
                }
            }
            return results;            
        }
    }

该方法先通过调用getEligibleServers方法来获取过滤后的实例清单,如果返回清单为null,则返回Optional.absent()表示不存在,否则采用轮询获取一个实例。
那么getEligibleServers是如何过滤清单的呢?
从源码上看,通过遍历服务清单,使用this.apply方法判断实例是否需要保留,如果是就添加到列表中。apply方法是Predicate接口中的方法。这里的策略是“先过滤清单,再轮询选择”,至于如何过滤,就需要我们在实现类上实现apply方法进行过滤。

(9)AvailabilityFilteringRule
AvailabilityFilteringRule,继承于PredicateBasedRule,所以它继承了“先过滤清单,再轮询选择”的基本策略。其中过滤条件则在构造方法中使用了AvailabilityPredicate实现。

public class AvailabilityPredicate extends  AbstractServerPredicate {
        
    ......
    
    @Override
    public boolean apply(@Nullable PredicateKey input) {
        LoadBalancerStats stats = getLBStats();
        if (stats == null) {
            return true;
        }
        return !shouldSkipServer(stats.getSingleServerStat(input.getServer()));
    }
    
    
    private boolean shouldSkipServer(ServerStats stats) {        
        if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped()) 
                || stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) {
            return true;
        }
        return false;
    }

}

我们可以看到它的过滤逻辑为shouldSkipServer方法,它主要判断服务实例的两个内容。
是否故障,即断路器是否生效已断开。
实例的并发请求数大于阀值,默认为2^32-1。
只要有一个满足apply就返回false,都不满足返回true。

而AvailabilityFilteringRule的choose方法也作了一些优化。

 public Server choose(Object key) {
        int count = 0;
        Server server = roundRobinRule.choose(key);
        while (count++ <= 10) {
            if (predicate.apply(new PredicateKey(server))) {
                return server;
            }
            server = roundRobinRule.choose(key);
        }
        return super.choose(key);
    }

它并没有像父类一样,遍历所有的节点然后进行过滤。而是用轮询的方式选择实例,判断该实例是否满足要求,满足就直接使用,不满足则选择下一个。当过程重复了十次还是没有找到符合要求的实例,就采用父类的方法。

(10)ZoneAvoidanceRule

ZoneAvoidanceRule,继承了PredicateBasedRule。它使用了CompositePredicate来进行服务实例清单的过滤,这是一个组合过滤条件,在构造方法中知道,它以ZoneAvoidancePredicate作为主过滤条件,AvailabilityPredicate作为次过滤条件。

public class ZoneAvoidanceRule extends PredicateBasedRule {

    private static final Random random = new Random();
    
    private CompositePredicate compositePredicate;
    
    public ZoneAvoidanceRule() {
        super();
        ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this);
        AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this);
        compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate);
    }
    .......
    }

ZoneAvoidanceRule没有重写choose方法,所以它完全遵循父类的“先过滤清单,再轮询选择”。我们看下它的过滤方法。

 public List<Server> getEligibleServers(List<Server> servers, Object loadBalancerKey) {
        List<Server> result = super.getEligibleServers(servers, loadBalancerKey);
        Iterator<AbstractServerPredicate> i = fallbacks.iterator();
        while (!(result.size() >= minimalFilteredServers && result.size() > (int) (servers.size() * minimalFilteredPercentage))
                && i.hasNext()) {
            AbstractServerPredicate predicate = i.next();
            result = predicate.getEligibleServers(servers, loadBalancerKey);
        }
        return result;
    }

使用主过滤条件对所有实例过滤并返回过滤后的实例清单。
依次使用次过滤条件列表中的过滤条件对主过滤条件的结果进行过滤。
每次过滤之后,都要判断两个条件。只要有一个符合就不再过滤。将当前结果返回轮询选择。
(1)过滤后的实例总数>=最小过滤实例数(minimalFilteredServers,默认为1)
(2)过滤后的实例比例>最小过滤百分比(minimalFilteredPercentage,默认为0)

配置详解

Ribbon中定义的每个接口都有好多种不同的策略实现,Spring Cloud Ribbon为我们自动化配置了默认的实现。可以在RibbonClientConfiguration类中看到默认的实现配置。

IClientConfig:Ribbon的客户端配置。默认使用com.netflix.client.config.DefaultClientConfigImpl实现。

IRule:Ribbon的负载均衡策略。默认使用com.netflix.loadbalancer.ZoneAvoidanceRule实现。

IPing:Ribbon的实例检查策略。默认使用com.netflix.loadbalancer.DummyPing实现。实际上并不检查,始终返回true。

ServerList< Server >:服务实例清单的维护机制。默认使用com.netflix.loadbalancer.ConfigurationBasedServerList实现。

ServerListFilter< Server >:服务实例清单的过滤机制。默认使用org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter实现。该策略能够优先过滤出与请求调用方处于同区域的服务实例。

ILoadBalancer:负载均衡器。默认使用com.netflix.loadbalancer.ZoneAwareLoadBalancer实现。具备区域感知的能力。

如果这些类需要配置我们自己所需要的实现类话,只需要在我们的类中注入相应的Bean即可以覆盖默认配置。如下

@Bean
public IPing  ribbonPing(IClientConfig config){
		return new PingUrl();
}

当然这样配置有点不太方便,我们可以直接在配置文件中进行配置。PropertiesFactory这个类指定了参数的一些key。

但是当Eureka和Ribbon整合时,会触发Eureka对Ribbon的自动配置。这时ServerList的维护机制将被com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList覆盖实现。该实现会将服务清单列表交给Eureka的服务治理机制来维护。IPing的实现将由com.netflix.niws.loadbalancer.NIWSDiscoveryPing覆盖,将实例检查的任务交给了服务治理框架。

重试机制

由于Spring Cloud Eureka实现的服务治理机制强调了CAP原理中的AP(可用性和可靠性)。Eureka为了实现更高的可用性,牺牲了一定的一致性。比如:当服务注册中心的网络发生故障断开,这时所有的服务实例都无法续约,这时会把所有的服务实例都剔除,但是Eureka这时会触发保护机制,注册中心此时会保留所有服务节点,以实现服务间依然可以相互调用,即使其中有部分故障节点。当调用到故障节点的时候,Eureka提供了重试机制,访问到故障服务时,会尝试再访问一次当前实例(次数可以配置),不行就换一个实例进行尝试(更换的次数也可以配置),最后不行返回失败信息。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值