第三章 服務治理: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}

  經過上面的配置,利用應用名+隨機數的方式來區分不一樣的實例,從而實如今同一個主機上,不指定端就能輕鬆啓動多個實例的效果。

  

跨平臺支持

相關文章
相關標籤/搜索