服务治理:Spring Cloud Eureka

服务治理:Spring Cloud Eureka

Eureka服务治理

构建服务注册中心

服务注册:

每个服务单元向注册中心登记自己提供的服务,将主机与端口号、版本号、通信协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。
服务注册中心会以心跳的方式监测清单中的服务是否可用。

服务发现:

服务间的调用通过向服务名发起请求调用而非通过指定具体的实例地址。

服务注册中心构建流程

  1. 引入依赖坐标
    <dependencies>
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-eureka-server</artifactId>
		</dependency>
	</dependencies>   
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.cloud</groupId>
				<artifactId>spring-cloud-dependencies</artifactId>
				<version>Brixton.SR5</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>
  1. 开启服务注册功能
    @EnableEurekaServer//此注解用于开启服务注册中心 
    @SpringBootApplication
    public class Application {
    	public static void main(String[] args) {
    		new SpringApplicationBuilder(Application.class).web(true).run(args);
    	}

}
  1. 配置服务注册属性
    spring.application.name=eureka-server //服务名称
    server.port=1111//服务端口号
    
    eureka.instance.hostname=localhost//服务主机名
    eureka.client.register-with-eureka=false//默认true 设置为false代表不向注册中心注册自己(注册中心集群设置true)
    eureka.client.fetch-registry=false//默认true 是否检索服务,由于注册中心的职责为维护服务实例故而为false则不拉取提供者服务(注册中心集群设置true)
    eureka.client.serviceUrl.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/  # 服务的url localhost:1111 

高可用注册中心

Eureka的服务治理设计中,所有节点既是服务提供方,也是服务消费方,服务注册中心也不例外

Eureka Server的高可用实际上就是将自身作为服务向其他服务注册中心注册自己,这样便可形成一组互相注册的服务注册中心,以实现服务清单的互相同步,达到高可用的效果。

让服务注册中心注册自己  a,b皆有
eureka.client.register-with-eureka=true//默认true
eureka.client.fetch-registry=true//默认true
启动类注解@EnableEurekaServer
注册中心a的配置
spring.application.name=eureka-server # 此处可不同
server.port=1111
eureka.instance.hosthome=a #设置主机名为a  需要在/etc/hosts文件中添加对a和b的转换让配置的host形式serviceUrl能在本地正确访问到;Window系统路径为c;\Window\System32\drivers\etc\hosts   127.0.0.1  a  127.0.0.1 b
eureka.client.serviceUrl.defaultZone=http://b:1112/eureka/    #指定服务注册中心的地址
注册中心b的配置
spring.application.name=eureka-server
server.port=1112
eureka.instance.hosthome=b #设置主机名为b  
eureka.client.serviceUrl.defaultZone=http://a:1111/eureka/    #指定服务注册中心的地址

设置了多节点的服务注册中心之后,服务提供方需要配置多个节点的地址

spring.application.name=hello-server
eureka.client.serviceUrl.defaultZone=http://a:1111/eureka/,http://b:1112/eureka/

注意:若不想用主机名定义注册中心地址,亦可使用IP地址的形式,需要在配置文件中增加配置参数

    eureka.instance.prefer-ip-address=true//该值默认为false

服务注册与服务发现

常用注解:
    @EnableDiscoveryClient:服务发现注解(服务提供者)
    @EnableEurekaClient:服务发现注解
    @EnableEurekaServer:启动服务注册中心

服务提供者

  1. 依赖坐标
    只改变这个eureka-server改成eureka
    <dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-eureka</artifactId>
	</dependency>
  1. 注入DiscoveryClient对象可以获得服务的相关内容
  2. 在主类(启动类)添加@EnableDiscoveryClient or @EnableEurekaClient注解,激活Eureka中的DiscoveryClient实现

@EnableEurekaClient和@EnableDiscoveryClient的区别:spring cloud中discovery service有许多种实现(eureka、consul、zookeeper等等)

  • @EnableDiscoveryClient基于spring-cloud-commons
  • @EnableEurekaClient基于spring-cloud-netflix

