Spring Cloud Eureka 源码解析

写在前面

在看具体源码之前我们先回顾一下之前在《服务治理:Spring-cloud Eureka入门实例详解》所实现的内容。首先,对于服务注册中心、服务提供者、服务消费者这三个主要元素来说,后两者(也就是Eurake客户端)在整个运行机制中是大部分通信行为的主动发起者,而注册中心主要是处理请求的接受者,所以,我们可以从Eurake的客户端作为入口看看它是如何完成这些主要通信行为的。
我们将一个普通的Spring Boot应用注册到Eureka Server或是从Eureka Server中获取服务列表时,主要做了以下两件事:

  1. 在应用主类配置了@EnableDiscoveryClient注解
  2. 在application.properties中用eurake.serverUrl.defaultZonoe参数指定了服务注册中心的位置

一、@EnableDiscoveryClient注解

现在我们来看一下@EnableDiscoveryClient的源码

/**
 * Annotation to enable a DiscoveryClient implementation.
 * @author Spencer Gibb
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {
   

	/**
	 * If true, the ServiceRegistry will automatically register the local server.
	 * 如果为真,ServiceRegistry将自动注册本地服务器
	 */
	boolean autoRegister() default true;
}

通过注释我们可以知道它主要用来开启DiscoveryClient的实例。接下来我们来看一下DiscoveryClient相关的类与接口。
在这里插入图片描述
其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud的接口,它定义了用来发现服务的常用抽象方法,通过该接口可以有效地屏蔽服务治理的实现细节。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对该接口的实现,实现的是对Eureka发现服务的封装。右边的接口和类均来自com.netflix.discovery包,EurekaDiscoveryClient依赖了Netflix Eurake的com.netflix.discovery.EurakeClient接口,EurekaClient继承了LookuoServer接口,它们都是Netflix开源包的内容,主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient类。

1.1 com.netflix.discovery.DiscoveryClient类

根据com.netflix.discovery.DiscoveryClient类的头部注释,我们可以知道以下信息:

这个类用于帮助Eurake Server互相协作。
Eureka Client负责下面的任务:  

 - 向Eureka Server注册服务实例
 - 向Eureka Server服务续约
 - 当服务关闭期间,向Eureka Server取消租约
 - 查询Eureka Server中的服务实例列表
 
 Eureka Client还需要配置一个Eureka Server的URL列表

在具体看Eureka Client负责完成的任务之前,我们先看看在哪里对Eureka Server的URL列表进行配置,根据我们配置的属性名eureka.client.serviceUrl.defaultZone,通过serviceUrl可以找到该属性相关的加载属性,但是在SR5版本中它们都被@Deprecated标注为不再建议使用,并@link到替代类com.netflix.discovery.endpoint.EndpointUtils,所以我们在com.netflix.discovery.endpoint.EndpointUtils类下找这个方法

/**
     * Get the list of all eureka service urls from properties file for the eureka client to talk to.
     *  从属性文件中获取所有eureka服务url的列表,以便与eureka客户机通信
     * @param clientConfig the clientConfig to use
     * @param instanceZone The zone in which the client resides
     * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise
     * @return The list of all eureka service urls for the eureka client to talk to
     */
    public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
   
        List<String> orderedUrls = new ArrayList<String>();
        //重点部分
        String region = getRegion(clientConfig);
        String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
        
        if (availZones == null || availZones.length == 0) {
   
            availZones = new String[1];
            availZones[0] = DEFAULT_ZONE;
        }
        logger.debug("The availability zone for the given region {} are {}", region, availZones);
        
        	//重点部分
        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
        List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
        
        if (serviceUrls != null) {
   
            orderedUrls.addAll(serviceUrls);
        }
        int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
        while (currentOffset != myZoneOffset) {
   
            serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
            if (serviceUrls != null) {
   
                orderedUrls.addAll(serviceUrls);
            }
            if (currentOffset == (availZones.length - 1)) {
   
                currentOffset = 0;
            } else {
   
                currentOffset++;
            }
        }

        if (orderedUrls.size() < 1) {
   
            throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
        }
        return orderedUrls;
    }

1.2 Region、Zone

在上面的函数中,可以发现,客户端依次加载了两个内容,第一个是Region,第二个是Zone;

  • 通过getRegion函数,我们可以看到它从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region,如果不特别配置则默认default。若我们要自己设置,可以通过eureka.client.region属性来定义。
