1.1springcloud项目搭建笔记1涉及eureka源码解读

一、Eureka Client源码(在eureka-client-1.9.13.jar中,com.netflix.discovery下)

1. client启动时候去Eureka Server注册服务,在启动类上添加@EnableDiscoveryClient增加注解,这个注解是为了开启一个DiscoveryClient实例,这个类实现了EurekaClient接口,EurekaClient接口又继承了LookupService接口

2. DiscoveryClient这个类是帮助和Eureka Server互相协作的,可以进行服务注册,服务续约,服务下线,获取服务列表;列表就是我们在配置文件中配置的eureka.client.service-url.defaultZone这一选项,这个地址就是Eureka Server的地址,服务注册、服务续约以及其他的操作,都是向这个地址发送请求的

3. DiscoveryClient类构造器里面调用了initScheduledTasks()方法,这个方面里面有两层判断

    a. 第一层判断是shouldFetchRegistry(),这个取值是配置文件中eureka.client.fetch-registry,默认是true,这层里面会初始化一个服务获取的定时任务,这个默认30s,也就是这个配置:eureka.client.registry-fetch-interval-seconds=30,意思是从eureka获取注册表信息的频率(秒)是30秒;cacheRefreshExecutorExponentialBackOffBound代表缓存刷新重试。这里面这个定时任务会定时获取服务注册列表

    b. 第二层判断是shouldRegisterWithEureka(),这个取值是配置文件中的eureka.client.register-with-eureka。这里面有个定时续约的定时任务,续约时间默认30s,这个时间点会不断的发送请求来维持心跳的。而这个InstanceInfoReplicator类实现了Runnable接口,他的run方法里调用了discoveryClient.register()方法进行服务注册,会通过REST请求把客户端的元数据发送给Eureka Server。

    上面涉及源码如下:

private void initScheduledTasks() {
  //获取服务注册列表信息
  if (clientConfig.shouldFetchRegistry()) {
    //服务注册列表更新的周期时间
    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();
    //应用启动可见此日志,内容是:Starting heartbeat executor: renew interval is: 30
    logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);
    // 定时续约
    scheduler.schedule(
        new TimedSupervisorTask(
            "heartbeat",
            scheduler,
            heartbeatExecutor,
            renewalIntervalInSecs,
            TimeUnit.SECONDS,
            expBackOffBound,
            new HeartbeatThread() //该线程执行续约的具体逻辑
        ),
        renewalIntervalInSecs, TimeUnit.SECONDS);
 
    //这个Runable中含有服务注册的逻辑
    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");
  }
}
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);
        }
}

    所以,在initScheduledTasks方法中,做了两个操作,向Eureka Server注册服务,并且在条件满足的情况下,创建服务获取和服务续约两个定时任务

4. 客户端要注册到注册中心,首先需要知道注册中心的地址,在DiscoveryClient类中getEurekaServiceUrlsFromConfig这个方法,这个会找到注册中心的地址,源码如下:

@Deprecated
    public static List<String> getEurekaServiceUrlsFromConfig(String instanceZone, boolean preferSameZone) {
        return EndpointUtils.getServiceUrlsFromConfig(staticClientConfig, instanceZone, preferSameZone);
}

    这个方法有两个参数会调用EndpointUtils.getServiceUrlsFromConfig,这个方法有如下三个参数:

    clientConfig:客户端配置类(配置文件)

    instanceZone:客户端所在的zone

    preferSameZone:如果为True,将选择与客户端所在同一个zone的注册中心

    源码分析如下:

public static List<String> getServiceUrlsFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
        List<String> orderedUrls = new ArrayList<String>();
        //第一步,返回客户端所在region,一个微服务应用只属于一个region,默认是default,可以通过eureka.client.region设置
        String region = getRegion(clientConfig);
        //第二步,该方法是通过客户端的region返回所有可用的该region下的人zone,region和zone是一对多的关系。如果返回的zone的数组为空,说明该region下没有可用的zone,那就给他一个默认的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, availZones);
        //第三步,拿到客户端的region、所有该region的zone,接下来就要加载Eurekaserver的具体地址了,该方法的作用是:返回要使用的zone在刚才获取的该region下所有的zone数组的位置,返回的是一个int型
        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);

        //第四步,获取serviceUrl了,返回的是一个String的List
        List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[myZoneOffset]);
        if (serviceUrls != null) {
            orderedUrls.addAll(serviceUrls);
        }

        //第五步
        //1.1将所有的zone的serviceUrl全部获取,只不过是与客户端相同zone的serviceUrl放到List<String>的前面,其他的zone的serviceUrl会加入到该List中
        //1.2如果availZone中只有一个zone,而且是客户端相同的zone,上面的这段代码就会跳过了,因为没有其他的zone,也就没有serviceUrl了
        //1.3假如我们availZone = [“zone1”,”zone2”,”zoneClient”,”zone4”],当前region的可用zone有4个,恰好与客户端相同的zone在availZone的第三个位置
        //1.4如果myZoneOffset不是availZone数组的最后一个值时,currentOffSet = myZoneOffset+1,否则currentOffSet = 0
        //1.5我们可以得到下列变量的值myZOneOffset= 2,currentOffSet= 3
        int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
        while (currentOffset != myZoneOffset) {

            //第六步,调用clientConfig.getEurekaServerServiceUrls(availZones[3])方法,可以看出我们获取了第四个zone的所有serviceUrl,并把他们加到orderdUrls中
            serviceUrls = clientConfig.getEurekaServerServiceUrls(availZones[currentOffset]);
            if (serviceUrls != null) {
                orderedUrls.addAll(serviceUrls);
            }

            //第七步,我们已经遍历了availZone数组的最后一个值,但是现在availZone的前两个值的zone的serviceUrl还没有获取到,如果当前的currentOffSet是availZone的最后一个值,那么就讲currenOffSet设置为0,否则currentOffSet就继续加一,到这里就可以从头开始遍历了,一直到currentOffSet =myZoneOffSet,最后,所有的zone的serviceUrl都被获取到了
            if (currentOffset == (availZones.length - 1)) {
                currentOffset = 0;
            } else {
                currentOffset++;
            }
        }

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

