(git上的源碼:https://gitee.com/rain7564/spring_microservices_study/tree/master/second-discovery-euraka)
何為服務(wù)發(fā)現(xiàn)
在許多分布式系統(tǒng)架構(gòu)中,都需要去獲取機(jī)器的物理地址(微服務(wù)實(shí)例部署的服務(wù)器地址及端口)。這一認(rèn)知在分布式系統(tǒng)架構(gòu)開始的時候就已經(jīng)存在,而等到分布式計(jì)算出現(xiàn)的時候,被正式稱為服務(wù)發(fā)現(xiàn)(service discovery)。
服務(wù)發(fā)現(xiàn)可以做一些簡單的事情,比如維護(hù)一個帶有所有遠(yuǎn)程服務(wù)地址的屬性文件,或一個UDDI(Universal Description, Discovery and Integration)存儲庫。
服務(wù)發(fā)現(xiàn)對微服務(wù)、分布式應(yīng)用/基于云的應(yīng)用至關(guān)重要。有兩個主要原因:
- 它為應(yīng)用程序開發(fā)團(tuán)隊(duì)提供了快速擴(kuò)展的能力,以及縮減在一個環(huán)境中運(yùn)行的服務(wù)實(shí)例數(shù)量。
通過服務(wù)發(fā)現(xiàn),服務(wù)消費(fèi)者(service consumers)可以從服務(wù)提供者(service provider)的物理地址中抽象出來。因?yàn)橄M(fèi)者并不知道提供者的實(shí)例的實(shí)際物理地址(多個實(shí)例就有多個地址),新的服務(wù)實(shí)例也可以添加到"可用服務(wù)池",失效的服務(wù)也會被移除,即消費(fèi)者根本沒辦法具體知道會消費(fèi)服務(wù)提供者哪個的實(shí)例,服務(wù)提供者實(shí)例的物理地址對消費(fèi)者來說是透明的。
以往,龐大的單體應(yīng)用要擴(kuò)大業(yè)務(wù)處理能力,只能通過將應(yīng)用部署到更大更好的機(jī)器上(縱向擴(kuò)展),這種辦法取得的效果與成本的比值越來越低,而且也無法無限擴(kuò)展;而服務(wù)發(fā)現(xiàn)能讓開發(fā)團(tuán)隊(duì)從這一困境走出,因?yàn)榉?wù)發(fā)現(xiàn)使將服務(wù)部署到更多的廉價機(jī)器上(橫向擴(kuò)展)成為可能。 - 服務(wù)發(fā)現(xiàn)使整體應(yīng)用更有彈性。當(dāng)機(jī)器出現(xiàn)異常使正在運(yùn)行的服務(wù)實(shí)例處理能力變?nèi)?頻繁報錯)或不可用,服務(wù)發(fā)現(xiàn)會將該實(shí)例從可用服務(wù)列表中移除,外部或應(yīng)用內(nèi)的其他微服務(wù)不會再"消費(fèi)"這個服務(wù)實(shí)例。這樣,因意外導(dǎo)致部分服務(wù)實(shí)例不可用,服務(wù)發(fā)現(xiàn)會將對該服務(wù)的"消費(fèi)"路由到其他可用的服務(wù)實(shí)例,繞過了已衰掉的實(shí)例,該服務(wù)還是能正常被"消費(fèi)",從而將影響降到了最低。
服務(wù)發(fā)現(xiàn)的實(shí)現(xiàn)方案
我們已經(jīng)初步了解服務(wù)發(fā)現(xiàn)帶來的好處。那么有沒有好的方案來實(shí)現(xiàn)"服務(wù)發(fā)現(xiàn)"?例如DNS或負(fù)載均衡器(此處指服務(wù)端負(fù)載均衡,還有客戶端負(fù)載均衡。注意:后文分析DNS+負(fù)載均衡模式,都是指服務(wù)端負(fù)載均衡)。
開發(fā)過程中,經(jīng)常會有一種情況——從第三方獲取資源,在獲取的時候就必須定位這些資源所在的物理地址。若不是基于云架構(gòu)的應(yīng)用,大都采用DNS和負(fù)載均衡器實(shí)現(xiàn)。大體架構(gòu)如下圖所示:

