Spring Cloud的系列文章考阱,按理來說應(yīng)該從微服務(wù)的介紹開始,我的確是這樣做的粘姜。在開始本文之前鬓照,我還寫了一篇介紹微服務(wù)的文章,然而效果并不滿意孤紧,所以暫且不發(fā)豺裆,網(wǎng)上關(guān)于微服務(wù)概念的介紹很是泛濫,其中不乏優(yōu)質(zhì)文章号显,也無需我在其中濫竽充數(shù)臭猜。我希望在完成整個Spring Cloud系列文章后,再重頭寫這篇引文押蚤,我會在其中針對性的埋下雷點蔑歌,讓讀者能夠從解決問題的角度,來認識微服務(wù)和Spring Cloud提供的構(gòu)建揽碘、部署工具次屠,以求最大之成效。今天聊的是微服務(wù)的服務(wù)發(fā)現(xiàn)工具Eureka钾菊。
我認為在技術(shù)學(xué)習(xí)的過程中帅矗,一個不小的障礙來自我們對技術(shù)名詞預(yù)設(shè)的難度∩诽蹋“面向切面編程”浑此、“服務(wù)發(fā)現(xiàn)”可能就是這樣一類,聽上去挺復(fù)雜滞详,其實也就那么回事兒凛俱。
假設(shè)有這樣一個場景紊馏,我們在豆瓣上瀏覽電影信息,那么服務(wù)端電影基本信息蒲犬,電影豆瓣評分和豆瓣用戶信息由三個服務(wù)分別提供朱监。
可以想見,無論這個client是客戶端還是其它的service原叮,我們在coding的時候赫编,都需要將這三個服務(wù)的ip和port作為配置項記錄下來,然后在對應(yīng)的請求處調(diào)用》芰ィ現(xiàn)在看來不需要發(fā)現(xiàn)機制擂送,用配置文件管理就可以!
然而隨著項目進行唯欣,用戶量的增加嘹吨,為了保證產(chǎn)品的健壯,我們可能對于一些重要服務(wù)進行分布式部署境氢,比如movie info是咱們網(wǎng)站的主要提供內(nèi)容蟀拷,我們會將同一個movie info application部署到多個實例上,這樣一來萍聊,即便訪問量增加问芬,也可以保證每個實例的負載在可承受的范圍內(nèi),同時脐区,一旦其中一臺服務(wù)器宕機愈诚,整個系統(tǒng)仍然可以正常運轉(zhuǎn),此時的架構(gòu)如下:
這個時候會讓程序員為難牛隅,之前在配置文件中,對于movie info application服務(wù)直接用ip1:port1指代酌泰,現(xiàn)在一個服務(wù)由3個ip-port指代媒佣,代碼中如何體現(xiàn)呢?另外我們怎么知道某一時刻應(yīng)該訪問哪一個服務(wù)呢陵刹?當(dāng)然你可以用很tricky的方法默伍,比如將新增的服務(wù)地址一樣寫入配置文件,然后針對每一個請求都以輪詢的方式調(diào)用不同的配置地址衰琐。不難看出這種寫法擴展性差也糊,而且還有一個隱患,如果我們的服務(wù)是在云端羡宙,往往服務(wù)器的ip地址動態(tài)狸剃,因為產(chǎn)品擴容,發(fā)布失敗等原因狗热,所在服務(wù)器的ip地址會發(fā)生改變钞馁,類似以上方式虑省,將ip-port硬編碼在配置文件中,可能會導(dǎo)致不停修改配置文件的尷尬局面僧凰。
基于此我們希望有這樣一個中間件探颈,服務(wù)跑起來的時候,會主動去告訴中間件“我是誰训措,我在哪”伪节,中間件記錄該服務(wù),客戶端在請求的時候绩鸣,只要告訴中間件對應(yīng)服務(wù)的名字架馋,就會獲得該服務(wù)的真實路徑。這就是服務(wù)注冊和服務(wù)發(fā)現(xiàn)的過程:
下面認識一下本文的主角——Eureka /ju?ri?k?/
Eureka起初是Netflix(制作過紙牌屋全闷、絕命毒師)因為自身微服務(wù)項目孵化出來的開源產(chǎn)品叉寂,Spring將其融合進了Spring Cloud全家桶,因此你在Spring官網(wǎng)找相關(guān)資源時总珠,實際上它是在Spring Cloud Netflix項目當(dāng)中屏鳍。
在Eureka的官方描述當(dāng)中對其定義和架構(gòu)有所描述:Eureka是CS架構(gòu),其服務(wù)端是一個基于REST(具象狀態(tài)傳輸)的服務(wù)局服,主要用于AWS云中定位服務(wù)钓瞭,以實現(xiàn)中間層服務(wù)器的負載平衡和故障轉(zhuǎn)移;客戶端設(shè)有內(nèi)置的基礎(chǔ)輪詢式的負載均衡器淫奔。所以我們回復(fù)一下之前的實際問題山涡,服務(wù)消費者發(fā)起對movie-info-application的請求,該請求會到達Eureka服務(wù)發(fā)現(xiàn)唆迁,查詢注冊表鸭丛,將所有名叫movie-info-application的服務(wù)地址返回,再由Eureka Client中的負載均衡模塊經(jīng)過計算唐责,確定最終訪問哪一個地址并進行訪問鳞溉。
我們看一下Netflix自建的Eureka高級架構(gòu):
圖中的三個Eureka Server可以看作是一個Eureka集群(cluster),一個region(圖中的region為us-east-1鼠哥,這是aws里面的概念)會部署一個集群熟菲,每個zone(圖中zone為c、d朴恳、e)中最少部署一個Eureka Server抄罕。服務(wù)提供方(Application Service)和服務(wù)消費方(Application Client)都會集成Eureka Client。就服務(wù)提供方(圖中最左側(cè)的Application Service)而言于颖,每次項目啟動之后呆贿,都會向us-east-1c中的Eureka Server進行注冊,注冊成功后恍飘,不同的Eureka Sever之間會進行注冊表的拷貝榨崩,保證注冊表同步谴垫。服務(wù)提供方在默認情況下會每個30s向Eureka發(fā)一次Renew請求,告訴它自己還活著母蛛,避免自己從注冊表中被踢掉翩剪,也就是心跳檢測。如果Eureka在90s內(nèi)沒有收到某個服務(wù)的renew請求彩郊,則將其從注冊表中除名前弯。如果服務(wù)掛了,或者正常關(guān)閉秫逝,則服務(wù)提供方會發(fā)送cancel請求給Eureka告訴它刪除注冊表記錄恕出。另一方面,當(dāng)服務(wù)消費方需要調(diào)用服務(wù)時违帆,它會向與同一zone的Eureka發(fā)出get registry請求浙巫,獲取注冊表,而后由Eureka Client自帶的負載均衡模塊決定具體的服務(wù)器地址刷后,從而進行真正的服務(wù)請求(make remote call)的畴。
我們可以快速的構(gòu)建三個Eureka項目,其中一個作為Eureka Server尝胆,剩余為Eureka Client丧裁。
實踐步驟:
- 創(chuàng)建Eureka Server
- 通過Eureka Client在Server中注冊各個微服務(wù)
- 通過Eureka Client調(diào)用其它微服務(wù)
- 創(chuàng)建Eureka Server
-> 進入https://start.spring.io/選擇add dependency:Eureka Server
-> Download
Spring Initialzr
-> 配置Eureka Server application.properties
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defalt-zone=http://${eureka.instance.hostname}:${server.port}/eureka/
-> 在項目入口添加@EnableEurekaServer注解
@SpringBootApplication
@EnableEurekaServer
public class SpringcloudEurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudEurekaServerApplication.class, args);
}
}
此時我們直接運行項目,然后訪問默認的8080端口含衔,可以看到Eureka控制臺煎娇。可見當(dāng)前沒有任何注冊到Server的服務(wù)(No instances availavle)如果你留意一下此時的項目日志贪染,你會發(fā)現(xiàn)系統(tǒng)一直在報錯:
為什么會這樣缓呛!明明我們build的應(yīng)用就是eureka server,它怎么還在獲取all instance registry抑进!其實每個eureka server在默認情況下也是一個eureka client强经,還記得之前提到過,Netflix推薦使用eureka server集群么寺渗,它們相互之間互為eureka client,它們相互注冊兰迫,這樣一來即便一個server掛掉信殊,其它的Server也有它的注冊信息。在我們的demo中汁果,只有一個eureka server涡拘,它不需要跟任何其它的server進行replicate,配置application.properties
//通常情況据德,eureka server端口為8761
server.port=8761
eureka.instance.hostname=localhost
//作為client鳄乏,是否將自己注冊到Eureka Server跷车,默認為true
eureka.client.register-with-eureka=false
//作為client,是否從Eureka Server獲取注冊表信息橱野,默認為true
eureka.client.fetch-registry=false
通過配置告訴eureka server關(guān)閉client身份朽缴,僅作為獨立的server。
2.通過Eureka Client在Server中注冊各個微服務(wù)
在此之前水援,我們創(chuàng)建簡單的3個application密强,它們對應(yīng)的端口分別是本地的8082、8083和8084蜗元。
創(chuàng)建三個service:movie-catalog-service(8082)或渤,movie-info-service(8083),ratings-data-service(8084)奕扣。其中catalog會去調(diào)用info和data取一些數(shù)據(jù)薪鹦,業(yè)務(wù)邏輯不重要,關(guān)鍵在于以服務(wù)發(fā)現(xiàn)的形式調(diào)用惯豆。
- movie-info-service
application.properties
//設(shè)置該應(yīng)用的名稱池磁,該名稱會展示在Eureka控制臺中,也是作為該服務(wù)在注冊表中的key值(value為ip:port)
spring.application.name=movie-info-service
server.port=8083
MovieResource.java
@RestController
@RequestMapping("/movies")
public class MovieResource {
@RequestMapping("/{movieId}")
public Movie getMovieInfo(@PathVariable("movieId") String movieId) {
//定義一個Movie.java這里不贅述
return new Movie("1", "東邪西毒", "很好的一步電影");
}
}
-
ratings-data-service
application.properties
spring.application.name=ratings-data-service
server.port=8083
RatingsResource.java
@RestController
@RequestMapping("/ratingsdata")
public class RatingsResource {
@RequestMapping("/user/{userId}")
public UserRating getUserRatings(@PathVariable("userId") String userId) {
UserRating userRating = new UserRating();
userRating.setUserId(userId);
//為了方便hard code循帐。UserRating.java不贅述
userRating.setRating = 80框仔;
return userRating;
}
}
-
movie-catalog-service
application.properties
spring.application.name=movie-catalog-service
server.port=8082
MovieCatalogServiceApplication.java
@SpringBootApplication
@EnableDiscoveryClient
public class MovieCatalogServiceApplication {
public static void main(String[] args) {
SpringApplication.run(MovieCatalogServiceApplication.class, args);
}
@LoadBalanced //加上該注解后,restTemplate才會將application-name作為key拄养,去注冊中心查找离斩,否則application-name會被當(dāng)作域名而無法訪問。
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
CatalogResource.java
@RestController
@RequestMapping("/catalog")
public class CatalogResource {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/{userId}")
public List<CatalogItem> getCatalog(@PathVariable("userId") String userId) {
//直接指定application name
UserRating userRating = restTemplate.getForObject("http://RATINGS-DATA-SERVICE/ratingsdata/user/" + userId, UserRating.class);
return userRating.getRatings().stream()
.map(rating -> {
Movie movie = restTemplate.getForObject("http://MOVIE-INFO-SERVICE/movies/" + rating.getMovieId(), Movie.class);
return new CatalogItem(movie.getName(), movie.getDescription(), rating.getRating());
})
.collect(Collectors.toList());
}
}
幾個model的為代碼:CatalogItem Movie Rating UserRating
public class CatalogItem {
private String name;
private String desc;
private int rating;
}
public class Movie {
private String movieId;
private String name;
private String description;
}
public class Rating {
private String movieId;
private int rating;
}
public class UserRating {
private String userId;
private int rating;
}
整個過程如下圖所示:
這里需要說明的是如果同一個服務(wù)被分布式的存儲在多個服務(wù)器上瘪匿,舉個例子movie-info-application擠在192.168.0.113:8083上存在跛梗,也在192.168.0.124:8083存在,那么根據(jù)key在服務(wù)注冊服務(wù)器返回得到的是包含113和114的地址列表棋弥,這個列表返回給movie-catalog-service核偿,由ribbon進行負載均衡,從中選則一個ip作為請求發(fā)送的ip顽染。
以上是Eureka的基本介紹漾岳,下一節(jié)繼續(xù)Spring Cloud——斷路器!