Spring Could-03-負載均衡Ribbon

1. RestTemplate簡介

        RestTemplate 是Spring Resources 中一個訪問第三方RESTful   API 接口的網絡請求框架。RestTemplate 的設計原則和其他Spring Template (例如JdbcTemplate 、JmsTemplate )類似,都是爲執行復雜任務提供了一個具有默認行爲的簡單方法。
        RestTemplate 是用來消費REST 服務的,所以RestTemplate 的主要方法都與REST 的Http協議的一些方法緊密相連,例如HEAD 、GET 、POST 、PUT 、DELETE 和OPTIONS 等方法,這些方法在RestTemplate 類對應的方法爲headF or Headers ()、getForObject() 、postForObject () 、put()和delete () 等。
    RestTemplate 支持常見的Http 協議的請求方法,例如Post 、Put 、Delete 等,所以用RestTemplate 很容易構建RESTful API。 RestTemplate 的使用很簡單,它支持Xml 、JSON數據格式,默認實現了序列化,可以自動將JOSN 字符串轉換爲實體。

2. Ribbon簡介

        負載均衡是指將負載分攤到多個執行單元上,常見的負載均衡有兩種方式。一種是獨立進程單元,通過負載均衡策略,將請求轉發到不同的執行單元上,例如Ngnix 。另一種是將負載均衡邏輯以代碼的形式封裝到服務消費者的客戶端上,服務消費者客戶端維護了一份服務提供者的信息列表,有了信息列表,通過負載均衡策略將請求分攤給多個服務提供者,從而達到負載均衡的目的。
        Ribbon 是Netflix 公司開源的一個負載均衡的組件,它屬於上述的第二種方式,是將負載均衡邏輯封裝在客戶端中,並且運行在客戶端的進程裏。Ribbon 是一個經過了雲端測試的IPC 庫,司以很好地控制HTTP 和TCP 客戶端的負載均衡行爲。
        在Spring Cloud 構建的微服務系統中, Ribbon 作爲服務消費者的負載均衡器,有兩種使用方式, 一種是和RestTemplate 相結合,另一種是和Feign 相結合。Feign 已經默認集成了Ribbon,關於Feign 的內容將會在下一章進行詳細講解。
        Ribbon 有很多子模塊,但很多模塊沒有用於生產環境,目前Netflix 公司用於生產環境的Ribbon子模塊如下。

  • Ribbon-loadbalancer:可以獨立使用或與其他模塊一起使用的負載均衡器API 。
  • Ribbon-eureka :Ribbon 結合Eureka 客戶端的API ,爲負載均衡器提供動態服務註冊列表信息。
  • Ribbon-core: Ribbon 的核心API 。

3. 使用RestTemplate和Ribbon來消費服務

源碼:

鏈接:https://pan.baidu.com/s/14428_td2HyhWuNnF1oHFUg 
提取碼:vnoe

本案例是在上一章的基礎上進行改造的的(https://editor.csdn.net/md/?articleId=106909651)
啓動eureka-server,端口爲8761。啓動兩個eureka-client實例,端口爲8762和8763
idea中兩個eureka-client實例啓動方法如下:
在這裏插入圖片描述
將EurekaClientApplication複製兩份分別命名爲EurekaClient01,EurekaClient02,分別修改EurekaClient01,EurekaClient02的配置
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
啓動完成後,在瀏覽器上訪問: http://localhost:8761/,瀏覽器顯示eureka-client的兩個實例已經成功向服務註冊中心註冊,它們的端口分別爲8762和8763 ,如下圖:
在這裏插入圖片描述
再創建一個module工程eureka-ribbon-client,作爲服務消費者,通過RestTemplate來遠程調用eureka-client服務API接口的"hi",即消費服務。
pom.xml中引入依賴:

    <dependencies>
        <!--Eureka Client起步依賴-->
        <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>
        </dependency>
        <!--Web起步依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

在工程中配置application.yml文件的相關配置,包括指定程序名稱爲eureka-ribbon-client,程序的端口號爲8764,服務的註冊地址爲http://localhost:8761/eureka/,代碼如下:

spring:
  application:
    name: eureka-ribbon-client
server:
  port: 8764

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

在啓動類上加上註解@EnableEurekaClient開啓Eureka Client功能,代碼如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

/**
 * Created by : wyc
 * Created time 2020/6/27 7:28
 */
@EnableEurekaClient
@SpringBootApplication
public class EurekaRibbonClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaRibbonClientApplication.class,args);
    }
}