應(yīng)用調(diào)用第三方服務(wù)過程中,通過DNS將域名解析得到一個商業(yè)負(fù)載均衡器的物理地址,然后將請求轉(zhuǎn)發(fā)到給負(fù)載均衡器,負(fù)載均衡器維護(hù)了一張路由表,然后根據(jù)這張路由表將請求路由給正確的服務(wù)。
為了獲得高可用,會有一個處于空閑狀態(tài)的次負(fù)載均衡器一直發(fā)送ping請求來確認(rèn)主負(fù)載均衡器是否正常運(yùn)行,若已經(jīng)衰掉,次負(fù)載均衡器會被激活然后接管主負(fù)載均衡器。
然而這種解決方案并不適合基于云的微服務(wù)應(yīng)用,原因如下:
- 單點(diǎn)故障:負(fù)載均衡會成為整體架構(gòu)的一個單點(diǎn)故障。首先,如果負(fù)載均衡器衰掉,那么所有依賴負(fù)載均衡器的服務(wù)都會受到牽連。雖然主次負(fù)載均衡器能實(shí)現(xiàn)高可用,但當(dāng)請求劇增時,會成為整體架構(gòu)的性能瓶頸,單位時間內(nèi)能處理的請求數(shù)有限。
- 限制水平擴(kuò)展能力:通過負(fù)載均衡器將服務(wù)集中到單個集群,限制了負(fù)載均衡架構(gòu)水平擴(kuò)展的能力。大多數(shù)商業(yè)負(fù)載平衡器使用冗余的熱交換模型,所以只有一臺服務(wù)器來處理負(fù)載,而故障轉(zhuǎn)移的次負(fù)載平衡器只有在停機(jī)的情況下才成為主負(fù)載均衡器。
- 靜態(tài)托管:許多商業(yè)負(fù)載均衡器并不支持為服務(wù)快速注冊/注銷。他們將路由規(guī)則存儲在一個集中式數(shù)據(jù)庫中,而且添加新路由規(guī)則通常需要通過供應(yīng)商提供的專有API。
- 復(fù)雜:因?yàn)樨?fù)載均衡器只是服務(wù)的一個代理,服務(wù)消費(fèi)者的請求必須通過它才能定位到服務(wù)提供者的物理地址。整個應(yīng)用架構(gòu)因?yàn)槎嗔素?fù)載均衡這一轉(zhuǎn)換層變得更復(fù)雜,因?yàn)樗新酚梢?guī)則都是通過人工定義和發(fā)布的。
以上四個原因并不是想說明服務(wù)端負(fù)載均衡架構(gòu)不好,而是為了說明不適合基于云的微服務(wù)應(yīng)用。因?yàn)榉?wù)端負(fù)載均衡能很好的作用于應(yīng)用的大小、規(guī)模能通過集中式網(wǎng)絡(luò)架構(gòu)支撐的情況。
然而,基于云的應(yīng)用中,需要處理大量的事務(wù)和數(shù)據(jù)信息,集中式網(wǎng)絡(luò)架構(gòu)并不能很好支撐這種情況,因?yàn)檫@種架構(gòu)擴(kuò)展能力弱,擴(kuò)展成本效益低。下面開始介紹如何實(shí)現(xiàn)一個健壯的擴(kuò)展性強(qiáng)的適用于云微服務(wù)應(yīng)用的服務(wù)發(fā)現(xiàn)。
適用于云的服務(wù)發(fā)現(xiàn)
基于云的微服務(wù)環(huán)境的服務(wù)發(fā)現(xiàn)解決方案,必須具備以下特征:
- 高可用:服務(wù)發(fā)現(xiàn)需要具備支持"熱"集群環(huán)境的能力。即服務(wù)的信息可以在服務(wù)發(fā)現(xiàn)集群的多個節(jié)點(diǎn)中共享。當(dāng)集群中的某個節(jié)點(diǎn)不可用,其它節(jié)點(diǎn)可以完全接管。
- 所有節(jié)點(diǎn)對等:集群中的所有節(jié)點(diǎn)共享所有注冊到服務(wù)發(fā)現(xiàn)的服務(wù)實(shí)例的狀態(tài)信息。
- 負(fù)載均衡:服務(wù)發(fā)現(xiàn)需要動態(tài)地、均衡地將請求分配到所有注冊到服務(wù)發(fā)現(xiàn)的服務(wù)實(shí)例。當(dāng)然,服務(wù)發(fā)現(xiàn)可以根據(jù)不同的分配策略進(jìn)行分配,比如輪詢、隨機(jī)等。服務(wù)發(fā)現(xiàn)取代了類似上文提及的靜態(tài)的、需要人工配置的負(fù)載均衡器。
- 彈性:服務(wù)發(fā)現(xiàn)的客戶端(服務(wù)發(fā)現(xiàn)分服務(wù)端和客戶端,后文會詳細(xì)說明)應(yīng)該在本地緩存服務(wù)實(shí)例信息。這樣做有很多好處,比如:不用每次遠(yuǎn)程請求都去服務(wù)發(fā)現(xiàn)的服務(wù)端獲取目標(biāo)服務(wù)的信息、當(dāng)服務(wù)發(fā)現(xiàn)服務(wù)端衰掉了,短時間內(nèi)還可以依賴本地緩存繼續(xù)正常運(yùn)行等。
- 容錯:服務(wù)發(fā)現(xiàn)需要及時發(fā)現(xiàn)那些不可用的服務(wù)實(shí)例并將其從可用服務(wù)列表中移除。即可以自動發(fā)現(xiàn)并處理而不用認(rèn)為干預(yù)。
至此,應(yīng)該對適用于云的服務(wù)發(fā)現(xiàn)有一定的了解,接下來將會對如下幾個方面進(jìn)行分析:
- 基于云的服務(wù)發(fā)現(xiàn)代理是如何運(yùn)作
- 當(dāng)服務(wù)發(fā)現(xiàn)代理不可用時,客戶端負(fù)載均衡是如何能繼續(xù)運(yùn)作
- 如何使用Spring Cloud和Netflix Eureka實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)代理
服務(wù)發(fā)現(xiàn)架構(gòu)
在討論服務(wù)發(fā)現(xiàn)架構(gòu)之前,需要了解四個概念,這四個概念在所有服務(wù)發(fā)現(xiàn)架構(gòu)都會涉及到的,如下:
- 服務(wù)注冊:服務(wù)是怎樣把自己注冊到服務(wù)發(fā)現(xiàn)代理?
- 客戶端獲取服務(wù)地址:客戶端是怎樣從服務(wù)發(fā)現(xiàn)代理獲取其他同樣注冊到服務(wù)發(fā)現(xiàn)代理的服務(wù)的信息?
- 信息共享:服務(wù)發(fā)現(xiàn)代理集群之間是如何共享服務(wù)注冊信息?
- 健康監(jiān)控:服務(wù)客戶端是怎樣通知服務(wù)發(fā)現(xiàn)代理自己的狀態(tài)信息?
下圖中序號1-4分別對應(yīng)上面所說的四個概念。

