服務注冊和發(fā)現(xiàn) Eureka

在前兩章介紹了微服務簡介,spring-cloud簡介這一章學習spring cloud中的第一個組件,Eureka。這是一個用于服務注冊和發(fā)現(xiàn)的組件。分為Eureka Server和Eureka Client,Eureka Server為Eureka服務注冊中心,Eureka Client為Eureka客戶端。

基本架構

Eureka主要包括三種角色:
Register Service:服務注冊中心,他是一個Eureka Server,提供服務注冊和發(fā)現(xiàn)的功能。
Provider Service:服務提供者,它是一個Eureka Client,提供服務。
Consumer Service:服務消費者,它是一個Eureka Client,消費服務。

服務消費的基本流程

首先需要啟動一個Eureka Server,做為服務注冊中心,服務提供者Eureka Client向服務注冊中心Eureka Server注冊,將自己的服務名和IP地址等信息通過REST API的形式提交給服務注冊中心Eureka Server。同樣,服務消費者Eureka Client也向服務注冊中心Eureka Server注冊,同時服務消費者獲取一份服務注冊列表的信息,該列表包含了所有向服務注冊中心Eureka Server注冊的服務信息。獲取服務注冊列表信息之后,服務消費者就知道服務提供者的IP地址,可以通過Http遠程調度來消費服務提供者的服務。

編寫Eureka Server

第一步:引入Eureka Server的起步依賴spring-cloud-starter-eureka-server,以及spring boot測試的起步依賴spring-boot-starter-test。最后引入spring boot的maven插件spring-boot-maven-plugin,該插件的作用是可以試用maven插件的方式來啟動spring boot工程。

 <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>

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

第二步:在application.yml中做程序的相關配置。Eureka Server會向自己注冊,這時需要配置eureka.client.registerWithEureka和eureka.client.fetchRegistry為false,防止自己注冊自己。配置如下:

server:
  port: 8761
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

最后在工程啟用類加上注解@EnableEurekaServer,開啟Eureka Server的功能。代碼如下:

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

啟動項目,訪問http://localhost:8761,可以在瀏覽器查看Eureka Server的主界面。在界面Instance currently registered with Eureka這一項上沒有任何注冊的實列,接下來編寫Eureka Client,并注冊到Eureka Server。

編寫Euraka Client

引入依賴:

<dependencies>
        <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>

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

在工程配置文件application.yml中做相關配置。其中defaultZone為默認的Zone,來源于AWS的概念。區(qū)域(Region)和可用區(qū)(Availability Zone,AZ)是AWS的另外兩個概念。區(qū)域是指服務器所在的區(qū)域,比如北美洲、歐洲、亞洲等,每個區(qū)域一般由多個可用區(qū)組成。在配置中defaultZone是指Eureka Server的注冊地址。

server:
  port: 8763
spring:
  application:
    name: eureka-client
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

在項目啟動類上加上注解@EnableEurekaClient開啟Eureka Client功能。代碼如下:

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

啟動Client,啟動成功后,控制臺會打印出如下信息:

 DiscoveryClient_EUREKA-CLIENT/DESKTOP-NB1C2U4:eureka-client:8763 - registration status: 204

說明已經(jīng)向Eureka Server注冊了。在瀏覽器訪問http://localhost:8761,可在主界面看到有一個實列注冊,Application為EUREKA-CLIENT,Status為UP,端口為8763,說明注冊成功。

eureka-server.png

構建高可用的Eureka Server集群

在實際的項目中,可能由幾十個或者幾百個的微服務實例,這時Eureka Server承擔了非常高的負載。由于Eureka Server在微服務架構中有著舉足輕重的作用,所以需要對Eureka Server進行高可用集群。

第一步更改eureka-server的配置文件application.yml,在配置文件中采用多profile的格式,具體代碼如下:

spring:
  profiles: peer1
server:
  port: 8761
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://peer2:8762/eureka/
---
spring:
  profiles: peer2
server:
  port: 8762
  client:
    register-with-eureka: false
    fetch-registry: false
    service-url:
      defaultZone: http://peer1:8761/eureka/

由于是本地代建Eureka Server集群,需要在本機hosts文件做域名映射。

127.0.0.1 peer1
127.0.0.1 peer2

通過mvn clean package編譯項目,成功后在目錄target文件夾下生成jar包。通過java -jar命令并指定spring-profiles-active參數(shù)。命令如下:

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

修改eureka-client配置文件,注意其中只是向8761端口的服務器注冊,配置如下:

server:
  port: 8763
spring:
  application:
    name: eureka-client
eureka:
  client:
    service-url:
      defaultZone: http://peer1:8761/eureka/

啟動eureka-client,訪問 http://localhost:8762/,在DS Replicas選項中顯示了其它多實例節(jié)點。此時發(fā)現(xiàn)eureka-client在配置文件中只是向8761端口的server注冊,但在8762端口的服務器同樣可以看到eureka-client的注冊信息,可見peer1的注冊列表信息已經(jīng)同步到了peer2節(jié)點。