寫一個RESTFul API接口,在該API接口內部需要調用eureka-client的API接口"/hi",及服務消費。由於eureka-client爲兩個實例,它們的端口爲8762和8763。在調用eureka-client的API接口"/hi"時希望做到輪流訪問這兩個實例,這時候就需要RestTemplate和Ribbon相結合進行負載均衡。只需要在程序的IOC容器中注入一個restTemplate的Bean,並在這個Bean上加上@LoadBalanced註解,此時RestTemplate就結合了Ribbon來氣了負載均衡功能,代碼如下:

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;
/**
 * Created by : wyc
 * Created time 2020/6/27 9:44
 */
@Configuration
public class RibbonConfig {

    /**
     * 在此類中爲IoC 容器中注入一個RestTemplate 的Bean , 並在這個Bean 上加上@LoadBalanced 註解,此時RestTemplate 就結合了
     * Ribbon 開啓了負載均衡功能。
     * @return
     */
    @Bean
    @LoadBalanced
    RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

寫一個RibbonService類加上@Service註解,在該類的hi()方法用restTemplate調用eureka-client的API接口。 restTemplate中的Url不需要使用硬編碼(比如IP),只需要寫服務名eureka-client即可程序會根據服務名稱 eureka-client到Eureka-server註冊中心去自動獲取IP和端口信息。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

/**
 * Created by : wyc
 * Created time 2020/6/27 10:01
 */
@Service
public class RibbonService {

    @Autowired
    private RestTemplate restTemplate;


    /**
     * 在該類的hi()方法用restTemplate調用eureka-client的API接口  Uri 上不需要使用硬編碼(比如IP),只需要寫服務名eureka-client即可
     * 程序會根據服務名稱 eureka-client到Eureka-server註冊中心去自動獲取IP和端口信息。
     * @param name
     * @return
     */
    public String hi(String name){
        return restTemplate.getForObject("http://eureka-client/hi?name="+name,String.class);
    }
}

寫一個RibbonController類,爲該類加上@RestController 註解,開啓RestController 的功能,寫一個"/hi" Get 方法的接口,調用RibbonService 類的hi()方法。

import com.wyc.service.RibbonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by : wyc
 * Created time 2020/6/27 10:09
 */
@RestController
public class RibbonController {
    
    @Autowired
    private RibbonService ribbonService;
    
    @GetMapping("/hi")
    public String hi(@RequestParam(required = false,defaultValue = "wyc") String name){
        return this.ribbonService.hi(name);
    }
}

啓動eureka-server,兩個eureka-client,eureka-ribbon-client,訪問:http://localhost:8761,顯示的Eureka Server的主界面如下圖。在主界面上發現有兩個服務被註冊,分別爲eureka-client和eureka-ribbon-client,其中eureka-client有兩個實例,端口號分別爲8762和8763,eureka-ribbon-client的端口號爲8764。
在這裏插入圖片描述
在瀏覽器上多次訪問: http://localhost:8764/hi?name=wyc,瀏覽器會輪流顯示如下內容:

hi wyc,i am from port:8762
hi wyc,i am from port:8763

這時可以發現,當訪問http://localhost:8764/hi?name=wyc的API接口時,負載均衡器起到了作用,負載均衡器會輪流的請求eureka-client的兩個實例中 的"/hi"API接口。

4. LoadBalancerClient簡介

