SpringCloud系列之客戶端負載均衡Netflix Ribbon

smileNicky發表於2020-07-31

1. 什麼是負載均衡?

負載均衡是一種基礎的網路服務,它的核心原理是按照指定的負載均衡演算法,將請求分配到後端服務叢集上,從而為系統提供並行處理和高可用的能力。提到負載均衡,你可能想到nginx。對於負載均衡,一般分為服務端負載均衡和客戶端負載均衡

  • 服務端負載均衡:在消費者和服務提供方中間使用獨立的代理方式進行負載,有硬體的負載均衡器,比如 F5,也有軟體,比如 Nginx。
    在這裡插入圖片描述

  • 客戶端負載均衡:所謂客戶端負載均衡,就是客戶端根據自己的請求情況做負載,本文介紹的Netflix Ribbon就是客戶端負載均衡的元件
    在這裡插入圖片描述

2. 什麼是Netflix Ribbon?

上一章的學習中,我們知道了微服務的基本概念,知道怎麼基於Ribbon+restTemplate的方式實現服務呼叫,接著上篇部落格,我們再比較詳細學習客戶端負載均衡Netflix Ribbon,學習本部落格之前請先學習上篇部落格,然後再學習本篇部落格

Ribbon 是由 Netflix 釋出的負載均衡器,它有助於控制 HTTP 和 TCP 的客戶端的行為。Ribbon 屬於客戶端負載均衡。

3. Netflix Ribbon實驗環境準備

環境準備:

  • JDK 1.8
  • SpringBoot2.2.1
  • SpringCloud(Hoxton.SR6)
  • Maven 3.2+
  • 開發工具
    • IntelliJ IDEA
    • smartGit

建立一個SpringBoot Initialize專案,詳情可以參考我之前部落格:SpringBoot系列之快速建立專案教程

可以引入Eureka Discovery Client,也可以單獨新增Ribbon
在這裡插入圖片描述

Spring Cloud Hoxton.SR6版本不需要引入spring-cloud-starter-netflix-ribbon,已經預設整合
在這裡插入圖片描述
也可以單獨新增Ribbon依賴:
在這裡插入圖片描述
本部落格的是基於spring-cloud-starter-netflix-eureka-client進行試驗,試驗前要執行eureka服務端,eureka服務提供者,程式碼請參考上一章部落格

補充:IDEA中多例項執行方法

step1:如圖,不要加上勾選
在這裡插入圖片描述

step2:指定不同的server埠和例項id,如圖:
在這裡插入圖片描述
啟動成功後,是可以看到多個例項的
在這裡插入圖片描述

4. Netflix Ribbon API使用

使用LoadBalancerClient

 @Autowired
    LoadBalancerClient loadBalancerClient;

    @Test
    void contextLoads() {
        ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER");
        URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort()));
        System.out.println(uri.toString());
    }

構建BaseLoadBalancer 例項例子:

 @Test
    void testLoadBalancer(){
        // 服務列表
        List<Server> serverList = Arrays.asList(new Server("localhost", 8083), new Server("localhost", 8084));
        // 構建負載例項
        BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder().buildFixedServerListLoadBalancer(serverList);
        loadBalancer.setRule(new RandomRule());
        for (int i = 0; i < 5; i++) {
            String result = LoadBalancerCommand.<String>builder().withLoadBalancer(loadBalancer).build()
                    .submit(new ServerOperation<String>() {
                        public Observable<String> call(Server server) {
                            try {
                                String address = "http://" + server.getHost() + ":" + server.getPort()+"/EUREKA-SERVICE-PROVIDER/api/users/mojombo";
                                System.out.println("呼叫地址:" + address);
                                return Observable.just("");
                            } catch (Exception e) {
                                return Observable.error(e);
                            }
                        }
                    }).toBlocking().first();
            System.out.println("result:" + result);
        }
    }

5. 負載均衡@LoadBalanced

Ribbon負載均衡實現,RestTemplate 要加上@LoadBalanced

package com.example.springcloud.ribbon.configuration;

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;

/**
 * <pre>
 *  RestConfiguration
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/07/31 09:43  修改內容:
 * </pre>
 */
@Configuration
public class RestConfiguration {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

yaml配置:

server:
  port: 8082
spring:
  application:
    name: eureka-service-consumer
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
    fetch-registry: true
    register-with-eureka: false
    healthcheck:
      enabled: false
  instance:
    status-page-url-path: http://localhost:8761/actuator/info
    health-check-url-path: http://localhost:8761/actuator//health
    prefer-ip-address: true
    instance-id: eureka-service-consumer8082

在這裡插入圖片描述
關鍵點,使用SpringCloud的@LoadBalanced,才能調http://EUREKA-SERVICE-PROVIDER/api/users/? 介面的資料,瀏覽器是不能直接調的


import com.example.springcloud.ribbon.bean.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.net.URI;

@SpringBootApplication
@EnableEurekaClient
@RestController
@Slf4j
public class SpringcloudRibbonApplication {


    @Autowired
    RestTemplate restTemplate;

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

    @GetMapping("/findUser/{username}")
    public User index(@PathVariable("username")String username){
        return restTemplate.getForObject("http://EUREKA-SERVICE-PROVIDER/api/users/"+username,User.class);
    }

}

在這裡插入圖片描述

6. 定製Netflix Ribbon client

具體怎麼定製?可以參考官網,@RibbonClient指定定製的配置類既可
在這裡插入圖片描述

package com.example.springcloud.ribbon.configuration;

import com.example.springcloud.ribbon.component.MyRule;
import com.netflix.loadbalancer.IPing;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PingUrl;
import org.springframework.cloud.netflix.ribbon.ZonePreferenceServerListFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * <pre>
 *   Ribbon Clients configuration
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改記錄
 *    修改後版本:     修改人:  修改日期: 2020/07/29 14:22  修改內容:
 * </pre>
 */
//@Configuration(proxyBeanMethods = false)
//@IgnoreComponentScan
public class RibbonClientConfiguration {

//    @Autowired
//    IClientConfig config;

