③SpringCloud 實戰:使用 Ribbon 客戶端負載均衡

這是SpringCloud實戰系列中第三篇文章,瞭解前面第兩篇文章更有助於更好理解本文內容:html

①SpringCloud 實戰:引入Eureka組件,完善服務治理java

②SpringCloud 實戰:引入Feign組件,發起服務間調用git

簡介

Ribbon 是由 Netflix 發佈的一個客戶端負載均衡器,它提供了對 HTTP 和 TCP 客戶端行爲的大量控制。Ribbon 能夠基於某些負載均衡的算法,自動爲客戶端選擇發起理論最優的網絡請求。常見的負載均衡算法有:輪詢,隨機,哈希,加權輪詢,加權隨機等。github

客戶端負載均衡的意思就是發起網絡請求的端根據本身的網絡請求狀況來作相應的負載均衡策略,與之相對的非客戶端負載均衡就有好比硬件F五、軟件Nginx,它們更可能是介於消費者和提供者之間的,並不是客戶端。算法

改造eureka-provider項目

在使用以前咱們先把第二節裏面的 eureka-provider 項目改造一下,在HelloController 裏面新增一個接口,輸出本身項目的端口信息,用於區別驗證待會兒客戶端負載均衡時所調用的服務。spring

  1. 新增接口方法,返回本身的端口號信息:api

    @Controller
    public class HelloController{
        @Value("${server.port}")
        private int serverPort;
    	...
    		
    	@ResponseBody
        @GetMapping("queryPort")
        public String queryPort(){
            return "hei, jinglingwang, my server port is:"+serverPort;
        }
    }
  2. 分別以8082,8083,8084端口啓動該項目:eureka-provider
    下圖是 IDEA 快速啓動三個不一樣端口項目方法截圖,固然你也能夠用其餘辦法網絡

  3. 而後啓動,訪問三個接口測試一下是否正常返回了對應端口app

至此,服務提供者的接口準備工做就作好了。負載均衡

新建Ribbon-Client 項目

咱們使用 Spring Initializr 生成SpringCloud項目基礎框架,而後修改pom.xml裏面的SpringBoot和SpringCloud的版本,對應版本修改請求以下:

...
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.3.RELEASE</version> <!--修改了版本jinglingwang.cn-->
    <relativePath/> <!-- lookup parent from repository -->
</parent>
... 略
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.SR4</spring-cloud.version><!--修改了版本-->
</properties>
... 略
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

爲何要單獨修改版本呢?由於從 Spring Cloud Hoxton.M2 版本開始,Spring Cloud 已經再也不默認使用Ribbon來作負載均衡了,而是使用 spring-cloud-starter-loadbalancer替代。因此咱們在使用 Spring Initializr 生成項目框架的時,若是使用最新版本Spring Cloud將再也不提供Ribbon相關的組件。須要咱們本身引入或者使用低一點的版本。

以後就是在ribbon-client項目引入eureka-client依賴和openfeign的依賴,這個過程省略,若是不會的話請看前兩篇文章。

Ribbon 的三種使用方式

咱們在新建的ribbon-client項目裏面來使用三種方式調用eureka-provider的queryPort接口,由於eureka-provider服務啓動了三個節點,到時候只要觀察三種方式的響應結果,就能夠判斷負載均衡是否有生效。

1、使用原生API

直接使用LoadBalancerClient來得到對應的實例,而後發起URL請求,編寫對應的RibbonController:

@RestController
public class RibbonController{

    @Autowired
    private LoadBalancerClient loadBalancer;

    @GetMapping("ribbonApi")
    public String ribbonApi() throws Exception{
        ServiceInstance instance = loadBalancer.choose("eureka-provider");
        System.out.println(instance.getUri());
        URL url = new URL("http://localhost:" + instance.getPort() + "/queryPort");
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        InputStream inputStream = conn.getInputStream();
        BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
        String line = null;
        StringBuffer buffer = new StringBuffer();
        while ((line = reader.readLine()) != null) {
            buffer.append(line);
        }
        reader.close();
        return Observable.just(buffer.toString()).toBlocking().first();
    }
}

啓動Ribbon-Client服務,訪問http://localhost:7071/ribbonApi 接口,屢次刷新接口發現採用的是輪詢方式,運行效果圖以下:

2、結合RestTemplate使用

使用 RestTemplate 的話,咱們只須要再結合@LoadBalanced註解一塊兒使用便可:

@Configuration
public class RestTemplateConfig{
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

編寫RibbonController:

@RestController
public class RibbonController{
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping("queryPortByRest")
    public String queryPortByRest(){
        return restTemplate.getForEntity("http://eureka-provider/queryPort",String.class).getBody();
    }
}

啓動ribbon-client服務,訪問http://localhost:7071/queryPortByRest 接口,屢次刷新接口發現採用的也是輪詢方式,運行效果圖以下:

3、結合Feign使用

新建一個Feign:

@FeignClient(value = "eureka-provider")
public interface ProviderFeign{
    /**
     * 調用服務提供方,其中會返回服務提供者的端口信息
     * @return jinglingwang.cn
     */
    @RequestMapping("/queryPort")
    String queryPort();
}

編寫調用接口:

@RestController
public class RibbonController{
    ...略
    @Autowired
    private ProviderFeign providerFeign;
    ...
    @GetMapping("queryPort")
    public String queryPort(){
	// 經過feign ribbon-client 調用 eureka-provider
        return providerFeign.queryPort(); 
    }
}

啓動ribbon-client服務,訪問 http://localhost:7071/queryPort 接口,屢次刷新接口發現採用的也是輪詢方式,運行效果圖以下:

自定義Ribbon配置

爲指定的客戶端自定義負載均衡規則

在配置以前先作一點準備工做,咱們把以前的服務eureka-provider再起3個節點,啓動以前把端口改成808五、808六、8087,三個節點的服務名改成eureka-provider-temp。這樣作的目的是等會兒咱們新建一個Feign,可是名字和以前的區分開,至關於兩個不一樣的服務,而且都是多節點的。

以上準備工做作完以後你會在IDEA中看到以下圖的6個服務:

在註冊中心也能夠觀察到2個不一樣的服務,一共6個節點:

eureka-provide 和 eureka-provide-temp 他們惟一的區別就是服務名不同、端口不同。

JavaBean的配置方式

如今開始爲Feign配置ribbon:

  1. 新建一個Feign,命名爲:ProviderTempFeign

    @FeignClient(value = "eureka-provider-temp")
    public interface ProviderTempFeign{
    
        @RequestMapping("/queryPort")
        String queryPort();
    }
  2. 使用JAVA Bean的方式定義配置項

    public class ProviderTempConfiguration{
        @Bean
        public IRule ribbonRule(){
            System.out.println("new ProviderTempConfiguration RandomRule");
            return new RandomRule(); // 定義一個隨機的算法
        }
        @Bean
        public IPing ribbonPing() {
            //        return new PingUrl();
            return new NoOpPing();
        }
    }
  3. 使用註解@RibbonClient 配置負載均衡客戶端:

    @RibbonClient(name = "eureka-provider-temp",configuration = ProviderTempConfiguration.class)
    public class ProviderTempRibbonClient{
    
    }
  4. 在Controller新增一個接口,來調用新增Feign(eureka-provider-temp)的方法

    @GetMapping("queryTempPort")
    public String queryTempPort(){
        return providerTempFeign.queryPort();
    }
  5. 再爲另外一個Feign(eureka-provider)也配置一下ribbon,對外接口仍是上面已經寫好了

    public class ProviderConfiguration{
        @Bean
        public IRule ribbonRule(){
            System.out.println("new ProviderConfiguration BestAvailableRule");
            return new BestAvailableRule(); // 選擇的最佳策略
        }
        @Bean
        public IPing ribbonPing() {
            //        return new PingUrl();
            return new NoOpPing();
        }
    }
    
