Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下

Posted 热爱编程的大忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下相关的知识,希望对你有一定的参考价值。

Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下


引言

书接上篇,本文我们将来看看SpringCloud团队如何巧妙设计,完成客户端负载均衡器能够轻松从各个不同注册中心获取服务实例列表的过程。

上一篇文章结尾处也提到了,完成这个过程的核心类是NamedContextFactory,本文就来好好分析一下这个类都干了啥。


NamedContextFactory

这里我们先来简单说一下客户端负载均衡器根据服务名去获取服务实例列表的一个实现思路:

  • 客户端负载均衡器将服务名放入到当前IOC的环境上下文中
  • 不同的注册中心客户端需要为当前注册中心提供一个适配器,该适配器负责从IOC容器环境上下文中根据指定key获取到服务名,然后调用对应注册中心客户端去注册中心服务端根据服务名获取服务实例列表
  • 然后将获取到的服务实例列表添加到当前IOC容器中做为一个bean
  • 客户端负载均衡器从容器中获取到服务实例列表
  • 结束

这个方案的有很多问题,最大的问题在于不同的服务名和其关联的服务实例列表信息都混杂在同一个IOC容器中,为了进行区分管理,需要做很多额外的工作。

因此,最直接的想法就是每个服务名和其管理的服务实例列表都使用各自的子容器完成上述的通信过程,而这就是NamedContextFactory做的事情:

Ribbon 为每个 ServiceName 都拥有自己的 Spring Context 和 Bean 实例(不同服务之间的 LoadBalancer 和其依赖的 Bean 都是完全隔离的)。

使用子容器进行隔离还有如下好处:

  • 子容器之间数据隔离。不同的 LoadBalancer 只管理自己的服务实例,明确自己的职责。
  • 子容器之间配置隔离。不同的 LoadBalancer 可以使用不同的配置。例如报表服务需要统计和查询大量数据,响应时间可能很慢。而会员服务逻辑相对简单,所以两个服务的响应超时时间可能要求不同。
  • 子容器之间 Bean 隔离。可以让子容器之间注册不同的 Bean。例如订单服务的 LoadBalancer 底层通过 Nacos 获取实例,会员服务的 LoadBalancer 底层通过 Eureka 获取实例。也可以让不同的 LoadBalancer 采用不同的算法

经过上面的分析,我们知道了NamedContextFactory 可以为不同的服务名创建不同的子容器,每个子容器可以通过 Specification 定义 Bean:

	public interface Specification 
	    //该Specification返回的配置类是否只放入对应服务的子容器中,这是name是服务名
		String getName();
		//该Specification返回的配置类
		Class<?>[] getConfiguration();
	

Specification 类的getConfiguration返回的其实可以看做是不同注册中心的提供的适配器配置类,对应上图。

下面我们来看一下NamedContextFactory 的getInstance方法实现过程:

	public <T> T getInstance(String name, Class<T> type) 
		//每个服务名都对应一个子容器,根据服务名获取对应的子容器
		AnnotationConfigApplicationContext context = getContext(name);
		try 
		   //去当前子容器中获取对应包装服务实例列表的bean,这里的bean类型不固定
			return context.getBean(type);
		
		catch (NoSuchBeanDefinitionException e) 
			// ignore
		
		return null;
	

Ribbon使用ILoadBalancer来封装服务实例列表的管理相关操作,因此如果采用ribbon做客户端负载均衡器,相关注册中心的提供的适配器配置类,从对应注册中心服务端拉取到服务实例列表后,需要将服务实例列表信息转换为ILoadBalancer类型,然后注入容器,例如Eureka提供的适配器配置类名字就叫做: EurekaRibbonClientConfiguration ,可以看出这是Eureka为了适配Ribbon做客户端负载均衡器提供的适配器配置类。

getContext方法首先判断对应的服务名关联的子容器是否已经被创建了:

	protected AnnotationConfigApplicationContext getContext(String name) 
	    //DOUBLE CHECK确保多线程下对添加缓存操作实现的原子性
		if (!this.contexts.containsKey(name)) 
			synchronized (this.contexts) 
				if (!this.contexts.containsKey(name)) 
					this.contexts.put(name, createContext(name));
				
			
		
		return this.contexts.get(name);
	