    @Bean
    public IRule roundRobinRule() {
        return new MyRule();
    }

    @Bean
    public ZonePreferenceServerListFilter serverListFilter() {
        ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
        filter.setZone("myTestZone");
        return filter;
    }

    @Bean
    public IPing ribbonPing() {
        return new PingUrl();
    }

}

在Application類加上@RibbonClient,name是為服務名稱,跟bootstrap.yml配置的一樣既可

@RibbonClient(name = "eureka-service-provider",configuration = RibbonClientConfiguration.class)

特別注意:官網這裡特意提醒,這裡的意思是說@RibbonClient指定的配置類必須加@Configuration(不過在Hoxton.SR6版本經過我的驗證,其實是可以不加的,加了反而可能報錯),@ComponentScan掃描要排除自定義的配置類,否則,它由所有@RibbonClients共享。如果你使用@ComponentScan(或@SpringBootApplication)
在這裡插入圖片描述
其實就是想讓我們排除這個配置的全域性掃描,所以我們可以進行編碼,寫個註解類@IgnoreComponentScan ,作用於類,指定@Target(ElementType.TYPE)

package com.example.springcloud.ribbon.configuration;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface IgnoreComponentScan {

}

加上自定義的註解類
在這裡插入圖片描述

任何在Application加上程式碼,避免全域性掃描:

@ComponentScan(excludeFilters={@ComponentScan.Filter(type= FilterType.ANNOTATION,value= IgnoreComponentScan.class)})

7. Netflix Ribbon常用元件

ps:介紹Netflix Ribbon的負載策略之前,先介紹Netflix Ribbon常用元件及其作用:

元件 作用
ILoadBalancer 定義一系列的操作介面,比如選擇服務例項。
IRule 負載演算法策略,內建演算法策略來為服務例項的選擇提供服務。
ServerList 負責服務例項資訊的獲取(可以獲取配置檔案中的,也可以從註冊中心獲取。)
ServerListFilter 過濾掉某些不想要的服務例項資訊。
ServerListUpdater 更新本地快取的服務例項資訊。
IPing 對已有的服務例項進行可用性檢查,保證選擇的服務都是可用的。

8. 定製Netflix Ribbon策略

因為服務提供者是多例項的,所以再寫個介面測試,呼叫了哪個例項,來看看Netflix Ribbon的負載策略

@Autowired
    LoadBalancerClient loadBalancerClient;

    @GetMapping(value = {"/test"})
    public String test(){
        ServiceInstance serviceInstance = loadBalancerClient.choose("EUREKA-SERVICE-PROVIDER");
        URI uri = URI.create(String.format("http://%s:%s", serviceInstance.getHost() , serviceInstance.getPort()));
        System.out.println(uri.toString());
        return uri.toString();
    }

部署成功,多次呼叫,可以看到每次呼叫的服務例項都不一樣?其實Netflix Ribbon預設是按照輪詢的方式呼叫的
在這裡插入圖片描述

要定製Netflix Ribbon的負載均衡策略,需要實現AbstractLoadBalancerRule抽象類,下面給出類圖:
在這裡插入圖片描述
Netflix Ribbon內建瞭如下的負載均衡策略,引用https://juejin.im/post/6854573215587500045的歸納:
在這裡插入圖片描述

ok,接著我們可以在配置類,修改規則

@Bean
    public IRule roundRobinRule() {
        return new BestAvailableRule();
    }

測試,基本都是調8083這個例項,因為這個例項效能比較好
在這裡插入圖片描述
顯然,也可以自己寫個策略類,程式碼參考com.netflix.loadbalancer.RandomRule,網上也有很多例子,思路是修改RandomRule原來的策略,之前隨機調服務例項一次,現在改成每調5次後,再調其它的服務例項

package com.example.springcloud.ribbon.component;
 
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;


public class MyRule extends AbstractLoadBalancerRule
{
    // 總共被呼叫的次數,目前要求每臺被呼叫5次
    private int total = 0;
    // 當前提供服務的機器號
    private int index = 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;
            }

            //int index = chooseRandomInt(serverCount);
            //server = upList.get(index);
            if(total < 5)
            {
                server = upList.get(index);
                total++;
            }else {
                total = 0;
                index++;
                if(index >= upList.size())
                {
                    index = 0;
                }
            }

            if (server == null) {
                // 釋放執行緒
                Thread.yield();
                continue;
            }

            if (server.isAlive()) {
                return (server);
            }

            server = null;
            Thread.yield();
        }

        return server;
    }

    protected int chooseRandomInt(int serverCount) {
        return ThreadLocalRandom.current().nextInt(serverCount);
    }

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

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

修改IRule ,返回MyRule

 @Bean
    public IRule roundRobinRule() {
        return new MyRule();
    }

附錄:

ok,本部落格參考官方教程進行實踐,僅僅作為入門的學習參考資料,詳情可以參考Spring Cloud官方文件https://docs.spring.io/spring-cloud-netflix/docs/2.2.x-SNAPSHOT/reference/html/#customizing-the-ribbon-client

程式碼例子下載:code download

優質學習資料參考:

相關文章