Spring?Cloud負(fù)載均衡組件Ribbon原理解析
微服務(wù)體系下的 Spring Cloud Netflix 套件中 Ribbon 的主要用于負(fù)載均衡,底層默認(rèn)使用 RestTemplate 通訊,并提供了 7 種負(fù)載均衡策略
前言
在微服務(wù)中,對(duì)服務(wù)進(jìn)行拆分之后,必然會(huì)帶來微服務(wù)之間的通信需求,而每個(gè)微服務(wù)為了保證高可用性,又會(huì)去部署集群,那么面對(duì)一個(gè)集群微服務(wù)進(jìn)行通信的時(shí)候,如何進(jìn)行負(fù)載均衡也是必然需要考慮的問題。那么有需求自然就有供給,由此一大批優(yōu)秀的開源的負(fù)載均衡組件應(yīng)運(yùn)而生,本文就讓我們一起來分析一下 Spring Cloud Netflix
套件中的負(fù)載均衡組件 Ribbon
。
一個(gè)問題引發(fā)的思考
首先我們來看一個(gè)問題,假如說我們現(xiàn)在有兩個(gè)微服務(wù),一個(gè) user-center
,一個(gè) user-order
,我現(xiàn)在需要在 user-center
服務(wù)中調(diào)用 user-order
服務(wù)的一個(gè)接口。
這時(shí)候我們可以使用 HttpClient
,RestTemplate
等發(fā)起 http
請(qǐng)求,user-center
服務(wù)端口為 8001
,如下圖所示:
@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private RestTemplate restTemplate; @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ return restTemplate.getForObject("http://localhost:8002/order/query",String.class); } }
而 user-order
服務(wù)中只是簡(jiǎn)單的定義了一個(gè)接口,user-order
服務(wù)端口為 8002
:
@RestController @RequestMapping(value = "/order") public class UserOrderController { @GetMapping(value = "/query") public String queryAllOrder(){ return "all orders"; } }
這時(shí)候只需要將兩個(gè)服務(wù)啟動(dòng),訪問 http://localhost:8001/user/order
就可以獲取到所有的訂單信息。
可以看到,這樣是可以在兩個(gè)微服務(wù)之間進(jìn)行通訊的,但是,假如說我們的 user-order
服務(wù)是一個(gè)集群呢?這時(shí)候怎么訪問呢?因?yàn)?user-order
服務(wù)已經(jīng)是集群,所以必然需要一種算法來決定應(yīng)該請(qǐng)求到哪個(gè) user-order
服務(wù)中,最簡(jiǎn)單的那么自然就是隨機(jī)或者輪詢機(jī)制,輪詢或者隨機(jī)其實(shí)就是簡(jiǎn)單的負(fù)載均衡算法,而 Ribbon
就是用來實(shí)現(xiàn)負(fù)載均衡的一個(gè)組件,其內(nèi)部支持輪詢,等算法。
Ribbon的簡(jiǎn)單使用
接下來我們看看 Ribbon
的簡(jiǎn)單使用。
首先改造 user-order
服務(wù),在 user-order
服務(wù)中定義一個(gè)服務(wù)名配置:
spring.application.name=user-order-service
將 user-order
服務(wù)中的 UserOrderController
稍微改造一下,新增一個(gè)端口的輸出來區(qū)分:
@RestController @RequestMapping(value = "/order") public class UserOrderController { @Value("${server.port}") private int serverPort; @GetMapping(value = "/query") public String queryAllOrder(){ return "訂單來自:" + serverPort; } }
- 通過 VM 參數(shù)
-Dserver.port=8002
和-Dserver.port=8003
分別來啟動(dòng)兩個(gè)user-order
服務(wù)。 - 接下來改造
user-center
服務(wù),在user-center
服務(wù)中引入Ribbon
的相關(guān)依賴:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> <version>2.2.3.RELEASE</version> </dependency>
user-center
服務(wù)中新增一個(gè)Ribbon
相關(guān)配置,列舉出需要訪問的所有服務(wù):
user-order-service.ribbon.listOfServers=\ localhost:8002,localhost:8003
- 對(duì)
user-center
服務(wù)中的UserController
進(jìn)行改造:
@RestController @RequestMapping(value = "/user") public class UserController { @Autowired private RestTemplate restTemplate; @Autowired private LoadBalancerClient loadBalancerClient; @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ //獲取一個(gè) user-order 服務(wù) ServiceInstance serviceInstance = loadBalancerClient.choose("user-order-service"); String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort()) + "/order/query"; return restTemplate.getForObject(url,String.class); } }
這時(shí)候我們?cè)俅卧L問 http://localhost:8001/user/order
就可以看到請(qǐng)求的 user-order
服務(wù)會(huì)在 8002
和 8003
之間進(jìn)行切換。
Ribbon 原理分析
看了上面 Ribbon
的使用示例,會(huì)不會(huì)覺得有點(diǎn)麻煩,每次還需要自己去獲取 ip
和端口,然后格式化 url
,但是其實(shí)實(shí)際開發(fā)過程中我們并不會(huì)通過這么原始的方式來編寫代碼,接下來我們?cè)賹?duì)上面的示例進(jìn)行一番改造:
@RestController @RequestMapping(value = "/user") public class UserController3 { @Autowired private RestTemplate restTemplate; @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } @GetMapping("/order") public String queryOrder(){ return restTemplate.getForObject("http://user-order-service/order/query",String.class); } }
在這個(gè)示例中,主要就是一個(gè)關(guān)鍵主鍵起了作用:@LoadBalanced。
@LoadBalanced 注解
進(jìn)入 @LoadBalanced
注解中,我們可以看到,這個(gè)注解其實(shí)沒有任何邏輯,只是加了一個(gè) @Qualifier
注解:
這個(gè)注解大家應(yīng)該很熟悉了,常用語(yǔ)同一個(gè) Bean
有多個(gè)不同名稱注入的場(chǎng)景。
@Qualifier注解
下面我們通過一個(gè)例子來演示一下 Qualifier
注解的用法。
新建一個(gè)空的 TestDemo
類,并新增一個(gè) TestConfiguration
類來創(chuàng)建不同名稱的 TestDemo
:
@Configuration public class TestConfiguration { @Bean("testDemo1") public TestDemo testDemo(){ return new TestDemo(); } @Bean("testDemo2") public TestDemo testDemo2(){ return new TestDemo(); } }
這時(shí)候我們?nèi)绻枰⑷?TestDemo
,那么有很多種辦法,具體的使用就需要看業(yè)務(wù)需要來決定。
- 方法一:直接使用
@Autowired
,并使用List
集合來接收Bean
,這樣所有TestDemo
類型的Bean
都會(huì)被注入。 - 方法二:通過使用
@Resource(name = "testDemo1")
注解來指定名稱,這樣就可以只注入一個(gè)Bean
。 - 方法三:通過使用
@Resource
和@Qualifier(value = "testDemo1")
來指定一個(gè)Bean
,其實(shí)這種方式和方法二的效果基本一致。 - 方法四:使用
@Autowired
和@Qualifier
注解來注入,不指定任何名稱,如下所示:
@Configuration public class TestConfiguration { @Bean("testDemo1") public TestDemo testDemo(){ return new TestDemo(); } @Bean("testDemo2") public TestDemo testDemo2(){ return new TestDemo(); } }
這時(shí)候運(yùn)行之后我們發(fā)現(xiàn)不會(huì)有任何 Bean
被注入到集合中,這是因?yàn)楫?dāng)使用這種方式來注入時(shí),Spring
會(huì)認(rèn)為當(dāng)前只需要注入被 @Qualifier
注解標(biāo)記的 Bean
,而我們上面定義的兩個(gè) TestDemo
都沒有被 @Qualifier
修飾。
這時(shí)候,我們只需要在 TestConfiguration
稍微改造,在 TestDemo
的定義上加上 @Qualifier
修飾即可:
@Configuration public class TestConfiguration { @Bean("testDemo1") @Qualifier public TestDemo testDemo(){ return new TestDemo(); } @Bean("testDemo2") @Qualifier public TestDemo testDemo2(){ return new TestDemo(); } }
這時(shí)候再去運(yùn)行,就會(huì)發(fā)現(xiàn),testDemo1
和 testDemo2
都會(huì)被注入。
LoadBalancerAutoConfiguration 自動(dòng)裝配
SpringCloud
是基于 SpringBoot
實(shí)現(xiàn)的,所以我們常用的這些分布式組件都會(huì)基于 SpringBoot
自動(dòng)裝配來實(shí)現(xiàn),我們進(jìn)入 LoadBalancerAutoConfiguration
自動(dòng)裝配類可以看到,RestTemplate
的注入加上了 @LoadBalanced
,這就是為什么我們前面的例子中加上了 @LoadBalanced
就能被自動(dòng)注入的原因:
RestTemplateCustomizer
上面我們看到,RestTemplate
被包裝成為了 RestTemplateCustomizer
,而 RestTemplateCustomizer
的注入如下:
可以看到這里面加入了一個(gè)攔截器 LoadBalancerInterceptor
,事實(shí)上即使不看這里,我們也可以猜測(cè)到,我們直接使用服務(wù)名就可以進(jìn)行通訊的原因必然是底層有攔截器對(duì)其進(jìn)行轉(zhuǎn)換成 ip
形式,并在底層進(jìn)行負(fù)載均衡選擇合適的服務(wù)進(jìn)行通訊。
LoadBalancerInterceptor
LoadBalancerInterceptor
是 Ribbon
中默認(rèn)的一個(gè)攔截器,所以當(dāng)我們調(diào)用 RestTemplate
的 getObject
方法時(shí),必然會(huì)調(diào)用攔截器中的方法。
從源碼中可以看到,LoadBalancerInterceptor
中只有一個(gè) intercept()
方法:
RibbonLoadBalancerClient#execute
繼續(xù)跟進(jìn) execute
方法會(huì)進(jìn)入到 RibbonLoadBalancerClient
類(由 RibbonAutoConfiguration
自動(dòng)裝配類初始化)中:
這個(gè)方法中也比較好理解,首先獲取一個(gè)負(fù)載均衡器,然后再通過 getServer
方法獲取一個(gè)指定的服務(wù),也就是當(dāng)我們有多個(gè)服務(wù)時(shí),到這里就會(huì)選出一個(gè)服務(wù)進(jìn)行通訊。
進(jìn)入 getServer
方法:
我們看到,最終會(huì)調(diào)用 ILoadBalancer
中的 chooseServer
方法,而 ILoadBalancer
是一個(gè)頂層接口,這時(shí)候具體會(huì)調(diào)用哪個(gè)實(shí)現(xiàn)類那么就需要先來看一下類圖:
這里直接看類圖也無法看出到底會(huì)調(diào)用哪一個(gè),但是不論調(diào)用哪一個(gè),我們猜測(cè)他肯定會(huì)有一個(gè)地方去初始化這個(gè)類,而在 Spring
當(dāng)中一般就是自動(dòng)裝配類中初始化或者 Configuration
中初始化,而 ILoadBalancer
正是在 RibbonClientConfiguration
類中被加載的:
ZoneAwareLoadBalancer 負(fù)載均衡器
ZoneAwareLoadBalancer 的初始化會(huì)調(diào)用其父類 DynamicServerListLoadBalancer
進(jìn)行初始化,然后會(huì)調(diào)用 restOfInit
方法進(jìn)行所有服務(wù)的初始化。
如何獲取所有服務(wù)
使用 Ribbon
后,我們通訊時(shí)并沒有指定某一個(gè) ip
和端口,而是通過服務(wù)名來調(diào)用服務(wù),那么這個(gè)服務(wù)名就可能對(duì)應(yīng)多個(gè)真正的服務(wù),那么我們就必然需要先獲取到所有服務(wù)的 ip
和端口等信息,然后才能進(jìn)行負(fù)載均衡處理。
獲取所有服務(wù)有兩種方式:
- 從配置文件獲取
- 從
Eureka
注冊(cè)中心獲?。ㄐ枰胱?cè)中心)。
初始化服務(wù)的方式是通過啟動(dòng)一個(gè) Scheduled
定時(shí)任務(wù)來實(shí)現(xiàn)的,默認(rèn)就是 30s
更新一次,其實(shí)在很多源碼中都是通過這種方式來定時(shí)更新的,因?yàn)樵创a要考慮的使用的簡(jiǎn)單性所以不太可能引入一個(gè)第三方中間件來實(shí)現(xiàn)定時(shí)器。
具體的源碼如下所示:enableAndInitLearnNewServersFeature()
方法啟動(dòng)的定時(shí)任務(wù)最終仍然你是調(diào)用 updateListOfServers()
方法來更新服務(wù)。
最終在獲取到服務(wù)之后會(huì)調(diào)用父類 BaseLoadBalancer
中的將所有服務(wù)設(shè)置到 allServerList
集合中(BaseLoadBalancer
類中維護(hù)了一些負(fù)載均衡需要使用到的服務(wù)相關(guān)信息)。
如何判斷服務(wù)是否可用
當(dāng)我們獲取到配置文件(或者 Eureka
注冊(cè)中心)中的所有服務(wù),那么這時(shí)候能直接執(zhí)行負(fù)載均衡策略進(jìn)行服務(wù)分發(fā)嗎?顯然是不能的,因?yàn)橐呀?jīng)配置好的服務(wù)可能會(huì)宕機(jī)(下線),從而導(dǎo)致服務(wù)不可用,所以在 BaseLoadBalancer
中除了有一個(gè) allServerList
集合來維護(hù)所有服務(wù)器,還有一個(gè)集合 upServerList
用來維護(hù)可用服務(wù)集合,那么如何判斷一個(gè)服務(wù)是否可用呢?答案就是通過心跳檢測(cè)來判斷一個(gè)服務(wù)是否可用。
心跳檢測(cè) Task
在講心跳檢測(cè)之前,我們先看一下 BaseLoadBalancer
中的 setServersList
方法,有一段邏輯比較重要:
這段邏輯我們看到,默認(rèn)情況下,如果 Ping
的策略是 DummyPing
,那么默認(rèn) upServerList = allServerList
,而實(shí)際上,假如我們沒有進(jìn)行進(jìn)行特殊配置,其實(shí)默認(rèn)的就是 DummyPing
,這也是在 RibbonClientConfiguration
類中被加載的:
在 BaseLoadBalancer
初始化過程中,也會(huì)啟動(dòng)一個(gè) Scheduled
定時(shí)任務(wù)去定時(shí)更新任務(wù),最終和 forceQuickPing()
方法一樣,調(diào)用一個(gè)默認(rèn)策略來觸發(fā)心跳檢測(cè),而默認(rèn)策略就是 DummyPing
,也就是默認(rèn)所有服務(wù)都是可用的。
雖然默認(rèn)不執(zhí)行真正的心跳檢測(cè)操作,但是 Netflix
中提供了 PingUrl
等其他策略,PingUrl
其實(shí)就是發(fā)起一個(gè) http
請(qǐng)求,如果有響應(yīng)就認(rèn)為服務(wù)可用,沒響應(yīng)就認(rèn)為服務(wù)不可用。
修改心跳檢測(cè)策略可以通過如下配置切換(user-order-service
為客戶端的服務(wù)名),既然是可配置的,那么也可以自己實(shí)現(xiàn)一個(gè)策略,只需要實(shí)現(xiàn) IPing
接口即可。
user-order-service.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.PingUrl
Ribbon 的負(fù)載均衡算法
當(dāng)獲取到可用服務(wù)之后,那么最后應(yīng)該選擇哪一個(gè)服務(wù)呢?這就需要使用到負(fù)載均衡策略,在 Ribbon
中,可以通過配置修改,也可以自定義負(fù)載均衡策略(實(shí)現(xiàn) IRule
接口)。
- RandomRule:隨機(jī)算法
- RoundRobinRule:輪詢算法
- ZoneAvoidanceRule:結(jié)合分區(qū)統(tǒng)計(jì)信息篩選出合適的分區(qū)(默認(rèn)的負(fù)載均衡算法)
- RetryRule:在 deadline 時(shí)間內(nèi),如果請(qǐng)求不成功,則重新發(fā)起請(qǐng)求知道找到一個(gè)可用的服務(wù)。
- WeightedResponseTimeRule:根據(jù)服務(wù)器的響應(yīng)時(shí)間計(jì)算權(quán)重值,服務(wù)器響應(yīng)時(shí)間越長(zhǎng),這個(gè)服務(wù)器的權(quán)重就越小,會(huì)有定時(shí)任務(wù)對(duì)權(quán)重值進(jìn)行更新。
- AvailabilityFilteringRule:過濾掉短路(連續(xù) 3 次連接失?。┑姆?wù)和高并發(fā)的服務(wù)。
- BestAvailableRule:選擇并發(fā)數(shù)最低的服務(wù)器
負(fù)載均衡算法可通過以下配置進(jìn)行修改:
user-order-service.ribbon.NFLoadBalancerRuleClassName=Rule規(guī)則的類名
總結(jié)
本文主要講述了微服務(wù)體系下的 Spring Cloud Netflix
套件中 Ribbon
的使用,并結(jié)合部分源碼講述了 Ribbon
的底層原理,重點(diǎn)講述了 Ribbon
中是如何獲取服務(wù)以及如何判定一個(gè)服務(wù)是否可用,最后也介紹了 Ribbon
中默認(rèn)提供的 7
種負(fù)載均衡策略。
到此這篇關(guān)于Spring Cloud之負(fù)載均衡組件Ribbon原理分析的文章就介紹到這了,更多相關(guān)Spring Cloud負(fù)載均衡組件Ribbon內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring?Cloud?Ribbon的使用原理解析
- SpringCloud超詳細(xì)講解負(fù)載均衡組件Ribbon源碼
- SpringCloud?客戶端Ribbon負(fù)載均衡的實(shí)現(xiàn)方法
- Spring?Cloud?Ribbon?中的?7?種負(fù)載均衡策略的實(shí)現(xiàn)方法
- 關(guān)于SpringCloud?Ribbon替換輪詢算法問題
- 聊聊SpringCloud中的Ribbon進(jìn)行服務(wù)調(diào)用的問題
- spring cloud 集成 ribbon負(fù)載均衡的實(shí)例代碼
- 深入理解Java SpringCloud Ribbon 負(fù)載均衡
- Spring Cloud Ribbon客戶端詳細(xì)介紹
相關(guān)文章
10個(gè)Elasticsearch查詢的實(shí)用技巧分享
Elasticsearch是一個(gè)非常流行的搜索引擎,已經(jīng)成為了許多企業(yè)的首選解決方案。本文將向大家介紹10個(gè)實(shí)用的Elasticsearch查詢技巧,并配上對(duì)應(yīng)的代碼示例,希望對(duì)大家有所幫助2023-04-04SSM框架整合之Spring+SpringMVC+MyBatis實(shí)踐步驟
大家都知道Spring是一個(gè)輕量級(jí)的控制反轉(zhuǎn)(IoC)和面向切面(AOP)的容器框架,本文主要介紹三大框架的整合包含spring和mybatis的配置文件,還有spring-mvc的配置文件的詳細(xì)介紹,通過項(xiàng)目實(shí)踐步驟給大家詳細(xì)介紹,感興趣的朋友一起看看吧2021-06-06MyBatis中的SQL映射文件配置結(jié)果映射的操作指南
MyBatis?是一款優(yōu)秀的?ORM?框架,它提供了多種配置方式來定義?SQL?語(yǔ)句以及結(jié)果映射規(guī)則,本文將介紹?MyBatis?中的?SQL?映射文件如何配置結(jié)果映射,包括常規(guī)類型、集合類型等多種情況,需要的朋友可以參考下2023-07-07SpringBoot@Profile注解和Spring?EL(多環(huán)境注入)
為了方便, Spring還提供了 Profile機(jī)制, 使我們可以很方便地實(shí)現(xiàn)各個(gè)環(huán)境之間的切換,在使用DI來依賴注入的時(shí)候,能夠根據(jù)@profile標(biāo)明的環(huán)境,將注入符合當(dāng)前運(yùn)行環(huán)境的相應(yīng)的bean,本文通過示例代碼介紹SpringBoot@Profile注解和Spring?EL,需要的朋友可以參考下2024-02-02Java利用MultipartFile實(shí)現(xiàn)上傳多份文件的代碼
這篇文章主要介紹了Java利用MultipartFile實(shí)現(xiàn)上傳多份文件的代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-09-09