天天看點

SpringCloud(三)Ribbon負載均衡調用

寫在前面:
  • 你好,歡迎你的閱讀!
  • 我熱愛技術,熱愛分享,熱愛生活, 我始終相信:技術是開源的,知識是共享的!
  • 部落格裡面的内容大部分均為原創,是自己日常的學習記錄和總結,便于自己在後面的時間裡回顧,當然也是希望可以分享自己的知識。目前的内容幾乎是基礎知識和技術入門,如果你覺得還可以的話不妨關注一下,我們共同進步!
  • 除了分享部落格之外,也喜歡看書,寫一點日常雜文和心情分享,如果你感興趣,也可以關注關注!
  • 微信公衆号:傲驕鹿先生

說明:

1、專欄涉及到的源碼已經同步至 https://github.com/SetAlone/springcloud2020(持續更新)

2、springcloud系列博文内容為學習《尚矽谷2020最新版SpringCloud(H版&alibaba)架構開發教程》的記錄,此系列課程是目前看來個人覺得非常不錯的資源,在此可以與大家進行分享。

3、個人收集到了課程源碼和所需的腦圖、筆記等資源,如需要私信我即可。查找資源不易,希望可以給個點贊和關注

1.1 Ribbon是什麼

Spring Cloud Ribbon是基于Netflix Ribbon實作的一套 用戶端負載均衡 工具;

簡單的說,Ribbon是Netflix釋出的開源項目,主要功能是提供 用戶端軟體的負載均衡和服務調用。Ribbon用戶端元件提供一系列完善的配置項,如連接配接逾時,重試等。總之,就是在配置檔案中列出 Load Balancer (檢查LB)後面所有的機器,Ribbon會自動的幫助你基于某種規則(如簡單輪詢,随機連接配接等)去連接配接這些機器。我們很容易使用Ribbon實作自定義的負載均衡算法。

1.2 Ribbon能幹嘛

Ribbon主要是用來做負載均衡,簡單的說就是将使用者的請求配置設定到多個服務上,進而達到系統的HA(高可用)。常見的負載均衡軟體有:Nginx,LVS和硬體 F5等。

Ribbon本地負載均衡用戶端和Nginx服務端負載均衡的差別:

首先了解兩個概念:集中式負載均衡和程序内負載均衡

集中式負載均衡:在服務的消費方和服務提供方之間使用獨立的LB設施(可以是硬體F5,也可以是軟體,如Nginx),由該設施負責把通路請求通過某種政策轉發至服務的提供方;

程序内負載均衡:将負載均衡的邏輯 內建到消費方,消費方從服務注冊中心擷取有哪些位址可以調用,然後自己再從這些位址中選出一個合适的伺服器。Ribbon就屬于程序内LB,它隻是一個類庫,內建在消費方程序中,消費方通過它來擷取服務提供方的位址。

Nginx是服務端負載均衡,用戶端所有的請求都會交給Nginx,然後由Nginx實作請求的轉發;即負載均衡是由服務端實作的。

Ribbon的本地負載均衡,是在調用微服務接口的時候,會在注冊中心上擷取注冊資訊服務清單之後,緩存到JVM本地,進而在本地實作RPC遠端服務調用的技術。Ribbon實作負載均衡是通過:@LoadBalanced 和 RestTemplate 實作的

SpringCloud(三)Ribbon負載均衡調用

Ribbon的負載均衡機制在工作時分兩步:

1、優先選擇Eureka Server,它會優先選擇同一區域内負載較少的server;

2、然後根據使用者指定的政策(如輪詢,随機和根據響應時間權重等),從server的服務注冊清單中選擇一個位址。

總結:Ribbon其實就是一個軟負載均衡的用戶端元件, 他可以和其他所需請求的用戶端結合使用,和eureka結合隻是其中一個執行個體。

1.3 Ribbon整合(pom)