eureka-server-two.png

Eureka的一些概念

  • Register-服務注冊
    當Eureka Client向Eureka Server注冊時,client提供自身的元數(shù)據(jù),如IP地址、端口、運行狀態(tài)指標的Url、主頁地址等信息。
  • Renew-服務續(xù)約
    Eureka Client默認情況下每隔30秒發(fā)送一次心跳來進行服務續(xù)約。如果Eureka Server在90秒內沒有收到Eureka Client的心跳,Eureka Server會將Eureka Client實例從注冊列表中剔除。
  • Fetch Registries-獲取服務注冊列表信息
    Eureka Client從Eureka Server獲取服務注冊表信息,并緩存在本地,可根據(jù)注冊表信息查找服務,從而進行遠程調用。該注冊表信息每30秒更新一次。如果由于某種原因導致服務注冊列表信息不能及時匹配,Eureka Client會重新獲取整個注冊表信息。
  • Cancel-服務下線
    Eureka Client在程序關閉時可以向Eureka Server發(fā)送下線信息。發(fā)送請求后,該客戶端的實例信息將從Eureka Server的服務注冊列表中刪除。該下線請求不會自動完成,需要在程序關閉時調用以下代碼:
DiscoveryManager.getInstance().shutdownComponent();
  • Eviction-服務剔除
    默認情況下,當Eureka Client連續(xù)90秒沒有向Eureka Server發(fā)送服務續(xù)約(即心跳)時,Eureka Server會將該服務實例從服務注冊列表刪除。
  • Eureka的自我保護模式
    當一個新的Eureka Server出現(xiàn)時,會嘗試從相鄰的Peer節(jié)點獲取所有服務實例的注冊信息。如果從相鄰的Peer節(jié)點獲取信息時出現(xiàn)了故障,Eureka Server會嘗試其它的Peer節(jié)點。如果Eureka Server能夠成功獲取所有的服務實例信息,則根據(jù)配置信息設置服務續(xù)約的閾值。在任何時間,如果Eureka Server接收到的服務續(xù)約低于該配置的百分比(默認為15分鐘內低于85%),則服務器開啟自我保護模式,即不在剔除注冊列表的信息。這樣做的好處是如果是Eureka Server自身的網(wǎng)絡問題而導致Eureka Client無法續(xù)約,這樣在注冊列表中不會剔除注冊信息,這樣Eureka Client還可以被其他服務消費。同時這個功能也是有坑的,如果在服務較少時,服務由于意外情況掛掉后很容易閾值就低于85%,由于自我保護導致掛掉的服務在注冊列表中還是存在,但其它服務無法消費,這會迷惑開發(fā)人員排錯的方向。如果需要關閉該功能,在配置文件中添加如下代碼:
eureka:
    server:
        enable-self-preservation: false

源碼解析

接下來我們通過debug的形式,學習Eureka Client是如何進行注冊的。在工程的Maven的依賴包下,找到eureka-client-1.6.2.jar包,在com.netflix.discovery包下有一個DiscoverClient類,這個類是Eureka Client和Eureka Server交互的核心類。
啟動項目后首先會進入DiscoverClient的initScheduledTasks方法。在該方法中初始化并封裝了刷新服務注冊列表信息和發(fā)送心跳的定時任務。


eureka-init.png

在初始化定時任務后進入DiscoverClient的register()方法


eureka-register.png

具體進入EurekaHttpClient的register方法,可以看出通過http請求的Eureka Server路徑,以及攜帶的請求信息。
eureka-register2.png

同時來跟蹤Eureka Server端的代碼,Eureka server服務端請求入口為

ApplicationResource類,如下所示,可以看出Eureka Client是通過http post的方式去服務注冊。

server-rk.png

通過一系列的參數(shù)校驗。最后以以下方法進行注冊。

 registry.register(info, "true".equals(isReplication));

進入register方法,實際執(zhí)行的是PeerAwareInstanceRegistryImpl的register方法,該方法如下:

 @Override
    public void register(final InstanceInfo info, final boolean isReplication) {
        int leaseDuration = Lease.DEFAULT_DURATION_IN_SECS;
        if (info.getLeaseInfo() != null && info.getLeaseInfo().getDurationInSecs() > 0) {
            leaseDuration = info.getLeaseInfo().getDurationInSecs();
        }
       //調用父類的注冊方法
        super.register(info, leaseDuration, isReplication);
       // 注冊成功后同步Eureka中的服務注冊信息
        replicateToPeers(Action.Register, info.getAppName(), info.getId(), info, null, isReplication);
    }

