SpringCloud Ribbon 源码初读

SpringCloud Ribbon 源码初读

不想看源码,看的头疼。没办法啊,得看啊。

为什么要写这篇文章

起因是看见微服务项目中服务间调用都常用RestTemplate,在程序启动类中提供这样一个Bean,然后在其上加上@LoadBalanced 就能实现服务的负载均衡调用,想究其咋实现的。网上也看了众多文章和书籍,然后自己用IDE研究了下,故有此文章(看别人的不如自己亲自动手试下看下,这样记忆深刻,顺便记录下)。如有雷同纯属巧合。

从注解@LoadBalanced入手

看源码

image.png

此注解上的元注解,我就不在此赘述。

首先看这个注解的注释,英文注释,不怕,直接翻译一下。

大致意思是:这个注解标记RestTemplate ,用 LoadBalancerClient 这个类来配置。

现在进入LoadBalancerClient 看一下,它也是一个接口。

不截图了 截图 注释太多图片太大,我简化一下。

public interface LoadBalancerClient extends ServiceInstanceChooser {

	//根据传入的serviceId使用负载均衡器挑选服务实例执行请求内容(有一定的负载均衡策略默认是轮询)
	<t> T execute(String serviceId, LoadBalancerRequest<t> request) throws IOException;

	/**
	* 上面方法的重载,多了一个参数ServiceInstance,
    * 这是一个接口表示服务发现系统中的服务实例对象
    **/
	<t> T execute(String serviceId, ServiceInstance serviceInstance, LoadBalancerRequest<t> request) throws IOException;

	/**
	* 看方法名的意思,重建URI的方法,建造一个host:port 形式的URI。
	* 为保证高可用,一般一个服务有多个实例,一般我们调用服务时,使用服务名进行调用,
	* 避免使用具体IP地址
	* 即 将http://127.0.0.1:8099/service/ser 变为 http://someone-service/service/ser
	* 第一个参数 instance 是ServiceInstance类型,它的实例包含Host、port、uri等属性;
	* 第二个参数 original 只是使用 服务名 为host 的uri
	* 返回参数 URI 返回的是 将两个入参经过一定逻辑之后,拼接出的具体Host:Port 形式的请求地址
	**/
	URI reconstructURI(ServiceInstance instance, URI original);
}

别忘了,这个接口还继承了一个ServiceInstanceChooser ,看一下。

/**
 *
 * 由使用负载均衡器来选择发送请求到的服务器的类实现
 */
public interface ServiceInstanceChooser {

    /**
     * 根据传入serviceId 
     * 为指定的服务从LoadBalancer中选择服务实例
     */
    ServiceInstance choose(String serviceId);
}

看一下 LoadBalancerClient 所在的包

image.png

哎哟,不错。看到个好东西 LoadBalancerAutoConfiguration ,从名字可以看出这是负载均衡器的自动配置类,打开简单看下。

@Configuration
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
public class LoadBalancerAutoConfiguration {

	@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) {
                    customizer.customize(restTemplate);
                }
            }
        });
	}

	@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);
            };
		}
	}
    
    //...剩余代码未涉及未列出
}

有些代码已省略,因为根据@Condition系列注解这些省略的Bean不会加载。

根据LoadBalancerAutoConfiguration 类最上面的注解可以知道这是个配置类。

其成功加载的条件为下面两个

  • @ConditionalOnClass(RestTemplate.class)

    RestTemplate 类必须存在于当前工程环境。

  • @ConditionalOnBean(LoadBalancerClient.class)

    在Spring上下文所加载的Bean中必须存在LoadBalancerClient(interface) 的实现Bean对象

此类主要加载的对象为:

  • 最上面有一个restTemplates ,被 @LoadBalanced 所注释,其初始化了一个List<resttemplate> ,通过调用RestTemplateCustomizer 的实例来给所需客户端 RestTemplate 增加 LoadBalancerInterceptor 拦截器。

  • LoadBalancerInterceptor ,看名字知道,其是一个拦截器,主要用于拦截客户端所发的请求,来实现客户端的负载均衡。

  • RestTemplateCustomizer,看代码逻辑,用于给RestTemplate 添加拦截器即 LoadBalancerInterceptor

LoadBalancerInterceptor如何为RestTemplate的负载均衡赋能

看代码

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, requestFactory.createRequest(request, body, execution));
	}
}

