Spring Cloud Eruka

289 阅读6分钟

Spirng cloud的组件其实也写了几篇文章了,但是相对都是入门,今天要说的是Spirng Cloud Eruka就不仅仅是入门了,是精通,我所理解的精通,不是应用阶段,而是真正去读了他的源码,真正了解他的执行原理,他的设计架构

现在就来说一说Spring Cloud Eruka

优势

1 拥有良好的服务治理能力,通过服务注册,服务发现来实现

2 拥有高可用的注册中心,可以增强系统可用性

其实上面的两样我想大家都是耳熟能详了,所以我主要说,他们的实现,也就是源码部分,下面源码为1.1.5.RELEASE版本

查看源码要找一个切入点,如果没通过切入点去看源码,个人觉得很难找到重点,只有找到切入点才能顺藤摸瓜

@EnableEurekaServer 这个是开启Eruka的注解

看一下他的源码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({EurekaServerConfiguration.class})
public @interface EnableEurekaServer {
}

他是这个样子的

然后看到@Import({EurekaServerConfiguration.class})

我们看下这个注解

/**
 * @author Gunnar Hillert
 */
@Configuration
@Import(EurekaServerInitializerConfiguration.class)
@EnableDiscoveryClient
@EnableConfigurationProperties(EurekaDashboardProperties.class)
@PropertySource("classpath:/eureka/server.properties")
public class EurekaServerConfiguration extends WebMvcConfigurerAdapter {
...

我们发现 @EnableDiscoveryClient 这个注解,继续查看这个类

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

}

翻译一下他的注释,Annotation to enable a DiscoveryClient implementation. 声明一个开启客户端发现的实现,而这个实现就是DiscoveryClient类

这里说一下,我们看源码,会发现很多英文,每次都去查就很不方便,可以安装 Translate的idea插件,这样读起来就很方便

回到DiscoveryClient类,我们看一下他的关系

image.png

我们看到图中有两个DiscoveryClient类,右边那个是SpringCloud的DiscoveryClient类,他用来定义发现服务的抽象方法,而最左边的就是我们刚才说的DiscoveryClient类,他们之间的关系入图

ErukaClient实现了DiscoveryClint且ErukaClient和DiscoveryClient存在依赖关系

    @ImplementedBy(DiscoveryClient.class)
    public interface EurekaClient extends LookupService {
    /**
     * @deprecated see {@link com.netflix.discovery.endpoint.EndpointUtils} for replacement
     *
     * Get the list of all eureka service urls for the eureka client to talk to.
     *
     * @param zone the zone in which the client resides
     * @return The list of all eureka service urls for the eureka client to talk to.
     */
    @Deprecated
    public List<String> getDiscoveryServiceUrls(String zone);
    
    

@ImplementedBy(DiscoveryClient.class) 指定默认的接口实现方法为DiscoveryClient.class

ErukaClient和ErukaDiscoveryClient是组合关系,就是

   @ConstructorProperties({"config", "eurekaClient"})
    public EurekaDiscoveryClient(EurekaInstanceConfig config, EurekaClient eurekaClient) {
        this.config = config;
        this.eurekaClient = eurekaClient;
    }        
    
    

因为EurekaDiscoveryClient初始化一定需要eurekaClient,所以是组合关系

根据图中我们发现,SpringCloud的DiscoveryClient类其实是一个抽闲方法,而ErukaClient是他的实现,而ErukaClient中的方法大部分还是调用了Eruka中的DiscoveryClient类来实现他的方法,所以可以理解ErukaClient其实是对Eruka中的DiscoveryClient的方法的一层封装,最终的实现还是通过Eruka中的DiscoveryClient的方法来实现服务注册,服务获取与服务续约的。

然后我们通过配置文件来看看他是怎么读取配置文件并进行服务注册的

我们在启动eruka时有配置如下代码

# 注册中心的地址
eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

然后我们通过serviceUrl来看看,DiscoveryClient类中对其的获取,我们会找到getDiscoveryServiceUrls方法

/**
 * @deprecated see replacement in {@link com.netflix.discovery.endpoint.EndpointUtils}
 */
@Deprecated
@Override
public List<String> getDiscoveryServiceUrls(String zone) {
    return EndpointUtils.getDiscoveryServiceUrls(clientConfig, zone, urlRandomizer);
}  
    

我们可以发现服务的Url是调用了EndpointUtils类的getDiscoveryServiceUrls方法,注意这段注释

@deprecated see replacement in {@link com.netflix.discovery.endpoint.EndpointUtils}

这段注释也指明了,原方法废弃,最终调用了EndpointUtils类的getDiscoveryServiceUrls方法

也就是说service的处理移动到了EndpointUtils类中,在EndpointUtils类我们发现了getServiceUrlsMapFromConfig方法

   /**
     * Get the list of all eureka service urls from properties file for the eureka client to talk to.
     *
     * @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 an (ordered) map of zone -> list of urls mappings, with the preferred zone first in iteration order
     */
    public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
        Map<String, List<String>> orderedUrls = new LinkedHashMap<>();
        //获取Region
        String region = getRegion(clientConfig);
        //通过Region获取对应zone
        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);

        String zone = availZones[myZoneOffset];
        List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
        if (serviceUrls != null) {
            orderedUrls.put(zone, serviceUrls);
        }