之前我們的Eureka做服務注冊和發現的時候,我們已經實作了Ribbon的負載均衡,但是我們的pom檔案中,并沒有引入Ribbon的坐标。這是因為我們的 ribbon坐标,依賴了eureka client ,是以隻要引入了eureka client,也就自動擁有的Ribbon的負載均衡;可以檢視eureka client的坐标的依賴關系:

點選檢視源碼的時候,發現在eureka client依賴中已近引入了ribbon的依賴

SpringCloud(三)Ribbon負載均衡調用
SpringCloud(三)Ribbon負載均衡調用

這樣的依賴關系在maven中看的更加明顯:

SpringCloud(三)Ribbon負載均衡調用

2.1 示範Ribbon的負載均衡

啟動我們之前的:cloud-eureka-server7001 和 cloud-eureka-server7002 的Eureka Server ;

然後啟動服務提供者:cloud-provider-payment8001 和 cloud-provider-payment8002;

最後啟動消費方:cloud-consumer-order80

浏覽器通路:http://eureka7001.com:7001/ 和 http://eureka7001.com:7002/ 能看到我們的80,8001和8002都注冊到了Eureka Server

然後浏覽器多次通路:http://localhost/consumer/payment/get/1 可以看到傳回的資訊中,端口是8001和8002交替出現的,這說明我們基于輪詢的負載均衡已經實作了。

2.2 RestTemplate的getForEntity和postForEntity

之前我們的cloud-consumer-order80的OrderController,我們使用的是RestTemplate的getForObject和postForObject方法,其實RestTemplate還有兩個類似的方法:getForEntity和postForEntity方法;

getForObject方法的傳回對象為響應體中的資料轉化成的對象,可以簡單的了解為json對象;

getForEntity方法傳回的是ResponseEntity對象,包含了響應中的一些重要資訊,比如響應頭,響應狀态碼和響應體等;

postForObject和postForEntity方法的差别也類似,

在cloud-consumer-order80的OrderController中,加上了使用getForEntity和postForEntity調用服務的方法:

@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();
        }else{
            return new CommonResult<>(444,"操作失敗");
        }
    }
           

重新啟動order80服務,浏覽器通路:

http://localhost/consumer/payment/getForEntity/1 和 http://localhost/consumer/payment/get/1 都是成功的

3.1 核心元件IRule

IRule:根據特定算法從服務清單中選取一個要通路的服務;

SpringCloud(三)Ribbon負載均衡調用

IRule有以下實作:

com.netflix.loadbalancer.RoundRobinRule:輪詢;(預設就是輪詢)

com.netflix.loadbalancer.RandomRule:随機;

com.netflix.loadbalancer.RetryRule:先按照RoundRobinRule的政策擷取服務,如果擷取服務失敗,則在指定時間内進行重試,擷取可用的服務;

WeightedResponseTimeRule:對RoundRobinRule的擴充,響應速度越快的執行個體選擇權重越多大,越容易被選擇;

BestAvailableRule:會先過濾掉由于多次通路故障而處于斷路器跳閘狀态的服務,然後選擇一個并發量最小的服務;

AvailabilityFilteringRule:先過濾掉故障執行個體,再選擇并發較小的執行個體;

ZoneAvoidanceRule:預設規則,複合判斷server所在區域的性能和server的可用性選擇伺服器;

3.2 如何設定負載均衡算法

Ribbon的預設負載均衡算法是 RoundRobinRule(輪詢),如果我們不想使用預設的,那麼怎麼去設定使用其他的的負載均衡算法呢?

3.2.1 注意配置細節

官方文檔明确給出了警告:

這個自定義配置類不能放在@ComponentScan所掃描的目前包以及子包下,否則我們自定義的這個配置 類就會被所有的Ribbon用戶端所共享,也就是說達不到特殊化定制的目的了!

SpringCloud(三)Ribbon負載均衡調用

添加Ribbon的配置類的時候,注意該類必須配置在**

@SpringBootApplication