如果此时缓存中没有,那么需要通过createContext为当前服务名创建一个新的子容器:

	protected AnnotationConfigApplicationContext createContext(String name) 
	    //创建子容器
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
		//如果我们手动设置了针对某个服务名单独提供的配置类,那么会首先被加入到当前服务名关联的子容器中
		//这里的configurations是NamedContextFactory内部的Specification列表
		if (this.configurations.containsKey(name)) 
			for (Class<?> configuration : this.configurations.get(name)
					.getConfiguration()) 
				context.register(configuration);
			
		
		//如果Specification返回的name以default开头,那么默认对所有服务名生效
		//即返回的配置类会添加到每个服务名对应的子容器中
		for (Map.Entry<String, C> entry : this.configurations.entrySet()) 
			if (entry.getKey().startsWith("default.")) 
				for (Class<?> configuration : entry.getValue().getConfiguration()) 
					context.register(configuration);
				
			
		
		//PropertyPlaceholderAutoConfiguration负责加载解析占位符和el表达式的bean
		context.register(PropertyPlaceholderAutoConfiguration.class,
		//向当前服务子容器中注入与当前客户端负载均衡器相关的默认配置类
		//如果是ribbon,这里默认加载的是RibbonClientConfiguration配置类
				this.defaultConfigType);
		//向子容器环境上下文中添加当前负载均衡器的相关属性
		//如果采用的是ribbon作为客户端负载均衡器		
		context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
				//key为: ribbon
				this.propertySourceName,
				//value是一个map: 该map默认存放一个键值对,key为: ribbon.client.name value为: 服务名
				Collections.<String, Object>singletonMap(this.propertyName, name)));
		//设置当前容器为子容器的父容器		
		if (this.parent != null) 
			context.setParent(this.parent);
			context.setClassLoader(this.parent.getClassLoader());
		
		//设置当前子容器展示名
		context.setDisplayName(generateDisplayName(name));
		//刷新容器,注册到容器中的配置类在这一步被解析
		context.refresh();
		return context;
	

如果我们采用的是Ribbon+Eureka的组合,那么:


可以看到EurekaRibbonClientConfiguration是Ribbon与Eureka组件协同工作的关键类:

@Target( ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
		ElementType.ANNOTATION_TYPE )
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Value("$ribbon.client.name")
public @interface RibbonClientName 

@Configuration(proxyBeanMethods = false)
public class EurekaRibbonClientConfiguration 
    ....
    //@RibbonClientName上面已经给出了,本质是从当前子容器的IOC环境中使用ribbon.client.name作为key,去取出对应的value
    //也就是说,这里serviceId,拿到的是createContext创建子容器方法中放入子容器环境上下文中的服务名
	@RibbonClientName
	private String serviceId = "client";
    //Eureka相关配置类
	@Autowired(required = false)
	private EurekaClientConfig clientConfig;
	@Autowired(required = false)
	private EurekaInstanceConfig eurekaConfig;
	@Autowired
	private PropertiesFactory propertiesFactory;
    ...
	@Bean
	@ConditionalOnMissingBean
	public ServerList<?> ribbonServerList(IClientConfig config,
	       //拿到Eureka客户端
			Provider<EurekaClient> eurekaClientProvider) 
	    //先查缓存,如果有直接返回		
		if (this.propertiesFactory.isSet(ServerList.class, serviceId)) 
			return this.propertiesFactory.get(ServerList.class, config, serviceId);
		
		//DomainExtractingServerList由于内部组合了EurekaClient,所以不用想也知道
		//是根据EurekaClient请求EurekaServer获取到服务实例列表
		DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
				config, eurekaClientProvider);
		DomainExtractingServerList serverList = new DomainExtractingServerList(
				discoveryServerList, config, this.approximateZoneFromHostname);
		return serverList;
	
	...


DiscoveryEnabledNIWSServerList内部的obtainServersViaDiscovery方法调用eurekaClient通过服务名去拉取服务,具体代码大家可以自行翻阅源码阅读。

可以看到这里EurekaRibbonClientConfiguration 的ribbonServerList返回的并不是我们期望的ILoadBalancer类,而是ServerList,
那么ILoadBalancer是在何时被注入子容器中的呢?

		//PropertyPlaceholderAutoConfiguration负责加载解析占位符和el表达式的bean
		context.register(PropertyPlaceholderAutoConfiguration.class,
		//向当前服务子容器中注入与当前客户端负载均衡器相关的默认配置类
		//如果是ribbon,这里默认加载的是RibbonClientConfiguration配置类
				this.defaultConfigType);

creatContext方法中,还注入了一个RibbonClientConfiguration到当前子容器中,该配置类中注入了ILoadBalancer :

	@Bean
	@ConditionalOnMissingBean
	public ILoadBalancer ribbonLoadBalancer(IClientConfig config,
	        //获取到当前容器中的serverList列表实例
			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);
		
		//将服务列表实例信息交给ZoneAwareLoadBalancer进行管理
		return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
				serverListFilter, serverListUpdater);
	

此时子容器中的相关配置类已经被解析完毕了,再次回到getInstance方法:

	public <T> T getInstance(String name, Class<T> type) 
		AnnotationConfigApplicationContext context = getContext(name);
		try 
		    //可以从容器中获取到类型为ILoadBalancer的负载均衡器了
			return context.getBean(type);
		
		catch (NoSuchBeanDefinitionException e) 
			// ignore
		
		return null;
	

此时获取到的负载均衡器类型为ZoneAwareLoadBalancer,内部掌握了Eureak注册中心上所有服务注册信息,并且通过serverListUpdater动态更新服务相关信息。


Nacos扩展例子


使用nacos做注册中心,nacos会添加一个NacosRibbonClientConfiguration的配置类到子容器中。