        負載均衡器的核心類爲LoadBalancerClient, LoadBalancerCiient 可以獲取負載均衡的服務提供者的實例信息。
        在RibbonController類中寫一個接口"/testRibbon", 通過LoadBalancerClient 去選擇一個eureka-client 的服務實例的信息, 並將該信息返回。

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/testRibbon")
    public String testRibbon(){
        ServiceInstance instance = loadBalancerClient.choose("eureka-client");
        return instance.getHost() + ":" + instance.getPort();
    }

重新啓動eureka-ribbon-client,在瀏覽器上多次訪問http://localhost:8764/testRibbon,瀏覽器會輪流顯示如下內容:

localhost:8762
localhost:8763

負載均衡器是怎樣獲取到這些客戶端的信息的呢?
        負載均衡器LoadBalancerClient 是從Eureka Client 獲取服務註冊列表信息的,並將服務註冊列表信息緩存了一份。在LoadBalancerClient 調用choose()方法時,根據負載均衡策略選擇一個服務實例的信息,從而進行了負載均衡。LoadBalancerClient 也可以不從Eureka Client 獲取註冊列表信息,這時需要自己維護一份服務註冊列表信息。
注意: eureka client從eureka server獲取註冊列表信息,LoadBalancerClient從本地的eureka client獲取註冊列表。
        爲進一步瞭解,再創建一個新工程ribbon-client
pom.xml中引入依賴


    <properties>
        <java.version>1.8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

啓動類

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * Created by : wyc
 * Created time 2020/6/27 22:07
 */
@SpringBootApplication
public class RibbonClientApplication {

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

配置application.yml

#有兩個不同Uri 地址(例如example.com 和google.com )的服務實例,通過stores.ribbon.listOfServers 來配置這些服務實例的Uri
stores:
  ribbon:
    listOfServers: example.com,google.com

# 設置通過eureka獲取服務註冊列表功能  關閉
ribbon:
  eureka:
   enabled: false

# 關閉eureka的自我註冊功能
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false

server:
  port: 8769

新建一個RestController類,創建一個API 接口“/testRibbon02 ”。在RestController 類注入 LoadBalancerClient,通過LoadBalancerClient 的choose(“stores”)方法獲取服務實例的信息。

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@RestController
public class RibbonController {

    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping("/testRibbon02")
    public String  testRibbon02() {
        ServiceInstance instance = loadBalancer.choose("stores");
        return instance.getHost()+":"+instance.getPort();
    }
}

啓動服務後,在瀏覽器上訪問http://localhost:8769/testRibbon, 瀏覽器顯示如下信息:

example.com:80
google.com:80

        現在我們可以知道,在Ribbon 中的負載均衡客戶端爲LoadBalancerClient 。在Spring Cloud項目中,負載均衡器Ribbon 會默認從Eureka Client 的服務註冊列表中獲取服務的信息,並緩存一份。根據緩存的服務註冊列表信息,可以通過LoadBalancerClient 來選擇不同的服務實例, 從而實現負載均衡。如果禁止Ribbon 從Eureka 獲取註冊列表信息,則需要自己去維護一份服務註冊列表信息。根據自己維護服務註冊列表的信息, Ribbon 也可以實現負載均衡。

5. 源碼解析Ribbon

    爲了深入理解Ribbon,現在從源碼的角度來講解Ribbon,看它如何和Eureka結合,並如何和RestTemplate相結合來做負載均衡.首先,跟蹤LoadBalancerClient的源碼,它是一個接口類,繼承了ServiceInstanceChooser,它的實現類爲RibbonLoadBalancerClient,它們的關係如下圖所示:
在這裏插入圖片描述
        LoadBalancerClient是一個負載均衡的客戶端,有3種方法,其中2個execute()方法,均用來執行請求,reconstructURI()用於重構Url,代碼如下:

public interface LoadBalancerClient extends ServiceInstanceChooser {
    <T> T execute(String var1, LoadBalancerRequest<T> var2) throws IOException;