主類以外的包下**。不然的話所有的服務都會按照這個規則來實作。會被所有的RibbonClient共享。主要是主類的主上下文和Ribbon的子上下文起沖突了。父子上下文不能重疊。

  • 自定義的負載均衡算法,不能在SpringBoot啟動時掃描到,即自定義的負載均衡類,不能放在啟動類的子包或啟動類所在包中。
  • 定義配置類将自定義的負載均衡算法注入Spring容器中。(配置類也不能被啟動類掃描到)
  • 啟動類上添加注解@RibbonClient(name=“微服務名”, configuration=“裝載自定義負載均衡算法的配置類”)。

3.2.2 編寫自定義的 IRule 配置類

建立一個包,該包不能是主啟動的子包

在該包下面建立一個MyRibbonRule 配置類:定義負載均衡的算法是随機;

@Configuration
public class MySelfRule
{
    @Bean
    public IRule myRule()
    {
        return new RandomRule();//定義為随機
    }
}
           

3.2.3 主啟動類上添加注解@RibbonClient

@SpringBootApplication
@EnableEurekaClient  //這是一個Eureka的client端
@EnableDiscoveryClient  //服務發現開啟
//在啟動該微服務的時候就能去加載我們的自定義Ribbon配置類,進而使配置生效
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE", configuration = MySelfRule.class)
public class OrderMain80 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain80.class,args);
    }
}
           

@RibbonClient(name = “CLOUD-PAYMENT-SERVICE”, configuration = MySelfRule.class) :告訴程式,我們要使用自定義的 随機 的負載算法。

3.2.4 測試

啟動7001,7002,8001,8002和order80,然後多次浏覽器通路:http://localhost/consumer/payment/get/1

效果是:不再是之前的輪詢一樣,交替傳回8001和8002提供服務,而是随機的,可能是多次都是8001,然後再是8002。

4.1 Ribbon負載均衡算法

4.1.1 iRule源碼

package com.netflix.loadbalancer;

public interface IRule {
    Server choose(Object var1);//選擇哪個服務處理請求

    void setLoadBalancer(ILoadBalancer var1);//設定ILoadBalancer 類型的變量資訊

    ILoadBalancer getLoadBalancer();//擷取ILoadBalancer 類型的變量資訊
}
           

IRule接口中有兩個類型值得注意:Server和ILoadBalancer。Server封裝了是注冊進Eureka的微服務資訊,也就代表注冊進Eureka的微服務。而ILoadBalancer是一個接口,用來擷取注冊進Eureka的全部或部分或某個微服務資訊,如下:

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();
}
           

到這裡可以看出,IRule接口是通過ILoadBalancer來擷取Server,進而實作負載均衡。下面我們以**RoundRobinRule(輪詢)**為例分析IRule如何實作負載均衡,以及我們如何自定義實作負載均衡.

4.1.2 AbstractLoadBalancerRule源碼

AbstractLoadBalancerRule 抽象類實作了IRule接口:

package com.netflix.loadbalancer;

import com.netflix.client.IClientConfigAware;

public abstract class AbstractLoadBalancerRule implements IRule, IClientConfigAware {
    private ILoadBalancer lb;

    public AbstractLoadBalancerRule() {
    }

    public void setLoadBalancer(ILoadBalancer lb) {
        this.lb = lb;
    }

    public ILoadBalancer getLoadBalancer() {
        return this.lb;
    }
}
           

4.1.3 RoundRobinRule輪詢算法源碼

package com.netflix.loadbalancer;