看下他是做什么的,翻译下他的注释

从属性文件中获取所有eureka服务url的列表,以供eureka客户端与之对话。 参数: clientConfig -在clientConfig来使用 instanceZone客户端所在的区域 preferSameZone如果必须与客户端preferSameZone相同的区域, preferSameZone true,否则为false 返回值: 区域的(有序)映射-> url映射列表,首选区域按迭代顺序排在第一

注释指出了他就是对配置serviceUrl做处理的方法,方法先是获取region,这个region比较少配置,一般都是默认,如果自己想要配置,可以通过eruka.client.region来配置,然后我们看一下获取zone的源码,我们发现如果没有配置,获取的就是DEFAULT_ZONE,也就是eureka.client.serviceUrl.defaultZone的由来

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

通过getServiceUrlsMapFromConfig方法我们也能发现,region和zone是一对多的关系,具体在配置中是这样体现的

 eureka:
  client:
    region: beijing
    availability-zones:
      beijing: zone1, zone2
    service-url:
      zone1: http://localhost:8081/eureka, http://localhost:8082/eureka
      zone2: http://localhost:8083/eureka, http://localhost:8084/eureka

region可以理解成地理上的分区,而zone可以理解为机房。

image.png

一个region下面有多个zone,当其中一个zone不可用了,就会启用另一个zone,同时一个zone下面又有多个serviceUrl,形成多个节点,他们之间互相注册,当其中一个节点挂了,另一个节点补上,从而实现了高可用,这就是一个有效的区域性故障的容错集群。

下面我们继续看看服务注册

我们看一下DiscoverClient类的构造方法

@Inject
DiscoveryClient(ApplicationInfoManager applicationInfoManager, EurekaClientConfig config, DiscoveryClientOptionalArgs args,
                    Provider<BackupRegistry> backupRegistryProvider) {
   ...
    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);
....
   

在其中我们看到了下面的方法

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

创建了instanceInfoReplicator实例,而这个对象实现了Runable接口,是一个多线程类

看一下这个实例的构造方法

InstanceInfoReplicator(DiscoveryClient discoveryClient, InstanceInfo instanceInfo, int replicationIntervalSeconds, int burstSize) {
    this.discoveryClient = discoveryClient;
    this.instanceInfo = instanceInfo;
    this.scheduler = Executors.newScheduledThreadPool(1,
            new ThreadFactoryBuilder()
                    .setNameFormat("DiscoveryClient-InstanceInfoReplicator-%d")
                    .setDaemon(true)
                    .build());

    this.scheduledPeriodicRef = new AtomicReference<Future>();

    this.started = new AtomicBoolean(false);
    this.rateLimiter = new RateLimiter(TimeUnit.MINUTES);
    this.replicationIntervalSeconds = replicationIntervalSeconds;
    this.burstSize = burstSize;

    this.allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds;
    logger.info("InstanceInfoReplicator onDemand update allowed rate per min is {}", allowedRatePerMinute);
}


构造方法中通过Executors.newScheduledThreadPool 创建了一个周期性的线程池,用来实现定时任务,而他的实现在其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() 就是真正触发服务注册的方法

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

我们注意到传入了instanceInfo实例,这个实例就是客户端给服务端的元数据。

看完了服务注册,我们继续看服务的获取与续约,他们也实在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.shouldFetchRegistry())分支中,这里面他们会获取服务续约的相关参数配置

  eureka.instance.lease-expiration-duration-in-seconds
  eureka.instance.lease-renewal-interval-in-seconds

注意:配置相关描述可以查看spring-configuration-metadata.json文件

我们可以看到服务的续约也是由定时器来实现的,这就对应上了服务的心跳检测。

对源码的阅读往往是枯燥的,但是在阅读过程中,通过对其设计的思考,以及对其实现的了解,都是一种提高,技术这条路道阻且长,贵在坚持。