spring cloud --eureka简单分析

版权声明:转载请注明原链接 https://blog.csdn.net/lij231/article/details/82915489

大家知道,将一个普通的springboot 应用注册到Eureka Server只需要两步

  1. 在应用主类添加@EnableDiscoveryClient注解
  2. 在application.properties中配置eureka.client.serviceUrl.defaultZone

顺着这条思路,首先看下@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.
	 */
	boolean autoRegister() default true;
}

从注释就能知道,该注解主要是开启DiscoveryClient的实例。梳理关系后可得到下图

其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient是spring cloud的接口,定义了用来发现服务的常用抽象方法。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对接口的实现,从命名就可以知道,是对Eureka的发现服务的封装,所以依赖了com.netflix.discovery.EurekaClient接口,EurekaClient又继承于LookupService,主要是定义了发现服务的抽象方法,而真正去实现发现服务功能的类是com.netflix.discovery.DiscoveryClient。

要看一个类,首先就看类头部定义的注释

/**
 * The class that is instrumental for interactions with <tt>Eureka Server</tt>.
 *
 * <p>
 * <tt>Eureka Client</tt> is responsible for a) <em>Registering</em> the
 * instance with <tt>Eureka Server</tt> b) <em>Renewal</em>of the lease with
 * <tt>Eureka Server</tt> c) <em>Cancellation</em> of the lease from
 * <tt>Eureka Server</tt> during shutdown
 * <p>
 * d) <em>Querying</em> the list of services/instances registered with
 * <tt>Eureka Server</tt>
 * <p>
 *
 * <p>
 * <tt>Eureka Client</tt> needs a configured list of <tt>Eureka Server</tt>
 * {@link java.net.URL}s to talk to.These {@link java.net.URL}s are typically amazon elastic eips
 * which do not change. All of the functions defined above fail-over to other
 * {@link java.net.URL}s specified in the list in the case of failure.
 * </p>
 *
 * @author Karthik Ranganathan, Greg Kim
 * @author Spencer Gibb
 *
 */
@Singleton

这个类用于Eureka Server相互协作;

Eureka Client主要负责下面的任务:

  • 向Eureka Server注册服务实例
  • 向Eureka Server获取服务租约
  • 在服务关闭时,向Eureka Server取消租约
  • 查询Eureka Server中的服务实例列表

在DiscoveryClient的构造方法里可以看到,会调用initScheduledTasks(),首先看看initScheduledTasks

/**
     * Initializes all scheduled tasks.
     */
    private void initScheduledTasks() {
        if (clientConfig.shouldFetchRegistry()) {
            // registry cache refresh timer
            int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
            int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "cacheRefresh",
                            scheduler,
                            cacheRefreshExecutor,
                            registryFetchIntervalSeconds,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new CacheRefreshThread()
                    ),
                    registryFetchIntervalSeconds, TimeUnit.SECONDS);
        }

        if (clientConfig.shouldRegisterWithEureka()) {
            int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
            int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
            logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

            // Heartbeat timer
            scheduler.schedule(
                    new TimedSupervisorTask(
                            "heartbeat",
                            scheduler,
                            heartbeatExecutor,
                            renewalIntervalInSecs,
                            TimeUnit.SECONDS,
                            expBackOffBound,
                            new HeartbeatThread()
                    ),
                    renewalIntervalInSecs, TimeUnit.SECONDS);

            // InstanceInfo replicator
            instanceInfoReplicator = new InstanceInfoReplicator(
                    this,
                    instanceInfo,
                    clientConfig.getInstanceInfoReplicationIntervalSeconds(),
                    2); // burstSize

            statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
                @Override
                public String getId() {
                    return "statusChangeListener";
                }

                @Override
                public void notify(StatusChangeEvent statusChangeEvent) {
                    if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                            InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                        // log at warn level if DOWN was involved
                        logger.warn("Saw local status change event {}", statusChangeEvent);
                    } else {
                        logger.info("Saw local status change event {}", statusChangeEvent);
                    }
                    instanceInfoReplicator.onDemandUpdate();
                }
            };

            if (clientConfig.shouldOnDemandUpdateStatusChange()) {
                applicationInfoManager.registerStatusChangeListener(statusChangeListener);
            }

            instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
        } else {
            logger.info("Not registering with Eureka server per configuration");
        }
    }

