微服務(wù)架構(gòu)—不可或缺的注冊(cè)中心
從今天開(kāi)始,我們將以Java后端技術(shù)為切入點(diǎn),深入探討微服務(wù)架構(gòu)。本章的重點(diǎn)將聚焦于微服務(wù)中最關(guān)鍵的環(huán)節(jié)之一:服務(wù)發(fā)現(xiàn)與注冊(cè)。文章將循序漸進(jìn),由淺入深,逐步引領(lǐng)你進(jìn)入微服務(wù)的廣闊世界。不論你是技術(shù)新手還是經(jīng)驗(yàn)豐富的專(zhuān)家,我都希望通過(guò)這篇文章,能夠?yàn)槟闾峁┆?dú)特而有價(jià)值的見(jiàn)解與收獲。
好的,我們開(kāi)始!
單體架構(gòu)vs微服務(wù)架構(gòu)
單體架構(gòu)
首先,我們來(lái)看看以前的單體架構(gòu)。一個(gè)歸檔包(例如WAR格式)通常包含了應(yīng)用程序的所有功能和邏輯,這種結(jié)構(gòu)使得我們將其稱(chēng)為單體應(yīng)用。單體應(yīng)用的設(shè)計(jì)理念強(qiáng)調(diào)將所有功能模塊打包成一個(gè)整體,便于部署和管理。這種架構(gòu)模式被稱(chēng)為單體應(yīng)用架構(gòu),意指通過(guò)一個(gè)單一的WAR包來(lái)承載整個(gè)應(yīng)用的所有責(zé)任和功能。
圖片
正如我們所展示的這張簡(jiǎn)單示例圖所示,我們可以更深入地分析單體架構(gòu)的優(yōu)缺點(diǎn),以便全面理解其在軟件開(kāi)發(fā)和系統(tǒng)設(shè)計(jì)中的影響。
微服務(wù)架構(gòu)
微服務(wù)的核心理念是將傳統(tǒng)的單體應(yīng)用程序根據(jù)業(yè)務(wù)需求進(jìn)行拆分,將其分解為多個(gè)獨(dú)立的服務(wù),從而實(shí)現(xiàn)徹底的解耦。每個(gè)微服務(wù)專(zhuān)注于特定的功能或業(yè)務(wù)邏輯,遵循“一個(gè)服務(wù)只做一件事”的原則,類(lèi)似于操作系統(tǒng)中的進(jìn)程。這樣的設(shè)計(jì)使得每個(gè)服務(wù)都可以獨(dú)立部署,甚至可以擁有自己的數(shù)據(jù)庫(kù),從而提高了系統(tǒng)的靈活性和可維護(hù)性。
圖片
通過(guò)這種方式,各個(gè)小服務(wù)相互獨(dú)立,能夠更有效地應(yīng)對(duì)業(yè)務(wù)變化,快速迭代開(kāi)發(fā)和發(fā)布,同時(shí)降低了系統(tǒng)整體的復(fù)雜性,這就是微服務(wù)架構(gòu)的本質(zhì)。當(dāng)然,微服務(wù)架構(gòu)同樣存在其優(yōu)缺點(diǎn),因?yàn)闆](méi)有任何一種“銀彈”能夠完美解決所有問(wèn)題。接下來(lái),讓我們深入分析一下這些優(yōu)缺點(diǎn):
優(yōu)點(diǎn)
- 服務(wù)小而內(nèi)聚:微服務(wù)將應(yīng)用拆分為多個(gè)獨(dú)立服務(wù),每個(gè)服務(wù)專(zhuān)注于特定功能,使得系統(tǒng)更具靈活性和可維護(hù)性。與傳統(tǒng)單體應(yīng)用相比,修改幾行代碼往往需要了解整個(gè)系統(tǒng)的架構(gòu)和邏輯,而微服務(wù)架構(gòu)則允許開(kāi)發(fā)人員僅專(zhuān)注于相關(guān)的功能,提升了開(kāi)發(fā)效率。
- 簡(jiǎn)化開(kāi)發(fā)過(guò)程:不同團(tuán)隊(duì)可以并行開(kāi)發(fā)和部署各自負(fù)責(zé)的服務(wù),這提高了開(kāi)發(fā)效率和發(fā)布頻率。
- 按需伸縮:微服務(wù)的松耦合特性允許根據(jù)業(yè)務(wù)需求對(duì)各個(gè)服務(wù)進(jìn)行獨(dú)立擴(kuò)展和部署,便于根據(jù)流量變化動(dòng)態(tài)調(diào)整資源,優(yōu)化性能。
- 前后端分離:作為Java開(kāi)發(fā)人員,我們可以專(zhuān)注于后端接口的安全性和性能,而不必關(guān)注前端的用戶交互體驗(yàn)。
- 容錯(cuò)性:某個(gè)服務(wù)的失敗不會(huì)影響整個(gè)系統(tǒng)的可用性,提高了系統(tǒng)的可靠性。
缺點(diǎn)
- 運(yùn)維復(fù)雜性增加:管理多個(gè)服務(wù)增加了運(yùn)維的復(fù)雜性,而不僅僅是一個(gè)WAR包,這大大增加了運(yùn)維人員的工作量,涉及的技術(shù)棧(如Kubernetes、Docker、Jenkins等)也更為復(fù)雜。
- 通信成本:服務(wù)之間的相互調(diào)用需要網(wǎng)絡(luò)通信,可能導(dǎo)致延遲和性能問(wèn)題。
- 數(shù)據(jù)一致性挑戰(zhàn):分布式系統(tǒng)中,維護(hù)數(shù)據(jù)一致性和處理分布式事務(wù)變得更加困難。
- 性能監(jiān)控與問(wèn)題定位:需要更多的監(jiān)控工具和策略來(lái)跟蹤各個(gè)服務(wù)的性能,問(wèn)題排查變得復(fù)雜。
應(yīng)用場(chǎng)景
所以微服務(wù)也并不是適合所有項(xiàng)目。他只適合部分場(chǎng)景這里列舉一些典型案例:
- 大型復(fù)雜項(xiàng)目:微服務(wù)架構(gòu)通過(guò)將系統(tǒng)拆分為多個(gè)小型服務(wù),降低了每個(gè)服務(wù)的復(fù)雜性,使得團(tuán)隊(duì)能夠更加專(zhuān)注于各自負(fù)責(zé)的功能模塊,從而顯著提升開(kāi)發(fā)和維護(hù)的效率。
- 快速迭代項(xiàng)目:微服務(wù)架構(gòu)能夠使得不同團(tuán)隊(duì)獨(dú)立開(kāi)發(fā)和發(fā)布各自的服務(wù),從而實(shí)現(xiàn)更高頻率的迭代和更快的市場(chǎng)反應(yīng)。
- 并發(fā)高的項(xiàng)目:微服務(wù)架構(gòu)則提供了靈活的彈性伸縮能力,各個(gè)服務(wù)可以根據(jù)需求獨(dú)立擴(kuò)展,確保系統(tǒng)在高并發(fā)情況下依然能保持良好的性能和穩(wěn)定性。
好的,關(guān)于微服務(wù)的基本概念我們已經(jīng)介紹完畢。接下來(lái),我們將深入探討微服務(wù)架構(gòu)中至關(guān)重要的一環(huán):服務(wù)注冊(cè)與發(fā)現(xiàn)。這一部分是微服務(wù)生態(tài)系統(tǒng)的核心,直接影響到系統(tǒng)的靈活性和可擴(kuò)展性。
注冊(cè)中心
從上面的討論中,我們可以看到,微服務(wù)架構(gòu)的核心在于將各個(gè)模塊獨(dú)立分開(kāi),以實(shí)現(xiàn)更好的靈活性和可維護(hù)性。然而,這種模塊化設(shè)計(jì)也帶來(lái)了網(wǎng)絡(luò)傳輸上的消耗,因此,理解微服務(wù)之間是如何進(jìn)行網(wǎng)絡(luò)調(diào)用的變得尤為重要。
接下來(lái),我們將逐步探討微服務(wù)之間的通信方式,以及這些方式如何影響系統(tǒng)的整體性能。
調(diào)用方式
讓我們先思考一個(gè)關(guān)鍵問(wèn)題:在微服務(wù)架構(gòu)中,如何有效地維護(hù)復(fù)雜的調(diào)用關(guān)系,以確保各個(gè)服務(wù)之間的協(xié)調(diào)與通信順暢?
如果你對(duì)微服務(wù)還不太熟悉,不妨換個(gè)角度考慮:我們的電腦是如何實(shí)現(xiàn)對(duì)其他網(wǎng)站的調(diào)用和訪問(wèn)的?
固定調(diào)用
我們最簡(jiǎn)單的做法是將 IP 地址或域名硬編碼在我們的代碼中,以便直接進(jìn)行調(diào)用。例如,考慮以下這段代碼示例:
//1:服務(wù)之間通過(guò)RestTemplate調(diào)用,url寫(xiě)死
String url = "http://localhost:8020/order/findOrderByUserId/"+id;
User result = restTemplate.getForObject(url,User.class);
//2:類(lèi)似還有其他http工具調(diào)用
String url = "http://localhost:8020/order/findOrderByUserId/" + id;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(url)
.build();
try (Response response = client.newCall(request).execute()) {
String jsonResponse = response.body().string();
// 處理 jsonResponse 對(duì)象。省略代碼
從表面上看,雖然將 IP 地址或域名硬編碼在代碼中似乎是一個(gè)簡(jiǎn)單的解決方案,但實(shí)際上這并不是一個(gè)明智的做法。就像我們?cè)谠L問(wèn)百度搜索時(shí),不會(huì)在瀏覽器中輸入其 IP 地址,而是使用更為便捷和易記的域名。微服務(wù)之間的通信同樣如此,每個(gè)微服務(wù)都有自己獨(dú)特的服務(wù)名稱(chēng)。
在這里,域名服務(wù)器的作用非常關(guān)鍵,它負(fù)責(zé)存儲(chǔ)域名與 IP 地址的對(duì)應(yīng)關(guān)系,從而使我們能夠準(zhǔn)確地調(diào)用相應(yīng)的服務(wù)器進(jìn)行請(qǐng)求和響應(yīng)。微服務(wù)架構(gòu)中也存在類(lèi)似的機(jī)制,這就是我們所說(shuō)的“服務(wù)發(fā)現(xiàn)與注冊(cè)中心”??梢韵胂?,這個(gè)注冊(cè)中心就像是微服務(wù)的“域名服務(wù)器”,它存儲(chǔ)了各個(gè)微服務(wù)的名稱(chēng)和它們的網(wǎng)絡(luò)位置。
圖片
在配置域名時(shí),我們需要在 DNS 記錄中填寫(xiě)各種信息;而在微服務(wù)的注冊(cè)中心中,類(lèi)似的配置工作也同樣重要,只是通常是在配置文件中完成。當(dāng)你的服務(wù)啟動(dòng)時(shí),它會(huì)自動(dòng)向注冊(cè)中心注冊(cè)自己的信息,確保其他服務(wù)能夠找到并調(diào)用它。
"域名"調(diào)用
因此,當(dāng)我們進(jìn)行服務(wù)調(diào)用時(shí),整個(gè)過(guò)程將變得更加熟悉和直觀。例如,考慮下面這段代碼示例:
//使用微服務(wù)名發(fā)起調(diào)用
String url = "http://mall‐order/order/findOrderByUserId/"+id;
List<Order> orderList = restTemplate.getForObject(url, List.class);
當(dāng)然,這其中涉及許多需要細(xì)致實(shí)現(xiàn)的技術(shù)細(xì)節(jié),但我們?cè)诔醪嚼斫鈺r(shí),可以先關(guān)注服務(wù)發(fā)現(xiàn)與注冊(cè)中心的核心功能。簡(jiǎn)而言之,它們的主要目的是為了方便微服務(wù)之間的調(diào)用,減少開(kāi)發(fā)者在服務(wù)通信時(shí)所需處理的復(fù)雜性。
通過(guò)引入服務(wù)發(fā)現(xiàn)與注冊(cè)中心,我們不再需要手動(dòng)維護(hù)大量的 IP 地址與服務(wù)名稱(chēng)之間的關(guān)系。
設(shè)計(jì)思路
作為注冊(cè)中心,它的主要功能是有效維護(hù)各個(gè)微服務(wù)的信息,例如它們的IP地址(當(dāng)然,這些地址可以是內(nèi)網(wǎng)的)。鑒于注冊(cè)中心本身也是一個(gè)服務(wù),因此在微服務(wù)架構(gòu)中,它可以被視為一個(gè)重要的組件。每個(gè)微服務(wù)在進(jìn)行注冊(cè)和發(fā)現(xiàn)之前,都必須進(jìn)行適當(dāng)?shù)呐渲?,才能確保它們能夠相互識(shí)別和通信。
這就類(lèi)似于在本地配置一個(gè)DNS服務(wù)器,如果沒(méi)有這樣的配置,我們就無(wú)法通過(guò)域名找到相應(yīng)的IP地址,進(jìn)而無(wú)法進(jìn)行有效的網(wǎng)絡(luò)通信。
圖片
在這個(gè)系統(tǒng)中,健康監(jiān)測(cè)扮演著至關(guān)重要的角色,其主要目的在于確??蛻舳四軌蚣皶r(shí)獲知服務(wù)器的狀態(tài),尤其是在服務(wù)器發(fā)生故障時(shí),盡管這種監(jiān)測(cè)無(wú)法做到完全實(shí)時(shí)。健康監(jiān)測(cè)的重要性在于,我們的微服務(wù)架構(gòu)中,每個(gè)模塊通常會(huì)啟動(dòng)多個(gè)實(shí)例。盡管這些實(shí)例的功能相同,目的在于分擔(dān)請(qǐng)求負(fù)載,但它們的可用性卻可能有所不同。
圖片
例如,同一個(gè)服務(wù)名稱(chēng)可能會(huì)對(duì)應(yīng)多個(gè)IP地址。然而,如果其中某個(gè)IP對(duì)應(yīng)的服務(wù)出現(xiàn)故障,客戶端就不應(yīng)該再?lài)L試調(diào)用這個(gè)服務(wù)的IP。相反,應(yīng)該優(yōu)先選擇其他可用的IP,這樣就能夠有效實(shí)現(xiàn)高可用性。
接下來(lái)談?wù)勜?fù)載均衡。在這里需要注意的是,每個(gè)服務(wù)節(jié)點(diǎn)僅將其IP地址注冊(cè)到注冊(cè)中心,而注冊(cè)中心本身并不負(fù)責(zé)具體調(diào)用哪個(gè)IP。這一切都完全取決于客戶端的設(shè)計(jì)和實(shí)現(xiàn)。因此,在之前討論域名調(diào)用的部分中提到,這里面的細(xì)節(jié)實(shí)際上還有很多。
注冊(cè)中心的角色相對(duì)簡(jiǎn)單,它的主要職責(zé)是收集和維護(hù)可用的IP地址,并將這些信息提供給客戶端。具體的實(shí)現(xiàn)細(xì)節(jié)和操作流程,可以參考下面的圖片
圖片
實(shí)戰(zhàn)
這樣一來(lái),關(guān)于系統(tǒng)架構(gòu)的各個(gè)方面,我們基本上都已經(jīng)有了全面的了解。接下來(lái),我們可以直接進(jìn)入實(shí)踐環(huán)節(jié),進(jìn)行具體的使用演示。在這里,我們將以Spring Cloud Alibaba為例,選擇Nacos作為我們的服務(wù)發(fā)現(xiàn)與注冊(cè)中心。
準(zhǔn)備工作
JDK:這是開(kāi)發(fā)必備的基礎(chǔ)環(huán)境。
Maven:仍然會(huì)用maven進(jìn)行項(xiàng)目的依賴(lài)管理。并啟動(dòng)Springboot項(xiàng)目。
Nacos Server:你需要自己搭建好一個(gè)nacos服務(wù)端。
Nacos Docker 快速開(kāi)始
如果你本身沒(méi)有nacos,我建議你可以在本地通過(guò)Docker快速搭建一個(gè)Nacos實(shí)例。具體步驟可以參考官方文檔中的快速入門(mén)指南:Nacos Quick Start with Docker。
通過(guò)這種方式,你可以在最短的時(shí)間內(nèi)搭建起一個(gè)穩(wěn)定的Nacos服務(wù)。
windows 本地
當(dāng)然,你也可以選擇在本地直接搭建Nacos服務(wù)。按照以下步驟進(jìn)行操作,這里就以此為例進(jìn)行說(shuō)明。首先下載:https://github.com/alibaba/nacos/releases
然后本地直接解壓后運(yùn)行命令即可成功,如下:
startup.cmd -m standalone
圖片
打開(kāi)本地地址:http://127.0.0.1:8848/nacos/index.html
圖片
Spring Boot 啟動(dòng)
那么現(xiàn)在,我們可以直接開(kāi)始啟動(dòng)本地的兩個(gè)服務(wù):一個(gè)是用戶模塊,另一個(gè)是訂單模塊。此外,我們還將創(chuàng)建一個(gè)公共模塊,以便于共享通用的功能和資源。為了簡(jiǎn)化演示,我們將編寫(xiě)最基本的代碼,主要目的是為學(xué)習(xí)和演示提供一個(gè)清晰的框架。我們的項(xiàng)目結(jié)構(gòu)如圖所示:
圖片
首先,公共模塊的主要職責(zé)是導(dǎo)入所有服務(wù)共享的依賴(lài),這樣可以確保各個(gè)模塊之間的一致性和復(fù)用性。這里就不演示了。我們只看下order和user模塊的依賴(lài)。他倆其實(shí)是一樣的,目的就是讓自己的服務(wù)注冊(cè)到中心去。
<dependencies>
<dependency>
<groupId>com.xiaoyu.mall</groupId>
<artifactId>mall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<!-- nacos服務(wù)注冊(cè)與發(fā)現(xiàn) -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
請(qǐng)?zhí)砑右恍┍匾呐渲梦募畔?,下面的?nèi)容相對(duì)簡(jiǎn)單。不過(guò),每個(gè)服務(wù)都需要獨(dú)立指定一個(gè)微服務(wù)名稱(chēng),這里僅提供一個(gè)示例供參考。
server:
port: 8040
spring:
application:
name: mall-user #微服務(wù)名稱(chēng)
#配置nacos注冊(cè)中心地址
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: 9f545878-ca6b-478d-8a5a-5321d58b3ca3
命名空間
如果不特別配置命名空間(namespace),則系統(tǒng)會(huì)默認(rèn)將資源部署在公共空間(public)中。在這種情況下,如果需要使用其他命名空間,用戶必須自行創(chuàng)建一個(gè)新的命名空間。例如:
圖片
好的,現(xiàn)在我們來(lái)啟動(dòng)這兩個(gè)服務(wù),看看運(yùn)行效果。這樣一來(lái),兩個(gè)服務(wù)都成功注冊(cè)了。不過(guò)需要特別注意的是,如果希望這兩個(gè)服務(wù)能夠相互通信,務(wù)必將它們部署在同一個(gè)命名空間下。
圖片
我們也可以查看每個(gè)服務(wù)的詳細(xì)信息,這些信息包含了豐富的內(nèi)容。
圖片
示例代碼
此時(shí),我們并沒(méi)有集成任何其他工具,而只是單獨(dú)將 Nacos 的 Maven 依賴(lài)集成到我們的項(xiàng)目中。在這個(gè)階段,我們已經(jīng)可以通過(guò)注解的方式來(lái)使服務(wù)名稱(chēng)生效,這樣就無(wú)需在代碼中硬編碼 IP 地址。接下來(lái),我們來(lái)看看配置類(lèi)的具體代碼如下:
@Bean
@LoadBalanced //mall-order => ip:port
public RestTemplate restTemplate() {
return new RestTemplate();
}
然后,我們可以將用戶端的業(yè)務(wù)代碼編寫(xiě)得更加簡(jiǎn)潔明了,如下所示:
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
log.info("根據(jù)userId:"+id+"查詢(xún)訂單信息");
// ribbon實(shí)現(xiàn),restTemplate需要添加@LoadBalanced注解
// mall-order ip:port
String url = "http://mall-order/order/findOrderByUserId/"+id;
R result = restTemplate.getForObject(url,R.class);
return result;
}
我們的訂單端業(yè)務(wù)代碼相對(duì)簡(jiǎn)單,呈現(xiàn)方式如下:
@RequestMapping("/findOrderByUserId/{userId}")
public R findOrderByUserId(@PathVariable("userId") Integer userId) {
log.info("根據(jù)userId:"+userId+"查詢(xún)訂單信息");
List<OrderEntity> orderEntities = orderService.listByUserId(userId);
return R.ok().put("orders", orderEntities);
}
我們來(lái)看下調(diào)用情況,以確認(rèn)是否確實(shí)能夠?qū)崿F(xiàn)預(yù)期的效果。
圖片
第三方組件OpenFeign
在單體架構(gòu)中,你會(huì)直接使用 RestTemplate 類(lèi)來(lái)調(diào)用自身的其他服務(wù)?顯然是不可能的,因此,在這種情況下,借助流行的第三方組件 OpenFeign 可以顯著簡(jiǎn)化服務(wù)之間的交互。OpenFeign 提供了一種聲明式的方式來(lái)定義 HTTP 客戶端,使得我們可以更方便地進(jìn)行服務(wù)調(diào)用,同時(shí)保持代碼的可讀性和可維護(hù)性。
首先,我們需要在項(xiàng)目的 pom.xml 文件中添加相應(yīng)的 Maven 依賴(lài)。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
初次之外,還需要加一個(gè)注解在啟動(dòng)類(lèi)上:
@SpringBootApplication
@EnableFeignClients //掃描和注冊(cè)feign客戶端bean定義
public class MallUserFeignDemoApplication {、
public static void main(String[] args) {
SpringApplication.run(MallUserFeignDemoApplication.class, args);
}
}
以前寫(xiě)ip地址那里換成類(lèi)的時(shí)候,我們需要單獨(dú)定義一下服務(wù)類(lèi):
@FeignClient(value = "mall-order",path = "/order")
public interface OrderFeignService {
@RequestMapping("/findOrderByUserId/{userId}")
R findOrderByUserId(@PathVariable("userId") Integer userId);
}
這樣一來(lái),我們?cè)谡{(diào)用服務(wù)時(shí)就可以采用更加簡(jiǎn)潔和直觀的寫(xiě)法。是不是覺(jué)得這種方式使用起來(lái)更加舒服?
@Autowired
OrderFeignService orderFeignService;
@RequestMapping(value = "/findOrderByUserId/{id}")
public R findOrderByUserId(@PathVariable("id") Integer id) {
//feign調(diào)用
R result = orderFeignService.findOrderByUserId(id);
return result;
}
同樣可以正常調(diào)用成功。
圖片
不過(guò),在實(shí)施過(guò)程中還有一些需要注意的細(xì)節(jié)。許多開(kāi)發(fā)者傾向于將這些調(diào)用封裝到一個(gè)單獨(dú)的微服務(wù)模塊——即 api-service,并將其作為子項(xiàng)目依賴(lài)于當(dāng)前的微服務(wù)。這種做法能夠有效地將外部 API 調(diào)用與內(nèi)部服務(wù)邏輯進(jìn)行區(qū)分,避免將不同類(lèi)型的功能混雜在同一個(gè)包中。看下:
圖片
好的,到此為止,我們已經(jīng)完成了一個(gè)完整的調(diào)用流程。這一切的設(shè)置和配置為我們后續(xù)的開(kāi)發(fā)奠定了堅(jiān)實(shí)的基礎(chǔ)。接下來(lái),我們就可以專(zhuān)注于實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯,比如數(shù)據(jù)庫(kù)的調(diào)用與存儲(chǔ)操作。
學(xué)習(xí)進(jìn)階
接下來(lái)我們將深入探討相關(guān)內(nèi)容。由于許多細(xì)節(jié)尚未詳盡講解,之前的實(shí)戰(zhàn)環(huán)節(jié)主要旨在讓大家對(duì)服務(wù)注冊(cè)與發(fā)現(xiàn)中心的作用有一個(gè)初步的理解。為了更好地掌握這一主題,我們需要關(guān)注一些關(guān)鍵問(wèn)題,例如客戶端的負(fù)載均衡、心跳監(jiān)測(cè)以及服務(wù)注冊(cè)與發(fā)現(xiàn)等。
接下來(lái),我們將通過(guò)分析源碼,帶領(lǐng)大家全面了解 Nacos 是如何高效解決注冊(cè)中心的三大核心任務(wù)的。
gRPC
在這里,我想先介紹一下 Nacos 的實(shí)現(xiàn)方式。自 Nacos 2.1 版本起,官方不再推薦使用 HTTP 等傳統(tǒng)的 RPC 調(diào)用方式,雖然這些方式仍然是被支持的。如果你計(jì)劃順利升級(jí)到 Nacos,需特別關(guān)注一個(gè)配置參數(shù):在 application.properties 文件中設(shè)置 nacos.core.support.upgrade.from.1x=true。
在之前的分析中,我們已經(jīng)探討過(guò) Nacos 1.x 版本的實(shí)現(xiàn),那個(gè)版本確實(shí)是通過(guò)常規(guī)的 HTTP 調(diào)用進(jìn)行交互的,Nacos 服務(wù)端會(huì)實(shí)現(xiàn)一些 Controller,就像我們自己構(gòu)建的微服務(wù)一樣,源碼的可讀性非常高,容易理解。調(diào)用方式如下面的圖示所示:
圖片
但是,自 Nacos 2.1 版本以來(lái),系統(tǒng)進(jìn)行了重要的升級(jí),轉(zhuǎn)而采用了 gRPC。gRPC 是一個(gè)開(kāi)源的遠(yuǎn)程過(guò)程調(diào)用(RPC)框架,最初由 Google 開(kāi)發(fā)。它利用 HTTP/2 作為傳輸協(xié)議,提供更高效的網(wǎng)絡(luò)通信,并使用 Protocol Buffers 作為消息格式,從而實(shí)現(xiàn)了快速且高效的數(shù)據(jù)序列化和反序列化。
圖片
性能優(yōu)化:gRPC 基于 HTTP/2 協(xié)議,支持多路復(fù)用,允許在一個(gè)連接上同時(shí)發(fā)送多個(gè)請(qǐng)求,減少延遲和帶寬使用。
二進(jìn)制負(fù)載: 與基于文本的 JSON/XML 相比,協(xié)議緩沖區(qū)序列化為緊湊的二進(jìn)制格式。
流控與雙向流:gRPC 支持流式數(shù)據(jù)傳輸,能夠?qū)崿F(xiàn)客戶端和服務(wù)器之間的雙向流通信,適用于實(shí)時(shí)應(yīng)用。
解決 GC 問(wèn)題:通過(guò)真實(shí)的長(zhǎng)連接,減少了頻繁連接和斷開(kāi)的對(duì)象創(chuàng)建,進(jìn)而降低了 GC(垃圾回收)壓力,提升了系統(tǒng)性能。
Nacos 升級(jí)使用 gRPC 是基于其眾多優(yōu)點(diǎn),但我也必須強(qiáng)調(diào),沒(méi)有任何技術(shù)是所謂的“銀彈”,這也是我一貫的觀點(diǎn)。最明顯的缺點(diǎn)是系統(tǒng)復(fù)雜性的增加。因此,在選擇技術(shù)方案時(shí),必須根據(jù)自身的業(yè)務(wù)需求做出明智的決策。
在新版 Nacos 的源碼中,你會(huì)發(fā)現(xiàn)許多以 .proto 后綴命名的文件。這些文件定義了消息的結(jié)構(gòu),其中每條消息代表一個(gè)小的信息邏輯記錄,包含一系列稱(chēng)為字段(fields)的名稱(chēng)-值對(duì)。這種定義方式使得數(shù)據(jù)的傳輸和解析變得更加高效和靈活。
例如,我們可以隨便找一個(gè) Nacos 中的緩沖區(qū)文件。
圖片
雖然這不是我們討論的重點(diǎn),但值得指出的是,gRPC 的引入將為 Nacos 帶來(lái)顯著的性能優(yōu)化。盡管我們?cè)谶@里不深入探討其具體實(shí)現(xiàn),但了解這一點(diǎn)是很重要的,因?yàn)樵诤罄m(xù)的所有調(diào)用中,gRPC 都將發(fā)揮關(guān)鍵作用。
服務(wù)注冊(cè)
當(dāng)我們的服務(wù)啟動(dòng)時(shí),會(huì)發(fā)生一個(gè)重要的過(guò)程:服務(wù)實(shí)例會(huì)向 Nacos 發(fā)起一次請(qǐng)求,以完成注冊(cè)。如下圖示:
image
為了提高效率,我們不再逐步進(jìn)行源碼追蹤,盡管之前已經(jīng)詳細(xì)講解過(guò)如何查看 Spring 的自動(dòng)配置。今天,我們將直接關(guān)注關(guān)鍵源碼的位置,以快速理解 Nacos 的實(shí)現(xiàn)細(xì)節(jié)。
@Override
public void register(Registration registration) {
//此處省略非關(guān)鍵代碼
NamingService namingService = namingService();
String serviceId = registration.getServiceId();
String group = nacosDiscoveryProperties.getGroup();
Instance instance = getNacosInstanceFromRegistration(registration);
try {
namingService.registerInstance(serviceId, group, instance);
log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
instance.getIp(), instance.getPort());
}
//此處省略非關(guān)鍵代碼
在服務(wù)注冊(cè)的過(guò)程中,我們可以觀察到構(gòu)建了一些自身的 IP 和端口信息。這些信息對(duì)于服務(wù)的正確識(shí)別和調(diào)用至關(guān)重要。此外,這里值得一提的是命名空間(Namespace)的概念。命名空間在 Nacos 中用于實(shí)現(xiàn)租戶(用戶)粒度的隔離,這對(duì)于微服務(wù)架構(gòu)中的資源管理尤為重要。
命名空間的常見(jiàn)應(yīng)用場(chǎng)景之一是不同環(huán)境之間的隔離,比如開(kāi)發(fā)、測(cè)試環(huán)境與生產(chǎn)環(huán)境的資源隔離。
圖片
接下來(lái),我們將進(jìn)行一個(gè)服務(wù)調(diào)用,這里使用的是 gRPC 協(xié)議。實(shí)際上,這個(gè)過(guò)程可以簡(jiǎn)化為一個(gè)方法調(diào)用。
private <T extends Response> T requestToServer(AbstractNamingRequest request, Class<T> responseClass)
throws NacosException {
try {
request.putAllHeader(
getSecurityHeaders(request.getNamespace(), request.getGroupName(), request.getServiceName()));
Response response =
requestTimeout < 0 ? rpcClient.request(request) : rpcClient.request(request, requestTimeout);
//此處省略非關(guān)鍵代碼
服務(wù)端處理
當(dāng) Nacos 服務(wù)端接收到來(lái)自客戶端的 gRPC 調(diào)用請(qǐng)求后,會(huì)立即啟動(dòng)一系列處理流程,以確保請(qǐng)求能夠得到有效響應(yīng)。關(guān)鍵代碼的實(shí)現(xiàn)細(xì)節(jié)可以參考下面這部分。
@Override
@TpsControl(pointName = "RemoteNamingServiceSubscribeUnSubscribe", name = "RemoteNamingServiceSubscribeUnsubscribe")
@Secured(action = ActionTypes.READ)
@ExtractorManager.Extractor(rpcExtractor = SubscribeServiceRequestParamExtractor.class)
public SubscribeServiceResponse handle(SubscribeServiceRequest request, RequestMeta meta) throws NacosException {
String namespaceId = request.getNamespace();
String serviceName = request.getServiceName();
String groupName = request.getGroupName();
String app = RequestContextHolder.getContext().getBasicContext().getApp();
String groupedServiceName = NamingUtils.getGroupedName(serviceName, groupName);
Service service = Service.newService(namespaceId, groupName, serviceName, true);
Subscriber subscriber = new Subscriber(meta.getClientIp(), meta.getClientVersion(), app, meta.getClientIp(),
namespaceId, groupedServiceName, 0, request.getClusters());
ServiceInfo serviceInfo = ServiceUtil.selectInstancesWithHealthyProtection(serviceStorage.getData(service),
metadataManager.getServiceMetadata(service).orElse(null), subscriber.getCluster(), false, true,
subscriber.getIp());
if (request.isSubscribe()) {
clientOperationService.subscribeService(service, subscriber, meta.getConnectionId());
NotifyCenter.publishEvent(new SubscribeServiceTraceEvent(System.currentTimeMillis(),
NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
service.getName()));
} else {
clientOperationService.unsubscribeService(service, subscriber, meta.getConnectionId());
NotifyCenter.publishEvent(new UnsubscribeServiceTraceEvent(System.currentTimeMillis(),
NamingRequestUtil.getSourceIpForGrpcRequest(meta), service.getNamespace(), service.getGroup(),
service.getName()));
}
return new SubscribeServiceResponse(ResponseCode.SUCCESS.getCode(), "success", serviceInfo);
}
這段代碼包括提取請(qǐng)求信息、創(chuàng)建相關(guān)對(duì)象、處理訂閱或取消訂閱的操作,并返回相應(yīng)的結(jié)果。通過(guò)這種方式,Nacos 可以高效管理微服務(wù)的服務(wù)發(fā)現(xiàn)和注冊(cè)功能。
心跳監(jiān)測(cè)
在 Nacos 2.1 版本之前,每個(gè)服務(wù)在運(yùn)行時(shí)都會(huì)向注冊(cè)中心發(fā)送一次請(qǐng)求,以通知其當(dāng)前的存活狀態(tài)和正常性。這種機(jī)制雖然有效,但在高并發(fā)環(huán)境下可能會(huì)引入額外的網(wǎng)絡(luò)負(fù)擔(dān)和延遲。
然而,升級(jí)到 2.1 版本后,這一過(guò)程發(fā)生了顯著的變化。首先,我們需要思考一下心跳監(jiān)測(cè)的本質(zhì)。顯然,心跳監(jiān)測(cè)是一種定期檢查機(jī)制,這意味著服務(wù)會(huì)在設(shè)定的時(shí)間間隔內(nèi)自動(dòng)發(fā)送心跳信號(hào)以確認(rèn)其存活狀態(tài)。因此,可以合理地推測(cè),這一功能在客戶端實(shí)現(xiàn)為一個(gè)定時(shí)任務(wù),它會(huì)按照預(yù)定的時(shí)間頻率定期向注冊(cè)中心報(bào)告服務(wù)的健康狀態(tài)。
為了更好地理解這一機(jī)制的實(shí)現(xiàn),我們接下來(lái)將重點(diǎn)關(guān)注相關(guān)的關(guān)鍵代碼。
public final void start() throws NacosException {
// 省略一些代碼
clientEventExecutor = new ScheduledThreadPoolExecutor(2, r -> {
Thread t = new Thread(r);
t.setName("com.alibaba.nacos.client.remote.worker");
t.setDaemon(true);
return t;
});
// 省略一些代碼
clientEventExecutor.submit(() -> {
while (true) {
try {
if (isShutdown()) {
break;
}
ReconnectContext reconnectContext = reconnectionSignal
.poll(keepAliveTime, TimeUnit.MILLISECONDS);
if (reconnectContext == null) {
// check alive time.
if (System.currentTimeMillis() - lastActiveTimeStamp >= keepAliveTime) {
boolean isHealthy = healthCheck();
if (!isHealthy) {
// 省略一些代碼
我將與健康監(jiān)測(cè)無(wú)關(guān)的代碼基本去除了,這樣你可以更加直觀地觀察 Nacos 是如何進(jìn)行實(shí)例健康監(jiān)測(cè)的。由于健康監(jiān)測(cè)的核心目的在于確認(rèn)服務(wù)的可用性,因此這一過(guò)程的實(shí)現(xiàn)相對(duì)簡(jiǎn)單。
在這段代碼中,我們可以清晰地看到,健康監(jiān)測(cè)并不涉及任何復(fù)雜的數(shù)據(jù)傳輸。其主要功能僅僅是向服務(wù)器發(fā)送請(qǐng)求,以檢測(cè)服務(wù)器是否能夠成功響應(yīng)。這種設(shè)計(jì)極大地降低了網(wǎng)絡(luò)開(kāi)銷(xiāo),使得監(jiān)測(cè)過(guò)程更加高效。
圖片
服務(wù)端的代碼同樣清晰且簡(jiǎn)單。如下所示:
@Override
@TpsControl(pointName = "HealthCheck")
public HealthCheckResponse handle(HealthCheckRequest request, RequestMeta meta) {
return new HealthCheckResponse();
}
總體而言,這種優(yōu)化顯著減少了網(wǎng)絡(luò) I/O 的消耗,提升了系統(tǒng)的整體性能。乍一看,似乎并沒(méi)有做什么復(fù)雜的操作,但這并不意味著我們就無(wú)法判斷客戶端是否能夠正常連接。實(shí)際上,關(guān)鍵的判斷邏輯被設(shè)計(jì)在外層代碼中。
Connection connection = connectionManager.getConnection(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
RequestMeta requestMeta = new RequestMeta();
requestMeta.setClientIp(connection.getMetaInfo().getClientIp());
requestMeta.setConnectionId(GrpcServerConstants.CONTEXT_KEY_CONN_ID.get());
requestMeta.setClientVersion(connection.getMetaInfo().getVersion());
requestMeta.setLabels(connection.getMetaInfo().getLabels());
requestMeta.setAbilityTable(connection.getAbilityTable());
//這里刷新下時(shí)間。用來(lái)代表它確實(shí)存活
connectionManager.refreshActiveTime(requestMeta.getConnectionId());
prepareRequestContext(request, requestMeta, connection);
//這次處理的返回
Response response = requestHandler.handleRequest(request, requestMeta);
別著急,服務(wù)端同樣運(yùn)行著一個(gè)定時(shí)任務(wù),負(fù)責(zé)定期掃描和檢查各個(gè)客戶端的狀態(tài)。我們看下:
public void start() {
initConnectionEjector();
// Start UnHealthy Connection Expel Task.
RpcScheduledExecutor.COMMON_SERVER_EXECUTOR.scheduleWithFixedDelay(() -> {
runtimeConnectionEjector.doEject();
MetricsMonitor.getLongConnectionMonitor().set(connections.size());
}, 1000L, 3000L, TimeUnit.MILLISECONDS);
//省略部分代碼,doEject方法再往后走,你就會(huì)發(fā)現(xiàn)這樣一段代碼
//outdated connections collect.
for (Map.Entry<String, Connection> entry : connections.entrySet()) {
Connection client = entry.getValue();
if (now - client.getMetaInfo().getLastActiveTime() >= KEEP_ALIVE_TIME) {
outDatedConnections.add(client.getMetaInfo().getConnectionId());
} else if (client.getMetaInfo().pushQueueBlockTimesLastOver(300 * 1000)) {
outDatedConnections.add(client.getMetaInfo().getConnectionId());
}
}
//省略部分代碼,
通過(guò)這些分析,你基本上已經(jīng)掌握了核心概念和實(shí)現(xiàn)細(xì)節(jié)。我們不需要再多做贅述。我們繼續(xù)往下看。
負(fù)載均衡
談到負(fù)載均衡,首先我們需要確保本地?fù)碛幸环莘?wù)器列表,以便于合理地分配負(fù)載。因此,關(guān)鍵在于我們?nèi)绾螐淖?cè)中心獲取這些可用服務(wù)的信息。那么,具體來(lái)說(shuō),我們應(yīng)該如何在本地有效地發(fā)現(xiàn)和獲取這些服務(wù)呢?
服務(wù)發(fā)現(xiàn)
服務(wù)發(fā)現(xiàn)的機(jī)制會(huì)隨著實(shí)例的增加或減少而動(dòng)態(tài)變化,因此我們需要定期更新可用服務(wù)列表。這就引出了一個(gè)重要的設(shè)計(jì)考量:為什么不將服務(wù)發(fā)現(xiàn)的檢索任務(wù)直接整合到心跳任務(wù)中呢?
首先,心跳任務(wù)的主要目的是監(jiān)測(cè)服務(wù)實(shí)例的健康狀態(tài),確保它們能夠正常響應(yīng)請(qǐng)求。而服務(wù)發(fā)現(xiàn)則側(cè)重于及時(shí)更新和獲取當(dāng)前可用的服務(wù)實(shí)例信息。這兩者的目的明顯不同,因此將它們混合在一起可能會(huì)導(dǎo)致邏輯上的混淆和功能上的復(fù)雜性。
此外,兩者的時(shí)間間隔也各有不同。心跳監(jiān)測(cè)可能需要更頻繁地進(jìn)行,以及時(shí)發(fā)現(xiàn)和處理服務(wù)故障,而服務(wù)發(fā)現(xiàn)的頻率可以根據(jù)具體需求適當(dāng)調(diào)整?;谶@些原因,將心跳監(jiān)測(cè)和服務(wù)發(fā)現(xiàn)分開(kāi)成兩個(gè)獨(dú)立的定時(shí)任務(wù),顯然是更合理的選擇。
接下來(lái),讓我們深入研究服務(wù)發(fā)現(xiàn)的關(guān)鍵代碼,看看具體是如何實(shí)現(xiàn)這一機(jī)制的:
public void run() {
//省略部分代碼
if (serviceObj == null) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
lastRefTime = serviceObj.getLastRefTime();
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
serviceObj = namingClientProxy.queryInstancesOfService(serviceName, groupName, clusters, 0, false);
serviceInfoHolder.processServiceInfo(serviceObj);
}
//省略部分代碼
當(dāng)然,接下來(lái)我們將探討服務(wù)器端的處理邏輯,以下是服務(wù)端處理的關(guān)鍵代碼部分:
public QueryServiceResponse handle(ServiceQueryRequest request, RequestMeta meta) throws NacosException {
String namespaceId = request.getNamespace();
String groupName = request.getGroupName();
String serviceName = request.getServiceName();
Service service = Service.newService(namespaceId, groupName, serviceName);
String cluster = null == request.getCluster() ? "" : request.getCluster();
boolean healthyOnly = request.isHealthyOnly();
ServiceInfo result = serviceStorage.getData(service);
ServiceMetadata serviceMetadata = metadataManager.getServiceMetadata(service).orElse(null);
result = ServiceUtil.selectInstancesWithHealthyProtection(result, serviceMetadata, cluster, healthyOnly, true,
NamingRequestUtil.getSourceIpForGrpcRequest(meta));
return QueryServiceResponse.buildSuccessResponse(result);
}
這樣一來(lái),我們便能夠獲得一些關(guān)鍵的服務(wù)信息。
負(fù)載均衡算法
如果同一個(gè)微服務(wù)存在多個(gè) IP 地址,那么在進(jìn)行服務(wù)調(diào)用時(shí),我們?cè)撊绾芜x擇具體的服務(wù)器呢?通常,我們會(huì)想到使用 Nginx 作為服務(wù)端的負(fù)載均衡工具。然而,除了在服務(wù)器端進(jìn)行負(fù)載均衡之外,我們同樣可以在微服務(wù)客戶端配置負(fù)載算法,以?xún)?yōu)化請(qǐng)求的分發(fā)。
此時(shí),我們要明確的是,這部分邏輯實(shí)際上并不屬于 Nacos 的職責(zé)范圍,而是由另一個(gè)組件——Ribbon 來(lái)負(fù)責(zé)。Ribbon 專(zhuān)注于實(shí)現(xiàn)客戶端負(fù)載均衡,確保在微服務(wù)架構(gòu)中,客戶端能夠智能地選擇合適的服務(wù)器進(jìn)行調(diào)用,從而提高系統(tǒng)的性能和穩(wěn)定性。
接下來(lái),我們可以深入查看 Ribbon 的關(guān)鍵代碼,了解它是如何選擇服務(wù)器的。,具體來(lái)說(shuō),Ribbon 通過(guò)一個(gè)名為 LoadBalance 的類(lèi)來(lái)攔截請(qǐng)求,并根據(jù)預(yù)設(shè)的負(fù)載均衡策略來(lái)挑選合適的服務(wù)器。
圖片
讓我們來(lái)深入分析一下關(guān)鍵代碼,其實(shí)所有的負(fù)載均衡算法邏輯都集中在 getServer 方法的實(shí)現(xiàn)中。
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)
throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer, hint);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server,
isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
我們可以對(duì)負(fù)載均衡策略進(jìn)行局部配置,以便根據(jù)特定的業(yè)務(wù)需求和場(chǎng)景靈活調(diào)整服務(wù)調(diào)用的行為。
#被調(diào)用的微服務(wù)名
mall‐order:
ribbon:
#指定使用Nacos提供的負(fù)載均衡策略(優(yōu)先調(diào)用同一集群的實(shí)例,基于隨機(jī)&權(quán)重)
NFLoadBalancerRuleClassName:com.alibaba.cloud.nacos.ribbon.NacosRule
當(dāng)然,我們也可以進(jìn)行全局配置,以便在整個(gè)系統(tǒng)范圍內(nèi)統(tǒng)一管理負(fù)載均衡策略和參數(shù)。
@Bean
public IRule ribbonRule() {
// 指定使用Nacos提供的負(fù)載均衡策略(優(yōu)先調(diào)用同一集群的實(shí)例,基于隨機(jī)權(quán)重)
return new NacosRule();
}
總結(jié)
隨著本文的深入探討,我們對(duì)微服務(wù)架構(gòu)中的服務(wù)發(fā)現(xiàn)與注冊(cè)機(jī)制有了更全面的認(rèn)識(shí)。從單體架構(gòu)的局限性到微服務(wù)的靈活性,我們見(jiàn)證了架構(gòu)演進(jìn)的歷程。服務(wù)發(fā)現(xiàn)與注冊(cè)作為微服務(wù)通信的基石,其重要性不言而喻。通過(guò)Nacos這一強(qiáng)大的注冊(cè)中心,我們不僅實(shí)現(xiàn)了服務(wù)的動(dòng)態(tài)注冊(cè)與發(fā)現(xiàn),還通過(guò)心跳監(jiān)測(cè)、負(fù)載均衡等機(jī)制,確保了服務(wù)的高可用性和穩(wěn)定性。
在技術(shù)選型上,Nacos的gRPC實(shí)現(xiàn)展示了其在性能優(yōu)化方面的潛力,同時(shí)也帶來(lái)了系統(tǒng)復(fù)雜性的挑戰(zhàn)。然而,通過(guò)精心設(shè)計(jì)的客戶端和服務(wù)端代碼,我們能夠有效地管理服務(wù)實(shí)例,實(shí)現(xiàn)服務(wù)的快速響應(yīng)和負(fù)載均衡。這些機(jī)制的實(shí)現(xiàn),不僅提升了系統(tǒng)的伸縮性和容錯(cuò)性,也為微服務(wù)的快速發(fā)展提供了堅(jiān)實(shí)的基礎(chǔ)。