爲何要使用Ribbon
上一章節咱們學習了註冊中心《阿里面試官問我:到底知不知道什麼是Eureka,此次,我沒沉默》,咱們知道但咱們存在多個服務提供者的時候,咱們會讓全部的服務提供者將服務節點信息都註冊到EurekaServer中,而後讓客戶端去拉取一份服務註冊列表到本地,服務消費者會從服務註冊列表中找到合適的服務實例信息,經過IP:Port的方式去調用服務。java
那麼,消費者是如何決定調用哪個服務實例呢,此時,本章節的主人公Ribbon默默的站了起來,笑着說,沒錯,正是在下。react
Srping Cloud Ribbon 是基於 Netflix Ribbon實現的一套 客戶端負載均衡的工具。web
簡單的說,Ribbon是Netflix發佈的開源頂目,主要功能是解析配置中或註冊中心的服務列表,經過客戶端的軟件負均衡算法來實現服務請求的分發。面試
Ribbon客戶端組件提供一系列完善配置項如鏈接超時,重試等。算法
簡單的說,就是在配置文件中列出LoadBalancer後面全部的機器,Ribbon會自動的幫助你基於某種規則(筒單輪洵,隨機鏈接等)去鏈接這些機器。spring
咱們也很容易使Ribbon實現自定義負載均衡算法。緩存
Ribbon和Nginx又有什麼不一樣呢?
集中式負載均衡
如圖所示就是集中式負載均衡集中式負載均衡就比如房屋中介,他手裏有不少房屋信息。服務器
服務消費者就比如是須要租房的租客,咱們並不能直接的和房屋主人進行租房交易,而是經過房屋中介選擇房屋信息達成租房交易。架構
也就是說,客戶端的請求信息並不會直接去請求服務實例,而是在到達負載均衡器的時候,經過負載均衡算法選擇某一個服務實例,而後將請求轉發到這個服務實例上。併發
集中式負載均衡又分爲硬件負載均衡,如F5,軟件負載均衡,如Nginx。
客戶端負載均衡
如圖所示就是客戶端負載均衡就比如,如今有不少租房app,不少房屋的主人並不想經過中介租房,想省一筆中介費。
不少租客也想省一筆中介費,不想經過中介租房,因而租客將App上的租房信息記錄在本身的筆記本上。
可是因爲租客租房經驗不足,並不知道應該選擇哪一套房,此時恰好租客有一個作房屋中介好友(就是這麼巧),因而租客將好友請到家裏來,讓他幫忙出謀劃策,選擇好房源,而後租客到時候直接去看房租房。
也就是說,此時客戶端請求不會再去負載均衡器上進行轉發了,客戶端本身維護了一套服務列表,要掉用的某個服務實例以前首先會經過負載均衡算法選擇一個服務節點,直接將請求發送到該服務節點上。
Ribbon 整體架構
首先咱們看一張圖:
接下來咱們詳細介紹下上圖所示的Ribbon核心的6個組件接口。
IRule
釋義:IRule就是根據特定算法中從服務器列表中選取一個要訪問的服務,Ribbon默認的算法爲輪詢算法。
接下來咱們來看一張IRule的類繼承關係圖:
其中用紅色方框圈出來的葉子節點是如今還在使用的負載均衡算法,而用紫色方框圈出來的是已經廢棄了的。
由圖可知,目前咱們使用的負載均衡算法有如下幾種:
RoundRobinRule和WeightedResponseTimeRule
首先說明下 RoundRobinRule(輪詢)策略,雖然我沒有圈出來可是他是很經常使用的負載均衡算法,表示表示每次都取下一個服務器。
線性輪詢算法實現:每一次把來自用戶的請求輪流分配給服務器,從1開始,直到N(服務器個數),而後從新開始循環。算法的優勢是其簡潔性,它無需記錄當前全部鏈接的狀態,因此它是一種無狀態調度。
經過圖上的繼承關係咱們可知RoundRobinRule和WeightedResponseTimeRule是繼承和被繼承的關係。
WeightedResponseTimeRule是根據平均響應時間計算全部服務的權重,響應時間越快的服務權重越大被選中的機率越大。
有一個默認每30秒更新一次權重列表的定時任務,該定時任務會根據實例的響應時間來更新權重列表。
可是因爲剛啓動時若是統計信息不足,則使用RoundRobinRule(輪詢)策略,等統計信息足夠,會切換到WeightedResponseTimeRule。
AvailabilityFilteringRule
AvailabilityFilteringRule會先過濾掉因爲屢次訪問故障而處於斷路器狀態的服務,還有併發的鏈接數量超過閾值的服務,而後對剩餘的服務列表按照輪詢策略進行訪問。
ZoneAvoidanceRule
綜合判斷Server所在區域的性能和Server的可用性選擇服務器。
BestAvailableRule
會先過濾掉因爲屢次訪問故障而處於斷路器跳閘狀態的服務,而後選擇一個併發量最小的服務。
RandomRule
隨機選取服務。
使用 ThreadLocalRandom.current().nextInt(serverCount);隨機選擇。
RetryRule
先按照RoundRobinRule(輪詢)的策略獲取服務,若是獲取的服務失敗側在指定的時間會進行重試,繼續獲取可用的服務。
自定義負載均衡算法
自定義負載均衡算法主要分三步:
-
實現IRule接口或者繼承AbstractLoadBalancerRule類
-
重寫choose方法
-
指定自定義的負載均衡策略算法類
首先咱們建立一個MyRule類,可是這個類不能隨便亂放。
官方文檔給出警告:這個自定義的類不能放在@ComponentScan所掃描的當前包以及子包下,不然咱們自定義的這個配置類就會被全部的Ribbon客戶端所共享,也就是咱們達不到特殊化指定的目的了。
MyRule
package javaer.study.RibbonTest;import com.netflix.loadbalancer.IRule;/** * 自定義負載均衡策略 * * @author javaMaster * 公衆號:【Java 學習部落】 * @create 2020 09 14 * @Version 1.0.0 */public class MyRule { public IRule myRule () { return new MyRule_CustomAlgorithm (); }}
接下自定義一個負載均衡策略算法 MyRule_CustomAlgorithm。
定義算法:每臺服務節點調用三次,代碼以下
package javaer.study.RibbonTest;import com.netflix.client.config.IClientConfig;import com.netflix.loadbalancer.AbstractLoadBalancerRule;import com.netflix.loadbalancer.ILoadBalancer;import com.netflix.loadbalancer.Server;import java.util.List;/** * 自定義負載均衡算法 * * @author javaMaster * 公衆號:【Java學習部落】 * @create 2020 09 14 * @Version 1.0.0 */public class MyRule_CustomAlgorithm extends AbstractLoadBalancerRule { // total = 0 // 當total==5之後,咱們指針才能往下走, // index = 0 // 當前對外提供服務的服務器地址, // total須要從新置爲零,可是已經達到過一個5次,咱們的index = 1 // 分析:咱們5次,可是微服務只有8001 8002 8003 三臺,OK? private int total = 0; // 總共被調用的次數,目前要求每臺被調用5次 private int currentIndex = 0; // 當前提供服務的機器號 public Server choose(ILoadBalancer lb, Object key){ if (lb == null) { return null; } Server server = null; while (server == null) { if (Thread.interrupted()) { return null; } List<Server> upList = lb.getReachableServers(); List<Server> allList = lb.getAllServers(); int serverCount = allList.size(); if (serverCount == 0) { return null; } if(total < 3){ server = upList.get(currentIndex); total++; }else { total = 0; currentIndex++; if(currentIndex >= upList.size()) { currentIndex = 0; } } if (server == null) { Thread.yield(); continue; } if (server.isAlive()) { return (server); } server = null; Thread.yield(); } return server; } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { }}
使用自定義負載均衡策略的方式:
第一種,直接在啓動類上添加
@RibbonClient(name = "order-service",configuration = MyRule.class)
name指的是服務名稱,configuration指的是自定義算法類。
第二種,在配置文件中指定自定義的負載均衡算法類。
# 指定order-service的負載策略user-service.ribbon.NFLoadBalancerRuleClassName=javaer.study.RibbonTest.MyRule
ServerList
ServerList用於獲取服務節點列表並存儲的組件。
存儲分爲靜態存儲和動態存儲兩種方式。
默認從配置文件中獲取服務節點列表並存儲稱爲靜態存儲。
從註冊中心獲取對應的服務實例信息並存儲稱爲動態存儲。
ServerListFilter
ServerListFilter主要用於實現服務實例列表的過濾,經過傳入的服務實例清單,根據規則返回過濾後的服務實例清單。
ServerListUpdater
ServerListUpdater是列表更新器,用於動態的更新服務列表。
ServerListUpdater經過任務調度去定時實現更新操做。因此它有個惟一實現子類:PollingServerListUpdater。
PollingServerListUpdater動態服務器列表更新器要更新的默認實現,使用一個任務調度器ScheduledThreadPoolExecutor完成定時更新。
IPing
緩存到本地的服務實例信息有可能已經沒法提供服務了,這個時候就須要有一個檢測的組件,來檢測服務實例信息是否可用。
IPing就是用來客戶端用於快速檢查服務器當時是否處於活動狀態(心跳檢測)
ILoadBalancer
ILoadBalancer是整個Ribbon中最重要的一個環節,它將負載均衡器最核心的資源也就是全部的服務的獲取,更新,過濾,選擇等操做都能安排的妥穩當當。
package com.netflix.loadbalancer;import java.util.List;public interface ILoadBalancer { void addServers(List<Server> var1); Server chooseServer(Object var1); void markServerDown(Server var1); /** @deprecated */ @Deprecated List<Server> getServerList(boolean var1); List<Server> getReachableServers(); List<Server> getAllServers();}
ILoadBalancer最重要的重要是獲取全部的服務節點信息,或者是獲取可訪問的服務節點信息,而後經過ServerListFilter按照指定策略過濾服務節點列表,經過ServerListUpdater動態更新一組服務列表,經過IPing剔除非存活狀態下的服務節點以及根據IRule從現有服務器列表中選擇一個服務。
Ribbon選擇一個可用服務的詳細流程
經過上圖可知,流程以下:
-
經過ServerList從配置文件或者註冊中心獲取服務節點列表信息。
-
某些狀況下咱們可能須要經過經過ServerListFilter按照指定策略過濾服務節點列表。
-
爲了不每次都要去註冊中心或者配置文件中獲取服務節點信息,咱們會將過濾後的服務列表信息存到本地內存。此時若是新增服務節點或者是下線某些服務時,咱們須要經過ServerListUpdater來動態更新服務列表。
-
當有些服務節點已經沒法提供服務後,咱們會經過IPing(心跳檢測)來剔除服務。
-
最後ILoadBalancer 接口經過IRule指定的負載均衡算法去服務列表中選取一個服務。
Ribbon的使用方式
整體來講Ribbon 的使用方式分爲三種
第一種,使用原生API的方式
首先咱們建立一個RibbonClient工程,而後建立一個RibbonTest類:
RibbonTest
package javaer.study.RibbonTest;import com.netflix.loadbalancer.BaseLoadBalancer;import com.netflix.loadbalancer.LoadBalancerBuilder;import com.netflix.loadbalancer.RandomRule;import com.netflix.loadbalancer.Server;import com.netflix.loadbalancer.reactive.LoadBalancerCommand;import com.netflix.loadbalancer.reactive.ServerOperation;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;import rx.Observable;import java.util.Arrays;import java.util.List;/** * 測試Ribbon原生Api用法 * * @author javaMaster * 公衆號:【Java學習部落】 * @create 2020 09 11 * @Version 1.0.0 */@RestController@RequestMapping("/ribbon")public class RibbonTest {// @Autowired// private LoadBalancerClient loadBalancer; @Autowired private RestTemplate restTemplate; @GetMapping("/test") public String getMsg() { //使用Ribbon原生API調用服務 //手動建立服務列表,固然也能夠從註冊中心中獲取到服務列表 List<Server> serverList = Arrays.asList(new Server("localHost", 7777), new Server("localHost", 8888), new Server("localHost", 9999)); BaseLoadBalancer baseLoadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList); //設置負載均衡策略IRule,默認使用輪詢,此處咱們設置爲隨機策略 baseLoadBalancer.setRule(new RandomRule()); for (int i = 0; i < 10; i++) { String result = LoadBalancerCommand.<String>builder().withLoadBalancer(baseLoadBalancer).build() .submit(new ServerOperation<String>() { public Observable<String> call(Server server) { try { String addr = "http://" + server.getHost() + ":" + server.getPort(); System.out.println("當前調用的服務地址爲:" + addr); return Observable.just(""); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().first(); } }}
啓動項目,訪問 http://localhost:8008/ribbon/test,運行結果以下:
由於咱們設置的IRule是隨機策略,因此咱們看到訪問結果是從服務列表隨機獲取服務地址進行訪問。
第二種,當咱們整合了Spring-Cloud時,咱們就可使用Ribbon + RestTemplate來實現負載均衡。
由於咱們要實現經過Ribbon + RestTemplate經過指定的負載均衡的策略去選取某一個服務進行調用,因此咱們先來建立一個訂單服務OrderService。
首先咱們在配置文件中添加配置信息;
//指定服務名稱spring.application.name=order-service//指定EurekaServer的訪問地址eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
接下來建立一個OrderController
package javaer.study.controller;import org.springframework.beans.factory.annotation.Value;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;/** * 訂單服務控制類 * * @author javaMaster * 公衆號:【Java 學習部落】 * @create 2020 09 12 * @Version 1.0.0 */@RestControllerpublic class OrderController { @Value("${server.port}") private String port; /** * 返回一條消息 */ @GetMapping("/test") public String test() throws InterruptedException { Thread.sleep(3000); return "調用服務的地址的端口爲: " + port; }}
最後咱們在啓動類上加上@EnableEurekaClient註解;
package javaer.study;import org.springframework.boot.SpringApplication;import org.springframework.boot.WebApplicationType;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.builder.SpringApplicationBuilder;import org.springframework.cloud.netflix.eureka.EnableEurekaClient;@SpringBootApplication@EnableEurekaClientpublic class OrderserviceApplication { public static void main(String[] args) { new SpringApplicationBuilder(OrderserviceApplication.class).web(WebApplicationType.SERVLET).run(args); }}
代碼部分完成,而後咱們啓動上一篇文章中搭建的EurekaServer服務。
啓動成功後,咱們訪問 http://localhost:8761/,結果以下,咱們發現此時並有服務註冊到Eureka註冊中心。
而後咱們在下圖處分別配置7777,8888,9999三個端口啓動。
而後咱們刷新以前打開的 http://localhost:8761/ 頁面,咱們發現此時已經有三個服務名爲order-service,端口號分別7777,8888,9999的服務註冊了進來:
好了,多個服務已經搭建好了,接下來,咱們就要經過Ribbon+RestTemplate的方式從Eureka註冊中心中獲取服務列表,並經過負載均衡策略訪問指定的服務節點。
第一步,咱們依舊使用RibbonClient工程,咱們建立一個RestTemplateConfig類來配置RestTemplate實例。
RestTemplateConfig
package javaer.study.RibbonTest;import org.springframework.cloud.client.loadbalancer.LoadBalanced;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.client.RestTemplate;/** * RestTemplate配置類 * * @author javaMaster * 公衆號:【Java學習部落】 * @create 2020 09 14 * @Version 1.0.0 */@Configurationpublic class RestTemplateConfig { @Bean @LoadBalanced public RestTemplate restTemplate() { return new RestTemplate(); }}
眼尖的同窗確定已經發現了,咱們添加了一個@LoadBalanced註解,添加了該註解後,咱們就不須要使用IP+端口的形式去調用服務了,咱們能夠直接使用服務名而且自帶負載均衡功能去調用服務。
最後咱們修改RibbonTest代碼:
package javaer.study.RibbonTest;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.client.RestTemplate;/** * 測試Ribbon原生Api用法 * * @author javaMaster * 公衆號:【Java 學習部落】 * @create 2020 09 11 * @Version 1.0.0 */@RestControllerpublic class RibbonTest { @Autowired private RestTemplate restTemplate; @GetMapping("/test") public String getMsg() { String msg = restTemplate.getForObject("http://order-service/test", String.class); return msg; }}
最後咱們啓動RibbonClient項目,因爲咱們並無設置負載均衡策略,因此默認使用輪詢策略來調度服務。
項目啓動成功後,咱們訪問 http://localhost:8008/ribbon/test,刷新三次頁面咱們,訪問結果依次以下:
可能你的訪問順序並非按照我這個順序來的,可是必定是三個端口循環調用。
第三種,使用Ribbon+Fegin,這種方式後續會有專文講解,此處不作多演示。
Ribbon 飢餓加載(eager-load)模式
咱們在搭建完springcloud微服務時,常常會發生這樣一個問題:咱們服務消費方調用服務提供方接口的時候,第一次請求常常會超時,再次調用就沒有問題了。
爲何會這樣?
主要緣由是Ribbon進行客戶端負載均衡的Client並非在服務啓動的時候就初始化好的,而是在調用的時候纔會去建立相應的Client,因此第一次調用的耗時不只僅包含發送HTTP請求的時間,還包含了建立RibbonClient的時間,這樣一來若是建立時間速度較慢,同時設置的超時時間又比較短的話,從而就會很容易發生請求超時的問題。
解決方法
既然超時的緣由是第一次調用時還須要建立RibbonClient,那麼咱們能不能提早建立RibbonClient呢?
既然咱們都能想到,那麼SpringCloud開發者確定也能想到。
因此咱們能夠經過設置下面兩個屬性來提早建立RibbonClient:
//開啓Ribbon的飢餓加載模式ribbon.eager-load.enabled=true//指定須要飢餓加載的服務名ribbon.eager-load.clients=cloud-shop-userservice
Ribbon 總結
本文介紹了Ribbon的使用場景,介紹了Ribbon和Nginx的區別,從Ribbon的總體架構入手,詳細介紹了Ribbon的五大組件IRule,IPing,ServerList,ServerListFilter,ServerListUpdater。而且詳細說明的負載均衡器的核心接口ILoadBalancer。以及Ribbon的使用方式和Ribbon的飢餓加載模式。
Ribbon負載均衡是SpringCloud生態系統中不可缺乏的一環,也是面試中常常會出現的高頻面試題。
原創不易,若是你們喜歡,賞個分享點贊在看三連吧。和你們一塊兒成爲這世界上最優秀的人。