可以看到,它有个判断clientConfig.shouldRegisterWithEureka(),默认是true,在if里,会创建一个InstanceInfoReplicator的实例对象,它会执行一个定时任务,默认周期是30秒,点进去查看InstanceInfoReplicator的run方法,】

public void run() {
        try {
            discoveryClient.refreshInstanceInfo();

            Long dirtyTimestamp = instanceInfo.isDirtyWithTime();
            if (dirtyTimestamp != null) {
                discoveryClient.register();
                instanceInfo.unsetIsDirty(dirtyTimestamp);
            }
        } catch (Throwable t) {
            logger.warn("There was a problem with the instance info replicator", t);
        } finally {
            Future next = scheduler.schedule(this, replicationIntervalSeconds, TimeUnit.SECONDS);
            scheduledPeriodicRef.set(next);
        }
    }

可以看到discoveryClient.register();这一行就是真正触发调用注册的地方了。继续查看register方法

 boolean register() throws Throwable {
        logger.info(PREFIX + appPathIdentifier + ": registering service...");
        EurekaHttpResponse<Void> httpResponse;
        try {
            httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
        } catch (Exception e) {
            logger.warn("{} - registration failed {}", PREFIX + appPathIdentifier, e.getMessage(), e);
            throw e;
        }
        if (logger.isInfoEnabled()) {
            logger.info("{} - registration status: {}", PREFIX + appPathIdentifier, httpResponse.getStatusCode());
        }
        return httpResponse.getStatusCode() == 204;
    }

 可以看到,注册操作也是通融过REST请求的方式进行的,同时,注册的时候要传入一个instanceInfo对象,就是客户端的元数据,当服务端返回204时,就代表注册成功。

上面分析了Eureka Client是如何注册的,下面来看一些额外的东西

  1. 配置Eureka Server的URL列表是如何获取的。根据配置的参数名eureka.client.serviceUrl.dedfaultZone,在DiscoveryClient搜索serviceUrl可以找到相关属性的加载,但在SR5版本已经被标记为@Deprecated,从注释上可以看到替代的类com.netflix.discovery.endpoint.EndpointUtils,在EndpointUtils中可以找到这个 方法getServiceUrlsFromConfig
    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, Arrays.toString(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;
        }

    从getServiceUrlsFromConfig方法中可以看到,依次加载了两个东西Region和Zone

    1. 通过getRegion方法可以看到,从配置里读取了一个Region,默认是default,自定义的话是通过eureka.client.region
       

        public static String getRegion(EurekaClientConfig clientConfig) {
              String region = clientConfig.getRegion();
              if (region == null) {
                  region = DEFAULT_REGION;
              }
              region = region.trim().toLowerCase();
              return region;
          }
    2. 通过getAvailabilityZones来获取Zone,默认是defaultZone,这也是配置注册url的属性 eureka.client.region.defaultZone的由来了。若要自定义,配置名是eureka.client.availability-zones,要配置多个,则以逗号隔开
       

      public String[] getAvailabilityZones(String region) {
      		String value = this.availabilityZones.get(region);
      		if (value == null) {
      			value = DEFAULT_ZONE;
      		}
      		return value.split(",");
      	}

    在获取到Region和Zone后,就会加载具体的Eureka Server地址,方法是clientConfig.getEurekaServerServiceUrls,查看实现

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

 这里之所以要有个Zone参数,是因为在使用Ribbon调用服务的时候,Ribbon的默认策略会优先访问和客户端处于同一个Zone的服务端实例,只有在同一个Zone没有可用服务端实例的时候才会访问其他Zone的服务端实例。

-----------------------------------------------------------------------------------------------------------------------------

猜你喜欢

转载自blog.csdn.net/lij231/article/details/82915489