    <T> T execute(String var1, ServiceInstance var2, LoadBalancerRequest<T> var3) throws IOException;

    URI reconstructURI(ServiceInstance var1, URI var2);
}

        ServiceInstanceChooser接口有一個方法用於根據serviceId獲取ServiceInstance,即通過服務名來選擇服務實例,代碼如下:

public interface ServiceInstanceChooser {
    ServiceInstance choose(String var1);
}

        LoadBalancerClient的實現類爲RibbonLoadBalancerClient。RibbonLoadBalancerClient是一個非常重要的類,最終負載均衡的請求處理由它執行。RibbonLoadBalancerClient的部分源碼如下:

public class RibbonLoadBalancerClient implements LoadBalancerClient {

    //省略代碼.........
    public ServiceInstance choose(String serviceId) {
        Server server = this.getServer(serviceId);
        return server == null ? null : new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
    }
      protected Server getServer(String serviceId) {
        return this.getServer(this.getLoadBalancer(serviceId));
    }

    protected Server getServer(ILoadBalancer loadBalancer) {
        return loadBalancer == null ? null : loadBalancer.chooseServer("default");
    }

    protected ILoadBalancer getLoadBalancer(String serviceId) {
        return this.clientFactory.getLoadBalancer(serviceId);
    }
}

        在RibbonLoadBalancerClient 的源碼中,choose()方法用於選擇具體服務實例。該方法通過getServer()方法去獲取實例,經過源碼追蹤,最終交給ILoadBalancer 類去選擇服務實例。
        ILoadBalancer 是一個接口,改接口定義了一系列實現負載均衡的方法,源碼如下:

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();
}
  • addServers() : 用於添加一個Server集合
  • chooseServer() : 用於根據key去獲取server集合
  • markServerDown() : 用於標記某個服務下線
  • getReachableServers() : 獲取可用的server集合
  • getAllServers() : 獲取所有的server集合
            ILoadBalancer 的子類是BaseLoadBalancer,BaseLoadBalancer的實現類是DynamicServerListLoadBalancer,三者關係如下圖:
    在這裏插入圖片描述
            查看DynamicServerListLoadBalancer源碼,DynamicServerListLoadBalancer需要配置IClientConfig 、IRule、 IPing、 ServerList、 ServerListFilter和ILoadBalancer。查看BaseLoadBalancer類的源碼,在默認的情況下,實現瞭如下配置:
  • IClientConfig ribbonClientConfig : DefaultClientConfigImpl
  • IRule ribbonRule : RoundRibbonRule
  • IPing ribbonPing : DummyPing
  • ServerList ribbonServerList : ConfigurationBasedServerList
  • ILoadBalancer ribbonLoadBalancer : ZoneAwareLoadBalancer
            IClientConfig 用於配置負載均衡的客戶端,IClientConfig 的默認實現類是DefaultClientConfigImpl。
            IRule用於配置負載均衡的策略,IRule有三個方法,其中choose()是根據key來獲取 server實例的,setLoadBalancer()和getLoadBalancer()是用來設置和獲取ILoadBalancer 的,它的源碼如下:
public interface IRule {
    Server choose(Object var1);

    void setLoadBalancer(ILoadBalancer var1);

    ILoadBalancer getLoadBalancer();
}

        IRule 有很多的默認實現類,這些實現類可以根據不同的算法和邏輯來處理負載均衡的策略.IRule 的默認實現類有以下7種.在大多數情況下,這些默認的實現類是可以滿足需求的,如果有特殊的需求,可以自己實現.IRule 和其它實現類之間的關係如下圖:
在這裏插入圖片描述

