深度剖析服務發現組件Netflix Eureka

轉自:http://geek.csdn.net/news/detail/130223   作者:錢曙光

一、背景介紹

Eureka是Netflix開源的一款提供服務註冊和發現的產品。

其官方文檔中對自己的定義是:

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers. We call this service, the Eureka Server. Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier. The client also has a built-in load balancer that does basic round-robin load balancing.

我們在研發Apollo配置中心時(https://github.com/ctripcorp/apollo),考慮到配置中心是基礎服務,有非常高的可用性要求,爲了更好地支持服務動態擴容、縮容、失效剔除等特性,所以就選擇了使用Eureka來提供服務註冊和發現功能。

本着“當你選擇一款開源產品後,你就應當對它負責,既要信任它又要挑戰它”的原則,我花了一些時間比較深入地研究了Eureka的實現細節(好在Eureka的實現短小精悍,通讀源碼也沒花太多時間),今天就來詳細介紹一下。

二、Why Eureka?

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

1. 它提供了完整的Service Registry和Service Discovery實現

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

2. 和Spring Cloud無縫集成

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

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

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

3. Open Source

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

三、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倉庫:https://github.com/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-servicereservation-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) {
    new SpringApplicationBuilder(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服務請求,調用PeerAwareInstanceRegistryImplregister方法
  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. eureka.instance.leaseRenewalIntervalInSeconds 
    Renew頻率。默認是30秒,也就是每30秒會向Eureka Server發起Renew操作;
  2. eureka.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節。

四、Summary

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

相信對正在使用Eureka的同學會有一些幫助,同時希望對暫不使用Eureka的同學也能有一定的啓發,畢竟服務註冊、發現還是比較基礎和通用的,瞭解了實現方式後,在使用上應該能更得心應手一些吧~


發佈了12 篇原創文章 · 獲贊 8 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章