當(dāng)服務(wù)啟動后,它們會將自己注冊到一個或多個服務(wù)發(fā)現(xiàn)代理,即將物理地址、端口等信息發(fā)送。雖然某個服務(wù)的所有實(shí)例有不同的IP和端口號,但它們都會注冊在同一個服務(wù)ID(上文已經(jīng)提到的邏輯名稱,application name)下,這個服務(wù)ID只是被服務(wù)發(fā)現(xiàn)代理用來給不同服務(wù)分組而已,即相同服務(wù)的不同實(shí)例,它們的服務(wù)ID是相同的。
一般情況下,一個服務(wù)只需要注冊到一個服務(wù)發(fā)現(xiàn)代理。因?yàn)榇蠖喾?wù)發(fā)現(xiàn)實(shí)現(xiàn)都采用數(shù)據(jù)共享模型,即一個服務(wù)發(fā)現(xiàn)集群的所有節(jié)點(diǎn)會共享它們維護(hù)的數(shù)據(jù)。
當(dāng)一個服務(wù)實(shí)例注冊到服務(wù)發(fā)現(xiàn)代理,就意味著它隨時可以被其他應(yīng)用或服務(wù)調(diào)用,注冊到服務(wù)發(fā)現(xiàn)的所有服務(wù)之間的調(diào)用是相互的??蛻舳朔?wù)在"發(fā)現(xiàn)"服務(wù)上有多種模式。比如:客戶端服務(wù)的每次遠(yuǎn)程調(diào)用,都依靠服務(wù)發(fā)現(xiàn)引擎去解析得到目標(biāo)服務(wù)的地址。這種模式(服務(wù)端服務(wù)發(fā)現(xiàn),如DNS+負(fù)載均衡器)是非常脆弱的,因?yàn)榭蛻舳朔?wù)的每一個遠(yuǎn)程調(diào)用完全依賴于服務(wù)發(fā)現(xiàn)引擎,所以需要有一個更好、更健壯的模式——客戶端負(fù)載均衡。下圖說明了何為客戶端負(fù)載均衡:

另外,當(dāng)服務(wù)調(diào)用失敗,客戶端會讓本地緩存失效,然后重新從服務(wù)發(fā)現(xiàn)代理獲取新的服務(wù)注冊信息。
使用Netflix Eureka實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)
接下來,我們會創(chuàng)建一個服務(wù)發(fā)現(xiàn)代理并向該代理注冊兩個服務(wù)。然后其中的一個服務(wù)使用從服務(wù)發(fā)現(xiàn)代理獲取的服務(wù)信息去調(diào)用另一個服務(wù)。Netflix Eureka的服務(wù)發(fā)現(xiàn)引擎可以實(shí)現(xiàn)服務(wù)發(fā)現(xiàn),而客戶端負(fù)載均衡的實(shí)現(xiàn)則使用Netflix的Ribbon庫。
當(dāng)然,Spring Cloud為實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)提供了多種解決方案。下文也會分析各自的優(yōu)劣勢。
在上一節(jié),我們已經(jīng)創(chuàng)建了一個微服務(wù)應(yīng)用,該應(yīng)用中只有l(wèi)icense一個微服務(wù)?,F(xiàn)在我們會在現(xiàn)有應(yīng)用的基礎(chǔ)上,再創(chuàng)建一個organization服務(wù)和一個服務(wù)發(fā)現(xiàn)代理,并將license服務(wù)和organization服務(wù)注冊到服務(wù)發(fā)現(xiàn)代理。
假設(shè)當(dāng)license服務(wù)被調(diào)用,它會去調(diào)用organization服務(wù),organization服務(wù)再根據(jù)organization ID獲取對應(yīng)的organization信息并返回。在從organization服務(wù)獲取數(shù)據(jù)前,必須先知道organization服務(wù)可用的實(shí)例信息,這些信息是organization服務(wù)啟動的時候就注冊到服務(wù)發(fā)現(xiàn)代理并由它維護(hù)的。而license服務(wù)在調(diào)用organization服務(wù)時,會從本地獲取organization服務(wù)實(shí)例的信息。下圖說明了這個過程:

