1. 程式人生 > >spring cloud服務發現元件Eureka詳解

spring cloud服務發現元件Eureka詳解

 

Eureka是Netflix開發的服務發現元件,本身是一個基於REST的服務。Spring Cloud將它整合在其子專案spring-cloud-netflix中,以實現Spring Cloud的服務發現功能。目前Eureka 專案相當活躍,程式碼更新相當頻繁,目前最新的版本是1.5.5。Eureka 2.0也在緊鑼密鼓地開發中,2.0將會帶來更強的功能和更好的擴充套件性,但是由於還沒有Release,故而不作討論。

本文講解的Spring Cloud Camden SR1所使用的Eureka版本是1.4.11,還是比較新的。同時有了Eureka 1.x的基礎,未來上手Eureka 2.x也會比較容易。

Eureka的Github:https://github.com/Netflix/Eureka

Region、Zone解析

Eureka的官方文件對regin、zone幾乎沒有提及,由於概念抽象,新手很難理解。因此,在分析Eureka原理之前,我們先來了解一下region、zone、Eureka叢集三者的關係,如圖4-2。

regin-zone-eureka

圖4-2 region、zone、Eureka叢集之間的關係

region和zone(或者Availability Zone)均是AWS的概念。在非AWS環境下,我們可以簡單地將region理解為Eureka叢集,zone理解成機房。這樣圖4-2就很好理解了——一個Eureka叢集被部署在了zone1機房和zone2機房中。

對region和zone感興趣的讀者可前往http://blog.csdn.net/awschina/article/details/17639191 擴充套件閱讀。Spring Cloud中預設的region是us-east-1 。

Eureka架構

eureka 架構

圖4-3 Eureka架構圖

圖4-3是來自Eureka官方的架構圖,大致描述了Eureka叢集的工作過程。圖中包含的元件非常多,可能比較難以理解,我們用通俗易懂的語言解釋一下:

  • Application Service 相當於本書中的服務提供者,Application Client相當於本書中的服務消費者;
  • Make Remote Call,可以簡單理解為呼叫RESTful API;
  • us-east-1c、us-east-1d等都是zone,它們都屬於us-east-1這個region;

由圖可知,Eureka包含兩個元件:Eureka Server 和 Eureka Client,它們的作用如下:

  • Eureka Client是一個Java客戶端,用於簡化與Eureka Server的互動;
  • Eureka Server提供服務發現的能力,各個微服務啟動時,會通過Eureka Client向Eureka Server進行註冊自己的資訊(例如網路資訊),Eureka Server會儲存該服務的資訊;
  • 微服務啟動後,會週期性地向Eureka Server傳送心跳(預設週期為30秒)以續約自己的資訊。如果Eureka Server在一定時間內沒有接收到某個微服務節點的心跳,Eureka Server將會登出該微服務節點(預設90秒);
  • 每個Eureka Server同時也是Eureka Client,多個Eureka Server之間通過複製的方式完成服務登錄檔的同步;
  • Eureka Client會快取Eureka Server中的資訊。即使所有的Eureka Server節點都宕掉,服務消費者依然可以使用快取中的資訊找到服務提供者。

Why Eureka?

那麼為什麼我們在專案中使用了Eureka呢?我大致總結了一下,有以下幾方面的原因:

1)它提供了完整的Service RegistryService Discovery實現

首先是提供了完整的實現,並且也經受住了Netflix自己的生產環境考驗,相對使用起來會比較省心。

2)和Spring Cloud無縫整合

我們的專案本身就使用了Spring Cloud和Spring Boot,同時Spring Cloud還有一套非常完善的開原始碼來整合Eureka,所以使用起來非常方便。

另外,Eureka還支援在我們應用自身的容器中啟動,也就是說我們的應用啟動完之後,既充當了Eureka的角色,同時也是服務的提供者。這樣就極大的提高了服務的可用性。

這一點是我們選擇Eureka而不是zketcd等的主要原因,為了提高配置中心的可用性和降低部署複雜度,我們需要儘可能地減少外部依賴。

3)Open Source

最後一點是開源,由於程式碼是開源的,所以非常便於我們瞭解它的實現原理和排查問題。

3、Dive into Eureka

相信大家看到這裡,已經對Eureka有了一個初步的認識,接下來我們就來深入瞭解下它吧~

3.1 Overview

3.1.1 Basic Architecture

 

圖1

 

上圖簡要描述了Eureka的基本架構,由3個角色組成:

Eureka Server

提供服務註冊和發現

Service Provider

服務提供方

將自身服務註冊到Eureka,從而使服務消費方能夠找到