    @RibbonClient(name = "eureka-provider",configuration = ProviderConfiguration.class)
    public class ProviderRibbonClient{
    
    }
  6. 啓動服務以後分別訪問兩個接口(http://localhost:7071/queryPorthttp://localhost:7071/queryTempPort),觀察接口的端口返回狀況

若是以上過程順利的話,你訪問queryPort接口的時候返回的端口不是隨機的,幾乎沒怎麼變化,訪問queryTempPort接口的時候,接口返回的端口是隨機的,說明咱們以上配置是可行的。並且第一次訪問接口的時候,咱們在控制檯打印了出對應的算法規則,你能夠觀察一下。

配置文件的配置方式

以上的配置也能夠寫到配置文件中,效果是同樣的:

# 經過配置文件 分別爲每一個客戶端配置
eureka-provider.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.BestAvailableRule
eureka-provider.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing

eureka-provider-temp.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
eureka-provider-temp.ribbon.NFLoadBalancerPingClassName=com.netflix.loadbalancer.NoOpPing

配置的規則是: .ribbo. = xxxXXX,其中configKey能夠在 CommonClientConfigKey.class類中查看。經常使用的有:

NFLoadBalancerClassName
NFLoadBalancerRuleClassName
NFLoadBalancerPingClassName
NIWSServerListClassName
NIWSServerListFilterClassName

爲全部的客戶端自定義默認的配置

這裏須要用到的註解是@RibbonClients

@Configuration()
public class DefaultRibbonConfiguration{

    @Bean
    public IRule iRule() {
        // 輪詢
        return new RoundRobinRule();
    }
    @Bean
    public IPing ribbonPing() {
        return new DummyPing();
    }
}
@RibbonClients(defaultConfiguration = DefaultRibbonConfiguration.class)
public class DefaultRibbonClient{

****}

啓動咱們的ribbon-client服務,測試訪問下咱們的http://localhost:7071/queryPort 接口,發現返回的數據每次都不同,變爲輪詢的方式返回接口信息了。

測試到這裏的時候,配置文件中的相關配置我並無註釋掉,Java Bean方式的@RibbonClient被註釋掉了,也就是說測試的時候同時配置了配置文件和@RibbonClients,最後測試下來是@RibbonClients配置生效了,配置文件中配置的策略沒有生效。
測試下來,@RibbonClients 的優先級最高,以後是配置文件,再是@RibbonClient,最後是Spring Cloud Netflix 默認值。

同時使用@RibbonClients和@RibbonClient

若是同時使用@RibbonClients和@RibbonClient,全局默認配置和自定義單個ribbon配置,會按照哪一個配置生效呢?

我把配置文件中的相關配置都註釋,而後把兩個配置 @RibbonClient 的地方都放開,而後重啓項目,訪問http://localhost:7071/queryPorthttp://localhost:7071/queryTempPort

測試結果是都報錯,報錯信息以下:

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.netflix.loadbalancer.IRule' available: expected single matching bean but found 2: providerRule,iRule

報錯信息的意思是預期須要一個bean,可是結果找到了兩個(providerRule 和 iRule),結果不知道該用哪個了,因此拋出異常。

那這個問題怎麼解決呢?

首先直接說結論吧,就是給你想要生效的那個bean加@Primary註解,代碼以下所示,若是eureka-provider 不加仍是會繼續報錯:

public class ProviderTempConfiguration{
    @Primary
    @Bean("providerTempRule")
    public IRule ribbonRule(){
        System.out.println("new ProviderTempConfiguration RandomRule");
        return new RandomRule();
    }
    ...
}

再說下排查這個問題的思路:

  1. 經過查看異常輸出棧的錯誤日誌信息,定位到拋出異常的地方

  2. 以後繼續往前面找相關的邏輯,加斷點,慢慢調試,發現有一個字段(autowiredBeanName)爲空,纔會進入到後面拋異常的邏輯

  3. 斷點也顯示matchingBeans裏面有兩條數據,說明確實是匹配到了2個bean

  4. 而後咱們進入到determineAutowireCandidate方法,發現裏面有個看起來很不通常的字段:primaryCandidate,若是這個字段不爲空,會直接返回,那這個字段的值是怎麼確認的呢?

  5. 繼續進入到determinePrimaryCandidate方法,發現這個方法的主要功能就是從給定的多個bean中肯定一個主要的候選對象bean,說白了就是選一個bean,那這個方法是怎麼選的呢?上源代碼:

    @Nullable
    protected String determinePrimaryCandidate(Map<String, Object> candidates, Class<?> requiredType) {
    	String primaryBeanName = null;
      // candidates 是匹配到的多個bean
      // requiredType 是要匹配的目標依賴類型
    	for (Map.Entry<String, Object> entry : candidates.entrySet()) { // 遍歷map
    		String candidateBeanName = entry.getKey();
    		Object beanInstance = entry.getValue();
    		if (isPrimary(candidateBeanName, beanInstance)) { // 最重要的邏輯,看是否是主要的bean,看到這有經驗的其實都知道要加@Primary註解了
    			if (primaryBeanName != null) {
    				boolean candidateLocal = containsBeanDefinition(candidateBeanName);
    				boolean primaryLocal = containsBeanDefinition(primaryBeanName);
    				if (candidateLocal && primaryLocal) {
    					throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
    							"more than one 'primary' bean found among candidates: " + candidates.keySet());
    				}
    				else if (candidateLocal) {
    					primaryBeanName = candidateBeanName;
    				}
    			}
    			else {
    				primaryBeanName = candidateBeanName;
    			}
    		}
    	}
    	return primaryBeanName;
    }
  6. 進入到isPrimary(candidateBeanName, beanInstance)方法,最後實際就是返回的如下邏輯:

    @Override
    public boolean isPrimary() {
    	return this.primary;
    }
  7. 因此解決上面的問題,只須要在咱們的ProviderTempConfiguration類裏面爲bean 再添加一個@Primary註解

Ribbon超時時間

全局默認配置

# 全局ribbon超時時間
#讀超時
ribbon.ReadTimeout=3000
#鏈接超時
ribbon.ConnectTimeout=3000
#同一臺實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetries=0
#重試負載均衡其餘的實例最大重試次數,不包括首次調用
ribbon.MaxAutoRetriesNextServer=1

爲每一個client單獨配置

# 爲每一個服務單獨配置超時時間
eureka-provider.ribbon.ReadTimeout=4000
eureka-provider.ribbon.ConnectTimeout=4000
eureka-provider.ribbon.MaxAutoRetries=0
eureka-provider.ribbon.MaxAutoRetriesNextServer=1

自定義Ribbon負載均衡策略

Ribbon定義瞭如下幾個屬性支持自定義配置:

<clientName>.ribbon.NFLoadBalancerClassName: Should implement ILoadBalancer
<clientName>.ribbon.NFLoadBalancerRuleClassName: Should implement IRule
<clientName>.ribbon.NFLoadBalancerPingClassName: Should implement IPing
<clientName>.ribbon.NIWSServerListClassName: Should implement ServerList
<clientName>.ribbon.NIWSServerListFilterClassName: Should implement ServerListFilter

這裏以自定義負載均衡策略規則爲例,只須要實現IRule接口或者繼承AbstractLoadBalancerRule

public class MyRule implements IRule{
    private static Logger log = LoggerFactory.getLogger(MyRule.class);

    private ILoadBalancer lb;
    @Override
    public Server choose(Object key){
        if (lb == null) {
            return null;
        }
        Server server = null;

        while (server == null) {
            if (Thread.interrupted()) {
                return null;
            }
            List<Server> allList = lb.getAllServers();
            int serverCount = allList.size();
            if (serverCount == 0) {
                log.warn("No up servers available from load balancer: " + lb);
                return null;
            }
            // 是輪詢、隨機、加權、hash?本身實現從server list中選擇一個server
            // 這裏寫簡單點,老是請求第一臺服務,這樣的邏輯是不會用到真實的環境的
            server = allList.get(0);
        }
        return server;
    }

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

    @Override
    public ILoadBalancer getLoadBalancer(){
        return lb;
    }
}

而後就能夠用Java Bean的方式或者配置文件的方式進行配置了,其餘像自定義ping的策略也差很少。

Ribbon總結

  1. Ribbon 沒有相似@EnableRibbon這樣的註解
  2. 新版的SpringCloud已經不使用Ribbon做爲默認的負載均衡器了
  3. 可使用@RibbonClients@RibbonClient 註解來負載均衡相關策略的配置
  4. 實現對應的接口就能夠完成自定義負載均衡策略
  5. Ribbon 配置的全部key均可以在CommonClientConfigKey類中查看

代碼示例:Github ribbon client

相關文章
相關標籤/搜索