為什么要使用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中最重要的一個環節,它將負載均衡器最核心的資源也就是所有的服務的獲取,更新,過濾,選擇等操作都能安排的妥妥當當。
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
*/
@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生態系統中不可缺少的一環,也是面試中經常會出現的高頻面試題。
原創不易,如果大家喜歡,賞個分享點贊在看三連吧。和大家一起成為這世界上最優秀的人。