Service Consumer

服務消費方

從Eureka獲取註冊服務列表,從而能夠消費服務

需要注意的是,上圖中的3個角色都是邏輯角色。在實際執行中,這幾個角色甚至可以是同一個例項,比如在我們專案中,Eureka Server和Service Provider就是同一個JVM程序。

3.1.2 More in depth

 

圖2

 

上圖更進一步的展示了3個角色之間的互動。

  1. Service Provider會向Eureka Server做Register(服務註冊)、Renew(服務續約)、Cancel(服務下線)等操作。
  2. Eureka Server之間會做註冊服務的同步,從而保證狀態一致
  3. Service Consumer會向Eureka Server獲取註冊服務列表,並消費服務

3.2 Demo

為了給大家一個更直觀的印象,我們可以通過一個簡單的demo來實際執行一下,從而對Eureka有更好的瞭解。

3.2.1 Git Repository

Git倉庫:nobodyiam/spring-cloud-in-action

這個專案使用了Spring Cloud相關類庫,包括:

3.2.2 準備工作

Demo專案使用了Spring Cloud Config做配置,所以第一步先在本地啟動Config Server。

由於專案基於Spring Boot開發,所以直接執行com.nobodyiam.spring.cloud.in.action.config.ConfigServerApplication即可。

3.2.3 Eureka Server Demo

Eureka Server的Demo模組名是:eureka-server。

3.2.3.1 Maven依賴

eureka-server是一個基於Spring Boot的Web應用,我們首先需要做的就是在pom中引入Spring Cloud Eureka Server的依賴。

<dependency>    
<groupId>org.springframework.cloud</groupId>    
<artifactId>spring-cloud-starter-eureka-server</artifactId><version>1.2.0.RELEASE</version>
</dependency>

3.2.3.2 啟用Eureka Server

啟用Eureka Server非常簡單,只需要加上@EnableEurekaServer即可。

