1. 程式人生 > 實用技巧 >SpringCloud學習筆記【八】:Ribbon負載均衡服務呼叫

SpringCloud學習筆記【八】:Ribbon負載均衡服務呼叫

目錄

本篇要點

  • 介紹Ribbon的基本功能。
  • 介紹負載均衡的相關概念。
  • 演示Ribbon負載均衡。
  • 學習Ribbon預設自帶的負載均衡規則。
  • 學習輪詢演算法原理。

Ribbon是什麼?

Ribbon是Netflix釋出的開源專案,主要功能是提供客戶端的軟體負載均衡演算法和服務呼叫

,將Netflix的中間層服務連線在一起。

Ribbon客戶端元件提供一系列完善的配置項如連線超時,重試等。簡單的說,就是在配置檔案中列出Load Balancer(簡稱LB)後面所有的機器,Ribbon會自動的幫助你基於某種規則(如簡單輪詢,隨機連線等)去連線這些機器。我們很容易使用Ribbon實現自定義的負載均衡演算法。

目前Ribbon專案的狀態處於:維護中。

https://github.com/Netflix/ribbon/wiki

Spring Cloud Ribbon是基於Netflix Ribbon實現的一套客戶端負載均衡的工具。

LoadBalance負載均衡

負載均衡簡單的說就是將使用者的請求平攤的分配到多個服務上,從而達到系統的HA【高可用】。

常見的負載均衡有軟體Nginx,LVS,硬體 F5等。

Ribbon與Nginx負載均衡的區別

Nginx是伺服器負載均衡,客戶端所有請求都會交給nginx,nginx實現轉發請求,負載均衡由服務端實現。

Ribbon是本地負載均衡,在呼叫微服務介面時候,會在註冊中心上獲取註冊資訊服務列表之後快取到JVM本地,從而在本地實現RPC遠端服務呼叫。

集中式LB與程序內LB

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

程序內負載均衡:將LB邏輯整合到消費方,消費方從服務註冊中心獲知有哪些地址可用,然後自己再從這些地址中選擇出一個合適的伺服器。
Ribbon就屬於程序內LB

,它只是一個類庫,集成於消費方程序,消費方通過它來獲取到服務提供方的地址。

Ribbon負載均衡演示

Ribbon是一個軟負載均衡客戶端元件,它可以和其他所需請求的客戶端結合使用,和Eureka結合只是其中一個例項。

Ribbon工作步驟

  1. 選擇EurekaServer,優先選擇在同一區域內負載較少的server。
  2. 根據使用者指定的策略,從server獲取到的服務註冊列表中選擇一個地址。策略包括:輪詢,隨機,根據響應時間加權。

整合Ribbon

我們要整合Ribbon,當然需要引入Ribbon響應的依賴:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
        </dependency>

事實上,我們之前Eureka的例子,通過80埠,輪詢訪問8001和8002埠,就是客戶端負載均衡的體現,我們之前引入的依賴如下:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

其實就已經整合了Ribbon,:

    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
      <version>2.2.6.RELEASE</version>
      <scope>compile</scope>
    </dependency>

這就是為什麼我們不用顯式地去引入Ribbon的依賴,我們也可以知道Ribbon的實現其實就是:負載均衡+RestTemplate呼叫

RestTemplate

https://docs.spring.io/spring-framework/docs/5.2.6.RELEASE/javadoc-api/org/springframework/web/client/RestTemplate.html

getForEntity:返回物件為ResponseEntity物件,包含了響應中的一些重要資訊,如響應頭,響應狀態碼,響應體等。

getForObject:返回物件為響應體中資料轉化成的物件,基本可以理解為json。

Ribbon預設自帶的負載規則IRule

com.netflix.loadbalancer.IRule介面定義了負載均衡的策略,包括輪詢,響應時間加權等等。

/**
 * Interface that defines a "Rule" for a LoadBalancer. A Rule can be thought of
 * as a Strategy for loadbalacing. Well known loadbalancing strategies include
 * Round Robin, Response Time based etc.
 * 
 * @author stonse
 * 
 */
public interface IRule{
    /*
     * choose one alive server from lb.allServers or
     * lb.upServers according to key
     * 
     * @return choosen Server object. NULL is returned if none
     *  server is available 
     */

    public Server choose(Object key);
    
    public void setLoadBalancer(ILoadBalancer lb);
    
    public ILoadBalancer getLoadBalancer();    
}

  • com.netflix.loadbalancer.RoundRobinRule:輪詢,最有名也是最主要的負載均衡策略。
  • com.netflix.loadbalancer.RandomRule:隨機,從存在的servers中隨機找一個。
  • com.netflix.loadbalancer.RetryRule:先按照輪詢的策略獲取服務,獲取失敗則在指定時間內獲取服務。
  • com.netflix.loadbalancer.WeightedResponseTimeRule:對RoundRobinRule的擴充套件,響應速度越快的權重越大,越容易被選擇。
  • com.netflix.loadbalancer.BestAvailableRule:先過濾掉由於多次訪問故障而處於斷路器跳閘狀態的服務,然後選擇一個併發量最小的服務。
  • com.netflix.loadbalancer.AvailabilityFilteringRule:先過濾故障例項,再選擇併發較小的例項。
  • com.netflix.loadbalancer.ZoneAvoidanceRule:複合判斷server所在區域的效能和server的可用性選擇伺服器。

