萬字詳解Ribbon架構,針對面試高頻題多角度細說Ribbon
大咖揭祕Java人都栽在了哪?點選免費領取《大廠面試清單》,攻克面試難關~>>>
為什麼要使用Ribbon
上一章節我們學習了註冊中心《阿里面試官問我:到底知不知道什麼是Eureka,這次,我沒沉默》,我們知道但我們存在多個服務提供者的時候,我們會讓所有的服務提供者將服務節點資訊都註冊到EurekaServer中,然後讓客戶端去拉取一份服務註冊列表到本地,服務消費者會從服務註冊列表中找到合適的服務例項資訊,通過IP:Port的方式去呼叫服務。
那麼,消費者是如何決定呼叫哪一個服務例項呢,此時,本章節的主人公Ribbon默默的站了起來,笑著說,沒錯,正是在下。
Srping Cloud Ribbon 是基於 Netflix Ribbon實現的一套 客戶端負載均衡的工具。
簡單的說,Ribbon是Netflix釋出的開源頂目,主要功能是解析配置中或註冊中心的服務列表,通過客戶端的軟體負均衡演算法來實現服務請求的分發。
Ribbon客戶端元件提供一系列完善配置項如連線超時,重試等。
簡單的說,就是在配置檔案中列出LoadBalancer後面所有的機器,Ribbon會自動的幫助你基於某種規則(筒單輪洵,隨機連線等)去連線這些機器。
我們也很容易使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中最重要的一個環節,它將負載均衡器最核心的資源也就是所有的服務的獲取,更新,過濾,選擇等操作都能安排的妥妥當當。
packagecom.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
*/
@RestController
public 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
@EnableEurekaClient
public 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
*/
@Configuration
public 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
*/
@RestController
public 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生態系統中不可缺少的一環,也是面試中經常會出現的高頻面試題。
原創不易,如果大家喜歡,賞個分享點贊在看三連吧。和大家一起成為這世界上最優秀的人。