总结:就是如果选用的注册中心是eureka,那么就推荐@EnableEurekaClient,
如果是其他的注册中心,那么推荐使用@EnableDiscoveryClient。


  1. 配置属性
    命名服务名称
    spring.application.name=hello-service
    指定服务注册中心的地址
    eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/
    # 分配端口号
    server.port=8081
    # eureka注册中心每个服务的status显示ip地址的设置
    eureka.instance.prefer-ip-address=true
    eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}
    注意:springboot2.0则为instance-id: ${spring.cloud.client.ip-address}:${server.port}

注意:以下可以获得服务实例

    @Autowired
    private DiscoveryClient discoveryClient;
    @Autowired
    private Registration registration;
    
    List<ServiceInstance> instances = discoveryClient.getInstances(registration.getServiceId());// 获取当前本地服务的实例
        if (instances != null && instances.size() > 0) {
            for (ServiceInstance instance : instances) {
                if (instance.getPort() == 8081) {
                     hostName = instance.getHost();
                     serviceId = instance.getServiceId()
                     // host:196.xx.xx.xx  service_id:USER  获得USER服务的实例
                }
            }
        }
    

服务消费者

服务消费者有两个目标,发现服务以及消费服务

  1. Ribbon
  2. Feign

很多时候,客户端既是服务提供者也是服务消费者

Ribbon:

Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它可以通过客户端中配置的ribbonServerList服务端列表取轮询访问以达到均衡负载的作用
导入依赖:
	<dependency>
		<groupId>org.springframework.cloud</groupId>
		<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
	</dependency>
在应用主类Application上添加注解:
// @EnableDiscoveryClient注解让该应用为Eureka客户端应用,获得服务发现的能力
// 在主类创建RestTemplate的SpringBean实例,通过@LoadBalanced注解开启客户端负载均衡

@SpringBootApplication
@EnableDiscoveryClient
public class RibbonConsumerApplication {
	@Bean
	@LoadBalanced
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
Controller类的接口的调用:注意,访问的地址是服务名USER不是一个具体的地址

@RestController
@RequestMapping("/ribbon")
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @GetMapping("/consumer")
    public String helloConsumer() {
        return restTemplate.getForEntity("http://USER/user/hello", String.class).getBody();
    }
}
配置文件:
spring.application.name=ribbon-consumer
server.port=9000
eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/,http://localhost:1112/eureka/,http://localhost:1113/eureka/
# eureka注册中心每个服务的status显示ip地址的设置
eureka.instance.prefer-ip-address=true
eureka.instance.instance-id=${spring.cloud.client.ipAddress}:${server.port}

Eureka详解:

服务治理机制

服务提供者

服务注册:

服务提供者在启动时通过发送REST请求的方式将自己注册到Eureka Server上,同时带有自身服务的元数据。
Eureka Server接收到这个REST请求,将元数据信息存储在一个双层Map中,第一层的key是服务名,第二层的key是具体服务的实例名(USER UP(2)-172.20.108.69:8088,172.20.108.165:8081)一个USER服务有2个实例 如此以这样的双层Map形式存储
服务注册时,需确认eureka.client.register-with-eureka=true(是否将自己注册) 参数正确,该值默认true,设置为false将不会启动注册操作

服务同步:

不同的服务注册在不同的服务注册中心上,由于服务中心的互相注册,从而实现注册中心之间的服务同步,通过服务同步,两个服务提供者的信息可通过任意一台服务注册中心获取

服务续约:Renew

注册完服务后,服务提供者会维护一个心跳请求Eureka Server,防止Eureka Server剔除任务将该服务实例从服务列表排除
关于服务续约有两个重要属性,可以根据需要调整
    eureka.instance.lease-renewal-interval-in-seconds=30 # 该参数用于定义服务续约任务的调用间隔,默认30s
    eureka.instance.lease-expiration-duration-in-seconds=90 # 定义服务失效时间,默认90s
服务消费者

获取服务:

当服务消费者启动时,它会发送一个REST请求给服务注册中心,以获取注册的服务清单。
为了性能考虑,Eureka Server会维护一份只读的服务清单来返回给客户端,同时该清单每30s更新一次。

获取服务是服务消费者的基础,故而必须确保eureka.client.fetch-registry=true(是否获取注册信息)参数为true,默认值true
若希望修改缓存清单的更新时间,通过eureka.client.registry-fetch-interval-seconds=30参数修改,默认值30s

服务调用:

服务消费者在获取服务清单后,通过服务名获得具体提供服务的实例名和该实例的元数据信息。因为有这些服务实例的详细信息,所以客户端可以根据自己的需要决定具体调用哪个实例,在Ribbon中默认采用轮询的方式进行调用,实现客户端负载均衡。

对于访问实例的选择,Eureka中有Region和Zone的概念,一个Region中可以包含多个Zone,每个服务客户端需要被注册到一个Zone中,所以每个客户端对应一个Region和一个Zone。
在进行服务调用时,优先访问同处一个Zone中的服务提供方,访问不到再访问其他的Zone。

服务下线:

服务实例正常关闭会触发一个服务下线的REST请求给Eureka Server,通知其下线。服务端接收到请求会将服务状态置DOWN
服务注册中心

失效剔除:

服务实例非正常下线(OOM,网络故障等),服务中心收不到服务下线请求,为了从服务列表将失效的服务实例剔除,Eureka Server在启动时会创建一个定时任务,默认间隔一段时间(默认60s)将当前清单中超时(默认90s)没有续约的服务剔除。

自我保护:

服务注册到Eureka Server后会维护一个心跳连接。
Eureka Server在运行期间,会统计心跳失败的比例在15分钟之内是否低于85%,若出现低于的情况,Eureka Server会将当前的实例注册信息保护起来,让这些实例不会过期。
eureka.server.enable-self-preservation=false 关闭保护机制,用于单机调试  保护期间实例出问题,客户端容易获取实际不存在的服务实例,故而客户端需要容错机制(请求重试,断路器)

源码分析

服务注册中心处理请求,接收者
服务提供者,服务消费者,Eureka客户端,通信的发起者

我们将一个服务注册到Eureka Server或从一个Eureka Server中获取列表时,主要做了两件事:

  • 在应用主类配置了@EnableDiscoveryClient注解
  • 在配置文件application.properties中用eureka.client.serviceUrl.defaultZone参数指定了服务注册中心的位置

分析@EnableDiscoveryClient源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
    boolean autoRegister() default true;
}
从该注解的注释可知,它主要用于开启DiscoveryClient的实例
DiscoveryClient有一个类和一个接口
org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud的接口,定义了发现服务的常用抽象方法,通过该接口可以有效地屏蔽服务治理的实现细节,所以使用Spring Cloud构建的微服务应用可以方便地切换不同服务治理框架。

org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对上接口的实现,实现的是对Eureka发现服务的封装。
EurekaDiscoveryClient依赖Netflix Eureka的com.netflix.discovery.EurekaClient接口,EurekaClient继承LookupService接口,这些主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的是Netflix包中的com.netflix.discovery.DiscoveryClient类

DiscoveryClient类:Netflix包

Eureka Client负责下面的任务:
    Eureka Server注册服务实例
    Eureka Server服务租约
    服务关闭,Eureka Server取消租约
    查询Eureka Server中的服务实例列表
    
    Eureka Client还需要配置一个Eureka Server的URL列表

对Eureka Server的URL列表进行配置:com.netflix.discovery.endpoint.EndpointUtils

public class EndpointUtils {
    public static final String DEFAULT_REGION = "default";
    public static final String DEFAULT_ZONE = "default";
    
    public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
        Map<String, List<String>> orderedUrls = new LinkedHashMap();
        String region = getRegion(clientConfig);
        String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
        if (availZones == null || availZones.length == 0) {
            availZones = new String[]{"default"};
        }
        ...
        int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);
        String zone = availZones[myZoneOffset];
        List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
        if (serviceUrls != null) {
            orderedUrls.put(zone, serviceUrls);
        }
        ...
        return orderedUrls;

