zoukankan      html  css  js  c++  java
  • 微服务之SpringCloud实战(四):SpringCloud Eureka源码分析

    Eureka源码解析:

      搭建Eureka服务的时候,我们会再SpringBoot启动类加上@EnableEurekaServer的注解,这个注解做了一些什么,我们一起来看。

    点进@EnableEurekaServer这个注解就会看到下面代码:

    /*
     * Copyright 2013-2017 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.cloud.netflix.eureka.server;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.context.annotation.Import;
    
    /**
     * Annotation to activate Eureka Server related configuration {@link EurekaServerAutoConfiguration}
     *
     * @author Dave Syer
     * @author Biju Kunjummen
     *
     */
    
    @EnableDiscoveryClient
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(EurekaServerMarkerConfiguration.class)
    public @interface EnableEurekaServer {
    
    }

    大家可以清楚的看到@EnableEurekaServer引用了@EnableDiscoveryClient这个注解,源码如下:

    /*
     * Copyright 2013-2015 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      http://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.cloud.client.discovery;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    import org.springframework.context.annotation.Import;
    
    /**
     * 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我们可以看到一个类和一个接口,得到下图关系:

    其中,1 是 Spring Cloud 的接口,它定义了用来发现服务的常用抽象方法,通过该接口可以有效的屏蔽服务治理的实现细节,所以使用 Spring Cloud 构建的微服务应用可以方便的切换不同服务治理框架,而不改动程序代码,只需要另外添加一些针对服务治理框架的配置即可。2 是对 1 接口的实现,从命名判断。它实现的是对 Eureka 发现服务的封装。所以 EurekaDiscoveryClient 依赖了 Netflix Eureka 的 EurekaClient 接口,EurekaClient 接口继承了 LookupService 接口,它们都是 Netflix 开源包中的内容,主要定义了针对 Eureka 的发现服务的抽象发放,而真正实现发现服务的则Netflix包中的 DiscoveryClient (5)类。

      接下来,我们就详细看看DiscoveryClient类。先看下该类的头部注释,大致内容如下:

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

    public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
            LinkedHashMap orderedUrls = new LinkedHashMap();
            String region = getRegion(clientConfig);
            String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
            if(availZones == null || availZones.length == 0) {
                availZones = new String[]{"default"};
            }
    
            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 serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
            if(serviceUrls != null) {
                orderedUrls.put(zone, serviceUrls);
            }
    
            int currentOffset = myZoneOffset == availZones.length - 1?0:myZoneOffset + 1;
    
            while(currentOffset != myZoneOffset) {
                zone = availZones[currentOffset];
                serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
                if(serviceUrls != null) {
                    orderedUrls.put(zone, serviceUrls);
                }
    
                if(currentOffset == availZones.length - 1) {
                    currentOffset = 0;
                } else {
                    ++currentOffset;
                }
            }
    
            if(orderedUrls.size() < 1) {
                throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
            } else {
                return orderedUrls;
            }
        }
    

    Region、Zone

      从上面的函数中可以发现,客户端依次加载了两个内容,第一个是Region,第二个是Zone,从其加载逻辑上可以判断它们之间的关系:

    • 通过 getRegion 函数,我们可以看到他从配置中读取了一个Region返回,所以一个微服务应用只可以属于一个Region,如果不特别配置,默认为default。若要自己配置,可以通过 eureka.client.region属性来定义。
    复制代码
     public static String getRegion(EurekaClientConfig clientConfig) {
            String region = clientConfig.getRegion();
            if(region == null) {
                region = "default";
            }
    
            region = region.trim().toLowerCase();
            return region;
        }
    复制代码
    • 通过 getAvailabilityZones 函数,可以知道当我们没有特别为 Region 配置 Zone 的时候,默认采用defaultZone , 这才是我们之前配置参数 eureka.client.service-url.defaultZone 的由来。若要为应用指定Zone,可以通过eureka.client.availability-zones 属性来设置。从该函数的 return 内容,可以知道 Zone 能够设置多个,并且通过逗号分隔来配置。由此,我们可以判断Region与Zone 是一对多的关系。
    复制代码
     public String[] getAvailabilityZones(String region) {
            String value = (String)this.availabilityZones.get(region);
            if(value == null) {
                value = "defaultZone";
            }
    
            return value.split(",");
        }
    复制代码

      serviceUrls

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

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

      具体获取serviceUrls 的实现,可以详细查看getEurekaServerServiceUrls 函数的具体实现类 EurekaClientConfigBean,用来加载配置文件中的内容,通过搜索defaultZone,我们可以很容易找到下面这个函数,它具体实现了如何解析该参数的过程,通过此内容,我们可以知道,eureka.client.service-url.defaultZone 属性可以配置多个,并且需要通过逗号分隔。

    复制代码
    public List<String> getEurekaServerServiceUrls(String myZone) {
            String serviceUrls = (String)this.serviceUrl.get(myZone);
            if(serviceUrls == null || serviceUrls.isEmpty()) {
                serviceUrls = (String)this.serviceUrl.get("defaultZone");
            }
    
            if(!StringUtils.isEmpty(serviceUrls)) {
                String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
                ArrayList eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
                String[] var5 = serviceUrlsSplit;
                int var6 = serviceUrlsSplit.length;
    
                for(int var7 = 0; var7 < var6; ++var7) {
                    String eurekaServiceUrl = var5[var7];
                    if(!this.endsWithSlash(eurekaServiceUrl)) {
                        eurekaServiceUrl = eurekaServiceUrl + "/";
                    }
    
                    eurekaServiceUrls.add(eurekaServiceUrl);
                }
    
                return eurekaServiceUrls;
            } else {
                return new ArrayList();
            }
        }
    复制代码

      当我们在微服务应用中使用Ribbon来实现服务调用时,对于Zone的设置可以在负载均衡时实现区域亲和特性:Ribbon的默认策略会优先访问同客户端处于一个Zone中的服务端实例,只有当同一个Zone 中没有可用服务端实例的时候才会访问其他Zone中的实例。所以通过Zone属性的定义,配合实际部署的物理结构,我们就可以有效地设计出对区域性故障的容错集群。

       服务注册

      在理解了多个服务注册中心信息的加载后,我们再回头看看DiscoveryClient类是如何实现“服务注册”行为的,通过查看它的构造类,可以找到调用了下面这个函数:

    复制代码
    private void initScheduledTasks() {
            int renewalIntervalInSecs;
            int expBackOffBound;
            if(this.clientConfig.shouldFetchRegistry()) {
                renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
                expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
                this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
            }
    
            if(this.clientConfig.shouldRegisterWithEureka()) {
                renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
                expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
                logger.info("Starting heartbeat executor: renew interval is: " + renewalIntervalInSecs);
                this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread(null)), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
                this.instanceInfoReplicator = new InstanceInfoReplicator(this, this.instanceInfo, this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 2);
                this.statusChangeListener = new StatusChangeListener() {
                    public String getId() {
                        return "statusChangeListener";
                    }
    
                    public void notify(StatusChangeEvent statusChangeEvent) {
                        if(InstanceStatus.DOWN != statusChangeEvent.getStatus() && InstanceStatus.DOWN != statusChangeEvent.getPreviousStatus()) {
                            DiscoveryClient.logger.info("Saw local status change event {}", statusChangeEvent);
                        } else {
                            DiscoveryClient.logger.warn("Saw local status change event {}", statusChangeEvent);
                        }
    
                        DiscoveryClient.this.instanceInfoReplicator.onDemandUpdate();
                    }
                };
                if(this.clientConfig.shouldOnDemandUpdateStatusChange()) {
                    this.applicationInfoManager.registerStatusChangeListener(this.statusChangeListener);
                }
    
                this.instanceInfoReplicator.start(this.clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
            } else {
                logger.info("Not registering with Eureka server per configuration");
            }
    
        }
    复制代码

      在上面的函数中,可以看到一个与服务注册相关的判断语句 if(this.clientConfig.shouldRegisterWithEureka())。在该分支内,创建了一个 InstanceInfoReplicator 类的实例,他会执行一个定时任务,而这个定时任务的具体工作可以查看该类的run() 函数,具体如下所示:

    复制代码
    public void run() {
            boolean var6 = false;
    
            ScheduledFuture next2;
            label53: {
                try {
                    var6 = true;
                    this.discoveryClient.refreshInstanceInfo();
                    Long next = this.instanceInfo.isDirtyWithTime();
                    if(next != null) {
                        this.discoveryClient.register();
                        this.instanceInfo.unsetIsDirty(next.longValue());
                        var6 = false;
                    } else {
                        var6 = false;
                    }
                    break label53;
                } catch (Throwable var7) {
                    logger.warn("There was a problem with the instance info replicator", var7);
                    var6 = false;
                } finally {
                    if(var6) {
                        ScheduledFuture next1 = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
                        this.scheduledPeriodicRef.set(next1);
                    }
                }
    
                next2 = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
                this.scheduledPeriodicRef.set(next2);
                return;
            }
    
            next2 = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next2);
        }
    复制代码

      这里有个 this.discoveryClient.register(); 这一行,真正触发调用注册的地方就在这里,继续查看register() 的实现内容,如下:

    复制代码
      boolean register() throws Throwable {
            logger.info("DiscoveryClient_" + this.appPathIdentifier + ": registering service...");
    
            EurekaHttpResponse httpResponse;
            try {
                httpResponse = this.eurekaTransport.registrationClient.register(this.instanceInfo);
            } catch (Exception var3) {
                logger.warn("{} - registration failed {}", new Object[]{"DiscoveryClient_" + this.appPathIdentifier, var3.getMessage(), var3});
                throw var3;
            }
    
            if(logger.isInfoEnabled()) {
                logger.info("{} - registration status: {}", "DiscoveryClient_" + this.appPathIdentifier, Integer.valueOf(httpResponse.getStatusCode()));
            }
    
            return httpResponse.getStatusCode() == 204;
        }
    复制代码

      可以看出,注册操作也是通过REST请求的方式进行的。同时,我们能看到发起注册请求的时候,传入了一个 instanceInfo 对象,该对象就是注册时客户端给服务端的服务的元数据。

      服务获取与服务续约

      顺着上面的思路,继续看 DiscoveryClient 的 initScheduledTasks 函数,不难发现在其中还有两个定时任务,分别是 “服务获取” 和 “服务续约” :

    复制代码
    private void initScheduledTasks() {
            int renewalIntervalInSecs;
            int expBackOffBound;
            if(this.clientConfig.shouldFetchRegistry()) {
                renewalIntervalInSecs = this.clientConfig.getRegistryFetchIntervalSeconds();
                expBackOffBound = this.clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
                this.scheduler.schedule(new TimedSupervisorTask("cacheRefresh", this.scheduler, this.cacheRefreshExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.CacheRefreshThread()), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
            }
    
            if(this.clientConfig.shouldRegisterWithEureka()) {
                renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
                expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
                logger.info("Starting heartbeat executor: renew interval is: " + renewalIntervalInSecs);
                this.scheduler.schedule(new TimedSupervisorTask("heartbeat", this.scheduler, this.heartbeatExecutor, renewalIntervalInSecs, TimeUnit.SECONDS, expBackOffBound, new DiscoveryClient.HeartbeatThread(null)), (long)renewalIntervalInSecs, TimeUnit.SECONDS);
                …………
            } else {
                logger.info("Not registering with Eureka server per configuration");
            }
       
        }
    复制代码

      从源码中可以看出,“服务获取” 任务相对于 “服务续约” 和 “服务注册” 任务更为独立。“服务续约” 与 “服务注册” 在同一个 if 逻辑中,这个不难理解,服务注册到Eureka Server 后,需要一个心跳去续约,防止被剔除,所以它们肯定是成对出现的。

      而 “服务获取” 的逻辑在一个独立的 if 判断中,而且是由eureka.client.fetch-registry=true 参数控制,它默认为true,大部分情况下不需关心。

       继续往下可以发现 “服务获取” 和 “服务续约” 的具体方法,其中 “服务续约” 的实现比较简单,直接以REST请求的方式进行续约:

    复制代码
    boolean renew() {
            try {
                EurekaHttpResponse httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
                logger.debug("{} - Heartbeat status: {}", "DiscoveryClient_" + this.appPathIdentifier, Integer.valueOf(httpResponse.getStatusCode()));
                if(httpResponse.getStatusCode() == 404) {
                    this.REREGISTER_COUNTER.increment();
                    logger.info("{} - Re-registering apps/{}", "DiscoveryClient_" + this.appPathIdentifier, this.instanceInfo.getAppName());
                    return this.register();
                } else {
                    return httpResponse.getStatusCode() == 200;
                }
            } catch (Throwable var3) {
                logger.error("{} - was unable to send heartbeat!", "DiscoveryClient_" + this.appPathIdentifier, var3);
                return false;
            }
        }
    复制代码

      而 “服务获取” 则复杂一些,会根据是否是第一次获取发起不同的 REST 请求和相应的处理。

      服务注册中心处理

      通过上面的源码分析,可以看到所有的交互都是通过 REST 请求发起的。下面看看服务注册中心对这些请求的处理。Eureka Server 对于各类 REST 请求的定义都位于 com.netflix.eureka.resources 包下。

      以 “服务注册” 请求为例(在ApplicationResource类中):

    复制代码
    @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.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 amazonInfo1 = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                            return Response.status(400).entity(amazonInfo1).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.InstanceRegister 对象中的 register( InstanceInfo info, int leaseDuration, boolean isReplication) 函数来进行服务注册:

     public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
            this.handleRegistration(info, leaseDuration, isReplication);
            super.register(info, leaseDuration, isReplication);
        }
     private void handleRegistration(InstanceInfo info, int leaseDuration, boolean isReplication) {
            this.log("register " + info.getAppName() + ", vip " + info.getVIPAddress() + ", leaseDuration " + leaseDuration + ", isReplication " + isReplication);
            this.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
        }

      在注册函数中,先调用publishEvent 函数,将该新服务注册的事件传播出去,然后调用 com.netflix.eureka.registry.AbstractInstanceRegistry 父类中的注册实现,将 InstanceInfo 中的元数据信息存储在一个 ConcurrentHashMap 对象中。正如之前所说,注册中心存储了两层 Map 结构,第一层的key 存储服务名: InstanceInfo 中的APPName 属性,第二层的 key 存储实例名:InstanceInfo中的 instanceId 属性。

    配置详解

      在 Eureka 的服务治理体系中,主要分为服务端和客户端两个不同的角色,服务端为服务注册中心,而客户端为各个提供接口的微服务应用。当我们构建了高可用的注册中心之后,该集群中所有的微服务应用和后续将要介绍的一些基础类应用(如配置中心、API网关等)都可以视为该体系下的一个微服务(Eureka客户端)。服务注册中心也一样,只是高可用环境下的服务注册中心除了服务端之外,还为集群中的其他客户端提供了服务注册的特殊功能。所以,Eureka 客户端的配置对象存在于所有 Eureka 服务治理体系下的应用实例中。在使用使用 Spring cloud Eureka 的过程中, 我们所做的配置内容几乎都是对 Eureka 客户端配置进行的操作,所以了解这部分的配置内容,对于用好 Eureka 非常有帮助。

      Eureka 客户端的配置主要分为以下两个方面:

    • 服务注册相关的配置信息,包括服务注册中心的地址、服务获取的间隔时间、可用区域等。
    • 服务实例相关的配置信息,包括服务实例的名称、IP地址、端口号、健康检查路径等。

      

    服务注册类配置

      关于服务注册类的配置信息,我们可以通过查看 org.springframework.cloud.netflix.eureka.EurekaClientConfigBean 的源码来获得比官方文档中更为详尽的内容,这些配置信息都已 eureka.client 为前缀。下面针对一些常用的配置信息做进一步的介绍和说明。

      指定注册中心

      在配置文件中通过 eureka.client.service-url 实现。该参数定义如下所示,它的配置值存储在HashMap类型中,并且设置有一组默认值,默认值的key为 defaultZone、value 为 http://localhost:8761/eureka/,类名为 EurekaClientConfigBean。

    复制代码
    private Map<String, String> serviceUrl = new HashMap();
    
    this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
    
    public static final String DEFAULT_URL = "http://localhost:8761/eureka/";
    public static final String DEFAULT_ZONE = "defaultZone";
    复制代码

      由于之前的服务注册中心使用了 8082 端口,所以我们做了如下配置,来讲应用注册到对应的 Eureka 服务端中。

    eureka.client.service-url.defaultZone=http://localhost:8082/eureka/

      当构建了高可用的服务注册中心集群时,可以为参数的value 值配置多个注册中心的地址(逗号分隔):

    eureka.client.service-url.defaultZone=http://peer1:1111/eureka/,http://peer2:1112/eureka/

      另外,为了服务注册中心的安全考虑,很多时候会为服务注册中心加入安全校验。这个时候,在配置serviceUrl时,需要在value 值的 URL 中加入响应的安全校验信息,比如: http://<username>:<password>@localhost:1111/eureka。其中<username>为安全校验信息的用户名,<password>为该用户的密码。

      其他配置

      这些参数均以 eureka.client 为前缀。

    服务实例类配置

      关于服务实例类的配置信息,可以通过查看 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 的源码来获取详细内容,这些配置均以 eureka.instance 为前缀。

      元数据

      在 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 的配置信息中,有一大部分内容都是对服务实例元数据的配置,元数据是 Eureka 客户端在向注册中心发送注册请求时,用来描述自身服务信息的对象,其中包含了一些标准化的元数据,比如服务名称、实例名称、实例IP、实例端口等用于服务治理的重要信息;以及一些用于负载均衡策略或是其他特殊用途的自定义元数据信息。

      在使用 Spring Cloud Eureka 的时候,所有的配置信息都通过 org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean 进行加载,但在真正进行服务注册时,还是会包装成 com.netflix.appinfo.InstanceInfo 对象发送给 Eureka 客户端。这两个类的定义非常相似,可以直接查看 com.netflix.appinfo.InstanceInfo 类中的详细定义来了解原声的 Eureka 对元数据的定义。其中,Map<String, String> metaData = new ConcurrentHashMap<String, String>(); 是自定义的元数据信息,而其他成员变量则是标准化的元数据信息。Spring Cloud 的EurekaInstanceConfigBean 对原生元数据对象做了一些配置优化处理,在后续的介绍中会提到这些内容。

      我们可以通过 eureka.instance.<properties>=<value> 的格式对标准化元数据直接进行配置,<properties> 就是 EurekaInstanceConfigBean 对象中的成员变量名。对于自定义元数据,可以通过 eureka.instance.metadataMap.<key>=<value> 的格式来进行配置。

      接着,针对一些常用的元数据配置做进一步的介绍和说明。

      实例名配置

      实例名,即 InstanceInfo 中的 instanceId 参数,它是区分同一服务中不同实例的唯一标识。在NetflixEureka 的原生实现中,实例名采用主机名作为默认值,这样的设置使得在同一主机上无法启动多个相同的服务实例。所以,在 Spring Cloud Eureka 的配置中,针对同一主机中启动多实例的情况,对实例名的默认命名做了更为合理的扩展,它采用了如下默认规则:

    ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id}:${server.port}

      对于实例名的命名规则,可以通过eureka.instance.instanceId 参数来进行配置。比如,在本地进行客户端负载均衡调试时,需要启动同一服务的多个实例,如果我们直接启动同一个应用必然会发生端口冲突。虽然可以在命令行中指定不同的server.port 来启动,但这样略显麻烦。可以直接通过设置 server.port=0 或者使用随机数 server.port=${random.int[10000,19999]} 来让Tomcat 启动的时候采用随机端口。但是这个时候会发现注册到 Eureka Server的实例名都是相同的,这会使得只有一个服务实例能够正常提供服务。对于这个问题,就可以通过设置实例名规则来轻松解决:

    eureka.instance.instanceId=${spring.application.name}:${random.int}

      通过上面的配置,利用应用名+随机数的方式来区分不同的实例,从而实现在同一个主机上,不指定端就能轻松启动多个实例的效果。

  • 相关阅读:
    从Java小白到收获BAT等offer,分享我这两年的经验和感悟
    我的Java秋招面经大合集
    从零基础到拿到网易Java实习offer,我做对了哪些事
    设计模式常见面试知识点总结(Java版)
    如何才能够系统地学习Java并发技术?
    这些喜闻乐见的Java面试知识点,你都掌握了吗?
    Java集合类常见面试知识点总结
    用大白话告诉你 :Java 后端到底是在做什么?
    16-使用Selenium模拟浏览器抓取淘宝商品美食信息
    15-分析Ajax请求并抓取今日头条街拍美图
  • 原文地址:https://www.cnblogs.com/gslblog/p/9959332.html
Copyright © 2011-2022 走看看