import com.netflix.client.config.IClientConfig;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class RoundRobinRule extends AbstractLoadBalancerRule {
    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() {
        this.nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        this.setLoadBalancer(lb);
    }

    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) {
                if (server == null && count++ < 10) {
                    List<Server> reachableServers = lb.getReachableServers();
                    List<Server> allServers = lb.getAllServers();
                    int upCount = reachableServers.size();
                    int serverCount = allServers.size();
                    if (upCount != 0 && serverCount != 0) {
                        int nextServerIndex = this.incrementAndGetModulo(serverCount);
                        server = (Server)allServers.get(nextServerIndex);
                        if (server == null) {
                            Thread.yield();
                        } else {
                            if (server.isAlive() && server.isReadyToServe()) {
                                return server;
                            }
                            server = null;
                        }
                        continue;
                    }
                    log.warn("No up servers available from load balancer: " + lb);
                    return null;
                }
                if (count >= 10) {
                    log.warn("No available alive servers after 10 tries from load balancer: " + lb);
                }
                return server;
            }
        }
    }

    private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextServerCyclicCounter.get();
            next = (current + 1) % modulo;
        } while(!this.nextServerCyclicCounter.compareAndSet(current, next));

        return next;
    }

    public Server choose(Object key) {
        return this.choose(this.getLoadBalancer(), key);
    }

    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}
           

4.2 原理分析

輪詢的負載均衡算法:Rest接口第幾次請求數 % 伺服器叢集總數量 = 實際調用伺服器位置下标,(每次服務重新開機後Rest接口計數從1開始)。

//通過服務别名擷取服務的執行個體
List<ServiceInstance> 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

當總請求數為1:2%2 = 0 對應的下标為0,則擷取的服務位址為 127.0.0.1:8002

當總請求數為1:3%2 = 1 對應的下标為1,則擷取的服務位址為 127.0.0.1:8001

當總請求數為1:4%2 = 0 對應的下标為0,則擷取的服務位址為 127.0.0.1:8002

依此類推…

4.3 自定義輪詢負載均衡算法

4.3.1 8001和8002的controller添加下面的測試方法

@GetMapping(value = "/payment/lb")
public String getPaymentLB() {
    return serverPort;
}
           

4.3.2 注釋掉order80的RestTemplate的@LoadBalanced注解

@Configuration
public class OrderConfig {

    /**
     * 注冊RestTemplate
     * @return
     */
    @Bean
    //@LoadBalanced  //注釋掉,使用自定義的Ribbon負載均衡算法
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}
           

4.3.3 自定義輪詢算法

接口:MyLoadBalancer

public interface MyLoadBalancer {
    /**
     * 擷取存活的服務執行個體清單
     * @param serviceInstances
     */
    ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
           

實作類:MyLoadBalancerImpl

/**
 * Ribbon手寫輪詢算法
 */
@Component
public class MyLoadBalancerImpl implements MyLoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement() {
        int current;
        int next;
        do {
            current = this.atomicInteger.get();
            // 超過最大值,為0,重新計數 2147483647 Integer.MAX_VALUE
            next = current >= 2147483647 ? 0 : current + 1;
            // 自旋鎖
        } while (!atomicInteger.compareAndSet(current, next));
        System.out.println("****第幾次通路,次數next:" + next);
        return next;
    }

    /**
     * 負載均衡算法:rest接口第幾次請求數%伺服器叢集總數量=實際調用伺服器位置下标,每次服務重新開機動後rest接口計數從1開始.
     *
     * @param serviceInstances
     */
    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}
           

4.3.4 OrderController添加方法

/**
 * 路由規則: 輪詢
 * http://localhost/consumer/payment/payment/lb
 */
@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 = myLoadBalancer.instances(instances);
    //擷取伺服器端uri
    URI uri = serviceInstance.getUri();
    return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
           

測試:http://localhost/consumer/payment/lb 也是8001和8002交替出現的,并且控制列印如下:

****第幾次通路,次數next:1
****第幾次通路,次數next:2
****第幾次通路,次數next:3
****第幾次通路,次數next:4
****第幾次通路,次數next:5
****第幾次通路,次數next:6
****第幾次通路,次數next:7
****第幾次通路,次數next:8
****第幾次通路,次數next:9
****第幾次通路,次數next:10
****第幾次通路,次數next:11
****第幾次通路,次數next:12
****第幾次通路,次數next:13
           

繼續閱讀