Ribbon如何更改負載規則

我們需要在@ComponentScan掃描不到的包下定義配置類,否則該配置類就會被所有Ribbon客戶端所共享,因而達不到定製的效果。

定製規則

包結構如下:

啟動類掃描com.hyh.springcloud包及其子包,我們配置在com.hyh.rules包下。

標識客戶端

@EnableEurekaClient
@SpringBootApplication
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MyRule.class)
public class Order80Application {

    public static void main(String[] args) {
        SpringApplication.run(Order80Application.class, args);
    }
}

指定為RibbonClient,name訪問CLOUD-PAYMENT-SERVICE提供的服務,configuration指定定義的規則。

再次測試,發現負載均衡的規則已經成為隨機獲取server。

Ribbon負載均衡演算法

輪詢原理

看看最重要也是最基礎的輪詢演算法吧,大致思想就是:rest介面第幾次請求數 % 伺服器叢集總數量 = 實際呼叫伺服器位置下標,每次重啟伺服器後rest介面計數從1開始。

我們不妨開啟原始碼看一下,可能會更加清楚一些:

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() {
        // 初始化AtmoicInteger = 0
        nextServerCyclicCounter = new AtomicInteger(0);
    }

    public RoundRobinRule(ILoadBalancer lb) {
        this();
        // 初始化設定LoadBalancer
        setLoadBalancer(lb);
    }

    public Server choose(ILoadBalancer lb, Object key) {
        // 不存在LoadBalancer
        if (lb == null) {
            log.warn("no load balancer");
            return null;
        }
		// server代表最終會被選擇的
        Server server = null;
        // 嘗試的次數
        int count = 0;
        while (server == null && count++ < 10) {
            // 獲取up and reachable的servers
            List<Server> reachableServers = lb.getReachableServers();
            // 所有的servers
            List<Server> allServers = lb.getAllServers();
            int upCount = reachableServers.size();
            int serverCount = allServers.size();
			// 不滿足選擇的條件,直接報錯+返回
            if ((upCount == 0) || (serverCount == 0)) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
			// 原子操作,獲取索引
            int nextServerIndex = incrementAndGetModulo(serverCount);
            // 取出server
            server = allServers.get(nextServerIndex);

            if (server == null) {
                /* Transient. */
                Thread.yield();
                continue;
            }
			// 滿足條件,返回server
            if (server.isAlive() && (server.isReadyToServe())) {
                return (server);
            }

            // Next.
            server = null;
        }

        if (count >= 10) {
            log.warn("No available alive servers after 10 tries from load balancer: "
                    + lb);
        }
        return server;
    }

    /**
     * Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}.
     *
     * @param modulo The modulo to bound the value of the counter.
     * @return The next value.
     */
    private int incrementAndGetModulo(int modulo) {
        for (;;) {
            int current = nextServerCyclicCounter.get();
            // 取餘
            int next = (current + 1) % modulo;
            // CAS 操作
            if (nextServerCyclicCounter.compareAndSet(current, next))
                return next;
        }
    }

    @Override
    public Server choose(Object key) {
        return choose(getLoadBalancer(), key);
    }

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }
}

嘗試模擬輪詢演算法

@Component
public class MyLoadBalancer implements LoadBalancer {

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    public final int getAndIncrement(){
        int current;
        int next;
        do{
            current = this.atomicInteger.get();
            next = current >= Integer.MAX_VALUE ? 0 : current + 1;
        }while (!this.atomicInteger.compareAndSet(current,next));
        System.out.println("訪問次數 next : " + next);
        return next;
    }

    @Override
    public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
        int index = getAndIncrement() % serviceInstances.size();
        return serviceInstances.get(index);
    }
}

@RestController
@Slf4j
public class OrderController {
    @Resource
    private RestTemplate restTemplate;

    @Resource
    private LoadBalancer loadBalancer;

    @Resource
    private DiscoveryClient discoveryClient;
    @GetMapping("/consumer/payment/lb")
    public String getPaymentLb() {
        List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
        if (instances == null || instances.size() == 0) {
            return null;
        }
        ServiceInstance instance = loadBalancer.instances(instances);
        URI uri = instance.getUri();
        return restTemplate.getForObject(uri + "/payment/lb", String.class);

    }
}

原始碼下載

本系列文章為《尚矽谷SpringCloud教程》的學習筆記【版本稍微有些不同,後續遇到bug再做相關說明】,主要做一個長期的記錄,為以後學習的同學提供示例,程式碼同步更新到Gitee:https://gitee.com/tqbx/spring-cloud-learning,並且以標籤的形式詳細區分每個步驟,這個系列文章也會同步更新。