@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {  
public static void main(String[] args{   
SpringApplication.run(EurekaServiceApplication.class, args);
}
}

做完以上配置後,啟動應用,Eureka Server就開始工作了!

啟動完,開啟http://localhost:8761,就能看到啟動成功的畫面了。

 

圖3

 

3.2.4 Service Provider and Service Consumer Demo

Service Provider的Demo模組名是:reservation-service。

Service Consumer的Demo模組名是:reservation-client。

3.2.4.1 Maven依賴

reservation-service和reservation-client都是基於Spring Boot的Web應用,我們首先需要做的就是在pom中引入Spring Cloud Eureka的依賴。

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

3.2.4.2 啟動Service Provider

啟用Service Provider非常簡單,只需要加上@EnableDiscoveryClient即可。

@EnableDiscoveryClient
@SpringBootApplication
public class ReservationServiceApplication { 
public static void main(String[] args) {    newSpringApplicationBuilder(ReservationServiceApplication.class)
.run(args);
}
}

做完以上配置後,啟動應用,Server Provider就開始工作了!

啟動完,開啟http://localhost:8761,就能看到服務已經註冊到Eureka Server了。

 

圖4

 

3.2.4.3 啟動Service Consumer

啟動Service Consumer其實和Service Provider一樣,因為本質上Eureka提供的客戶端是不區分Provider和Consumer的,一般情況下,Provider同時也會是Consumer。

@EnableDiscoveryClient
@SpringBootApplication
public class ReservationClientApplication {  
@Bean  
CommandLineRunner runner(DiscoveryClient dc) {    
return args -> {      
dc.getInstances("reservation-service")              
.forEach(si -> System.out.println(String.format(                      
"Found %s %s:%s", si.getServiceId(), si.getHost(), si.getPort())));    
};  
}  
public static void main(String[] args) {    SpringApplication.run(ReservationClientApplication.class, args);  
}
}

上述程式碼中通過dc.getInstances(“reservation-service”)就能獲取到當前所有註冊的reservation-service服務。

3.3 Eureka Server實現細節

看了前面的demo,我們已經初步領略到了Spring Cloud和Eureka的強大之處,通過短短几行配置就實現了服務註冊和發現!

相信大家一定想了解Eureka是如何實現的吧,所以接下來我們繼續Dive!首先來看下Eureka Server的幾個對外介面實現。

3.3.1 Register

首先來看Register(服務註冊),這個介面會在Service Provider啟動時被呼叫來實現服務註冊。同時,當Service Provider的服務狀態發生變化時(如自身檢測認為Down的時候),也會呼叫來更新服務狀態。

介面實現比較簡單,如下圖所示。

  1. ApplicationResource類接收Http服務請求,呼叫PeerAwareInstanceRegistryImpl的register方法
  2. PeerAwareInstanceRegistryImpl完成服務註冊後,呼叫replicateToPeers向其它Eureka Server節點(Peer)做狀態同步(非同步操作)

 

圖5

 

註冊的服務列表儲存在一個巢狀的hash map中:

  • 第一層hash map的key是app name,也就是應用名字
  • 第二層hash map的key是instance name,也就是例項名字

3.2.4.2中的截圖為例,RESERVATION-SERVICE就是app name,jason-mbp.lan:reservation-service:8000就是instance name。

Hash map定義如下:

private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry =new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();

3.3.2 Renew

Renew(服務續約)操作由Service Provider定期呼叫,類似於heartbeat。主要是用來告訴Eureka Server Service Provider還活著,避免服務被剔除掉。介面實現如下圖所示。

可以看到,介面實現方式和register基本一致:首先更新自身狀態,再同步到其它Peer。

圖6

3.3.3 Cancel

Cancel(服務下線)一般在Service Provider shut down的時候呼叫,用來把自身的服務從Eureka Server中刪除,以防客戶端呼叫不存在的服務。介面實現如下圖所示。

 

圖7

 

3.3.4 Fetch Registries

Fetch Registries由Service Consumer呼叫,用來獲取Eureka Server上註冊的服務。

為了提高效能,服務列表在Eureka Server會快取一份,同時每30秒更新一次。

 

圖8

 

3.3.5 Eviction

Eviction(失效服務剔除)用來定期(預設為每60秒)在Eureka Server檢測失效的服務,檢測標準就是超過一定時間沒有Renew的服務。

預設失效時間為90秒,也就是如果有服務超過90秒沒有向Eureka Server發起Renew請求的話,就會被當做失效服務剔除掉。

失效時間可以通過eureka.instance.leaseExpirationDurationInSeconds進行配置,定期掃描時間可以通過eureka.server.evictionIntervalTimerInMs進行配置。

介面實現邏輯見下圖:

 

圖9

 

3.3.6 How Peer Replicates

在前面的Register、Renew、Cancel介面實現中,我們看到了都會有replicateToPeers操作,這個就是用來做Peer之間的狀態同步。

通過這種方式,Service Provider只需要通知到任意一個Eureka Server後就能保證狀態會在所有的Eureka Server中得到更新。

具體實現方式其實很簡單,就是接收到Service Provider請求的Eureka Server,把請求再次轉發到其它的Eureka Server,呼叫同樣的介面,傳入同樣的引數,除了會在header中標記isReplication=true,從而避免重複的replicate。

Peer之間的狀態是採用非同步的方式同步的,所以不保證節點間的狀態一定是一致的,不過基本能保證最終狀態是一致的。

結合服務發現的場景,實際上也並不需要節點間的狀態強一致。在一段時間內(比如30秒),節點A比節點B多一個服務例項或少一個服務例項,在業務上也是完全可以接受的(Service Consumer側一般也會實現錯誤重試和負載均衡機制)。

所以按照CAP理論,Eureka的選擇就是放棄C,選擇AP。

3.3.7 How Peer Nodes are Discovered

那大家可能會有疑問,Eureka Server是怎麼知道有多少Peer的呢?

Eureka Server在啟動後會呼叫EurekaClientConfig.getEurekaServerServiceUrls來獲取所有的Peer節點,並且會定期更新。定期更新頻率可以通過eureka.server.peerEurekaNodesUpdateIntervalMs配置。

這個方法的預設實現是從配置檔案讀取,所以如果Eureka Server節點相對固定的話,可以通過在配置檔案中配置來實現。

如果希望能更靈活的控制Eureka Server節點,比如動態擴容/縮容,那麼可以override getEurekaServerServiceUrls方法,提供自己的實現,比如我們的專案中會通過資料庫讀取Eureka Server列表。

具體實現如下圖所示:

 

圖10

 

3.3.8 How New Peer Initializes

最後再來看一下一個新的Eureka Server節點加進來,或者Eureka Server重啟後,如何來做初始化,從而能夠正常提供服務。

具體實現如下圖所示,簡而言之就是啟動時把自己當做是Service Consumer從其它Peer Eureka獲取所有服務的註冊資訊。然後對每個服務,在自己這裡執行Register,isReplication=true,從而完成初始化。

 

圖11

 

3.4 Service Provider實現細節

現在來看下Service Provider的實現細節,主要就是Register、Renew、Cancel這3個操作。

3.4.1 Register

Service Provider要對外提供服務,一個很重要的步驟就是把自己註冊到Eureka Server上。

這部分的實現比較簡單,只需要在啟動時和例項狀態變化時呼叫Eureka Server的介面註冊即可。需要注意的是,需要確保配置eureka.client.registerWithEureka=true。

 

圖12

 

3.4.2 Renew

Renew操作會在Service Provider端定期發起,用來通知Eureka Server自己還活著。 這裡有兩個比較重要的配置需要注意一下:

  1. instance.leaseRenewalIntervalInSeconds

Renew頻率。預設是30秒,也就是每30秒會向Eureka Server發起Renew操作。

  1. instance.leaseExpirationDurationInSeconds

服務失效時間。預設是90秒,也就是如果Eureka Server在90秒內沒有接收到來自Service Provider的Renew操作,就會把Service Provider剔除。

具體實現如下:

 

圖13

 

3.4.3 Cancel

在Service Provider服務shut down的時候,需要及時通知Eureka Server把自己剔除,從而避免客戶端呼叫已經下線的服務。

邏輯本身比較簡單,通過對方法標記@PreDestroy,從而在服務shut down的時候會被觸發。

 

圖14

 

3.4.4 How Eureka Servers are Discovered

這裡大家疑問又來了,Service Provider是怎麼知道Eureka Server的地址呢?

其實這部分的主體邏輯和3.3.7 How Peer Nodes are Discovered幾乎是一樣的。

也是預設從配置檔案讀取,如果需要更靈活的控制,可以通過override getEurekaServerServiceUrls方法來提供自己的實現。定期更新頻率可以通過eureka.client.eurekaServiceUrlPollIntervalSeconds配置。

 

圖15

 

3.5 Service Consumer實現細節

Service Consumer這塊的實現相對就簡單一些,因為它只涉及到從Eureka Server獲取服務列表和更新服務列表。

3.5.1 Fetch Service Registries

Service Consumer在啟動時會從Eureka Server獲取所有服務列表,並在本地快取。需要注意的是,需要確保配置eureka.client.shouldFetchRegistry=true。

 

圖16

 

3.5.2 Update Service Registries

由於在本地有一份快取,所以需要定期更新,定期更新頻率可以通過eureka.client.registryFetchIntervalSeconds配置。

 

圖17

 

3.5.3 How Eureka Servers are Discovered

Service Consumer和Service Provider一樣,也有一個如何知道Eureka Server地址的問題。

其實由於Service Consumer和Service Provider本質上是同一個Eureka客戶端,所以這部分邏輯是一樣的,這裡就不再贅述了。詳細資訊見3.4.4節

4. Summary

本文主要介紹了Eureka的實現思路,通過深入瞭解Eureka Server、Service Provider、Service Consumer的實現,我們清晰地看到了服務註冊、發現的一系列過程和實現方式。

 

如何使用Spring Cloud Eureka?


下面實際操作一下,建立兩個微服務。先建立一個服務提供者,再建立一個服務消費者,服務消費者先通過普通的方式呼叫服務提供者,再通過Spring Eureka 呼叫服務提供者。

先建立一個microservice-provider專案,作為服務提供者。 
pom檔案中引入Spring Boot的依賴:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.4.3.RELEASE</version>
</parent>

再新增Spring MVC的支援:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>



spring-boot-starter-parent定義了Spring Boot版本的基礎依賴以及一些預設配置內容,比如,配置檔案application.properties的位置等。 
spring-boot-starter-web,是全棧web開發模組,包含嵌入式Tomcat,Spring MVC。 
配置完pom依賴之後,再新增MicroServiceProviderApplication檔案:

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

然後再新增一個Controller,用來提供服務,這裡為了簡單起見,直接返回hello world。

@RestController
public class ProviderController {

    @RequestMapping(value = "/provider", method = RequestMethod.GET)
    public String provider() {
        return "hello world";
    }
}



之後,編寫配置檔案application.properties,新增如下配置:

server.port=8000
spring.application.name=microservice-provider

到這裡,服務提供者專案就寫完了,接下來,再寫服務消費者微服務。 
建立一個microservice-consumer專案,作為服務消費者。配置和服務提供者類似,pom檔案同樣新增spring-boot-starter-parent和spring-boot-starter-web的依賴。編寫MicroServiceConsumeApplication作為啟動類,並在啟動類中例項化RestTemplate物件。

@SpringBootApplication
public class MicroServiceConsumeApplication {
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

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

然後建立ConsumerController作為消費者方法,用來去呼叫服務提供者,這裡通過restTemplate去請求服務提供者的API:

import org.springframework.web.client.RestTemplate;
@RestController
public class ConsumerController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/consumer", method = RequestMethod.GET)
    public String consumer() {
       return this.restTemplate.getForEntity("http://localhost:8000/provider", String.class).getBody();
    }
}


接下來配置服務消費者的application.properties,新增如下內容:

server.port=9000
spring.application.name=microservice-consumer

這樣,服務消費者也寫好了,本地啟動這兩個服務的話,訪問http://localhost:9000/consumer,就可以看到返回的hello world。 
但是這種呼叫方式,只是通過硬編碼的方式,在程式中寫死了服務提供者的地址,然後去請求的API,這樣就會存在很多問題,試用場景有限,無法動態伸縮。 
所以,就需要一個服務發現機制,服務消費者通過這種機制去發現服務提供者,然後再去呼叫,這樣就不是硬編碼了。 
接下來,建立一個Eureka Server作為註冊中心,然後再把服務提供者和服務消費者兩個微服務進行改造,改造成Eureka Client,並且註冊到註冊中心。這樣,當服務消費者再呼叫服務提供者的時候,就可以通過註冊中心,獲得服務提供者的地址,然後進行呼叫。

接下來,建立Eureka Server專案:eurekaserver 
在pom檔案中給專案新增依賴:

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



之後再編寫啟動類:EurekaServerApplication

@SpringBootApplication
@EnableEurekaServer//宣告這是一個Eureka server
public class EurekaServerApplication {

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


在啟動類上新增@EnableEurekaServer註解,宣告這是一個Eureka Server。 
最後在application.properties檔案中,新增配置項:

server.port=8761
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/

server.port=8761指明瞭專案啟動的埠。 
eureka.client.register-with-eureka=false,這個配置項表示是否將自己註冊到Eureka Server,預設為true(預設情況下,Eureka Server也是一個Eureka Client),由於我們這裡只有這一個Eureka Server,所以把這個配置項設定為false。 
eureka.client.fetch-registry=false,這個配置項表示是否從Eureka Server獲取配置資訊,預設為true。由於我們這裡只有這一個Eureka Server,所以把這個配置項設定為false。 
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ 這個配置項是與Eureka Server互動的地址。註冊服務、查詢服務,都是這個地址。 
經過上面幾步配置,我們的Eureka Server就開發完成了,接下來啟動這個專案,在瀏覽器輸入http://localhost:8761,就可以看到Eureka Server 的介面。 


接下來,改造服務提供者和服務消費者。 
先修改服務提供者,在pom檔案中新增依賴:

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

然後修改MicroServiceProviderApplication,在上面添加註解@EnableDiscoveryClient,宣告是一個Eureka client:

@EnableDiscoveryClient//宣告是一個Eureka client
@SpringBootApplication
public class MicroServiceProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(MicroServiceProviderApplication.class, args);
    }
}