  • BestAvailableRule : 選擇最小請求數
  • ClientConfigEnabledRoundRibbonRule : 輪詢
  • RandomRule : 隨機選擇一個server
  • RoundRibbonRule : 輪詢選擇server
  • RetryRule : 根據輪詢的方式重試
  • WeightedResponseTimeRule : 根據響應時間去分配一個weight,weight越低,被選擇的可能性就越低。
            IPing 用於向server發送"ping",來判斷server是否響應,從而判斷該server是否可用,它有一個isAlive()方法,源碼如下:
public interface IPing {
    boolean isAlive(Server var1);
}

        IPing 的實現類有PingUrl,PingConstant,NoOpPing,DummyPing和NIWSDiscoveryPing。如下圖:
在這裏插入圖片描述

  • PingUrl : 真實地去ping某個url,判斷是否可用
  • PingConstant : 固定返回某服務是否可用,默認返回true,即可用
  • NoOpPing : 不去Ping,直接返回true,即可用
  • DummyPing : 直接返回true,並實現了initWithNiwsConfig方法
  • NIWSDiscoveryPing : 根據DiscoveryEnabledServer的InstanceInfo和InstanceStatus去判斷,如果InstanceStatus.UP,則可用,否則不可用。
            ServerList 是定義獲取所有server的註冊列表信息的接口,它的代碼如下:
public interface ServerList<T extends Server> {
    List<T> getInitialListOfServers();

    List<T> getUpdatedListOfServers();
}

        閱讀DynamicServerListLoadBalancer源碼,DynamicServerListLoadBalancer的構造函數中有一個initWithNiwsConfig()方法,在該方法中有一系列的初始化配置,最終執行了restOfInit()方法,部分源碼如下:

    public DynamicServerListLoadBalancer(IClientConfig clientConfig) {
        this.isSecure = false;
        this.useTunnel = false;
        this.serverListUpdateInProgress = new AtomicBoolean(false);
        this.updateAction = new UpdateAction() {
            public void doUpdate() {
                DynamicServerListLoadBalancer.this.updateListOfServers();
            }
        };
        this.initWithNiwsConfig(clientConfig);
    }

 public void initWithNiwsConfig(IClientConfig clientConfig) {
        try {
            super.initWithNiwsConfig(clientConfig);
            String niwsServerListClassName = clientConfig.getPropertyAsString(CommonClientConfigKey.NIWSServerListClassName, "com.netflix.loadbalancer.ConfigurationBasedServerList");
            ServerList<T> niwsServerListImpl = (ServerList)ClientFactory.instantiateInstanceWithClientConfig(niwsServerListClassName, clientConfig);
            this.serverListImpl = niwsServerListImpl;
            if (niwsServerListImpl instanceof AbstractServerList) {
                AbstractServerListFilter<T> niwsFilter = ((AbstractServerList)niwsServerListImpl).getFilterImpl(clientConfig);
                niwsFilter.setLoadBalancerStats(this.getLoadBalancerStats());
                this.filter = niwsFilter;
            }

            String serverListUpdaterClassName = clientConfig.getPropertyAsString(CommonClientConfigKey.ServerListUpdaterClassName, "com.netflix.loadbalancer.PollingServerListUpdater");
            this.serverListUpdater = (ServerListUpdater)ClientFactory.instantiateInstanceWithClientConfig(serverListUpdaterClassName, clientConfig);
            this.restOfInit(clientConfig);
        } catch (Exception var5) {
            throw new RuntimeException("Exception while initializing NIWSDiscoveryLoadBalancer:" + clientConfig.getClientName() + ", niwsClientConfig:" + clientConfig, var5);
        }
    }

        在restOfInit()方法中,有一個updateListOfServers()方法,該方法是用來獲取所有的ServerList的.