進入父類的注冊方法,在AbstractInstanceRegistry類中可以看到Eureka真正的服務注冊實現(xiàn)的代碼。

 public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
        try {
            read.lock();
            Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
            REGISTER.increment(isReplication);
            if (gMap == null) {
                final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
                gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
                if (gMap == null) {
                    gMap = gNewMap;
                }
            }
            Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
            // Retain the last dirty timestamp without overwriting it, if there is already a lease
            if (existingLease != null && (existingLease.getHolder() != null)) {
                Long existingLastDirtyTimestamp = existingLease.getHolder().getLastDirtyTimestamp();
                Long registrationLastDirtyTimestamp = registrant.getLastDirtyTimestamp();
                logger.debug("Existing lease found (existing={}, provided={}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                if (existingLastDirtyTimestamp > registrationLastDirtyTimestamp) {
                    logger.warn("There is an existing lease and the existing lease's dirty timestamp {} is greater" +
                            " than the one that is being registered {}", existingLastDirtyTimestamp, registrationLastDirtyTimestamp);
                    logger.warn("Using the existing instanceInfo instead of the new instanceInfo as the registrant");
                    registrant = existingLease.getHolder();
                }
            } else {
                // The lease does not exist and hence it is a new registration
                synchronized (lock) {
                    if (this.expectedNumberOfRenewsPerMin > 0) {
                        // Since the client wants to cancel it, reduce the threshold
                        // (1
                        // for 30 seconds, 2 for a minute)
                        this.expectedNumberOfRenewsPerMin = this.expectedNumberOfRenewsPerMin + 2;
                        this.numberOfRenewsPerMinThreshold =
                                (int) (this.expectedNumberOfRenewsPerMin * serverConfig.getRenewalPercentThreshold());
                    }
                }
                logger.debug("No previous lease information found; it is new registration");
            }
            Lease<InstanceInfo> lease = new Lease<InstanceInfo>(registrant, leaseDuration);
            if (existingLease != null) {
                lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
            }
            gMap.put(registrant.getId(), lease);
            synchronized (recentRegisteredQueue) {
                recentRegisteredQueue.add(new Pair<Long, String>(
                        System.currentTimeMillis(),
                        registrant.getAppName() + "(" + registrant.getId() + ")"));
            }
            // This is where the initial state transfer of overridden status happens
            if (!InstanceStatus.UNKNOWN.equals(registrant.getOverriddenStatus())) {
                logger.debug("Found overridden status {} for instance {}. Checking to see if needs to be add to the "
                                + "overrides", registrant.getOverriddenStatus(), registrant.getId());
                if (!overriddenInstanceStatusMap.containsKey(registrant.getId())) {
                    logger.info("Not found overridden id {} and hence adding it", registrant.getId());
                    overriddenInstanceStatusMap.put(registrant.getId(), registrant.getOverriddenStatus());
                }
            }
            InstanceStatus overriddenStatusFromMap = overriddenInstanceStatusMap.get(registrant.getId());
            if (overriddenStatusFromMap != null) {
                logger.info("Storing overridden status {} from map", overriddenStatusFromMap);
                registrant.setOverriddenStatus(overriddenStatusFromMap);
            }

            // Set the status based on the overridden status rules
            InstanceStatus overriddenInstanceStatus = getOverriddenInstanceStatus(registrant, existingLease, isReplication);
            registrant.setStatusWithoutDirty(overriddenInstanceStatus);

            // If the lease is registered with UP status, set lease service up timestamp
            if (InstanceStatus.UP.equals(registrant.getStatus())) {
                lease.serviceUp();
            }
            registrant.setActionType(ActionType.ADDED);
            recentlyChangedQueue.add(new RecentlyChangedItem(lease));
            registrant.setLastUpdatedTimestamp();
            invalidateCache(registrant.getAppName(), registrant.getVIPAddress(), registrant.getSecureVipAddress());
            logger.info("Registered instance {}/{} with status {} (replication={})",
                    registrant.getAppName(), registrant.getId(), registrant.getStatus(), isReplication);
        } finally {
            read.unlock();
        }
    }

在該類中服務注冊信息其實就是存儲在一個 ConcurrentHashMap結構中。

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

流程總結:
Eureka Client:在DiscoveryClient先通過initScheduledTasks()方法封裝定時任務,然后調用register()方法通過http訪問服務器接口進行注冊。
Eureka Server:ApplicationResource類接收Http服務請求,調用PeerAwareInstanceRegistryImpl的register方法,PeerAwareInstanceRegistryImpl完成服務注冊后,調用replicateToPeers向其它Eureka Server節(jié)點(Peer)做狀態(tài)同步。

在以上只是對服務注冊進行了源碼跟蹤,感興趣的可以以同樣的方式對服務續(xù)約、服務剔除等進行源碼學習。

總結

在這一章節(jié)中,我們學習了Eureka的概念、架構。如何編寫Eureka Server和Eureka Client,并進行了Eureka Server高可用的演示。最后以服務注冊為列進行了源碼學習。在下一章學習如何使用RestTemplate和Ribbon結合作為服務消費者去消費服務。

PS:項目github地址:https://github.com/dzydzydzy/spring-cloud-example.git

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,443評論 6 532
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,530評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,407評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,981評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,759評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,204評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,263評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,415評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,955評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,650評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,892評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,675評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374