第三章 服务治理:Spring Cloud Eureka

  Spring Cloud Eureka是Spring Cloud Netflix 微服务套件中的一部分,它基于Netflix Eureka作了二次封装,主要负责完成微服务架构中的服务治理功能。Spring Cloud 经过为Eureka增长了Spring Boot风格的自动化配置,咱们只需经过引入依赖和注解配置就能让Spring Boot构建的微服务应用轻松的与Eureka服务治理体系进行整合。
 

服务治理:

  服务治理能够说是微服务架构中最为核心和基础的模块,主要用来实现各个微服务实例的自动化注册与发现。
使用服务治理的缘由:在服务引用并不算多的时候,能够经过静态配置来完成服务的调用,但随着业务的发展,系统功能愈来愈复杂,相应的微服务也不断增长,此时静态配置会变得愈来愈难以维护。而且面对不断发展的业务,集群规模,服务的位置、服务的命名等都有可能发生变化,若是仍是经过手工维护的方式,极易发生错误或是命名冲突等问题。同时,也将消耗大量的人力来维护静态配置的内容。为了解决微服务架构中的服务实例维护问题,就产生了大量的服务治理框架和产品。这些框架和产品的实现都围绕着服务注册与服务发现机制来完成对微服务应用实例的自动化管理。
 

服务注册:

  在服务治理框架中,一般都会构建一个注册中心,每一个服务单元向注册中心登记本身提供的服务,将主机与端口号、版本号、通讯协议等一些附加信息告知注册中心,注册中心按服务名分类组织服务清单。好比:有两个提供服务A的进程分别运行于192.168.0.100:8000 和192.168.0.101:8000 位置上,还有三个提供服务B的进程分别运行于192.168.0.100:9000、192.168.0.101:9000、192.168.0.102:9000位置上。当这些进程都启动,并向注册中心注册本身的服务以后,注册中心就会维护相似下面的一个服务清单。另外,注册中心还须要以心跳的方式去监测清单中的服务是否可用,若不可用须要从服务清单中剔除,达到排除故障服务的效果。java

服务发现:

  在服务治理框架的运做下,服务间的调用再也不经过指定具体的实例地址来实现,而是经过向服务名发起请求调用实现。因此,服务调用方在调用服务提供方接口时,并不知道具体的服务实例位置。所以,调用方须要向注册中心咨询服务,并获取全部服务的实例清单,以实现对具体服务实例的访问。好比:以上述服务为例,有服务C但愿调用服务A,服务C就向注册中心发起咨询请求,服务注册中心就会将服务A的位置清单返回给服务C,当服务C要发起调用时,便从该清单中以某种轮询策略取出一个位置来进行服务调用(客户端负载均衡)。
 

Netflix Eureka

  Spring cloud Eureka ,使用 Netflix Eureka 来实现服务注册与发现,它即包含了服务端组件,也包含了客户端组件,而且服务端和客户端均采用Java编写,因此 Eureka 主要适用于经过 Java实现的分布式系统,或是与JVM兼容语言构建的系统。可是,Eureka服务端的服务治理机制提供了完备的RESTful API,因此也支持将非 Java语言构建的微服务应用归入Eureka 的服务治理体系中来。只是在使用其余语言平台时,须要本身来实现Eureka的客户端程序。
  Eureka服务端:也称为服务注册中心。它和其余服务注册中心同样,支持高可用配置。
  Eureka客户端:主要处理服务的注册与发现。客户端服务经过注解和参数配置的方式,嵌入在客户端应用程序的代码中,在应用程序运行时,Eureka客户端向注册中心注册自身提供的服务并周期性的发送心跳来更新它的服务租约。同时也能从服务端查询当前注册的服务信息并把它们缓存到本地并周期性的刷新服务状态。
 

搭建服务注册中心

  首先,建立spring boot 工程,命名为eureka-server,并在pom.xml 中引入必要的依赖内容(也能够经过spring initializer 快速构建项目):
  
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>eureka-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>eureka-server</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

  

  经过@EnableEurekaServer 注解启动一个服务注册中心提供给其余应用进行对话web

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

  

  在默认配置下,该服务注册中心也会将本身做为客户端来尝试注册它本身,因此须要禁用它的客户端注册行为,在application.properties文件中增长以下配置:
server.port=8082

eureka.instance.hostname=localhost
# 向注册中心注册服务
eureka.client.register-with-eureka=false
# 检索服务
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://${eureka.instance.hostname}:${server.port}/eureka/

  

  完成配置后,启动应用并访问 http://localhost:8082/。能够看到如下Eureka信息面板,其中Instances currently registered with Eureka 栏是空的,表示该注册中心尚未注册任何服务。
 

 

  

注册服务提供者

  完成服务注册中心的搭建后,就能够添加一个既有的spring boot应用到Eureka的服务治理体系中去。
  新建项目名为eureka-client的spring boot应用,将其做为一个微服务应用向服务注册中心发布本身。首先在pom.xml中增长spring cloud eureka 模块的依赖。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>eureka-client</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>eureka-client</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR2</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

  

  接着,新建RESTful API,经过注入DiscoveryClient对象,在日志中打印出服务的相关内容。算法

package com.example.demo.web;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-9
 */

@RestController
public class HelloController {

    private final Logger logger = Logger.getLogger(getClass());

    @Autowired
    private DiscoveryClient client;

    @RequestMapping(value = "/index")
    public String index(){
        ServiceInstance instance = client.getLocalServiceInstance();
        logger.info("/hello:host:"+instance.getHost()+" port:"+instance.getPort()
                +" service_id:"+instance.getServiceId());
        return "hello world!";
    }
}

 

  而后在主类中添加 @EnableDiacoveryClient 注解,激活Eureka 中的DiscoveryClient 实现(自动化配置,建立DiscoveryClient接口针对Eureka客户端的EurekaDiscoveryClient实例),才能实现上述对服务信息的输出。
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class, args);
    }
}

  

  最后修改application.properties文件,经过spring.application.name属性为服务命名,再经过eureka.client.service-url.defaultZone 属性来指定服务注册中心的地址,地址和注册中心设置的地址一致:spring

server.port=2222
spring.application.name=hello-service

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

  

  下面分别启动服务注册中心以及服务提供方,在hello-service服务控制台中,DiscoveryClient对象打印了该服务的注册信息:
2017-08-09 17:17:27.635  INFO 8716 --- [           main] c.example.demo.EurekaClientApplication   : Started EurekaClientApplication in 9.844 seconds (JVM running for 10.772)
2017-08-09 17:17:27.797  INFO 8716 --- [nfoReplicator-0] com.netflix.discovery.DiscoveryClient    : DiscoveryClient_HELLO-SERVICE/chanpin-PC:hello-service:2222 - registration status: 204

  

  在注册中心控制台能够看到hello-service的注册信息:
2017-08-09 17:17:27.786  INFO 10396 --- [nio-8082-exec-1] c.n.e.registry.AbstractInstanceRegistry  : Registered instance HELLO-SERVICE/chanpin-PC:hello-service:2222 with status UP (replication=false)
2017-08-09 17:17:47.792  INFO 10396 --- [a-EvictionTimer] c.n.e.registry.AbstractInstanceRegistry  : Running the evict task with compensationTime 0ms

  

  此处的输出内容为HelloController中注入的DiscoveryClient接口对象,从服务注册中心获取的服务相关信息。
 

高可用注册中心

  在微服务架构这样的分布式环境中,须要充分考虑发生故障的状况,因此在生产环境中必须对各个组件进行高可用部署,对于微服务如此,对于服务注册中心也同样。
  Eureka Server 的设计一开始就考虑了高可用问题,在Eureka的服务治理中,全部节点既是服务提供方,也是服务消费方,服务注册中心也同样。
  Eureka Server 的高可用实际上就是将本身做为服务向其余服务注册中心注册本身,这样就能够造成一组互相注册的服务注册中心,以实现服务清单的相互同步,达到高可用的效果。下面尝试搭建一个高可用服务注册中心的集群。在以前的服务注册中心的基础上进行扩展,构建一个双节点的服务注册中心集群。
  • 建立 application-peer1.properties,做为peer1 服务中心的配置,并将serviceUrl指向peer2:
spring.application.name=eureka-server
server.port=1111

eureka.instance.hostname=peer1
eureka.client.service-url.defaultZone=http://peer2:1112/eureka/

  

  • 建立 application-peer2.properties,做为peer2 服务中心的配置,并将serviceUrl指向peer1:
spring.application.name=eureka-server
server.port=1112

eureka.instance.hostname=peer2
eureka.client.service-url.defaultZone=http://peer1:1111/eureka/

  

  • 在C:\Windows\System32\drivers\etc\hosts 文件中添加对peer1 和 peer2 中的转换,让上面配置的host形式的serviceURL能在本地正确访问到;
    127.0.0.1 peer1
    127.0.0.1 peer2

     

  • 经过spring.profiles.active 属性来分别启动peer1 和 peer2(打开两个terminal进行启动,在一个terminal中先启动的peer1 会报错,但不影响,是由于它所注册的服务peer2 还未启动,在另外个terminal中把peer2 启动便可,不用启动主类) :
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer1
java -jar eureka-server-0.0.1-SNAPSHOT.jar --spring.profiles.active=peer2

  

  此时访问peer1的注册中心 http://localhost:1111/ 能够看到,registered-replicas 中已经有 peer2 节点的eureka-server了。一样的访问peer2 的注册中心 http://localhost:1112/ 也能够看到registered-replicas 中有 peer1 节点, 而且这些节点在可用分片(available-replicase)之中。当关闭了peer1 节点后,刷新peer2 注册中心,能够看到 peer1 的节点变成了不可用分片(unavailable-replicas)。

 

 

  • 在设置了多节点的服务注册中心以后,服务提供方还须要作一些简单的配置才能将服务注册到Eureka Server 集群中。以hello-service为例,修改配置文件以下:
server.port=2222
spring.application.name=hello-service

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

  

  主要是将eureka.client.service-url.defaultZone 的注册中心指向以前搭建的peer1 和 peer2。
  下面启动该服务,经过访问 http://localhost:1112/ 或者 http://localhost:1111/ 能够看到 hello-service 服务同时被注册到了peer1 和 peer2 上。

 

  若此时断开 peer1 ,因为 hello-service 同时也向peer2 上注册了,所以在peer2 上的其余服务依然能访问到hello-service,从而实现了服务注册中心的高可用。
 

 

   若是不想使用主机名来定义注册中心的地址,也可使用IP地址的形式,可是须要在配置文件中增长配置参数 eureka.instance.prefer-ip-address=true,该值默认为false。apache

 

服务发现与消费

  经过上面的内容介绍与实践,已经搭建起微服务架构中的核心组件——服务注册中心(包括单节点模式和高可用模式)。同时,还经过简单的配置,将hello-service服务注册到Eureka注册中心上,成为该服务治理体系下的一个服务。如今已经有了服务注册中心和服务提供者,下面就构建一个服务消费者,它主要完成两个目标,发现服务和消费服务。其中,服务发现的任务由Eureka客户端完成,而服务消费的任务由Ribbon完成。Ribbon是一个基于HTTP和TCP的客户端负载均衡器,它能够在经过客户端中配置的ribbonServerList服务端列表去轮询访问以达到均衡负载的做用。当Ribbon与Eureka联合使用时,Ribbon的服务实例清单RibbonServerList会被DiscoveryEnabledNIWSServerList重写,扩展成从Eureka注册中心获取服务端列表。同时也会用NIWSDiscoveryPing来取代IPing,它将职责委托给Eureka来肯定服务端是否已经启动。
  • 准备工做:启动以前实现的服务注册中心eureka-server以及hello-service服务,为了实验Ribbon的客户端负载均衡功能,咱们经过java -jar 命令行的方式来启动两个端口不一样的hello-service,具体以下:
  • 修改配置文件:
server.port=2222
spring.application.name=hello-service

eureka.client.service-url.defaultZone=http://localhost:8082/eureka/
  • 再将hello-service应用打包:mvn clean package
  • 经过下列命令启动应用程序:
java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8011
java -jar eureka-client-0.0.1-SNAPSHOT.jar --server.port=8012
  • 成功启动两个服务后,能够在注册中心看到名为HELLO-SERVICE的服务中出现两个实例单元:

 

  • 建立一个Spring boot项目来实现服务消费者,取名为ribbon-consumer,并在pom.xml中引入以下的依赖内容。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Dalston.SR2</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-ribbon</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>
  • 在主类中经过@EnableDiscoveryClient注解让该应用注册为Eureka客户端应用,以获取服务发现的能力,同时,在该主类中建立RestTemplate的Spring Bean实例,并经过@LoadBalanced 注解开启客户端负载均衡。
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class DemoApplication {

    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

 

  • 建立ConsumerController类并实现/ribbon-consumer接口。在该接口中,经过上面建立的RestTemplate 来实现对HELLO-SERVICE 服务提供的 /hello 接口进行调用。此处的访问地址是服务名 HELLO-SERVICE ,而不是一个具体的地址,在服务治理框架中,这是一个重要特性。
package com.example.demo.web;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author lxx
 * @version V1.0.0
 * @date 2017-8-9
 */

@RestController
public class ConsumerController {

    @Autowired
    RestTemplate restTemplate;

    @RequestMapping(value = "ribbon-consumer", method = RequestMethod.GET)
    public String helloConsumer(){
        return restTemplate.getForEntity("http://HELLO-SERVICE/index",
                String.class).getBody();
    }
}
  • 在application.properties中配置Eureka服务注册中心的位置,须要与以前的HELLO-SERVICE同样,同时设置该消费者的端口为3333,不与以前启动的应用端口冲突便可。
server.port=3333
spring.application.name=ribbon-consumer

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

  

  • 启动ribbon-consumer应用后,能够在Eureka信息面板中看到,除了HELLO-SERVICE外,还多了实现的RIBBON-CONSUMER服务。

 

  • 经过向 http://localhost:3333/ribbon-consumer 发起访问, 成功返回字符串 “hello world”。在消费者控制台中打印出服务列表状况。
  • 多发送几回请求,能够在服务提供方hello-service的控制台中看到一些打印信息,能够看出两个控制台基本是交替访问,实现了客户端的负载均衡。
 

Eureka详解

基础架构(核心三要素)

  • 服务注册中心:Eureka提供的服务端,提供服务注册与发现的功能,即以前的eureka-server。
  • 服务提供者:提供服务的应用,能够是spring boot应用,也能够是其余技术平台且遵循Eureka通讯机制的应用。它将本身提供的服务注册到Eureka,以供其余应用发现。即以前的HELLO-SERVICE.
  • 服务消费者:消费者从服务注册中心获取服务列表,从而使消费者能够知道去何处调用其所须要的服务,在上一节中使用了Ribbon来实现服务消费,后续还会介绍使用Feign的消费方式
 

服务治理机制

  体验了Spring cloud Eureka 经过简单的注解配置就能实现强大的服务治理功能以后,进一步了解一下Eureka基础架构中各个元素的一些通讯行为,以此来理解基于Eureka实现的服务治理体系是如何运做起来的。以上图为例,其中有几个重要元素:json

  • “服务注册中心-1” 和 “服务注册中心-2”,他们互相注册组成高可用集群。
  • “服务提供者” 启动了两个实例,一个注册到“服务注册中心-1” 上,另一个注册到 “服务注册中心-2” 上。
  • 还有两个 “服务消费者” ,它们也都分别指向了一个注册中心。

  根据上面的结构,能够详细了解从服务注册开始到服务调用,及各个元素所涉及的一些重要通讯行为。缓存

服务提供者

  服务注册

  “服务提供者” 在启动的时候会经过发送REST请求的方式将本身注册到Eureka Server 上,同时带上了自身服务的一些元数据信息。Eureka Server 接收到这个REST请求后,将元数据信息存储在一个双层结构Map中,其中第一层的key是服务名,第二层的key 是具体服务的实例名。安全

  在服务注册时,须要确认eureka.client.register-with-eureka=true参数是否正确,若为false,将不会启动注册操做。网络

  服务同步

  如图所示,这里的两个服务提供者分别注册到了两个不一样的服务注册中心上,即它们的信息分别被两个服务注册中心维护。因为服务注册中心之间为互相注册,当服务提供者发送注册请求到一个服务注册中心时,它会将请求转发给集群中相连的其余注册中心,从而实现注册中心之间的服务同步。经过服务同步,两个服务提供者的服务信息就能够经过这两个服务注册中心中的任意一台获取到。架构

  服务续约

  在注册完服务以后,服务提供者会维护一个心跳用来持续告诉 Eureka Server :“我还活着”,以防止 Eureka Server 的 “剔除任务” 将该服务实例从服务列表中排除出去,咱们称该操做为服务续约。

 

服务消费者

  获取服务

  到这里,在服务注册中心已经注册了一个服务,而且该服务有两个实例。当咱们启动服务消费者时,它会发送一个REST请求给服务注册中心,来获取上面注册的服务清单。为了性能考虑,Eureka Server 会维护一份只读的服务清单来返回给客户端,同时该缓存清单会每隔30秒更新一次。

  获取服务是服务消费者的基础,因此要确保 eureka-client-fetch-registery=true 参数没有被修改为false,该值默认为 true。若想修改缓存清单的更新时间,能够经过 eureka-client.registry-fetch-interval-seconds=30 参数来进行修改,该值默认为30,单位为秒。

  服务调用

  服务消费者在获取服务清单后,经过服务名能够得到具体提供服务的实例名和该实例的元数据信息。由于有这些服务实例的详细信息,因此客户端能够根据本身的须要决定具体须要调用的实例,在Ribbon中会默认采用轮询的方式进行调用,从而实现客户端的负载均衡。

  服务下线

  在系统运行过程当中必然会面临关闭或重启服务的某个实例的状况,在服务关闭期间,咱们天然不但愿客户端会继续调用关闭了的实例。因此在客户端程序中,当服务实例进行正常的关闭操做时,它会触发一个服务下线的REST请求给 Eureka Server,告诉服务注册中心:“我要下线了”。服务端在接收到请求以后,将该服务状态设置为下线(DOWN),并把该下线事件传播出去。

 

服务注册中心

  失效剔除

  当一些外部缘由如内存溢出、网络故障等致使服务实例非正常下线,而服务注册中心并未收到“服务下线”的请求。为了从服务列表中将这些没法提供服务的实例剔除,Eureka Server 在启动的时候会建立一个定时任务,默认每隔一段时间(默认60秒)将当前清单中超时(默认90秒)没有续约的服务剔除出去。

  自我保护

  当咱们在本地调试基于 Eureka 的程序时,基本上都会在服务注册中心的信息面板上出现相似下面的红色警告信息:

  实际上,该警告就是触发了Eureka Server的自我保护机制。以前介绍过,服务注册到Eureka Server以后,会维护一个心跳链接,告诉Eureka Server 本身还活着。Eureka Server 在运行期间,会统计心跳失败的比例在15分钟以内低于85%,若是出现低于的状况,Eureka Server 会将当前的实例信息保护起来,让这些实例不会过时,尽量保护这些注册信息。可是,在保护期间内实例若出现问题,那么客户端很容易拿到实际已经不存在的服务实例,会出现调用失败的状况,因此客户端必需要有容错机制,好比可使用请求重试、断路器等机制。

  因为在本地调试很容易触发注册中心的保护机制,使得注册中心维护的服务实例不那么准确。能够在本地进行开发时,使用 eureka-server.enable-self-preservation=false 参数来关闭保护机制,确保注册中心将不可用的实例正确剔除。

  

源码分析

  上面,咱们对Eureka中各个核心元素的通讯行为作了详细的介绍,为了更深刻的理解它的运做和配置,下面咱们结合源码来分别看看各个通讯行为是如何实现的。

  在看具体源码以前,先回顾一下以前所实现的内容,从而找到一个合适的切入口去分析。首先,对于服务注册中心、服务提供者、服务消费者这三个主要元素来讲,后二者(Eureka客户端)在整个运行机制中是大部分通讯行为的主动发动着,而注册中心主要是处理请求的接受者。因此,咱们从Eureka客户端做为入口看看它是如何完成这些主动通讯行为的。

  咱们将一个普通的spring boot 应用注册到Eureka Server 或是从 Eureka Server 中获取服务列表时,主要就作了两件事:

  • 在应用类中配置了 @EnableDiscoveryClient 注解。
  • 在application.properties 中用 eureka-client.service-url.defaultZone 参数指定了注册中心的位置。

  顺着上面的线索,咱们看看 @EnableDiscoveryClient 的源码,具体以下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

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.cloud.client.discovery.EnableDiscoveryClientImportSelector;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({EnableDiscoveryClientImportSelector.class})
public @interface EnableDiscoveryClient {
    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}

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

  

跨平台支持

相关文章
相关标签/搜索