@Configuration(proxyBeanMethods = false)
@ConditionalOnRibbonNacos
public class NacosRibbonClientConfiguration 

	@Autowired
	private PropertiesFactory propertiesFactory;

	@Bean
	@ConditionalOnMissingBean
	public ServerList<?> ribbonServerList(IClientConfig config,
			NacosDiscoveryProperties nacosDiscoveryProperties) 
		//针对服务做特定配置的--下面会将	
		if (this.propertiesFactory.isSet(ServerList.class, config.getClientName())) 
			ServerList serverList = this.propertiesFactory.get(ServerList.class, config,
					config.getClientName());
			return serverList;
		
		//注入的是NacosServerList 
		NacosServerList serverList = new NacosServerList(nacosDiscoveryProperties);
		//设置服务名
		serverList.initWithNiwsConfig(config);
		return serverList;
	

	@Bean
	@ConditionalOnMissingBean
	public NacosServerIntrospector nacosServerIntrospector() 
		return new NacosServerIntrospector();
	


public class NacosServerList extends AbstractServerList<NacosServer> 

	private NacosDiscoveryProperties discoveryProperties;

	private String serviceId;

	public NacosServerList(NacosDiscoveryProperties discoveryProperties) 
		this.discoveryProperties = discoveryProperties;
	

	@Override
	public List<NacosServer> getInitialListOfServers() 
		return getServers();
	

	@Override
	public List<NacosServer> getUpdatedListOfServers() 
		return getServers();
	

	private List<NacosServer> getServers() 
		try 
		    //分组信息
			String group = discoveryProperties.getGroup();
			List<Instance> instances = discoveryProperties.namingServiceInstance()
					//通过服务名,分组去查询对应的真实服务实例列表了
					.selectInstances(serviceId, group, true);
			return instancesToServerList(instances);
		
		catch (Exception e) 
			throw new IllegalStateException(
					"Can not get service instances from nacos, serviceId=" + serviceId,
					e);
		
	

	private List<NacosServer> instancesToServerList(List<Instance> instances) 
		List<NacosServer> result = new ArrayList<>();
		if (CollectionUtils.isEmpty(instances)) 
			return result;
		
		for (Instance instance : instances) 
			result.add(new NacosServer(instance));
		

		return result;
	

	public String getServiceId() 
		return serviceId;
	

	@Override
	public void initWithNiwsConfig(IClientConfig iClientConfig) 
		this.serviceId = iClientConfig.getClientName();
	




注册中心如何适配到ribbon这个体系中来呢?


具体注册进configurations集合是通过注册中心提供一个配置类,并通过@RibbonClient注解标注需要放入子容器的配置类完成的:

@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target( ElementType.TYPE )
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients 
	RibbonClient[] value() default ;
	Class<?>[] defaultConfiguration() default ;

RibbonClientConfigurationRegistrar负责解析这些配置类上的@RibbonClient注解,然后将注解中指定的配置类设置为RibbonClientSpecification中的configuration属性值,并将RibbonClientSpecification类作为bean注册到容器中:

ribbon的自动配置类扫描这些类型为RibbonClientSpecification的bean,然后加入SpringClientFactory保存:


每个注册中心提供的注册到子容器中的配置类,必须向容器中注入这两个类型的bean:

public interface ServerList<T extends Server> 
    public List<T> getInitialListOfServers();
    public List<T> getUpdatedListOfServers();   


public interface ServerIntrospector 
	boolean isSecure(Server server);
	Map<String, String> getMetadata(Server server);


Ribbon通过负载均衡算法挑选可用服务实例

我们再次回到RibbonLoadBalancerClient的execute方法:

	public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
			throws IOException 
	    //我们此时已经成功根据服务名获取到了对应的客户端负载均衡器		
		ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
		//下一步就是根据负载均衡器挑选一个可用服务实例
		Server server = getServer(loadBalancer, hint);
		...
		return execute(serviceId, ribbonServer, request);
	

直接调用客户端负载均衡器的chooseServer方法获取一个可用服务实例:

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

通过上面的分析,我们知道此事客户端负载均衡器的类型为ZoneAwareLoadBalancer:

DynamicServerListLoadBalancer,采用的是线性轮询的方式来选择调用服务实例,该算法实现简单并没有区域Zone的概念,所以它会把所有实例视为一个Zone下的节点来看待,这样就会周期性地产生跨区域访问的情况,由于跨区域会产生更高的延迟,这些实例主要以防止区域故障实现高可用的目的而不能作为常规访问的实例。所以在多区域部署的情况下会有一定的性能问题。ZoneAwareLoadBalancer可用避免这样的问题。

ZoneAwareLoadBalancer会将得到的可用服务列表按照zone进行分组:

ZoneAwareLoadBalancer的chooseServer方法实现如下:

    @Override
    public Server chooseServer(Object key) 
        //如果zone只存在一个,那么调用父类方法正常选择一个可用服务实例
        if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) 
            logger.debug("Zone aware logic disabled or there is onl

以上是关于Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下的主要内容,如果未能解决你的问题,请参考以下文章

Eurkea,Zookeeper,Consul三个服务注册中心的异同

RestTemplat发送HTTP请求

RestTemplat发送HTTP请求

41 ribbon和feign

Spring Cloud Netflix:ribbon.NIWSServerListClassName 和ribbon.listOfServers 有啥区别?

Ribbon 和 Feign 的区别