• 【源码系列】Eureka源码分析


      对于服务注册中心、服务提供者、服务消费者这个三个主要元素来说,服务提供者和服务消费者(即Eureka客户端)在整个运行机制中是大部分通信行为的主动发起者(服务注册、续约、下线等),而注册中心主要是处理请求的接收者。所以,我们从Eureka的客户端为入口分析它是如何完成这些主动通信的。
      一般情况下,我们将一个SpringBoot应用注册到 Eureka Server 或者从 Eureka Server 获取服务器列表时,就做了两件事:

    1. 在应用启动类添加注解 @EnableDiscoveryClient
    2. 在 application.properties 文件上用 eureka.client.service-url.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 实例,然后我们搜索 DiscoveryClient 会发现一个类和一个接口,它们的关系如图。

     

    enter description here
    enter description here

    右边的org.springframework.cloud.client.discovery.DiscoveryClient 是SpringCloud的接口,体现了面向接口编程的思想,定义了用来发现服务的常用抽象方法。org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是该接口的实现,是对Eureka发现服务的封装,内部依赖了一个EurekaClient接口,所以真正实现发现服务的是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
    public class DiscoveryClient implements EurekaClient {
        ...
    }
    

    这个类用于帮助与 Eureka Server 相互协作
    Eureka Client客户端负责以下内容:

    1. 向Eureka Server 注册服务实例
    2. 向 Eureka Server 服务续约
    3. 服务关闭时取消租约
    4. 查询注册在 Eureka Server 上的服务或实例列表
      Eureka Client 还需要配置一个 Eureka Server 的服务列表。

    哪里对Eureka Server的URL列表配置?

    根据我们配置的属性名eureka.client.serviceUrl.defaultZone,通过serviceUrl可以找到该属性相关的加载属性,就是DiscoveryClient里有个getEurekaServiceUrlsFromConfig()方法但是弃用了,改用EndpointUtils这个工具类,代码如下:

    1. /** 
    2. * Get the list of all eureka service urls from properties file for the eureka client to talk to. 
    3. * 
    4. * @param clientConfig the clientConfig to use 
    5. * @param instanceZone The zone in which the client resides 
    6. * @param preferSameZone true if we have to prefer the same zone as the client, false otherwise 
    7. * @return an (ordered) map of zone -> list of urls mappings, with the preferred zone first in iteration order 
    8. */ 
    9. public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) { 
    10. Map<String, List<String>> orderedUrls = new LinkedHashMap<>(); 
    11. String region = getRegion(clientConfig); 
    12. String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); 
    13. if (availZones == null || availZones.length == 0) { 
    14. availZones = new String[1]; 
    15. availZones[0] = DEFAULT_ZONE; 
    16. logger.debug("The availability zone for the given region {} are {}", region, availZones); 
    17. int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones); 
    18.  
    19. String zone = availZones[myZoneOffset]; 
    20. List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 
    21. if (serviceUrls != null) { 
    22. orderedUrls.put(zone, serviceUrls); 
    23. int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1); 
    24. while (currentOffset != myZoneOffset) { 
    25. zone = availZones[currentOffset]; 
    26. serviceUrls = clientConfig.getEurekaServerServiceUrls(zone); 
    27. if (serviceUrls != null) { 
    28. orderedUrls.put(zone, serviceUrls); 
    29. if (currentOffset == (availZones.length - 1)) { 
    30. currentOffset = 0
    31. } else
    32. currentOffset++; 
    33.  
    34. if (orderedUrls.size() < 1) { 
    35. throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!"); 
    36. return orderedUrls; 
    37.  

    Region,Zone

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

    getAvailabilityZones()方法可以看出Region与Zone的关系,一个Region可以有多个Zone,设置时可以用逗号来分隔。默认采用defaultZone。

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

    在获取Region和Zone的信息后,根据传入的参数按一定的算法确定加载位于哪一个Zone的serviceUrls。

     

    enter description here
    enter description here

    getEurekaServerServiceUrls方法是EurekaClientConfigBean的实现类,该方法用来获取一个Zone下配置的所以serviceUrl,通过标注出来的地方可以知道,eureka.client.serviceUrl.defaultZone属性可以配置多个,用逗号来分隔。

     

     

    enter description here
    enter description here

    注意: Ribbon具有区域亲和特性,Ribbon的默认策略会优先访问同客户端处于同一个Zone中的实例。所以通过Zone属性的定义,配置实际部署的物理结构,我们就可以有效地设计出对区域性故障的容错集群。

     

    服务注册

    前面说了多个服务注册中心信息的加载,这里再看看 DiscoveryClient 类是如何实现服务注册的。通过查看该类的构造函数,发现它调用了以下方法。

     /**
         * Initializes all scheduled tasks.
         */
        private void initScheduledTasks() {
            ...
    
            if (clientConfig.shouldRegisterWithEureka()) {
               ...
    
                // 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
    
                ...
                instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
            } else {
                logger.info("Not registering with Eureka server per configuration");
            }
        }
    
    

    这里先根据配置判断是不是要注册到 Eureka,然后创建心跳检测任务,获取 instanceInfoReplicator。InstanceInfoReplicator类实现 Runnable接口,instanceInfoReplicator实例会执行一个定时任务,这个定时任务的内容可以查看该类的run()方法。

     

    enter description here
    enter description here

    这里定时刷新实例信息,discoveryClient.register()这里触发了服务注册,register()的内容如下:

     

     

    服务注册的方法
    服务注册的方法

    通过注释也能看出来,这里是通过发送REST请求的方式进行的,com.netflix.appinfo.InstanceInfo就是注册时客户端给服务端的元数据。

     

    服务获取与服务续约

    上面说到的 initScheduledTasks() 方法还有两个定时任务,分别是服务获取和服务续约。

    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);
    
               	...
                instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
            } else {
                logger.info("Not registering with Eureka server per configuration");
            }
        }
    

    clientConfig.shouldFetchRegistry()这里其实是通过eureka.client.fetch-registry参数来判断的,默认为true,它可以定期更新客户端的服务清单,从而客户端能访问到健康的服务实例。
    服务续约也是发送REST请求实现的。

     boolean renew() {
            EurekaHttpResponse<InstanceInfo> httpResponse;
            try {
                httpResponse = eurekaTransport.registrationClient.sendHeartBeat(instanceInfo.getAppName(), instanceInfo.getId(), instanceInfo, null);
                logger.debug(PREFIX + "{} - Heartbeat status: {}", appPathIdentifier, httpResponse.getStatusCode());
                if (httpResponse.getStatusCode() == 404) {
                    REREGISTER_COUNTER.increment();
                    logger.info(PREFIX + "{} - Re-registering apps/{}", appPathIdentifier, instanceInfo.getAppName());
                    long timestamp = instanceInfo.setIsDirtyWithTime();
                    boolean success = register();
                    if (success) {
                        instanceInfo.unsetIsDirty(timestamp);
                    }
                    return success;
                }
                return httpResponse.getStatusCode() == 200;
            } catch (Throwable e) {
                logger.error(PREFIX + "{} - was unable to send heartbeat!", appPathIdentifier, e);
                return false;
            }
        }
    

    服务获取的过程省略。

    服务下线

    服务端根据实例Id和appName执行remove操作。

     void unregister() {
            // It can be null if shouldRegisterWithEureka == false
            if(eurekaTransport != null && eurekaTransport.registrationClient != null) {
                try {
                    logger.info("Unregistering ...");
                    EurekaHttpResponse<Void> httpResponse = eurekaTransport.registrationClient.cancel(instanceInfo.getAppName(), instanceInfo.getId());
                    logger.info(PREFIX + "{} - deregister  status: {}", appPathIdentifier, httpResponse.getStatusCode());
                } catch (Exception e) {
                    logger.error(PREFIX + "{} - de-registration failed{}", appPathIdentifier, e.getMessage(), e);
                }
            }
        }
    

    注册中心处理

    前面的分析都是从客户端出发的,现在看看 Eureka Server是如何处理各种Rest请求的。这种请求的定义都在com.netflix.eureka.resources包下。
    以服务注册为例:
    调用 ApplicationResource 类下的 addInstance()方法。

    @POST
        @Consumes({"application/json", "application/xml"})
        public Response addInstance(InstanceInfo info, @HeaderParam("x-netflix-discovery-replication") String isReplication) {
            logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
            if(this.isBlank(info.getId())) {
                return Response.status(400).entity("Missing instanceId").build();
            } else if(this.isBlank(info.getHostName())) {
                return Response.status(400).entity("Missing hostname").build();
            } else if(this.isBlank(info.getIPAddr())) {
                return Response.status(400).entity("Missing ip address").build();
            } else if(this.isBlank(info.getAppName())) {
                return Response.status(400).entity("Missing appName").build();
            } else if(!this.appName.equals(info.getAppName())) {
                return Response.status(400).entity("Mismatched appName, expecting " + this.appName + " but was " + info.getAppName()).build();
            } else if(info.getDataCenterInfo() == null) {
                return Response.status(400).entity("Missing dataCenterInfo").build();
            } else if(info.getDataCenterInfo().getName() == null) {
                return Response.status(400).entity("Missing dataCenterInfo Name").build();
            } else {
                DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
                if(dataCenterInfo instanceof UniqueIdentifier) {
                    String dataCenterInfoId = ((UniqueIdentifier)dataCenterInfo).getId();
                    if(this.isBlank(dataCenterInfoId)) {
                        boolean experimental = "true".equalsIgnoreCase(this.serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
                        if(experimental) {
                            String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                            return Response.status(400).entity(entity).build();
                        }
    
                        if(dataCenterInfo instanceof AmazonInfo) {
                            AmazonInfo amazonInfo = (AmazonInfo)dataCenterInfo;
                            String effectiveId = amazonInfo.get(MetaDataKey.instanceId);
                            if(effectiveId == null) {
                                amazonInfo.getMetadata().put(MetaDataKey.instanceId.getName(), info.getId());
                            }
                        } else {
                            logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
                        }
                    }
                }
    
                this.registry.register(info, "true".equals(isReplication));
                return Response.status(204).build();
            }
        }
        
    

    在对注册信息进行校验后,会调用org.springframework.cloud.netflix.eureka.server.InstanceRegistry的register(InstanceInfo info, int leaseDuration, boolean isReplication)方法。

     

    enter description here
    enter description here

     

     

    enter description here
    enter description here

    首先会把新服务注册事件传播出去,然后调用父类com.netflix.eureka.registry.AbstractInstanceRegistry中的实现。

     

     public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
            try {
                this.read.lock();
                Map<String, Lease<InstanceInfo>> gMap = (Map)this.registry.get(registrant.getAppName());
                ...
            } finally {
                this.read.unlock();
            }
    
        }
    

     

    保存实例信息的双层Map
    保存实例信息的双层Map

    InstanceInfo的元数据信息保存在一个ConcurrentHashMap中,它是一个双层的Map结构,第一层的key是服务名(即InstanceInfo的appName属性),第二层的key是实例名(即InstanceInfo的InstanceId属性)。
    ApplicationResource中的其他方法可以自行研究。

     

  • 相关阅读:
    SpringFramework|@Autowired
    SpringFramework|@Required的使用
    SpringFramework|基于XML的两种自动装配
    SpringFramework|基于XML的各类集合注入
    SpringFramework|基于XML的依赖注入
    erlang vim
    svn 强制输入提交日志
    vim配置
    克隆centos6后配置网络
    git 免密
  • 原文地址:https://www.cnblogs.com/2YSP/p/11072255.html
Copyright © 2020-2023  润新知