(十)、Ribbon負載均衡和呼叫
Ribbon入門介紹
Spring Cloud Ribbon是基於Netflix Ribbon實現的一套客戶端負載均衡的工具。
簡單的說,Ribbon是Netflix釋出的開源專案,主要功能是提供客戶端的軟體負載均衡演算法和服務呼叫。Ribbon客戶端元件提供一系列完善的配置項如連線超時,重試等。
簡單的說,就是在配置檔案中列出Load Balancer(簡稱LB)後面所有的機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連線等)去連線這些機器。我們很容易使用Ribbon實現自定義的負載均衡演算法。
https://github.com/Netflix/ribbon/wiki/Getting-Started
Ribbon目前也進入維護模式。
Ribbon未來可能被Spring Cloud LoadBalacer替代。
LB負載均衡(Load Balance)是什麼
簡單的說就是將使用者的請求平攤的分配到多個服務上,從而達到系統的HA (高可用)。
常見的負載均衡有軟體Nginx,LVS,硬體F5等。
Ribbon本地負載均衡客戶端VS Nginx服務端負載均衡區別
Nginx是伺服器負載均衡,客戶端所有請求都會交給nginx,然後由nginx實現轉發請求。即負載均衡是由服務端實現的。
Ribbon本地負載均衡,在呼叫微服務介面時候,會在註冊中心上獲取註冊資訊服務列表之後快取到JVM本地,從而在本地實現RPC遠端服務呼叫技術。
集中式LB
即在服務的消費方和提供方之間使用獨立的LB設施(可以是硬體,如F5, 也可以是軟體,如nginx),由該設施負責把訪問請求通過某種策略轉發至服務的提供方;
程序內LB
將LB邏輯整合到消費方,消費方從服務註冊中心獲知有哪些地址可用,然後自己再從這些地址中選擇出一個合適的伺服器。
Ribbon就屬於程序內LB,它只是一個類庫,集成於消費方程序,消費方通過它來獲取到服務提供方的地址。
一句話
負載均衡 + RestTemplate呼叫
Ribbon的負載均衡和Rest呼叫
架構說明
總結:Ribbon其實就是一個軟負載均衡的客戶端元件,它可以和其他所需請求的客戶端結合使用,和Eureka結合只是其中的一個例項。
Ribbon在工作時分成兩步:
- 第一步先選擇EurekaServer ,它優先選擇在同一個區域內負載較少的server。
- 第二步再根據使用者指定的策略,在從server取到的服務註冊列表中選擇一個地址。
其中Ribbon提供了多種策略:比如輪詢、隨機和根據響應時間加權。
POM
先前工程專案沒有引入spring-cloud-starter-ribbon也可以使用ribbon。
<dependency>
<groupld>org.springframework.cloud</groupld>
<artifactld>spring-cloud-starter-netflix-ribbon</artifactid>
</dependency>
這是因為spring-cloud-starter-netflix-eureka-client自帶了spring-cloud-starter-ribbon引用。
RestTemplate的使用
RestTemplate Java Doc
getForObject() / getForEntity() - GET請求方法
getForObject():返回物件為響應體中資料轉化成的物件,基本上可以理解為Json。
getForEntity():返回物件為ResponseEntity物件,包含了響應中的一些重要資訊,比如響應頭、響應狀態碼、響應體等。
在cloud-consumer-order80模組測試getForEntity方法
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id)
{
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();//getForObject()
}else{
return new CommonResult<>(444,"操作失敗");
}
}
測試
Ribbon預設自帶的負載規則
lRule:根據特定演算法中從服務列表中選取一個要訪問的服務
- RoundRobinRule 輪詢
- RandomRule 隨機
- RetryRule 先按照RoundRobinRule的策略獲取服務,如果獲取服務失敗則在指定時間內會進行重
- WeightedResponseTimeRule 對RoundRobinRule的擴充套件,響應速度越快的例項選擇權重越大,越容易被選擇
- BestAvailableRule 會先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務
- AvailabilityFilteringRule 先過濾掉故障例項,再選擇併發較小的例項
- ZoneAvoidanceRule 預設規則,複合判斷server所在區域的效能和server的可用性選擇伺服器
Ribbon負載規則替換
1.修改cloud-consumer-order80
2.注意配置細節官方文件明確給出了警告
這個自定義配置類不能放在@ComponentScan所掃描的當前包下以及子包下,
否則我們自定義的這個配置類就會被所有的Ribbon客戶端所共享,達不到特殊化定製的目的了。(也就是說不要將Ribbon配置類與主啟動類同包)
3.新建package - com.ylc.myrule
4.在com.ylc.myrule下新建MySelfRule規則類
package com.ylc.myrule;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MySelfRule {
public IRule myRule()
{
return new RandomRule();//定義為隨機
}
}
5.主啟動類新增@RibbonClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
6.測試
開啟cloud-eureka-server7001,cloud-consumer-order80,cloud-provider-payment8001,cloud-provider-payment8002
瀏覽器-輸入http://localhost/consumer/payment/get/1
返回結果中的serverPort在8001與8002兩種間隨機出現
Ribbon預設負載輪詢演算法原理
預設負載輪訓演算法: rest介面第幾次請求數 % 伺服器叢集總數量 = 實際呼叫伺服器位置下標,每次服務重啟動後rest介面計數從1開始。
List
如:
List [0] instances = 127.0.0.1:8002
List [1] instances = 127.0.0.1:8001
8001+ 8002組合成為叢集,它們共計2臺機器,叢集總數為2,按照輪詢演算法原理:
當總請求數為1時:1%2=1對應下標位置為1,則獲得服務地址為127.0.0.1:8001
當總請求數位2時:2%2=О對應下標位置為0,則獲得服務地址為127.0.0.1:8002
當總請求數位3時:3%2=1對應下標位置為1,則獲得服務地址為127.0.0.1:8001
當總請求數位4時:4%2=О對應下標位置為0,則獲得服務地址為127.0.0.1:8002
如此類推…
RoundRobinRule原始碼分析
IRule介面
根據特定演算法從服務列表中選取一個將要訪問的服務
package com.netflix.loadbalancer;
public interface IRule {
//叢集選擇
Server choose(Object var1);
void setLoadBalancer(ILoadBalancer var1);
ILoadBalancer getLoadBalancer();
}
RoundRobinRule
public class RoundRobinRule extends AbstractLoadBalancerRule {
//AtomicInteger原子整型類,用於計算對應請求呼叫伺服器的下標
private AtomicInteger nextServerCyclicCounter;
private static final boolean AVAILABLE_ONLY_SERVERS = true;
private static final boolean ALL_SERVERS = false;
private static Logger log = LoggerFactory.getLogger(RoundRobinRule.class);
public RoundRobinRule() {
//初始為0
this.nextServerCyclicCounter = new AtomicInteger(0);
}
public RoundRobinRule(ILoadBalancer lb) {
this();
this.setLoadBalancer(lb);
}
//負載均衡核心方法 返回的server物件即為選定使用的呼叫伺服器
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;//初始化
int count = 0;
while(true) {
//還沒選到執行的server,並且選擇的次數沒超過10次,進行選擇server
if (server == null && count++ < 10) {
//返回所有可用的服務例項,即狀態為up的
List<Server> reachableServers = lb.getReachableServers();
//請求的服務下所有服務例項
List<Server> allServers = lb.getAllServers();
//例項的數量
int upCount = reachableServers.size();
int serverCount = allServers.size();
//如果有可用的服務
if (upCount != 0 && serverCount != 0) {
//直接計算返回了呼叫伺服器server的下標
int nextServerIndex = this.incrementAndGetModulo(serverCount);
//根據下標獲取伺服器
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
//當選取的server存活並可用
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
//如果沒有可用的服務
log.warn("No up servers available from load balancer: " + lb);
return null;
}
//選擇超過10次 無法獲取到server,列印日誌
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
//計算返回呼叫伺服器server下標的關鍵方法
private int incrementAndGetModulo(int modulo) {
int current;
int next;
do {
//獲取value - CAS中對應舊的預期值 初始為0
current = this.nextServerCyclicCounter.get();
//加1取餘 (0+1)%2=1
next = (current + 1) % modulo;
//進行CAS判斷,保證上一步計算過程中,沒有被其他執行緒或外部修改。若當前nextServerCyclicCounter與current值相同,則為true並將其設定為計算後的next。
} while(!this.nextServerCyclicCounter.compareAndSet(current, next));//自旋鎖
return next;
}
public Server choose(Object key) {
return this.choose(this.getLoadBalancer(), key);
}
public void initWithNiwsConfig(IClientConfig clientConfig) {
}
}
CAS : Conmpare And Swap是用於實現多執行緒同步的原子指令。CAS機制當中使用了3個基本運算元:記憶體地址V,舊的預期值A,要修改的新值B。更新一個變數的時候,只有當變數的預期值A和記憶體地址V當中的實際值相同時,才會將記憶體地址V對應的值修改為B
Ribbon之手寫輪詢演算法
-
7001/7002叢集啟動
-
8001/8002微服務改造- controller
@GetMapping(value = "/payment/lb") public String getPaymentLB() { return serverPort;//返回服務介面 }
80訂單微服務改造
1.ApplicationContextConfig去掉註解@LoadBalanced,OrderMain80去掉註解@RibbonClient
2.建立LoadBalancer介面
package com.ylc.cloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
public interface LoadBalancer {
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
3.實現LoadBalancer介面
package com.ylc.cloud.lb;
import org.springframework.cloud.client.ServiceInstance;
import java.util.List;
public class MyLb implements LoadBalancer{
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
return null;
}
}
4.controller
@Resource
private LoadBalancer loadBalancer;
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/consumer/payment/lb")
public String getPaymentLB()
{
List<ServiceInstance> instances = discoveryClient.getInstances("cloud-payment-service");
if(instances == null || instances.size() <= 0){
return null;
}
ServiceInstance serviceInstance = loadBalancer.instances(instances);
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri+"/payment/lb",String.class);
}
5.測試 不停地重新整理http://localhost/consumer/payment/lb,可以看到8001/8002交替出現。