1. 程式人生 > 其它 >(十)、Ribbon負載均衡和呼叫

(十)、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 instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");

如:

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交替出現。