通过它的源码和之前自动配置类的代码,明显能发现其在拦截器中需要注入 LoadBalancerClient 的实现。当一个被@LoadBalanced 注解修饰的 RestTemplate 对象向外发起HTTP请求,会被 LoadBalancerInterceptor 的这个 intercept() 方法拦截。由于我们在使用RestTemplate 时采用服务名调用,所以在 final URI originalUri = request.getURI();
String serviceName = originalUri.getHost(); 这两个语句运行后,能拿到服务名,其后调用了execute() 去根据服务名来选择实例发起实际的请求。

分析至此,LoadBalancerClient 其实还只是一个接口,看下其实现,顺着可以找到 RibbonLoadBalancerClient ,在包 org.springframework.cloud.netflix.ribbon 下,简单看下它所覆写的 execute 方法。

public class RibbonLoadBalancerClient implements LoadBalancerClient {
    
    @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);
	}
    
    @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;
	}
}

第一个execute方法中,首先通过 getServer 方法根据传入的serviceId去获取具体的服务实例,下面是getServer方法:

image.png

可以看到这里获取具体实例时并没有用到我们上面说的 LoadBalancerClient 接口中的choose 函数,而是使用了Netflix Ribbon 自身的ILoadBalancer 接口中定义的 chooseServer 函数。下面来看ILoadBalancer 接口。

public interface ILoadBalancer {
    /**
    * 向负载均衡器中维护的实例列表增加服务实例
    **/
    void addServers(List<server> newServers);

    /**
    * 通过某种策略,从负载均衡器中选择一个具体实例
    **/
    Server chooseServer(Object key);

    /**
    * 通知并标识负载均衡器中的某个实例已停止服务,不然负载均衡器在下一个获取服务实例列表周期前会认为此实例是可用的。
    **/
    void markServerDown(Server server);

    /** 
    * 获取服务器的当前列表。
    * 如果为true,则应该只返回活动的和可用的服务器
    * 2016-01-20 已被标记为过时 推荐使用下面两个方法
    **/
    @Deprecated
    List<server> getServerList(boolean availableOnly);

    /**
    * 获取只有启动并可访问的服务。
    **/
    List<server> getReachableServers();

    /**
    * 获取所有服务,包括可正常访问和不可正常访问的。
    **/
    List<server> getAllServers();
}

该接口中涉及的 Server 对象,定义为一个典型的服务端节点对象,该类存储一些元数据,包括但不限于host、port、zone以及一些部署信息。

查看ILoadBalancer 的实现,发现下面的类

image.png

  • AbstractLoadBalancer 包含大多数负载均衡实现所需的特性。
  • BaseLoadBalancer 实现了基础的负载均衡。
  • DynamicServerListLoadBalancer 能够使用动态源获取候选服务器列表。也就是说,服务器列表可能在运行时被更改。
    它还包含一些工具,其中服务器列表可以通过一个过滤条件来过滤出不满足所需条件的服务器。
  • NoOpLoadBalancer: 继承了AbstractLoadBalancer ,但其实没有LB
  • ZoneAwareLoadBalancer 负载均衡器,在选择服务器时可以避免一个区域作为一个整体。

Srping Cloud Ribbon 默认的负载均衡策略

public class RibbonClientConfiguration {	
	@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);
	}
}

通过查看RibbonClientConfiguration 类,可以知道其整合时 默认采用了 ZoneAwareLoadBalancer

继续回到 RibbonLoadBalancerClientexecute 执行,在通过 ZoneAwareLoadBalancerchooseServer 函数 获取了负载均衡策略分配到的服实例的对象Server之后,将其内容包装成RibbonServer 对象,此对象除了存储服务实例的信息之外,还包含了服务名serviceId、是否使用https,一个metadata(Map类型)等。然后使用该对象再回调LoaderbalancerInterceptor 请求拦截器中LoadBalancerRequestapply(ServiceInstance instance) 函数,向一个实际的具体服务实例发起请求,从而实现一开始服务名为host 的URI请求到host:post 形式的实际访问地址的转换。

apply 入参ServiceInstance 接口对象是对服务实例的抽象定义。如下,

image.png

该接口抽象了服务治理体系下每个服务实例需要的一些基本信息,包括serviceId、host、port等等。

而上面提到的 RibbonServer 对象就是ServiceInstance 的具体实现,正如我上面所说它除了包含Server对象之外,还存储了服务名、是否使用https,以及一个metadata的Map集合。如下:

image.png

继续深入,apply 函数在得到ServiceInstance 对象后如何通过LoadBalancerClient 接口中的 reconstructURI 来拼出具体请求地址。深入apply之后可以得到如下类:

public class AsyncLoadBalancerInterceptor implements AsyncClientHttpRequestInterceptor {