在上一節(jié)創(chuàng)建的應(yīng)用的基礎(chǔ)上,再創(chuàng)建一個organization微服務(wù),由于創(chuàng)建步驟與創(chuàng)建license服務(wù)時大同小異,這里就不貼代碼出來,需要的可以去git上查看。(由于需要將license服務(wù)和organization服務(wù)注冊到Eureka上,實(shí)現(xiàn)這一功能的代碼會在下文貼出)
接下來我們開始進(jìn)入本節(jié)的重頭戲,服務(wù)發(fā)現(xiàn)代理的搭建。
搭建Spring Eureka服務(wù)
我們會使用Spring Boot搭建Eureka服務(wù),因?yàn)榉?wù)發(fā)現(xiàn)本質(zhì)上也是一個微服務(wù)。
- pom文件
首先,把注意力放在pom.xml文件上,代碼如下:
eureka服務(wù):
<?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>cn.study.microservice</groupId>
<artifactId>discovery-eureka</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>discovery-eureka</name>
<description></description>
<parent>
<groupId>cn.study.microservice</groupId>
<artifactId>second-discovery-euraka</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka-server</artifactId>
</dependency>
</dependencies>
</project>
可以看到,pom文件中的依賴很少,只有一個"spring-cloud-starter-eureka-server",這是因?yàn)镾pring Cloud的Eureka server啟動依賴,其中包含多個必須的jar包,如"spring-cloud-netflix-eureka-server"、"spring-cloud-starter-ribbon"等。
同樣的,因?yàn)閘icense服務(wù)和organization服務(wù)會注冊到Eureka上,所以對應(yīng)的pom文件也需要作出修改,如下:
license服務(wù):
<?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>cn.study.microservice</groupId>
<artifactId>license-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>license-service</name>
<description></description>
<parent>
<groupId>cn.study.microservice</groupId>
<artifactId>second-discovery-euraka</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<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.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
</project>
organization服務(wù):
<?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>cn.study.microservice</groupId>
<artifactId>organization-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>organization-service</name>
<description></description>
<parent>
<groupId>cn.study.microservice</groupId>
<artifactId>second-discovery-euraka</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<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-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
</dependencies>
</project>
上面兩個pom文件中,可以看到都引入了一個與eureka相關(guān)的依賴——"spring-cloud-starter-eureka",與服務(wù)發(fā)現(xiàn)引入的依賴——"spring-cloud-starter-eureka-server"略有不同,多了一個"-server",那么不同在哪里呢?可以打開idea提供的"Maven Project"視圖,打開方法如下:

打開后可以看到類似如下圖所示:

可以看到,"spring-cloud-starter-eureka-server"會引入"spring-cloud-netflix-eureka-server",而"spring-cloud-starter-eureka"會引入"spring-cloud-netflix-eureka-client",分別代表eureka服務(wù)端和客戶端。
另外,license、organization兩個服務(wù)還添加"spring-boot-starter-data-jpa"、"com.h2database.h2"。即數(shù)據(jù)庫使用的是h2,因?yàn)橐_保教程的代碼能開箱即用,如果使用mysql等其他開源數(shù)據(jù)庫,讀者還要去新建數(shù)據(jù)庫,如果數(shù)據(jù)庫賬戶密碼不同,還需要去改配置文件;數(shù)據(jù)訪問層框架使用的是"spring data jpa","spring-boot-starter-data-jpa"是spring boot提供的啟動依賴,里邊包含spring data jpa和hibernate-core等jar包,若之前未了解過spring data jpa,建議先去熟悉下。
最后,license服務(wù)pom文件中還多出了一個依賴:"spring-cloud-starter-feign"。該依賴是spring cloud提供的啟動依賴,主要包含了Netflix的一個開源項(xiàng)目feign項(xiàng)目的jar包,feign主要功能是對上文提及的實(shí)現(xiàn)了客戶端負(fù)載均衡的Ribbon的封裝,下文會詳細(xì)說明。
- 配置文件
eureka服務(wù)的application.yml配置文件:
#默認(rèn)端口號為8761
server:
port: 8761
eureka:
client:
#由于該應(yīng)用為注冊中心,所以設(shè)置為false,代表不向注冊中心注冊自己
registerWithEureka: false
#由于注冊中心的職責(zé)就是維護(hù)服務(wù)實(shí)例,它并不需要去檢索服務(wù),所以也設(shè)置為false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
#關(guān)閉自我保護(hù)模式。自我保護(hù)模式是指,出現(xiàn)網(wǎng)絡(luò)分區(qū)、eureka在短時間內(nèi)丟失過多客戶端時,會進(jìn)入自我保護(hù)模式。
#自我保護(hù):一個服務(wù)長時間沒有發(fā)送心跳包,eureka也不會將其刪除,默認(rèn)為true。
enable-self-preservation: false
#在Eureka服務(wù)器獲取不到集群里對等服務(wù)器上的實(shí)例時,需要等待的時間,單位為毫秒,默認(rèn)為1000 * 60 * 5
wait-time-in-ms-when-sync-empty: 0
license服務(wù)的application.yml配置文件:
server:
port: 10000
#服務(wù)發(fā)現(xiàn)客戶端
eureka:
instance:
#向Eureka注冊時,是否使用IP地址+端口號作為服務(wù)實(shí)例的唯一標(biāo)識。推薦設(shè)置為true
prefer-ip-address: true
#============分界線================
#以下為基本不需要配置的屬性,屬性的值為默認(rèn)值
#服務(wù)續(xù)約的調(diào)用時間間隔,默認(rèn)30秒
lease-renewal-interval-in-seconds: 30
#服務(wù)失效的時間,默認(rèn)90秒
lease-expiration-duration-in-seconds: 90
#非安全的通信端口號
non-secure-port: 80
#安全的通信端口號
secure-port: 443
#是否啟用非安全的通信端口號
non-secure-port-enabled: true
#是否啟用安全的通信端口號
secure-port-enabled: false
client:
#是否將自身的實(shí)例信息注冊到Eureka服務(wù)端
register-with-eureka: true
#是否拉取并緩存其他服務(wù)注冊表副本到本地
fetch-registry: true
#注冊到哪個Eureka服務(wù)實(shí)例
service-url:
defaultZone: http://localhost:8761/eureka/
#============分界線================
#以下為基本不需要配置的屬性,屬性的值為默認(rèn)值
#更新其他服務(wù)注冊表時間間隔,默認(rèn)30秒
registry-fetch-interval-seconds: 30
#更新實(shí)例信息的變化到Eureka服務(wù)端的間隔時間,單位為秒
instance-info-replication-interval-seconds: 30
#初始化實(shí)例信息到Eureka服務(wù)端的間隔時間,單位為秒
initial-instance-info-replication-interval-seconds: 40
#輪詢Eureka服務(wù)端地址更改的間隔時間,單位為秒。
#當(dāng)我們與Sping Cloud Config配合,動態(tài)刷新Eureka的service url地址時需要關(guān)注該參數(shù)
eureka-service-url-poll-interval-seconds: 300
#讀取Eureka Server信息的超時時間,單位為秒
eureka-server-read-timeout-seconds: 8
#連接Eureka Server的超時時間,單位為秒
eureka-server-connect-timeout-seconds: 5
#從Eureka客戶端到所有Eureka服務(wù)端的連接總數(shù)
eureka-server-total-connections: 200
#從Eureka客戶端到每個Eureka服務(wù)端主機(jī)的連接總數(shù)
eureka-server-total-connections-per-host: 50
#Eureka服務(wù)連接的空閑關(guān)閉時間,單位為秒
eureka-connection-idle-timeout-seconds: 30
#心跳連接池的初始化線程數(shù)
heartbeat-executor-thread-pool-size: 2
#心跳超時重試延遲時間的最大乘數(shù)值
heartbeat-executor-exponential-back-off-bound: 10
#緩存刷新線程池的初始化線程數(shù)
cache-refresh-executor-thread-pool-size: 2
#緩存刷新重試延遲時間的最大乘數(shù)值
cache-refresh-executor-exponential-back-off-bound: 10
#使用DNS來獲取Eureka服務(wù)端的service url
use-dns-for-fetching-service-urls: false
#是否優(yōu)先使用處于相同Zone的Eureka服務(wù)端
perfer-same-zone-eureka: true
#獲取實(shí)例時是否過濾,僅保留UP狀態(tài)的實(shí)例
filter-only-up-instances: true
#數(shù)據(jù)源的配置
spring:
datasource:
platform: h2
schema: classpath:schema.sql
data: classpath:data.sql
driver-class-name: org.h2.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: none
organization服務(wù)的application.yml配置文件:
server:
port: 11000
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
datasource:
platform: h2
schema: classpath:schema.sql
data: classpath:data.sql
driver-class-name: org.h2.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: none
每一個注冊到eureka的服務(wù)實(shí)例,eureka中都有兩個屬性與它關(guān)聯(lián)起來,分別為:application ID、instance ID。application ID是用來標(biāo)識某一個服務(wù)的實(shí)例集合,即同一個服務(wù)的不同實(shí)例,它們的application ID都是相同的;基于Spring Boot的微服務(wù),可以通過設(shè)置屬性spring.application.name來定義application ID(一般在bootstrap.yml中配置),上面的配置,license服務(wù)的application ID為licensingservice,organization服務(wù)的為organizationservice;instance ID默認(rèn)是一個隨機(jī)數(shù),用來定位服務(wù)的實(shí)例集合中某一實(shí)例,通常會配置eureka.instance.instance-id,如${spring.cloud.client.ipAddress}:${server.port},上面給出的配置并沒有配置該屬性,讀者可自行配置。
下面介紹幾個主要的屬性的作用:
屬性eureka.instance.preferIpAddress:會通知eureka使用服務(wù)實(shí)例的IP地址進(jìn)行注冊,而不是它的主機(jī)名(hostname)。一般情況下,該屬性會被設(shè)置為true,其中一個原因是:基于云的微服務(wù)的生存周期很短暫且是無狀態(tài)的,這些微服務(wù)可以隨意啟動和關(guān)閉。因此使用IP地址更適合這種微服務(wù)。
屬性eureka.client.registerWithEureka:代表該服務(wù)是否將自己注冊到eureka中;屬性eureka.client.fetchRegistry表明該服務(wù)是否從eureka服務(wù)端獲取其他服務(wù)的注冊信息到本地,將該屬性設(shè)置為true,就不用在每一次調(diào)用其他服務(wù)的接口時都要去eureka獲取目標(biāo)服務(wù)的信息。另外,若eureka.client.fetchRegistry設(shè)置為true,服務(wù)每隔30s會從eureka刷新服務(wù)信息到本地。
屬性eureka.client.serviceUrl.defaultZone:代表服務(wù)會注冊到哪個eureka服務(wù)端實(shí)例。實(shí)現(xiàn)eureka的高可用需要關(guān)注這個屬性,這里不過多說明,需要了解更多關(guān)于eureka高可用,請自行Google或百度。
配置文件中的大多數(shù)屬性的作用都已在文件中給出注釋,就不過多贅述了。
另外license、organization兩個服務(wù)還多出一個bootstrap.yml文件,該文件主要配置微服務(wù)的application.name和profiles.active,該文件會在application.yml之前被加載。
- 啟動類
eureka服務(wù):
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
觀察上面的代碼,可以看出相比普通的微服務(wù)啟動類,只多出了一個注解——@EnableEurekaServer,該注解表明該微服務(wù)會成為一個Eureka服務(wù),即服務(wù)發(fā)現(xiàn)的服務(wù)端。
啟動該服務(wù),可以看到控制臺會打印如下字符串:
Started Eureka Server
Tomcat started on port(s): 8761 (http)
表明eureka服務(wù)已成功啟動,并且在端口8761啟動,之前在配置文件中配置的server.port: 8761已經(jīng)起作用了。
接著在瀏覽器訪問: http://localhost:8761/,如下圖所示:

上圖圈中的區(qū)域是一個注冊到該eureka服務(wù)實(shí)例的所有服務(wù)實(shí)例列表,因?yàn)榇藭r尚未有任何服務(wù)注冊,所以列表為空。
organization服務(wù):
//該注解表明該類是項(xiàng)目(微服務(wù))的啟動類
@SpringBootApplication
@EnableEurekaClient
public class Application {
//運(yùn)行該方法,會啟動整個Spring Boot服務(wù)
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
觀察上面organization服務(wù)啟動類的代碼,與eureka服務(wù)的相比,@EnableEurekaServer注解變成了@EnableEurekaClient,使用該注解的服務(wù),會在服務(wù)啟動的時候?qū)⒆约鹤缘絜ureka服務(wù)端。如果查看@EnableEurekaClient的源碼,如下:
@EnableDiscoveryClient
public @interface EnableEurekaClient {
}
可以看到,該注解其實(shí)是Netflix Eureka對注解@EnableDiscoveryClient的封裝,注解@EnableDiscoveryClient可以說是服務(wù)可以使用DiscoveryClient和Ribbon庫的觸發(fā)器,并將自己注冊到服務(wù)發(fā)現(xiàn)代理。所以你也可以選擇使用@EnableDiscoveryClient代替@EnableEurekaClient。
啟動organization服務(wù),可以看到控制臺有類似下圖的輸出:

此時若再次訪問http://localhost:8761/,可以看到類似如下圖的界面:

若出現(xiàn)的界面與上圖類似,說明到此為止應(yīng)用的搭建還算順利??梢钥吹缴蠄D的注冊到eureka服務(wù)的所有服務(wù)實(shí)例列表不為空,多出了一行,這一行列出的信息代表有一個application Id為"ORGANIZATIONSERVICE",instance ID為"10.10.1.216:organizationservice:11000"的服務(wù)實(shí)例注冊到eureka,"UP(1)"則代表該服務(wù)只有一個實(shí)例正在運(yùn)行。
license服務(wù):
此時先不考慮license服務(wù)會消費(fèi)organization服務(wù),所以啟動類的代碼跟organization服務(wù)的相同,這里就不貼出來。
啟動license服務(wù),然后刷新http://localhost:8761/,可以看到向eureka服務(wù)注冊的服務(wù)列表中多出了一行,說明剛剛啟動的license服務(wù)也注冊成功。

使用服務(wù)發(fā)現(xiàn)
接下來我們會通過三種方式來實(shí)現(xiàn)服務(wù)的發(fā)現(xiàn)。分別如下:
- Spring Discovery client
- Netflix Ribbon client
- Netflix Feign client
首先,把license和organization服務(wù)關(guān)掉,eureka無所謂;然后把organization服務(wù)的其他相關(guān)代碼加上,如controller包、repository包、service包,詳細(xì)代碼請參考git上的代碼。我們把重點(diǎn)放在license服務(wù)代碼編寫上。下面開始進(jìn)入正題。
(License和Organization實(shí)體類請讀者自行加上)
- Spring Discovery client
首先創(chuàng)建一個服務(wù)發(fā)現(xiàn)客戶端類——OrganizationDiscoveryClient,路徑:cn.study.microservice.license.client.OrganizationDiscoveryClient,如下:
@Component
public class OrganizationDiscoveryClient {
//當(dāng)服務(wù)啟動類加上@EnableEurekaClient或@EnableDiscoveryClient注解后,會自動注入實(shí)現(xiàn)DiscoveryClient接口的對象。此處會注入EurekaDiscoveryClient對象
@Autowired
private DiscoveryClient discoveryClient;
public Organization getOrganization(String organizationId) {
RestTemplate restTemplate = new RestTemplate();
//會從eureka服務(wù)端獲取organizationservice服務(wù)的所有實(shí)例集合
List<ServiceInstance> instances = discoveryClient.getInstances("organizationservice");
if (instances.size()==0) return null;
String serviceUri = String.format("%s/v1/organizations/%s",instances.get(0).getUri().toString(), organizationId);
System.out.println("!!!! SERVICE URI: " + serviceUri);
ResponseEntity< Organization > restExchange =
restTemplate.exchange(
serviceUri,
HttpMethod.GET,
null, Organization.class, organizationId);
return restExchange.getBody();
}
}
上面代碼中,涉及到一個類:RestTemplate,該類是Spring用于客戶端http同步訪問的主要類。具體用法,請讀者自行了解。
接著,創(chuàng)建cn.study.microservice.license.service.LicenseService,如下:
@Service
public class LicenseService {
@Autowired
private LicenseRepository licenseRepository;
@Autowired
private OrganizationDiscoveryClient organizationDiscoveryClient;
private Organization retrieveOrgInfo(String organizationId, String clientType){
Organization organization = null;
switch (clientType) {
case "discovery":
System.out.println("I am using the discovery client");
organization = organizationDiscoveryClient.getOrganization(organizationId);
break;
default:
organization = organizationDiscoveryClient.getOrganization(organizationId);
}
return organization;
}
public License getLicense(String organizationId, String licenseId, String clientType) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
Organization org = retrieveOrgInfo(organizationId, clientType);
return license
.withOrganizationName( org.getName())
.withContactName( org.getContactName())
.withContactEmail( org.getContactEmail() )
.withContactPhone( org.getContactPhone() )
.withComment("");
}
}
最后,編寫controller類,如下:
@RestController
@RequestMapping(value="v1/organizations/{organizationId}/licenses")
public class LicenseServiceController {
@Autowired
private LicenseService licenseService;
@RequestMapping(value="/{licenseId}/{clientType}",method = RequestMethod.GET)
public License getLicensesWithClient( @PathVariable("organizationId") String organizationId,
@PathVariable("licenseId") String licenseId,
@PathVariable("clientType") String clientType) {
return licenseService.getLicense(organizationId,licenseId, clientType);
}
}
到此,代碼編寫完畢,啟動服務(wù),然后使用postman訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/discovery,結(jié)果如下:

可以看到,返回的結(jié)果是licenseId為"f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a"的License對象的json字符串,其中"organizationName"、"contactName"、"contactPhone"和"contactEmail"都是根據(jù)organizationId訪問organization服務(wù)提供的接口:/v1/organizations/{organizationId},然后返回一個Organization對象,最后將對象中的信息賦值到License對象中。實(shí)現(xiàn)這一功能的核心代碼如下:
...
ResponseEntity< Organization > restExchange =
restTemplate.exchange(
serviceUri,
HttpMethod.GET,
null, Organization.class, organizationId);
return restExchange.getBody();
...
exchange方法的方法簽名為:
public <T> ResponseEntity<T> exchange(
String url, //需要訪問的資源的url
HttpMethod method, //HTTPMethod枚舉,分別代表http定義的各種安全方法,如:GET、POST、PUT、DELETE等。
HttpEntity<?> requestEntity, //請求實(shí)體,即請求攜帶的數(shù)據(jù)。為空時,賦null
Class<T> responseType, //返回對象的Class類型
Object... uriVariables //url路徑上變量對應(yīng)的值
)
此處,可以看成是license服務(wù)調(diào)用了organization服務(wù)的接口。該接口在organization服務(wù)的OrganizationServiceController類中被定義。讀者可以自行驗(yàn)證,在控制臺會答應(yīng)出變量"serviceUri"的值:http://10.10.1.216:11000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a,請讀者將"10.10.1.216"換成自己的IP或"localhost",然后用postman訪問。
至此,第一種實(shí)現(xiàn)服務(wù)發(fā)現(xiàn)方式完成。原理是:使用DiscoveryClient從服務(wù)端獲取對應(yīng)服務(wù)的所有實(shí)例列表,然后取其中的一個實(shí)例,并獲取該實(shí)例的物理地址,最后調(diào)用該服務(wù)的接口。看到這里,讀者應(yīng)該對服務(wù)發(fā)現(xiàn)有更深的理解。license服務(wù)在調(diào)用organization服務(wù)的接口時,沒有使用硬編碼也行不通,因?yàn)樵诖a編寫階段,根本不知道服務(wù)會被部署到哪個服務(wù)器的哪個端口,而通過服務(wù)發(fā)現(xiàn),則可以動態(tài)地獲取目標(biāo)服務(wù)實(shí)例的物理地址(前提是目標(biāo)服務(wù)也注冊到eureka),進(jìn)而調(diào)用該服務(wù)的接口。
- Netflix Ribbon client
在編寫服務(wù)發(fā)現(xiàn)客戶端類前,先修改啟動類,添加一下代碼:
//該注解告知Spring Cloud創(chuàng)建一個基于Ribbon的RestTemplate,才可以實(shí)現(xiàn)客戶端負(fù)載均衡
@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
接著,編寫服務(wù)發(fā)現(xiàn)客戶端類——OrganizationRestTemplateClient,如下:
@Component
public class OrganizationRestTemplateClient {
@Autowired
RestTemplate restTemplate;
public Organization getOrganization(String organizationId){
ResponseEntity<Organization> restExchange =
restTemplate.exchange(
"http://organizationservice/v1/organizations/{organizationId}",
HttpMethod.GET,
null, Organization.class, organizationId);
return restExchange.getBody();
}
}
然后,修改LicenseService類,注入OrganizationRestTemplateClient、修改方法retrieveOrgInfo,如下:
...
@Autowired
private OrganizationRestTemplateClient organizationRestClient;
...
switch (clientType) {
case "discovery":
System.out.println("I am using the discovery client");
organization = organizationDiscoveryClient.getOrganization(organizationId);
break;
case "rest":
System.out.println("I am using the rest client");
organization = organizationRestClient.getOrganization(organizationId);
break;
default:
organization = organizationDiscoveryClient.getOrganization(organizationId);
}
...
至此,代碼修改完畢,啟動服務(wù)。使用postman調(diào)用:http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest。正確返回后,可以發(fā)現(xiàn)返回結(jié)果與第一種方式返回的結(jié)果相同。接下來,開始分析第二種方式是如何內(nèi)部工作的:
查看服務(wù)發(fā)現(xiàn)客戶端類OrganizationRestTemplateClient,可以發(fā)現(xiàn)同樣是使用RestTemplate的exchange方法來調(diào)用organization服務(wù)的接口,唯一不同的是方法的第一個參數(shù)值,這里硬編碼為:"http://organizationservice/v1/organizations/{organizationId}"。為什么這里可以這樣直接寫死在程序里,而且是使用"organizationservice"代替常規(guī)的服務(wù)實(shí)例物理地址。還記得在啟動類中注入的RestTemplate嗎,我們在注入的時候另外加了一個注解@LoadBalanced,答案就在這里。
加了注解@LoadBalanced后,代表在注入RestTemplate時,Ribbon會對其進(jìn)行加工,加工后的RestTemplate的能力有:
- 在調(diào)用其他服務(wù)的接口時,會從訪問的url中截取得到目標(biāo)服務(wù)的application ID,此處為"organizationservice",然后根據(jù)該服務(wù)獲取對應(yīng)的實(shí)例,最后正常訪問;
- 實(shí)現(xiàn)客戶端負(fù)載均衡
讀者可以自行驗(yàn)證,把注解去掉,然后重啟服務(wù),再訪問一次,結(jié)果肯定是報錯。如下:
{
"timestamp": 1509695097081,
"status": 500,
"error": "Internal Server Error",
"exception": "org.springframework.web.client.ResourceAccessException",
"message": "I/O error on GET request for \"http://organizationservice/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a\": organizationservice; nested exception is java.net.UnknownHostException: organizationservice",
"path": "/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest"
}
可以看出,報的錯是未知主機(jī)名:organizationservice。如果將"organizationservice"換成"localhost:11000"就能正常訪問。
最后,我們來對可能是大家最關(guān)心的問題進(jìn)行驗(yàn)證,客戶端負(fù)載均衡。
- 打包項(xiàng)目
打開idea的Maven Projects,如下:

- 啟動多個organization服務(wù)
使用快捷鍵組合:Alt + F12,打開終端控制臺,然后進(jìn)入organization項(xiàng)目的target目錄,最后使用命令:
java -jar organization-service-0.0.1-SNAPSHOT.jar --server.port=11000
執(zhí)行該命令后,會在端口11000啟動一個organization服務(wù)。
接著按照同樣的方式在不同端口啟動一個或多個服務(wù)。首先點(diǎn)擊終端控制臺左上角的"+"圖標(biāo),創(chuàng)建一個新的終端,然后進(jìn)入target目錄,使用命令:
java -jar organization-service-0.0.1-SNAPSHOT.jar --server.port=11001
啟動多個organization服務(wù)后,多次訪問:http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/rest
觀察兩個終端控制臺,會出現(xiàn)如下圖所示的輸出:


由上圖可知客戶端負(fù)載均衡驗(yàn)證成功。license服務(wù)多次調(diào)用organization服務(wù)的接口,會隨機(jī)的訪問organization服務(wù)的某一實(shí)例,從而避免頻繁訪問某一個實(shí)例,達(dá)到負(fù)載均衡的目的。
使用Netflix Ribbon client可以很方便地實(shí)現(xiàn)對其它服務(wù)接口的調(diào)用,但每個接口的調(diào)用都要寫類似OrganizationRestTemplateClient.getOrganization方法的代碼,雖然不多,但有沒有更方便、簡介的實(shí)現(xiàn)方法呢?下面開始講如何使用"Netflix Feign client"實(shí)現(xiàn)。
- Netflix Feign client
在編寫服務(wù)發(fā)現(xiàn)客戶端類前,向啟動類添加一個類注解:@EnableFeignClients。

接著,編寫服務(wù)發(fā)現(xiàn)客戶端類——OrganizationFeignClient,其實(shí)是一個接口:
@FeignClient("organizationservice")
public interface OrganizationFeignClient {
@RequestMapping(
method= RequestMethod.GET,
value="/v1/organizations/{organizationId}",
consumes="application/json")
Organization getOrganization(@PathVariable("organizationId") String organizationId);
}
最后修改,LicenseService類,注入OrganizationRestTemplateClient、修改方法retrieveOrgInfo。如下:
...
@Autowired
OrganizationFeignClient organizationFeignClient;
...
switch (clientType) {
case "discovery":
System.out.println("I am using the discovery client");
organization = organizationDiscoveryClient.getOrganization(organizationId);
break;
case "rest":
System.out.println("I am using the rest client");
organization = organizationRestClient.getOrganization(organizationId);
break;
case "feign":
System.out.println("I am using the feign client");
organization = organizationFeignClient.getOrganization(organizationId);
break;
default:
organization = organizationDiscoveryClient.getOrganization(organizationId);
}
...
代碼修改完成,重啟license服務(wù)。然后訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/feign。返回的結(jié)果與之前兩種方式一樣。
下面分析接口OrganizationFeignClient:
可以看到該接口上方添加了一個注解——FeignClient,注解的value值為:organizationservice,看到這里,應(yīng)該能猜出organizationservice代表的是需要調(diào)用的接口所屬的服務(wù)的application ID,而該接口的作用是:用于通知Feign組件對該接口進(jìn)行代理,而不需要編寫任何邏輯代碼。在服務(wù)啟動時,F(xiàn)eign會掃描標(biāo)有@FeignClient注解的接口,生成代理,并注冊到Spring容器中,因此可通過@Autowired注入。
接著就是接口中的方法的編寫??梢钥吹椒椒╣etOrganization添加了一個@RequestMapping注解,注解的value值就是目標(biāo)服務(wù)的接口。如果與第二種方式調(diào)用RestTemplate.exchange方法做對比,服務(wù)application ID+@RequestMapping的value值與exchange的第一個參數(shù)url對應(yīng);@RequestMapping的method與第二個 參數(shù)對應(yīng);接口方法的返回值與第四個參數(shù)對應(yīng);接口方法的所有帶@PathVariable注解的參數(shù)合并成一個數(shù)組與第五個參數(shù)對應(yīng);至于第三個參數(shù),由于該接口的請求體為空,所以在方法getOrganization的簽名中沒體現(xiàn)出來。
假設(shè)啟動多個organization服務(wù)實(shí)例,那么多次訪問http://localhost:10000/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a/feign,也會負(fù)載均衡, 其負(fù)載均衡的默認(rèn)實(shí)現(xiàn)是基于 Netflix Ribbon。
完!