前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下

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

作者头像
大忽悠爱学习
发布2023-02-13 15:47:31
4850
发布2023-02-13 15:47:31
举报
文章被收录于专栏:c++与qt学习c++与qt学习

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:

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

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

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

代码语言:javascript
复制
	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方法首先判断对应的服务名关联的子容器是否已经被创建了:

代码语言:javascript
复制
	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为当前服务名创建一个新的子容器:

代码语言:javascript
复制
	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组件协同工作的关键类:

代码语言:javascript
复制
@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
		ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Value("${ribbon.client.name}")
public @interface RibbonClientName {
}
代码语言:javascript
复制
@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是在何时被注入子容器中的呢?

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

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

代码语言:javascript
复制
	@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方法:

代码语言:javascript
复制
	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的配置类到子容器中。

代码语言:javascript
复制
@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();
	}

}
代码语言:javascript
复制
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注解标注需要放入子容器的配置类完成的:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
@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:

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
	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方法获取一个可用服务实例:

代码语言:javascript
复制
	protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
		...
		return loadBalancer.chooseServer(hint != null ? hint : "default");
	}

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

在这里插入图片描述
在这里插入图片描述

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

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

在这里插入图片描述
在这里插入图片描述

ZoneAwareLoadBalancer的chooseServer方法实现如下:

代码语言:javascript
复制
    @Override
    public Server chooseServer(Object key) {
        //如果zone只存在一个,那么调用父类方法正常选择一个可用服务实例
        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;
        ...
                //随机选择一个zone
                String zone = ZoneAvoidanceRule.randomChooseZone(zoneSnapshot, availableZones);
                ...
                //然后从该zone下的serverList中挑选出一个可用的server
                if (zone != null) {
                    BaseLoadBalancer zoneLoadBalancer = getLoadBalancer(zone);
                    server = zoneLoadBalancer.chooseServer(key);
                }
        ...
        return server;
    }

最终还是会调用到BaseLoadBalancer 的chooseServer从一堆可用服务实例列表中选择一个返回:

代码语言:javascript
复制
    public Server chooseServer(Object key) {
        if (counter == null) {
            counter = createCounter();
        }
        counter.increment();
        if (rule == null) {
            return null;
        } else {
            try {
                //具体采用何种负载均衡算法,取决于Rule的实现
                return rule.choose(key);
            } catch (Exception e) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", name, key, e);
                return null;
            }
        }
    }


如何调整Ribbon的负载均衡算法

我们知道Ribbon最终通过BaseLoadBalancer的choose方法实现服务的选择,该方法最终调用IRule接口的实现类完成选,那么IRule接口的实现类又是何时被设置到BaseLoadBalancer中的呢?

在这里插入图片描述
在这里插入图片描述

RibbonClientConfiguration的riibonLoadBalancer方法会从容器中寻找一个可用的IRule类型bean,然后设置到LoadBalancer中。

在这里插入图片描述
在这里插入图片描述

RibbonClientConfiguration默认提供了一个Rule实现类型为ZoneAvoidanceRule。

在这里插入图片描述
在这里插入图片描述

我们如果想要更改负载均衡算法实现,只需要往容器中注入一个IRule对象实例,覆盖默认的即可,但是这是对全局所有服务实例生效的。


如何针对具体的服务设置负载均衡算法

如果我们直接向容器中注入IRule实现对象,那么该对象是针对所有服务生效,如果我们想针对某个服务采用特殊的负载均衡算法,我们可以在配置文件中这样声明:

代码语言:javascript
复制
userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule  # 负载均衡规则

为什么可以这样玩? 因为Ribbon提供的PropertiesFactory工具类

在这里插入图片描述
在这里插入图片描述
代码语言:javascript
复制
public class PropertiesFactory {

	@Autowired
	private Environment environment;
     
    //我们可以对哪些组件按照服务的不同进行特殊指定,如果要指定,又如何标识我指定的是哪个组件呢? 
	private Map<Class, String> classToProperty = new HashMap<>();
	public PropertiesFactory() {
		classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
		classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
		classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
		classToProperty.put(ServerList.class, "NIWSServerListClassName");
		classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
	}
    
    //检查当前容器的环境变量中是否存在userservice.ribbon.NFLoadBalancerRuleClassName的key
    //如果存在,说明针对userservice服务的IRule实现进行1
	public boolean isSet(Class clazz, String name) {
		return StringUtils.hasText(getClassName(clazz, name));
	}

	public String getClassName(Class clazz, String name) {
		if (this.classToProperty.containsKey(clazz)) {
			String classNameProperty = this.classToProperty.get(clazz);
			//userservice.ribbon.NFLoadBalancerRuleClassName
			//val就是我们指定的IRule实现类全类名
			String className = environment
					.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
			return className;
		}
		return null;
	}

    //实例化对应的组件
	public <C> C get(Class<C> clazz, IClientConfig config, String name) {
		String className = getClassName(clazz, name);
		if (StringUtils.hasText(className)) {
			try {
				Class<?> toInstantiate = Class.forName(className);
				return (C) SpringClientFactory.instantiateWithConfig(toInstantiate,
						config);
			}
			catch (ClassNotFoundException e) {
				throw new IllegalArgumentException("Unknown class to load " + className
						+ " for class " + clazz + " named " + name);
			}
		}
		return null;
	}

}

执行请求

我们已经完成了服务实例获取和挑选的过程,下面就是执行请求了:

代码语言:javascript
复制
	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是ServiceInstance的子类,包装一个具体的服务实例信息
		RibbonServer ribbonServer = new RibbonServer(serviceId, server,
				isSecure(server, serviceId),
				serverIntrospector(serviceId).getMetadata(server));
        //执行请求
		return execute(serviceId, ribbonServer, request);
	}
代码语言:javascript
复制
	public <T> T execute(String serviceId, ServiceInstance serviceInstance,
			LoadBalancerRequest<T> request) throws IOException {
		...
		    //执行请求,返回结果
			T returnVal = request.apply(serviceInstance);
	   ...
			return returnVal;
		...
	}
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Eurkea,Ribbon和RestTemplate是如何结合到一起完成服务注册与发现功能的? --下
  • 引言
  • NamedContextFactory
    • Nacos扩展例子
      • 注册中心如何适配到ribbon这个体系中来呢?
      • Ribbon通过负载均衡算法挑选可用服务实例
        • 如何调整Ribbon的负载均衡算法
          • 如何针对具体的服务设置负载均衡算法
      • 执行请求
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档