	private LoadBalancerClient loadBalancer;

	public AsyncLoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
		this.loadBalancer = loadBalancer;
	}

	@Override
	public ListenableFuture<clienthttpresponse> intercept(final HttpRequest request, final byte[] body,
			final AsyncClientHttpRequestExecution execution) throws IOException {
		final URI originalUri = request.getURI();
		String serviceName = originalUri.getHost();
		return this.loadBalancer.execute(serviceName,
				new LoadBalancerRequest<listenablefuture<clienthttpresponse>>() {
					@Override
					public ListenableFuture<clienthttpresponse> apply(final ServiceInstance instance)
							throws Exception {
						HttpRequest serviceRequest = new ServiceRequestWrapper(request,
								instance, loadBalancer);
						return execution.executeAsync(serviceRequest, body);
					}

				});
	}
}

在这个实现中,可以看到一个关键对象 ServiceRequestWrapper 该对象继承了HttpRequestWrapper 并重写了getURI函数,重写后的getURI 通过调用LoadBalancerClient 接口的 reconstructURI 函数重建URI来访问目标地址。具体如下:

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

AsyncLoadBalancerInterceptor 中,AsyncClientHttpRequestExecution(它是接口) 的实例具体执行execution.executeAsync(serviceRequest, body);时会调用InterceptingAsyncClientHttpRequest(org.springframework.http.client),下private class AsyncRequestExecution类中的executeAsync方法,如下:

private class AsyncRequestExecution implements AsyncClientHttpRequestExecution {

		private Iterator<asyncclienthttprequestinterceptor> iterator;

		public AsyncRequestExecution() {
			this.iterator = interceptors.iterator();
		}

		@Override
		public ListenableFuture<clienthttpresponse> executeAsync(HttpRequest request, byte[] body)
				throws IOException {

			if (this.iterator.hasNext()) {
				AsyncClientHttpRequestInterceptor interceptor = this.iterator.next();
				return interceptor.intercept(request, body, this);
			}
			else {
				URI uri = request.getURI();
				HttpMethod method = request.getMethod();
				HttpHeaders headers = request.getHeaders();

				Assert.state(method != null, "No standard HTTP method");
				AsyncClientHttpRequest delegate = requestFactory.createAsyncRequest(uri, method);
				delegate.getHeaders().putAll(headers);
				if (body.length > 0) {
					StreamUtils.copy(body, delegate.getBody());
				}

				return delegate.executeAsync();
			}
		}
	}

可以看到,在创建请求时 requestFactory.createAsyncRequest(uri, method); ,这里的uri由request.getURI()得到,这里会调用之前所说的 ServiceRequestWrapper 的getURI,此时,它就会使用RibbonLoadBalancerClient中实现的reconstructURI来组织具体请求的服务实例地址。如下

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

reconstructURI函数中我们可以看到,它通过ServiceInstance对象的serviceId,从SpringClientFactory(clientFactory)对象中获取对应serviceId的负载均衡器上下文RibbonLoadBalancerContext

说明:

  • SpringClientFactory :看名字知道是个工厂类,其实它主要用于创建客户端、负载均衡器和客户端配置实例。它为每个不同名称客户端创建一个Spring ApplicationContext,并从中提取它所需要的bean。即为每个不同名的Ribbon 客户端生成不同Spring上下文。
  • RibbonLoadBalancerContext:是LoadBalancerContext的子类 ,LoadBalancerContext类用于存储一些被负载均衡器使用的上下文内容和API操作,reconstructURIWithServer就在其中。

第一眼看见 reconstructURIWithServer 感觉和 reconstructURI 挺相似从名字可以看出来。只是前者入参需要传入一个Server对象(Netflix所定义的),后者需要的是ServiceInstance 对象(springcloud所定义的)。所以RibbonLoadBalancerClient 中覆写reconstructURI 的方法做了转换,使用ServiceInstance的host、port new了一个Server对象以便给reconstructURIWithServer 使用。reconstructURIWithServer 的具体实现中,他从Server对象中获取host、port,然后根据以服务名为host的URI 对象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);
        }
    }

至此,SpringCloud Ribbon实现客户端负载均衡的基本操作已经了解。包括LoadBalancerIntercept 拦截器对RestTemplate请求进行拦截、URI的转换、均衡器策略的选择(默认使用了ZoneAwareLoadBalancer,ILoadBalancer的实现)。


参考书籍或文章:

SpringCloud 微服务实战

posted @ 2021-06-30 20:00  桃子dev  阅读(87)  评论(0编辑  收藏  举报