04 | 负载均衡:Ribbon 如何保证微服务的高可用
上一講我們對 Nacos 的集群環(huán)境與實(shí)現(xiàn)原理進(jìn)行了講解,我們已經(jīng)可以輕松將單個(gè)微服務(wù)接入到 Nacos 進(jìn)行注冊,但是微服務(wù)本不是孤島,如何實(shí)現(xiàn)有效的服務(wù)間穩(wěn)定通信是本文即將介紹的主要內(nèi)容,本次我們將主要學(xué)習(xí)三方面知識:
Ribbon 負(fù)載均衡器
在介紹 Ribbon 之前,咱們先來認(rèn)識下負(fù)載均衡以及它的兩種實(shí)現(xiàn)方式。
負(fù)載均衡顧名思義,是指通過軟件或者硬件措施。它將來自客戶端的請求按照某種策略平均的分配到集群的每一個(gè)節(jié)點(diǎn)上,保證這些節(jié)點(diǎn)的 CPU、內(nèi)存等設(shè)備負(fù)載情況大致在一條水平線,避免由于局部節(jié)點(diǎn)負(fù)載過高產(chǎn)生宕機(jī),再將這些處理壓力傳遞到其他節(jié)點(diǎn)上產(chǎn)生系統(tǒng)性崩潰。
負(fù)載均衡按實(shí)現(xiàn)方式分類可區(qū)分為:服務(wù)端負(fù)載均衡與客戶端負(fù)載均衡。
服務(wù)端負(fù)載均衡顧名思義,在架構(gòu)中會提供專用的負(fù)載均衡器,由負(fù)載均衡器持有后端節(jié)點(diǎn)的信息,服務(wù)消費(fèi)者發(fā)來的請求經(jīng)由專用的負(fù)載均衡器分發(fā)給服務(wù)提供者,進(jìn)而實(shí)現(xiàn)負(fù)載均衡的作用。目前常用的負(fù)載均衡器軟硬件有:F5、Nginx、HaProxy 等。
客戶端負(fù)載均衡是指,在架構(gòu)中不再部署額外的負(fù)載均衡器,在每個(gè)服務(wù)消費(fèi)者內(nèi)部持有客戶端負(fù)載均衡器,由內(nèi)置的負(fù)載均衡策略決定向哪個(gè)服務(wù)提供者發(fā)起請求。說到這,我們的主角登場了,Netfilx Ribbon 是 Netflix 公司開源的一個(gè)負(fù)載均衡組件,是屬于客戶端負(fù)載均衡器。目前Ribbon 已被 Spring Cloud 官方技術(shù)生態(tài)整合,運(yùn)行時(shí)以 SDK 形式內(nèi)嵌到每一個(gè)微服務(wù)實(shí)例中,為微服務(wù)間通信提供負(fù)載均衡與高可用支持。為了更容易理解,我們通過應(yīng)用場景說明 Ribbon 的執(zhí)行流程。假設(shè)訂單服務(wù)在查詢訂單時(shí)需要附帶對應(yīng)商品詳情,這就意味著訂單服務(wù)依賴于商品服務(wù),兩者必然產(chǎn)生服務(wù)間通信,此時(shí) Ribbon 的執(zhí)行過程如下圖所示:
了解了 Ribbon 執(zhí)行流程后,咱們通過代碼方式體現(xiàn)這個(gè)完整流程
Ribbon+RestTemplate 實(shí)現(xiàn)服務(wù)間高可用通信
開始前,我們首先介紹下 Spring Cloud 自帶的 RestTemplate 對象,RestTemplate 對象是Spring Cloud 封裝的 RESTful 通信對象,它封裝了基于 HTTP 協(xié)議的操作,通過簡單的API便可發(fā)起 HTTP 請求并自動(dòng)處理響應(yīng)。RestTemplate 天然與 Ribbon 兼容,兩者配合可以極大簡化服務(wù)間通信過程。Ribbon + RestTemplate 提供了兩種開發(fā)模式:代碼模式,注解模式。
代碼模式
第一種代碼模式是指通過純 Java 代碼實(shí)現(xiàn)微服務(wù)間通信,雖然工作中代碼模式很少使用,但它卻是理解 Ribbon+RestTemplate 最直觀的途徑,所以我對它首先進(jìn)行講解。該模式使用主要分為兩個(gè)階段:
第一階段,創(chuàng)建服務(wù)提供者,服務(wù)提供者是請求的實(shí)際處理者,也是標(biāo)準(zhǔn)的 Spring Boot 工程,利用 Controller 對外暴露 RESTful API 供服務(wù)消費(fèi)者調(diào)用。
第一步,利用 Spring Initializr 向?qū)?chuàng)建 provider-service 微服務(wù)。
其中 pom.xml 要確保引入 web 與 nacos-discovery 兩個(gè)依賴。
第二步,在 application.yml 中調(diào)整微服務(wù)與 Nacos 的通信配置。
spring:application:name: provider-service #應(yīng)用/微服務(wù)名字cloud:nacos:discovery:server-addr: 192.168.31.102:8848 #nacos服務(wù)器地址username: nacos #用戶名密碼password: nacosserver:port: 80第三步,創(chuàng)建 ProviderController,通過 Controller 控制器對外暴露接口。
package com.lagou.providerservice.controller;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;@RestControllerpublic class ProviderController {@GetMapping("/provider/msg")public String sendMessage(){return "This is the message from provider service!";}}到這里服務(wù)提供者 provider-service 已經(jīng)開發(fā)完畢,細(xì)心的你應(yīng)該已注意到,服務(wù)提供者就是標(biāo)準(zhǔn)的 Spring Cloud 微服務(wù),向 Nacos 進(jìn)行注冊的同時(shí)對外暴露 msg 接口并返回一段靜態(tài)文本,并沒有任何與 Ribbon 相關(guān)的內(nèi)容。確實(shí),Ribbon 與 RestTemplate 應(yīng)出現(xiàn)在服務(wù)消費(fèi)者,而非提供者一端。
下面為了演示需要,我們需要準(zhǔn)備五臺虛擬機(jī):
在 provider-service 工程中利用 maven package 命令生成 provider-service-0.0.1-SNAPSHOT.jar。
分別上傳至到 111 至 113 節(jié)點(diǎn)后執(zhí)行 Java 命令啟動(dòng)微服務(wù)實(shí)例。
三個(gè)節(jié)點(diǎn)啟動(dòng)成功,在 Nacos 控制臺應(yīng)見到三個(gè)健康實(shí)例。
單獨(dú)訪問任意節(jié)點(diǎn),都能看到相同的返回文本。
到這里,第一階段開發(fā)服務(wù)提供者 provider-service 告一段落。
第二階段,開發(fā)服務(wù)消費(fèi)者 consumer-service,服務(wù)消費(fèi)者說白了就是服務(wù)的使用方,我們需要在服務(wù)消費(fèi)者內(nèi)置 Ribbon+RestTemplate 實(shí)現(xiàn)服務(wù)間高可用通信。
第一步,利用 Spring Initializr 創(chuàng)建 consumer-service微服務(wù)。
pom.xml 確保引入以下三個(gè)依賴。
這里需要重點(diǎn)說明,starter-netflix-ribbon 就是通過 Spring Boot Starter 向當(dāng)前微服務(wù)工程集成 Ribbon,無須做其他額外配置。與此同時(shí),用于 RESTful 通信的 RestTemplate 對象已被集成到 starter-web 模塊,無須額外依賴。
第二步,配置 application.yml,與 provider-service 除微服務(wù) id 外并無其他變化。
spring:application:name: customer-service #應(yīng)用/微服務(wù)名字cloud:nacos:discovery:server-addr: 192.168.31.102:8848 #nacos服務(wù)器地址username: nacos #用戶名密碼password: nacosserver:port: 80第三步,利用 Spring Java Config 方式聲明 RestTemplate。在 ConsumerServiceApplication 類中新增以下聲明代碼。
@SpringBootApplicationpublic class ConsumerServiceApplication {//Java Config聲明RestTemplate對象//在應(yīng)用啟動(dòng)時(shí)自動(dòng)執(zhí)行restTemplate()方法創(chuàng)建RestTemplate對象,其BeanId為restTemplate。@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}public static void main(String[] args) {SpringApplication.run(ConsumerServiceApplication.class, args);}}第四步,創(chuàng)建 ConsumerController,通過 Ribbon+RestTemplate 實(shí)現(xiàn)負(fù)載均衡通信,重要的代碼我通過注釋進(jìn)行說明。
package com.lagou.consumerservice.controller;...@RestControllerpublic class ConsumerController {private Logger logger = LoggerFactory.getLogger(ConsumerController.class);//注入 Ribbon 負(fù)載均衡器對象//在引入 starter-netflix-ribbo n后在 SpringBoot 啟動(dòng)時(shí)會自動(dòng)實(shí)例化 LoadBalancerClient 對象。//在 Controlle 使用 @Resource 注解進(jìn)行注入即可。@Resourceprivate LoadBalancerClient loadBalancerClient;@Resource//將應(yīng)用啟動(dòng)時(shí)創(chuàng)建的 RestTemplate 對象注入 ConsumerControllerprivate RestTemplate restTemplate;@GetMapping("/consumer/msg")public String getProviderMessage() {//loadBalancerClient.choose()方法會從 Nacos 獲取 provider-service 所有可用實(shí)例,//并按負(fù)載均衡策略從中選擇一個(gè)可用實(shí)例,封裝為 ServiceInstance(服務(wù)實(shí)例)對象//結(jié)合現(xiàn)有環(huán)境既是從192.168.31.111:80、192.168.31.112:80、192.168.31.113:80三個(gè)實(shí)例中選擇一個(gè)包裝為ServiceInstanceServiceInstance serviceInstance = loadBalancerClient.choose("provider-service");//獲取服務(wù)實(shí)例的 IP 地址String host = serviceInstance.getHost();//獲取服務(wù)實(shí)例的端口int port = serviceInstance.getPort();//在日志中打印服務(wù)實(shí)例信息logger.info("本次調(diào)用由provider-service的" + host + ":" + port + " 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理" );//通過 RestTemplate 對象的 getForObject() 方法向指定 URL 發(fā)送請求,并接收響應(yīng)。//getForObject()方法有兩個(gè)參數(shù)://1. 具體發(fā)送的 URL,結(jié)合當(dāng)前環(huán)境發(fā)送地址為:http://192.168.31.111:80/provider/msg//2. String.class說明 URL 返回的是純字符串,如果第二參數(shù)是實(shí)體類, RestTemplate 會自動(dòng)進(jìn)行反序列化,為實(shí)體屬性賦值String result = restTemplate.getForObject("http://" + host + ":" + port + "/provider/msg", String.class);//輸出響應(yīng)內(nèi)容logger.info("provider-service 響應(yīng)數(shù)據(jù):" + result);//向?yàn)g覽器返回響應(yīng)return "consumer-service 響應(yīng)數(shù)據(jù):" + result;}}第五步,利用 Maven Package 打包生成 Jar。
部署至 120 虛擬機(jī),執(zhí)行啟動(dòng)命令:
啟動(dòng)成功后,在 Nacos 中確認(rèn) consumer-service 已注冊。
在瀏覽器輸入http://192.168.31.120/consumer/msg,F5 多次刷新,看日志會得到以下結(jié)果
不難看出,因?yàn)樵?Nacos 中存在 3 個(gè) provider-service 的可用實(shí)例,默認(rèn) Ribbon 是以輪詢方式按順序逐次發(fā)送。如果遇到某個(gè) provider-service 實(shí)例宕機(jī),Nacos 心跳機(jī)制會檢測到并將其剔除,同時(shí)通知所有 consumer-service 實(shí)例,服務(wù)提供者節(jié)點(diǎn)狀態(tài)發(fā)生變化,之后 consumer-service 便不會向宕機(jī)節(jié)點(diǎn)發(fā)出請求。
以上便是代碼模式的處理過程。它清晰的說明了 Ribbon 的執(zhí)行過程,先從 Nacos 獲取可用服務(wù)提供者實(shí)例信息,再通過 RestTemplate.getForObject() 向該實(shí)例發(fā)起 RESTful 請求完成處理。但可能你也感覺到了,代碼模式使用復(fù)雜,需要自己獲取可用實(shí)例 IP、端口信息,再拼接 URL 實(shí)現(xiàn)服務(wù)間通信,那有沒有更簡單的辦法呢?答案是肯定的,利用 @LoadBalanced 注解可自動(dòng)化實(shí)現(xiàn)這一過程。
注解模式
注解模式仍然分為兩階段:
第一階段,創(chuàng)建服務(wù)提供者 provider-service,因服務(wù)提供者并不涉及 Ribbon,所以與代碼模式一階段代碼完全相同,這里不再復(fù)述。
第二階段,創(chuàng)建新的服務(wù)消費(fèi)者 consumer-service。
第一步,利用 Spring Initializr 創(chuàng)建 consumer-service 微服務(wù)。
同樣引用 3 個(gè)依賴。
第二步,配置 application.yml。在原有代碼模式基礎(chǔ)上,將 Debug 級別日志輸出,這樣便可看到負(fù)載均衡實(shí)例信息。
spring:application:name: consumer-service #應(yīng)用/微服務(wù)名字cloud:nacos:discovery:server-addr: localhost:8848 #nacos服務(wù)器地址username: nacos #用戶名密碼password: nacoslogging:level:root: debug第三步,關(guān)鍵點(diǎn)來了,在 Spring 初始化 RestTemplate 實(shí)例時(shí)增加 @LoadBalanced 注解,使 RestTemplate 進(jìn)行服務(wù)通信時(shí)自動(dòng)與 Ribbon 整合,自動(dòng)實(shí)現(xiàn)負(fù)載均衡。
@SpringBootApplicationpublic class ConsumerServiceApplication {@Bean@LoadBalanced //使RestTemplate對象自動(dòng)支持Ribbon負(fù)載均衡public RestTemplate restTemplate(){return new RestTemplate();}public static void main(String[] args) {SpringApplication.run(ConsumerServiceApplication.class, args);}}第四步,在 Controller 發(fā)起通信時(shí),原有 RestTemplate.getForObject() 方法書寫 URL 時(shí),將 IP 端口部分要替換為服務(wù)名,如下所示:
@RestControllerpublic class ConsumerController {private Logger logger = LoggerFactory.getLogger(ConsumerController.class); @Resourceprivate RestTemplate restTemplate;@GetMapping("/consumer/msg")public String getProviderMessage() {//關(guān)鍵點(diǎn):將原有IP:端口替換為服務(wù)名,RestTemplate便會在通信前自動(dòng)利用Ribbon查詢可用provider-service實(shí)例列表//再根據(jù)負(fù)載均衡策略選擇節(jié)點(diǎn)實(shí)例String result = restTemplate.getForObject("http://provider-service/provider/msg", String.class);logger.info("consumer-service獲得數(shù)據(jù):" + result);return "consumer-service獲得數(shù)據(jù):" + result;}}在新的 getProviderMessage 代碼中,不再出現(xiàn) LoadBalancerClient 與 ServiceInstance 對象,這一切都被 @LoadBalanced 進(jìn)行封裝,在 RestTemplate 查詢前自動(dòng)處理。
第五步,重新部署 consumer-service,多次訪問地址http://192.168.31.120/consumer/msg,在控制臺會看到 Debug 級別日志,通過實(shí)際IP地址也同樣印證 Ribbon 默認(rèn)采用輪詢策略進(jìn)行分配。
s.n.www.protocol.http.HttpURLConnection : {GET /provider/msg ...}{Accept: ...}{User-Agent: ...}{Host: 192.168.31.111:80}...s.n.www.protocol.http.HttpURLConnection : {GET /provider/msg ...}{Accept: ...}{User-Agent: ...}{Host: 192.168.31.112:80}...s.n.www.protocol.http.HttpURLConnection : {GET /provider/msg ...}{Accept: ...}{User-Agent: ...}{Host: 192.168.31.113:80}以上便是注解模式的使用辦法,相比代碼模式是不是簡單很多啊。對了,你注意到了嗎?無論注解模式還是代碼模式,默認(rèn)的負(fù)載均衡策略都是輪詢,即按順序依次訪問,作為 Ribbon 還支持哪些其他負(fù)載均衡策略呢?我們又該如何設(shè)置呢?本次最后一小節(jié),我將帶領(lǐng)你學(xué)習(xí)這塊知識。
如何配置 Ribbon 負(fù)載均衡策略
Ribbon 內(nèi)置多種負(fù)載均衡策略,常用的分為以下幾種。
- RoundRobinRule:輪詢策略,Ribbon 默認(rèn)策略。默認(rèn)超過 10 次獲取到的 server 都不可用,會返回?個(gè)空的 server。
- RandomRule:隨機(jī)策略,如果隨機(jī)到的 server 為 null 或者不可用的話。會不停地循環(huán)選取。
- RetryRule:重試策略,?定時(shí)限內(nèi)循環(huán)重試。默認(rèn)繼承 RoundRobinRule,也?持自定義注?,RetryRule 會在每次選取之后,對選舉的 server 進(jìn)?判斷,是否為 null,是否 alive,并且在 500ms 內(nèi)會不停地選取判斷。而 RoundRobinRule 失效的策略是超過 10 次,RandomRule 沒有失效時(shí)間的概念,只要 serverList 沒都掛。
- BestAvailableRule:最小連接數(shù)策略,遍歷 serverList,選取出可?的且連接數(shù)最小的?個(gè) server。那么會調(diào)用 RoundRobinRule 重新選取。
- AvailabilityFilteringRule:可用過濾策略。擴(kuò)展了輪詢策略,會先通過默認(rèn)的輪詢選取?個(gè) server,再去判斷該 server 是否超時(shí)可用、當(dāng)前連接數(shù)是否超限,都成功再返回。
- ZoneAvoidanceRule:區(qū)域權(quán)衡策略。擴(kuò)展了輪詢策略,除了過濾超時(shí)和鏈接數(shù)過多的 server,還會過濾掉不符合要求的 zone 區(qū)域??的所有節(jié)點(diǎn),始終保證在?個(gè)區(qū)域/機(jī)房內(nèi)的服務(wù)實(shí)例進(jìn)行輪詢。
這里所有負(fù)載均衡策略名本質(zhì)都是 com.netflix.loadbalancer 包下的類:
要更改微服務(wù)通信時(shí)采用的負(fù)載均衡策略也很簡單,在 application.yml 中采用下面格式書寫即可。
當(dāng)采用隨機(jī)策略,運(yùn)行時(shí)得到如下日志,雜亂的順序說明隨機(jī)策略已生效。
本次調(diào)用由 provider-service的192.168.31.112:80 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理consumer-service 獲得數(shù)據(jù):This is the message from provider service!本次調(diào)用由 provider-service 的 192.168.31.112:80 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理consumer-service 獲得數(shù)據(jù):This is the message from provider service!本次調(diào)用由 provider-service 的 192.168.31.113:80 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理consumer-service 獲得數(shù)據(jù):This is the message from provider service!本次調(diào)用由 provider-service 的 192.168.31.111:80 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理consumer-service 獲得數(shù)據(jù):This is the message from provider service!本次調(diào)用由 provider-service 的 192.168.31.113:80 實(shí)例節(jié)點(diǎn)負(fù)責(zé)處理consumer-service 獲得數(shù)據(jù):This is the message from provider service!講到這里,想必你對 Ribbon 已經(jīng)有了直觀的認(rèn)識,在項(xiàng)目中合理的使用 Ribbon 負(fù)載均衡可以使系統(tǒng)性能有顯著的提升,最后我們來做下總結(jié)。
小結(jié)與預(yù)告
本文我們介紹了三方面知識,開始介紹了 Ribbon 負(fù)載均衡器的作用,之后講解了 Ribbon 的兩種開發(fā)模式,最后講解了 Ribbon 的負(fù)載均衡策略與設(shè)置辦法。
這里給你留一道課外題:如果 Ribbon 自帶的負(fù)載均衡策略不能滿足實(shí)際項(xiàng)目的需要,我們?nèi)绾巫远x Ribbon 負(fù)載均衡策略呢?你可以自行查閱資料,將學(xué)到的知識分享在評論區(qū)中。
下一節(jié)課,我們將學(xué)習(xí)除 RestTemplate 之外另一項(xiàng)重要的服務(wù)間通信技術(shù) OpenFeign,看它又提供了哪些高級特性。
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的04 | 负载均衡:Ribbon 如何保证微服务的高可用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 03 | 高可用保证:Nacos 如何有
- 下一篇: 05 | REST消息通信:如何使用 O