/**
     * Get the region that this particular instance is in.
     * 获取这个特定实例所在的区域。
     * @return - The region in which the particular instance belongs to.
     */
    public static String getRegion(EurekaClientConfig clientConfig) {
   
        String region = clientConfig.getRegion();
        if (region == null) {
   
            region = DEFAULT_REGION;
        }
        region = region.trim().toLowerCase();
        return region;
    }
  • 通过getAvailabilityZones函数,可以知道当我们没有特别为Region配置Zone的时候,将默认采用defaultZone,这也是我们之前配置参数eureka.client.serviceUrl.defaultZone的由来。若要为应用指定Zone,可以通过eureka.client.avaliablitity-zones属性来进行设置,。从函数getAvailabilityZones返回的内容,我们知道Zone可以设置多个,用逗号进行分隔,由此,我们可以判断Region和Zone是一对多的关系
@Override
	public String[] getAvailabilityZones(String region) {
   
		String value = this.availabilityZones.get(region);
		if (value == null) {
   
			value = DEFAULT_ZONE;
		}
		return value.split(",");
	}

1.3 serviceUrls

在获取了Region和Zone的信息之后,开始真正加载Eureka Server的具体地址,它根据传入的参数按一定算法确定加载位于哪一个Zone配置的serviceUrls

 int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
 List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);

具体获取serviceUrls的实现,我们可以看getEurekaServerServiceUrls函数的具体实现类EurekaClientConfigBean,该类是EurekaClientConfig和EurekaConstants接口的实现,用来加载配置文件中的内容。

@Override
	public List<String> getEurekaServerServiceUrls(String myZone) {
   
		String serviceUrls = this.serviceUrl.get(myZone);
		if (serviceUrls == null || serviceUrls.isEmpty()) {
   
			serviceUrls = this.serviceUrl.get(DEFAULT_ZONE);
		}
		if (!StringUtils.isEmpty(serviceUrls)) {
   
			final String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
			List<String> eurekaServiceUrls = new ArrayList<>(serviceUrlsSplit.length);
			for (String eurekaServiceUrl : serviceUrlsSplit) {
   
				if (!endsWithSlash(eurekaServiceUrl)) {
   
					eurekaServiceUrl += "/";
				}
				eurekaServiceUrls.add(eurekaServiceUrl);
			}
			return eurekaServiceUrls;
		}

		return new ArrayList<>();
	}

当我们在为微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性:Ribbon的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone中没有可用服务端实例的时候才会访问其他Zone中的实例。

二、服务注册

在理解了多个服务注册中心信息的加载后,我们再回头看看DiscoveryClient类是如何实现“服务注册”行为的,来看一下它的构造函数

2.1 DiscoveryClient类构造函数

@Inject
    DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, AbstractDiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
   
        if (args != null) {
   
            this.healthCheckHandlerProvider = args.healthCheckHandlerProvider;
            this.healthCheckCallbackProvider = args.healthCheckCallbackProvider;
            this.eventListeners.addAll(args.getEventListeners());
            this.preRegistrationHandler = args.preRegistrationHandler;
        } else {
   
            this.healthCheckCallbackProvider = null;
            this.healthCheckHandlerProvider = null;
            this.preRegistrationHandler = null;
        }
        
        this.applicationInfoManager = applicationInfoManager;
        InstanceInfo myInfo = applicationInfoManager.getInfo();

        clientConfig = config;
        staticClientConfig = clientConfig;
        transportConfig = config.getTransportConfig();
        instanceInfo = myInfo;
        if (myInfo != null) {
   
            appPathIdentifier = instanceInfo.getAppName() + "/" + instanceInfo.getId();
        } else {
   
            logger.warn("Setting instanceInfo to a passed in null value");
        }

        this.backupRegistryProvider = backupRegistryProvider;

        this.urlRandomizer = new EndpointUtils.InstanceInfoBasedUrlRandomizer(instanceInfo);
        localRegionApps.set(new Applications());

        fetchRegistryGeneration = new AtomicLong(0);

        remoteRegionsToFetch = new AtomicReference<String>(clientConfig.fetchRegistryForRemoteRegions());
        remoteRegionsRef = new AtomicReference<>(remoteRegionsToFetch.get() == null ? null : remoteRegionsToFetch.get().split(","));

        if (config.shouldFetchRegistry()) {
   
            this.registryStalenessMonitor = new ThresholdLevelsMetric(this, METRIC_REGISTRY_PREFIX + "lastUpdateSec_", new long[]{
   15L, 30L, 60L, 120L, 240L, 480L});
        } else {
   
            this.registryStalenessMonitor = ThresholdLevelsMetric
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值