    void restOfInit(IClientConfig clientConfig) {
        boolean primeConnection = this.isEnablePrimingConnections();
        this.setEnablePrimingConnections(false);
        this.enableAndInitLearnNewServersFeature();
        this.updateListOfServers();
        if (primeConnection && this.getPrimeConnections() != null) {
            this.getPrimeConnections().primeConnections(this.getReachableServers());
        }

        this.setEnablePrimingConnections(primeConnection);
        LOGGER.info("DynamicServerListLoadBalancer for client {} initialized: {}", clientConfig.getClientName(), this.toString());
    }

        進一步跟蹤updateListOfServers()方法的源碼,最終由serverListImpl.getUpdatedListOfServers()獲取所有的服務列表,代碼如下:

    public void updateListOfServers() {
        List<T> servers = new ArrayList();
        if (this.serverListImpl != null) {
            servers = this.serverListImpl.getUpdatedListOfServers();
            LOGGER.debug("List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            if (this.filter != null) {
                servers = this.filter.getFilteredListOfServers((List)servers);
                LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}", this.getIdentifier(), servers);
            }
        }

        this.updateAllServerList((List)servers);
    }

        而serverListImpl是ServerList接口的具體實現類.ServerList的實現類爲DiscoveryEnabledNIWSServerList,該類有getInitialListOfServers(),getUpdatedListOfServers()方法,代碼如下:

    public List<DiscoveryEnabledServer> getInitialListOfServers() {
        return this.obtainServersViaDiscovery();
    }

    public List<DiscoveryEnabledServer> getUpdatedListOfServers() {
        return this.obtainServersViaDiscovery();
    }

        obtainServersViaDiscovery()方法是根據eurekaClientProvider.get()方法來獲取EurekaClient的,再根據EurekaClient來獲取服務註冊表信息,代碼如下;

    private List<DiscoveryEnabledServer> obtainServersViaDiscovery() {
        List<DiscoveryEnabledServer> serverList = new ArrayList();
        if (this.eurekaClientProvider != null && this.eurekaClientProvider.get() != null) {
            EurekaClient eurekaClient = (EurekaClient)this.eurekaClientProvider.get();
            if (this.vipAddresses != null) {
                String[] var3 = this.vipAddresses.split(",");
                int var4 = var3.length;

                for(int var5 = 0; var5 < var4; ++var5) {
                    String vipAddress = var3[var5];
                    List<InstanceInfo> listOfInstanceInfo = eurekaClient.getInstancesByVipAddress(vipAddress, this.isSecure, this.targetRegion);
                    Iterator var8 = listOfInstanceInfo.iterator();

                    while(var8.hasNext()) {
                        InstanceInfo ii = (InstanceInfo)var8.next();
                        if (ii.getStatus().equals(InstanceStatus.UP)) {
                            if (this.shouldUseOverridePort) {
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Overriding port on client name: " + this.clientName + " to " + this.overridePort);
                                }

                                InstanceInfo copy = new InstanceInfo(ii);
                                if (this.isSecure) {
                                    ii = (new Builder(copy)).setSecurePort(this.overridePort).build();
                                } else {
                                    ii = (new Builder(copy)).setPort(this.overridePort).build();
                                }
                            }

                            DiscoveryEnabledServer des = new DiscoveryEnabledServer(ii, this.isSecure, this.shouldUseIpAddr);
                            des.setZone(DiscoveryClient.getZone(ii));
                            serverList.add(des);
                        }
                    }

                    if (serverList.size() > 0 && this.prioritizeVipAddressBasedServers) {
                        break;
                    }
                }
            }

            return serverList;
        } else {
            logger.warn("EurekaClient has not been initialized yet, returning an empty list");
            return new ArrayList();
        }
    }

        其中,eurekaClientProvider 的實現類是LegacyEurekaClientProvider,它是一個獲取eurekaClient的實現類,代碼如下:

class LegacyEurekaClientProvider implements Provider<EurekaClient> {
    private volatile EurekaClient eurekaClient;

    LegacyEurekaClientProvider() {
    }