Region,Zone:

客户端依次加载了Region与Zone
通过getRegion函数,它从配置读取了一个Region返回,所以一个微服务应用只可以属于一个Region,若不配置,默认default。
自己配置Region  eureka.client.region
 public static String getRegion(EurekaClientConfig clientConfig) {
        String region = clientConfig.getRegion();
        if (region == null) {
            region = "default";
        }

        region = region.trim().toLowerCase();
        return region;
    }

Zone通过getAvailabilityZones函数获取,若没有为Region配置Zone的时候,默认采用defaultZone,这也是配置参数eureka.client.serviceUrl.defaultZone的由来。

若要为应用指定Zone,通过eureka.client.availability-zones属性配置。
从该函数的return内容,我们可知Zone能够设置多个,并且通过逗号分隔来配置。故而region与zone一对多的关系
EurekaClientConfigBean implements EurekaClientConfig 

    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<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
具体获取serviceUrls的实现,可以查看getEurekaServerServiceUrls函数的具体实现类EurekaClientConfigBean,此类是EurekaClientConfig和EurekaConstants接口的实现,用来加载配置文件中的内容
关于defaultZone的信息,由getEurekaServerServiceUrls具体实现解析,故而得知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);
            List<String> 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();
        }
    }

服务注册:DiscoveryClient实现服务注册行为

private void initScheduledTasks() {
    ...
    if (this.clientConfig.shouldRegisterWithEureka()) {
    ...
    // InstanceInfo replicator
        this.instanceInfoReplicator = new InstanceInfoReplicator(
            this, 
            this.instanceInfo, 
            this.clientConfig.getInstanceInfoReplicationIntervalSeconds(), 
            2); // burstSize
    ...
      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 next;
        label53: {
            try {
                var6 = true;
                this.discoveryClient.refreshInstanceInfo();
                Long dirtyTimestamp = this.instanceInfo.isDirtyWithTime();
                if (dirtyTimestamp != null) {
                    this.discoveryClient.register(); // 此处注册!!
                    this.instanceInfo.unsetIsDirty(dirtyTimestamp.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 next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
                    this.scheduledPeriodicRef.set(next);
                }
            }

            next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
            this.scheduledPeriodicRef.set(next);
            return;
        }

        next = this.scheduler.schedule(this, (long)this.replicationIntervalSeconds, TimeUnit.SECONDS);
        this.scheduledPeriodicRef.set(next);
    }
run方法中有一行discoveryClient.register();触发调用注册,由下面可知,注册操作也是REST请求的方式进行
同时发现,发起注册请求的时候,传入了一个com.netflix.appinfo.InstanceInfo对象,该对象是注册时客户端给服务端的服务的元数据
    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, httpResponse.getStatusCode());
        }

        return httpResponse.getStatusCode() == 204;
    }    

服务获取与服务续约:

顺着服务注册的思路,DiscoveryClient的initScheduledTasks方法还有两个定时任务,分别是服务获取和服务续约。
private void initScheduledTasks() {
    int renewalIntervalInSecs;
    int expBackOffBound;
    // 服务获取if逻辑
    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逻辑
    if (this.clientConfig.shouldRegisterWithEureka()) {
        renewalIntervalInSecs = this.instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
        expBackOffBound = this.clientConfig.getHeartbeatExecutorExponentialBackOffBound();
        logger.info("Starting heartbeat executor: renew interval is: " + renewalIntervalInSecs);
        
        // Heartbeat timer
        this.scheduler.schedule(
            new TimedSupervisorTask(
                "heartbeat", 
                this.scheduler, 
                this.heartbeatExecutor, 
                renewalIntervalInSecs, 
                TimeUnit.SECONDS, 
                expBackOffBound, 
                new DiscoveryClient.HeartbeatThread(null)
            ), 
            (long)renewalIntervalInSecs, TimeUnit.SECONDS);
        // InstanceInfo replicator
        ...
服务获取任务相对于服务注册和服务续约任务更为独立。
服务注册和服务续约在一个if逻辑,这个不难理解,服务注册到Eureka Server后,需要一个心跳取续约防止被剔除,所以它们成对出现。
从源码中清除的看到了服务续约相关的时间控制参数:
    eureka.instance.lease-renewal-interval-in-seconds=30
    eureka.instance.lease-expiration-duration-in-seconds=90
    
而服务获取的逻辑独立在一个if中,其判断依据是eureka.client.fetch-registry=true参数
为了定期更新客户端的服务清单,以保证客户端能够访问确实健康的服务实例,服务获取的请求不会只限于服务启动,而是一个定时执行的任务
从源码中可知,任务运行中的registryFetchIntervalSeconds参数对应的就是之前提到的eureka.client.registry-fetch-interval-seconds=30的配置参数

继续深入,可知服务获取和服务续约的具体方法:

服务续约直接以REST请求进行续约:
    boolean renew() {
        try {
            EurekaHttpResponse<InstanceInfo> httpResponse = this.eurekaTransport.registrationClient.sendHeartBeat(this.instanceInfo.getAppName(), this.instanceInfo.getId(), this.instanceInfo, (InstanceStatus)null);
            logger.debug("{} - Heartbeat status: {}", "DiscoveryClient_" + this.appPathIdentifier, 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包下:
以服务注册为例:
    @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);
        
        // validate that the instanceinfo contains all the necessary required fields  校验元数据包含所有必需字段
        
        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 {
        
        // handle cases where clients may be registering with bad DataCenterInfo with missing data  
            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)方法进行服务注册:
    public void register(InstanceInfo info, int leaseDuration, boolean isReplication) {
        if (log.isDebugEnabled()) {
            log.debug(...);
        }
        this.ctxt.publishEvent(new EurekaInstanceRegisteredEvent(this, info, leaseDuration, isReplication));
        
        super.register(info, leaseDuration, isReplication);
    }
在注册方法中,先调用publishEvent方法,将该新服务注册的事件传播出去,然后调用com.netflix.eureka.registry.AbstractInstanceRegistry父类中的注册实现,将InstanceInfo中的元数据信息存储在一个ConcurrentHashMap对象中。
注册中心存储了两层Map结构,第一层的key存储服务名:InstanceInfo中的appName属性,第二层的key存储实例名:InstanceInfo中的instanceId属性

配置详解

Eureka客户端的配置主要分为:服务注册类配置(注册中心的地址,服务获取的间隔时间,可用区域)与服务实例相关的配置信息(服务实例的名称,IP地址,端口号,健康检查路径)

服务注册类配置

服务注册类的配置信息以eureka.client为前缀,通过org.springframework.cloud.netflix.eureka.EurekaClientConfigBean查看更多

指定注册中心:

eureka.client.serviceUrl参数实现指定配置中心,该参数定义如下:它的配置值存储在HashMap类型中,并且设置有一组默认值,默认值的key为defaultZone,value为http://localhost:8761/eureka/
    private Map<String, String> serviceUrl = new HashMap();
    public static final String DEFAULT_URL = "http://localhost:8761/eureka/";
    public static final String DEFAULT_ZONE = "defaultZone";
    // 通过构造方法写入默认值
    public EurekaClientConfigBean() {
        this.serviceUrl.put("defaultZone", "http://localhost:8761/eureka/");
    }
通过eureka.client.serviceUrl.defaultZone  注册到Eureka服务器
当构建了高可用服务注册集群时,可以通过逗号隔开多个注册中心地址
另外为了服务注册中心的安全考量,会为其加入安全校验。此时在配置serviceUrl时,需要在value值的URL中加入相应的安全校验信息
    比如:http://<username>:<password>@localhost:1111/eureka
    其中,<username>是安全校验信息的用户名,<password>为该用户密码
    <dependency>  # 服务注册中心添加依赖
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

其他配置:

整理了一些org.springframework.cloud.netflix.eureka.EurekaClientConfigBean中定义的常用配置参数以及对应的说明和默认值,这些参数均以eureka.client为前缀
参数名说明默认值
enabled启用Eureka客户端true
registryFetchIntervalSeconds从Eureka服务端获取注册信息的间隔时间,单位秒30
instanceInfoReplicationIntervalSeconds更新实例信息的变化到Eureka服务端的间隔时间,单位秒30
initialInstanceInfoReplicationIntervalSeconds初始化实例信息到Eureka服务端的间隔时间,单位秒40
eurekaServiceUrlPoolIntervalSeconds轮询Eureka服务端地址更改的间隔时间,单位秒。当我们与Spring Cloud Config配合,动态刷新Eureka的serviceURL地址时需要关注该参数300
eurekaServerReadTimeoutSeconds读取Eureka Server信息的超时时间,单位秒8
eurekaServerConnectTimeoutSeconds连接Eureka Server的超时时间,单位秒5
eurekaServerTotalConnections从Eureka客户端到所有Eureka服务端的连接总数200
eurekaServerTotalConnectionsPerHost从Eureka客户端到每个Eureka服务端主机的连接总数50
eurekaConnectionIdleTimeoutSecondsEureka服务端连接的空闲关闭时间,单位秒30
heartbeatExecutorThreadPoolSize心跳连接池的初始化线程数2
heartbeatExecutorExponsentialBackOffBound心跳超时重试延迟时间的最大乘数值10
registerWithEureka是否将自身的实例信息注册到Eureka服务端true
fetchRegistry是否从Eureka服务端获取注册信息true

服务实例类配置

服务注册类的配置信息以eureka.instance为前缀,通过org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean查看更多

元数据:

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

使用Spring Cloud Eureka时,所有的配置信息都通过org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean进行加载,但在真正进行服务注册的时候还是会包装成com.netflix.appinfo.InstanceInfo对象发送给Eureka服务端
    public class EurekaInstanceConfigBean implements CloudEurekaInstanceConfig, EnvironmentAware {
        private Map<String, String> metadataMap = new HashMap();
        
    }
    
    // com.netflix.appinfo.InstanceInfo类详细定义了元数据
    @ProvidedBy(EurekaConfigBasedInstanceInfoProvider.class)
    @Serializer("com.netflix.discovery.converters.EntityBodyConverter")
    @XStreamAlias("instance")
    @JsonRootName("instance")
    public class InstanceInfo {
        private volatile Map<String, String> metadata;
        public InstanceInfo( @JsonProperty("metadata") HashMap<String, String> metadata, ...) {
            this.metadata = new ConcurrentHashMap();
            ...
            if (metadata == null) {
                this.metadata = Collections.emptyMap();
            } else if (metadata.size() == 1) {
                this.metadata = this.removeMetadataMapLegacyValues(metadata);
            } else {
                this.metadata = metadata;
            }
            // 注意EurekaInstanceConfigBean加载的metadata是HashMap  InstanceInfo发送给Eureka服务端的metadata是ConcurrentHashMap
        }
    }
    InstanceInfo的metadata是自定义的元数据信息,其他成员变量则是标准化的元数据信息。

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

实例名配置:

实例名,即InstanceInfo中的instanceId参数,用于区分同一服务中不同实例的唯一标识。
在Netflix Eureka的原生实现中,实例名采用主机名作为默认值,如此同一主机无法启动多个相同的服务实例。
Spring Cloud Eureka对实例名的默认命名做了扩展:
    ${spring.cloud.client.hostname}:${spring.application.name}:${spring.application.instance_id}:${server.port}
    如LAPTOP-0T:eureka-slave1:1111  主机名:应用名:端口号

对于实例名的命名规则,通过eureka.instance.instanceId参数配置,如在本地进行客户端负载均衡,需要启用同服务多个实例,此时可以通过如下方式启动:
    server.port=0 或者 采用随机数server.port=${random.int[10000,19999]}来让Tomcat启动时采用随机端口
    此时发现注册到Eureka Server的实例都相同,这会使得只有一个服务实例能够正常提供服务
        此时设置实例名规则解决
        :eureka.instance.instanceId=${spring.application.name}:${random.int}

端点配置:

在InstanceInfo中,有一些URL的配置信息,如homePageUrl(应用主页的URL),statusPageUrl(状态页的URL),healthCheckUrl(健康检查的URL)
其中,状态页和健康检查的URL在Spring Cloud Eureka中默认使用了spring-boot-actuator模块提供的/info端点和/health端点.

/health端点在发送元数据时,必须是一个能够被注册中心访问到的地址,否则服务注册中心不会根据应用的健康检查更改状态(仅当开启了healthcheak功能,以该端点信息作为健康检查标准)
/info端点不正确,导致Eureka面板单机服务实例无法访问到服务实例提供的信息接口

当为应用设置了context-path时,所有spring-boot-actuator模块的监控端点都会增加一个前缀,此时需做下列配置,为/info和/health端点也加上类似的前缀信息
    management.context-path=/hello
    
    eureka.instance.statusPageUrlPath=${management.context-path}/info
    eureka.instance.healthCheckUrlPath=${management.context-path}/health
有时为了安全考量,会修改/info和/health端点的原始路径
    endpoints.info.path=/appInfo
    endpoints.heahth.path=/checkHealth
    
    eureka.instance.statusPageUrlPath=/${endpoints.info.path}
    eureka.instance.healthCheckUrlPath=/${endpoints.health.path}

eureka.instance.statusPageUrlPath和eureka.instance.healthCheckUrlPath使用了相对路径进行配置
由于Eureka的服务注册中心默认以HTTP的方式访问和暴露这些端点,因此当客户端应用以HTTPS的方式来暴露服务和监控端点时,相对路径的配置满足不了需求
Spring Cloud Eureka提供了绝对路径的配置:
    eureka.instance.statusPageUrl=https://${eureka.instance.hostname}/info
    eureka.instance.healthCheckUrl=https://${eureka.instance.hostname}/health
    eureka.instance.homePageUrl=https://${eureka.instance.hostname}

健康检测:

默认情况下,Eureka中各个服务实例的健康检测并非通过actuator模块的/health端点实现,而是依靠客户端心跳的方式保持服务实例的存活
客户端心跳方式可以有效检查客户端进程是否正常运作,无法保证客户端应用能够正常提供服务,如微服务的外部依赖数据库,缓存,消息代理等
通过配置将Eureka客户端的健康检测交给actuator模块的/health端点。
    pom.xml中引入spring-boot-starter-actuator依赖
    在application.properties中增加参数配置eureka.client.healthcheck.enabled=true
    确保服务注册中心可以正确访问客户端的/health端点

其他配置:

org.springframework.cloud.netflix.eureka.EurekaInstanceConfigBean中其他以eureka.instance为前缀的配置参数及对应的说明,默认值
参数名说明默认值
preferIpAddress是否优先使用IP地址作为主机名的标识false
leaseRenewalIntervalInSecondsEureka客户端向服务端发送心跳的时间间隔,单位秒30
leaseExpirationDurationInSecondsEureka服务端在收到最后一次心跳之后等待的时间上限,单位为秒。超过该时间之后服务端会将该服务实例从服务清单中剔除,从而禁止服务调用请求被发送到该实例上90
appname服务名,默认spring.application.name的配置值,若没有则为unknown
hostname主机名,不配置的时候将根据操作系统的主机名来获取

跨平台支持

Eureka的通信机制使用了HTTP的REST接口实现,由于HTTP的平台无关性,虽然Eureka Server通过Java实现,但是在其下的微服务应用并不限于使用Java来进行开发

通信协议:

默认情况下,Eureka使用Jersey和XStream配合JSON作为Server与Client之间的通信协议。

Jersey是JAX-RS的参考实现

XStream是用来将对象序列化成XML(JSON)或反序列化为对象一个Java类库
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值