5. region和zone概念梳理:

    a. region:可以简单理解为地理上的分区,比如亚洲地区,或者华北地区,再或者北京等等,没有具体大小的限制。根据项目具体的情况,可以自行合理划分region

    b. zone:可以简单理解为region内的具体机房,比如说region划分为北京,然后北京有两个机房,就可以在此region之下划分出zone1,zone2两个zone

    c. 服务调用会优先调用同一个zone内的服务进行调用

    d.注册中心选择逻辑:
        1). 如果prefer-same-zone-eureka为false,按照service-url下的 list取第一个注册中心来注册,并和其维持心跳检测。不会再向list内的其它的注册中心注册和维持心跳。只有在第一个注册失败的情况下,才会依次向其它的注册中心注册,总共重试3次,如果3个service-url都没有注册成功,则注册失败。每隔一个心跳时间,会再次尝试。
        2). 如果prefer-same-zone-eureka为true,先通过region取availability-zones内的第一个zone,然后通过这个zone取service-url下的list,并向list内的第一个注册中心进行注册和维持心跳,不会再向list内的其它的注册中心注册和维持心跳。只有在第一个注册失败的情况下,才会依次向其它的注册中心注册,总共重试3次,如果3个service-url都没有注册成功,则注册失败。每隔一个心跳时间,会再次尝试

    e. 服务调用的配置文件

eureka:
  instance:
    metadata-map:
      zone: zone-1

        服务消费者和服务提供者分别属于哪个zone,均是通过eureka.instance.metadata-map.zone来判定的。
服务消费者会先通过ribbon去注册中心拉取一份服务提供者的列表,然后通过eureka.instance.metadata-map.zone指定的zone进行过滤,过滤之后如果同一个zone内的服务提供者有多个实例,则会轮流调用。
只有在同一个zone内的所有服务提供者都不可用时,才会调用其它zone内的服务提供者

    f. 拓展

        eureka.instance.lease-renewal-interval-in-seconds: 30
        服务和注册中心的心跳间隔时间,默认为30s

        eureka.instance.lease-expiration-duration-in-seconds: 90
        服务和注册中心的心跳超时时间,默认为90s

        也就是说,当一个服务异常down掉后,90s之后注册中心才会知道这个服务不可用了。在此期间,依旧会把这个服务当成正常服务。ribbon调用仍会把请求转发到这个服务上。为了避免这段期间出现无法提供服务的情况,要开启ribbon的重试功能,去进行其它服务提供者的重试。

二、Eureka-server的源码(com.netflix.eureka:eureka-core-1.4.6.jar中)

1. 这个包下有EurekaBootStrap这个类,这个类实现了ServletContextListener接口,它能够监听 ServletContext 对象的生命周期,当Servlet 容器启动或终止Web 应用时,会触发ServletContextEvent 事件,该事件由ServletContextListener 来处理。在 ServletContextListener 接口中定义了处理ServletContextEvent 事件的两个方法:contextInitialized和contextDestroyed

2.  EurekaServerContext方法,可以看到,在方法中,新建了几个类,PeerAwareInstanceRegistryImpl和PeerEurekaNodes

3. 在resources目录下有个ApplicationResource类,类中有个方法,addInstance,这个方法就是接收注册服务请求的,这个方法会调用registry.register(info, “true”.equals(isReplication))这个方法,进行服务注册。进入这个方法就是在EurekaBootStrap中初始化的PeerAwareInstanceRegistryImpl类中的方法,在方法中,会获取InstanceInfo的续约时间信息,默认是90秒。然后调用父类的register方法注册,注册完后,会调用replicateToPeers方法,把这个节点的注册信息告诉其它Eureka Server节点。注册的信息会存放在map中,而且还是个两层的ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>,外层map的key是appName,也就是服务名,内层map的key是instanceId,也就是实例名。更新完map信息后,还会更新缓存信息;replicateToPeers方法中会通过for循环遍历所有的PeerEurekaNode,调用replicateInstanceActionsToPeers方法,把信息复制给其他的Eureka Server节点,replicateInstanceActionsToPeers方法中会调用node.register(info),这个方法通过启动了一个任务,来向其它节点同步信息的,不是实时同步的

    参考文章,感谢博主的宝贵分享:

1. https://blog.csdn.net/chayangdz/article/details/82012937

2. https://blog.csdn.net/qq_23213739/article/details/78800857

3. https://blog.csdn.net/Michaelwubo/article/details/81449191

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值