之後配置檔案中新增配置項:

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

配置完成之後啟動專案,專案啟動的時候,就會作為一個Eureka client向Eureka Server註冊資訊,註冊的地址為http://localhost:8761/eureka/,這個地址就是我們Eureka Server啟動的時候,指定的註冊地址。 
此時再重新整理http://localhost:8761,就可以看到Eureka Server 的介面上已經註冊了一個服務了,服務名稱是microservice-provider。 


然後繼續再改造服務消費者,改造方式和microservice-provider一樣,也是在pom檔案中新增spring-cloud-starter-eureka的依賴,在MicroServiceConsumeApplication上面添加註解@EnableDiscoveryClient註解,在application.properties中新增服務註冊地址eureka.client.service-url.defaultZone=http://localhost:8761/eureka/ 
除此之外,在TestTemplate生成的地方,新增@LoadBalanced註解,使用負載均衡。(關於負載均衡,後續文章接續介紹)

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


修改restTemplate呼叫的地址,原來是呼叫http://localhost:8000/provider,現在改成呼叫http://MICROSERVICE-PROVIDER/provider。 
之後,再啟動microservice-consumer。重新整理http://localhost:8761,可以看到,microservice-consumer也註冊到Eureka Server上了。 


瀏覽器輸入http://localhost:9000/consumer,呼叫成功。

上面講了通過Eureka,搭建微服務並且進行服務治理的過程。先搭建了一個Eureka Server,然後再建立兩個Eureka Client,一個作為服務消費者,另一個作為服務提供者。 
服務提供者先去Eureka Server上面註冊自己的服務 
服務消費者去Eureka Server上獲取服務,然後進行消費。

參考資料: 
1.《Spring Cloud與Docker微服務架構實戰》 周立 著 
2.《Spring Cloud微服務實戰》 翟永超 著