    public synchronized EurekaClient get() {
        if (this.eurekaClient == null) {
            this.eurekaClient = DiscoveryManager.getInstance().getDiscoveryClient();
        }

        return this.eurekaClient;
    }
}

        由此可見,負載均衡器是從EurekaClient獲取服務列表信息的,並根據IRule的策略去路由,根據Ping去判斷服務的可用性。
        那麼現在還有一個問題,負載均衡器每隔多長時間從Eureka Client獲取註冊信息的呢?
        在BaseLoadBalancer源碼中,BaseLoadBalancer的構造方法開啓了一個PingTask任務,代碼如下:

    public BaseLoadBalancer(String name, IRule rule, LoadBalancerStats stats, IPing ping, IPingStrategy pingStrategy) {
        //省略代碼.........
        this.setupPingTask();
       //省略代碼.........
    }

        在setupPingTask()的具體代碼邏輯裏,開啓了ShutdownEnabledTimer的PingTask任務,在默認情況下,變量pingIntervalSeconds的值爲10,即10秒向EurekaClient發送一次心跳"ping"

    void setupPingTask() {
        if (!this.canSkipPing()) {
            if (this.lbTimer != null) {
                this.lbTimer.cancel();
            }

            this.lbTimer = new ShutdownEnabledTimer("NFLoadBalancer-PingTimer-" + this.name, true);
            this.lbTimer.schedule(new BaseLoadBalancer.PingTask(), 0L, (long)(this.pingIntervalSeconds * 1000));
            this.forceQuickPing();
        }
    }

        查看PingTaskde 源碼,PingTask創建了一個Pinger對象,並執行了runPinger()方法。

    class PingTask extends TimerTask {
        PingTask() {
        }

        public void run() {
            try {
                (BaseLoadBalancer.this.new Pinger(BaseLoadBalancer.this.pingStrategy)).runPinger();
            } catch (Exception var2) {
                BaseLoadBalancer.logger.error("LoadBalancer [{}]: Error pinging", BaseLoadBalancer.this.name, var2);
            }

        }
    }

        查看Pinger的runPinger()方法,最終根據pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers)來獲取服務的可用性,如果該返回結果與之前相同,則不向EurekaClient獲取註冊列表,如果不同,則通知ServerStatusChangeListener服務註冊列表信息發生了改變,進行重新更新或者重新拉取,代碼如下:

   public void runPinger() throws Exception {
            if (BaseLoadBalancer.this.pingInProgress.compareAndSet(false, true)) {
                Server[] allServers = null;
                boolean[] results = null;
                Lock allLock = null;
                Lock upLock = null;

                try {
                    allLock = BaseLoadBalancer.this.allServerLock.readLock();
                    allLock.lock();
                    allServers = (Server[])BaseLoadBalancer.this.allServerList.toArray(new Server[BaseLoadBalancer.this.allServerList.size()]);
                    allLock.unlock();
                    int numCandidates = allServers.length;
                    boolean[] resultsx = this.pingerStrategy.pingServers(BaseLoadBalancer.this.ping, allServers);
                    List<Server> newUpList = new ArrayList();
                    List<Server> changedServers = new ArrayList();

                    for(int i = 0; i < numCandidates; ++i) {
                        boolean isAlive = resultsx[i];
                        Server svr = allServers[i];
                        boolean oldIsAlive = svr.isAlive();
                        svr.setAlive(isAlive);
                        if (oldIsAlive != isAlive) {
                            changedServers.add(svr);
                            BaseLoadBalancer.logger.debug("LoadBalancer [{}]:  Server [{}] status changed to {}", new Object[]{BaseLoadBalancer.this.name, svr.getId(), isAlive ? "ALIVE" : "DEAD"});
                        }

                        if (isAlive) {
                            newUpList.add(svr);
                        }
                    }

                    upLock = BaseLoadBalancer.this.upServerLock.writeLock();
                    upLock.lock();
                    BaseLoadBalancer.this.upServerList = newUpList;
                    upLock.unlock();
                    BaseLoadBalancer.this.notifyServerStatusChangeListener(changedServers);
                } finally {
                    BaseLoadBalancer.this.pingInProgress.set(false);
                }
            }
        }
    }

        由此可見,LoadBalancerClient是在初始化時向Eureka獲取服務註冊列表信息,並且每10秒向EurekaClient發送"ping",來判斷服務的可用性,如果服務的可用性發生了改變或者服務數量和之前的不一致,則更新或者重新拉取.LoadBalancerClient有了這些服務註冊列表信息,就可以根據具體的IRule的策略來進行負載均衡.
        最後,回到問題的本身,爲什麼在RestTemplate類的Bean上加一個@LoadBalance註解就可以使用Ribbon的負載均衡呢?
        LoadBalancerAutoConfiguration類(LoadBalancer自動配置類),代碼如下:

@Configuration
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
    @LoadBalanced
    @Autowired(
        required = false
    )
    private List<RestTemplate> restTemplates = Collections.emptyList();
    @Autowired(
        required = false
    )
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();

    public LoadBalancerAutoConfiguration() {
    }

    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> {
            restTemplateCustomizers.ifAvailable((customizers) -> {
                Iterator var2 = this.restTemplates.iterator();

                while(var2.hasNext()) {
                    RestTemplate restTemplate = (RestTemplate)var2.next();
                    Iterator var4 = customizers.iterator();

                    while(var4.hasNext()) {
                        RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next();
                        customizer.customize(restTemplate);
                    }
                }

            });
        };
    }

    @Bean
    @ConditionalOnMissingBean
    public LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {
        return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);
    }

    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryInterceptorAutoConfiguration {
        public RetryInterceptorAutoConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRetryProperties properties, LoadBalancerRequestFactory requestFactory, LoadBalancedRetryFactory loadBalancedRetryFactory) {
            return new RetryLoadBalancerInterceptor(loadBalancerClient, properties, requestFactory, loadBalancedRetryFactory);
        }

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }

    @Configuration
    @ConditionalOnClass({RetryTemplate.class})
    public static class RetryAutoConfiguration {
        public RetryAutoConfiguration() {
        }

        @Bean
        @ConditionalOnMissingBean
        public LoadBalancedRetryFactory loadBalancedRetryFactory() {
            return new LoadBalancedRetryFactory() {
            };
        }
    }

    @Configuration
    @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
        }

        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }

        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(LoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
}

        在LoadBalancerAutoConfiguration類中,首先維護了一個被@LoadBalanced修飾的RestTemplate對象的List.在初始化的過程中,通過調用 customizer.customize(restTemplate)方法來給Resttemplate增加攔截器LoadBalancerInterceptor.LoadBalancerInterceptor用於實時攔截,在LoadBalancerInterceptor中實現了負載均衡的方法.LoadBalancerInterceptor類的攔截方法代碼如下:

    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }

        綜上所述,Ribbon的負載均衡主要是通過LoadBalancerClient來實現的,而LoadBalancerClient具體交給了ILoadBalancer來處理,ILoadBalancer通過配置IRule,IPing等,向EurekaClient獲取註冊列表信息,默認沒10秒向EurekaClient發送一次"ping",進而檢查是否需要更新服務的註冊列表信息.最後,在得到服務註冊列表信息後,ILoadBalancer根據IRule的策略進行負載均衡。
        而RestTemplate加上@LoadBalance註解後,在遠程調度時能夠負載均衡,主要是維護了一個被@LoadBalance註解的RestTemplate列表,並給該列表中的RestTemplate對象添加了攔截器,在攔截器的方法中,將遠程調度方法交給了Ribbon的負載均衡器LoadBalancerClient去處理,從而達到了負載均衡的目的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章