Spring Cloud Alibaba之負載均衡組件 - Ribbon

負載均衡

咱們都知道在微服務架構中,微服務之間老是須要互相調用,以此來實現一些組合業務的需求。例如組裝訂單詳情數據,因爲訂單詳情裏有用戶信息,因此訂單服務就得調用用戶服務來獲取用戶信息。要實現遠程調用就須要發送網絡請求,而每一個微服務均可能會存在有多個實例分佈在不一樣的機器上,那麼當一個微服務調用另外一個微服務的時候就須要將請求均勻的分發到各個實例上,以此避免某些實例負載太高,某些實例又太空閒,因此在這種場景必需要有負載均衡器。html

目前實現負載均衡主要的兩種方式:java

一、服務端負載均衡;例如最經典的使用Nginx作負載均衡器。用戶的請求先發送到Nginx,而後再由Nginx經過配置好的負載均衡算法將請求分發到各個實例上,因爲須要做爲一個服務部署在服務端,因此該種方式稱爲服務端負載均衡。如圖:
Spring Cloud Alibaba之負載均衡組件 - Ribbonnode

二、客戶端側負載均衡;之因此稱爲客戶端側負載均衡,是由於這種負載均衡方式是由發送請求的客戶端來實現的,也是目前微服務架構中用於均衡服務之間調用請求的經常使用負載均衡方式。由於採用這種方式的話服務之間能夠直接進行調用,無需再經過一個專門的負載均衡器,這樣可以提升必定的性能以及高可用性。以微服務A調用微服務B舉例,簡單來講就是微服務A先經過服務發現組件獲取微服務B全部實例的調用地址,而後經過本地實現的負載均衡算法選取出其中一個調用地址進行請求。如圖:
Spring Cloud Alibaba之負載均衡組件 - Ribbonweb

咱們來經過Spring Cloud提供的DiscoveryClient寫一個很是簡單的客戶端側負載均衡器,藉此直觀的瞭解一下該種負載均衡器的工做流程,該示例中採用的負載均衡策略爲隨機,代碼以下:算法

package com.zj.node.contentcenter.discovery;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;

import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

/**
 * 客戶端側負載均衡器
 *
 * @author 01
 * @date 2019-07-26
 **/
public class LoadBalance {

    @Autowired
    private DiscoveryClient discoveryClient;

    /**
     * 隨機獲取目標微服務的請求地址
     *
     * @return 請求地址
     */
    public String randomTakeUri(String serviceId) {
        // 獲取目標微服務的全部實例的請求地址
        List<String> targetUris = discoveryClient.getInstances(serviceId).stream()
                .map(i -> i.getUri().toString())
                .collect(Collectors.toList());
        // 隨機獲取列表中的uri
        int i = ThreadLocalRandom.current().nextInt(targetUris.size());

        return targetUris.get(i);
    }
}

使用Ribbon實現負載均衡

什麼是Ribbon:spring

  • Ribbon是Netflix開源的客戶端側負載均衡器
  • Ribbon內置了很是豐富的負載均衡策略算法

Ribbon雖然是個主要用於負載均衡的小組件,可是麻雀雖小五臟俱全,Ribbon仍是有許多的接口組件的。以下表:
Spring Cloud Alibaba之負載均衡組件 - Ribbonapi

Ribbon默認內置了八種負載均衡策略,若想自定義負載均衡策略則實現上表中提到的IRule接口或AbstractLoadBalancerRule抽象類便可。內置的負載均衡策略以下:
Spring Cloud Alibaba之負載均衡組件 - Ribbon網絡

  • 默認的策略規則爲ZoneAvoidanceRule

Ribbon主要有兩種使用方式,一是使用Feign,Feign內部已經整合了Ribbon,所以若是隻是普通使用的話都感知不到Ribbon的存在;二是配合RestTemplate使用,這種方式則須要添加Ribbon依賴和@LoadBalanced註解。架構

這裏主要演示一下第二種使用方式,因爲項目中添加的Nacos依賴已包含了Ribbon因此不須要另外添加依賴,首先定義一個RestTemplate,代碼以下:app

package com.zj.node.contentcenter.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;

/**
 * bean 配置類
 *
 * @author 01
 * @date 2019-07-25
 **/
@Configuration
public class BeanConfig {

    @Bean
    @LoadBalanced  // 加上這個註解表示使用Ribbon
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

而後使用RestTemplate調用其餘服務的時候,只須要寫服務名便可,不須要再寫ip地址和端口號。以下示例:

public ShareDTO findById(Integer id) {
    // 獲取分享詳情
    Share share = shareMapper.selectByPrimaryKey(id);
    // 發佈人id
    Integer userId = share.getUserId();
    // 調用用戶中心獲取用戶信息
    UserDTO userDTO = restTemplate.getForObject(
            "http://user-center/users/{id}",  // 只須要寫服務名
            UserDTO.class, userId
    );

    ShareDTO shareDTO = objectConvert.toShareDTO(share);
    shareDTO.setWxNickname(userDTO.getWxNickname());

    return shareDTO;
}

若是不太清楚RestTemplate的使用,能夠參考以下文章:


自定義Ribbon負載均衡配置

在實際開發中,咱們可能會遇到默認的負載均衡策略沒法知足需求,從而須要更換其餘的負載均衡策略。關於Ribbon負載均衡的配置方式主要有兩種,在代碼中配置或在配置文件中配置。

Ribbon支持細粒度的配置,例如我但願微服務A在調用微服務B的時候採用隨機的負載均衡策略,而在調用微服務C的時候採用默認策略,下面咱們就來實現一下這種細粒度的配置。

一、首先是經過代碼進行配置,編寫一個配置類用於實例化指定的負載均衡策略對象:

@Configuration
public class RibbonConfig {

    @Bean
    public IRule ribbonRule(){
        // 隨機的負載均衡策略對象
        return new RandomRule();
    }
}

而後再編寫一個用於配置Ribbon客戶端的配置類,該配置類的目的是指定在調用user-center時採用RibbonConfig裏配置的負載均衡策略,這樣就能夠達到細粒度配置的效果:

@Configuration
// 該註解用於自定義Ribbon客戶端配置,這裏聲明爲屬於user-center的配置
@RibbonClient(name = "user-center", configuration = RibbonConfig.class)
public class UserCenterRibbonConfig {
}

須要注意的是RibbonConfig應該定義在主啓動類以外,避免被Spring掃描到,否則會產生父子上下文掃描重疊的問題,從而致使各類奇葩的問題。而在Ribbon這裏就會致使該配置類被全部的Ribbon客戶端共享,即無論調用user-center仍是其餘微服務都會採用該配置類裏定義的負載均衡策略,這樣就會變成了一個全局配置了,違背了咱們須要細粒度配置的目的。因此須要將其定義在主啓動類以外:
Spring Cloud Alibaba之負載均衡組件 - Ribbon

關於這個問題能夠參考官方文檔的描述:

https://cloud.spring.io/spring-cloud-static/Greenwich.SR2/single/spring-cloud.html#_customizing_the_ribbon_client

二、使用配置文件進行配置就更簡單了,不須要寫代碼還不會有父子上下文掃描重疊的坑,只需在配置文件中增長以下一段配置就能夠實現以上使用代碼配置等價的效果:

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

兩種配置方式對比:
Spring Cloud Alibaba之負載均衡組件 - Ribbon

  • 關於優先級:細粒度配置文件配置 > 細粒度代碼配置 > 全局配置文件配置 > 全局代碼配置

最佳實踐總結:

  • 儘可能使用配置文件配置,配置文件知足不了需求的狀況下再考慮使用代碼配置
  • 在同一個微服務內儘可能保持單一性,例如統一使用配置文件配置,儘可能不要兩種方式混用,以避免增長定位問題的複雜度

以上介紹的是細粒度地針對某個特定Ribbon客戶端的配置,下面咱們再演示一下如何實現全局配置。很簡單,只須要把註解改成@RibbonClients便可,代碼以下:

@Configuration
// 該註解用於全局配置
@RibbonClients(defaultConfiguration = RibbonConfig.class)
public class GlobalRibbonConfig {
}

Ribbon默認是懶加載的,因此在第一次發生請求的時候會顯得比較慢,咱們能夠經過在配置文件中添加以下配置開啓飢餓加載:

ribbon:
  eager-load:
    enabled: true
    # 爲哪些客戶端開啓飢餓加載,多個客戶端使用逗號分隔(非必須)
    clients: user-center

支持Nacos權重

以上小節基本介紹完了負載均衡及Ribbon的基礎使用,接下來的內容須要配合Nacos,若沒有了解過Nacos的話能夠參考如下文章:

在Nacos Server的控制檯頁面能夠編輯每一個微服務實例的權重,服務列表 -> 詳情 -> 編輯;默認權重都爲1,權重值越大就越優先被調用:
Spring Cloud Alibaba之負載均衡組件 - Ribbon

權重在不少場景下很是有用,例如一個微服務有不少的實例,它們被部署在不一樣配置的機器上,這時候就能夠將配置較差的機器上所部署的實例權重設置得比較低,而部署在配置較好的機器上的實例權重設置得高一些,這樣就能夠將較大一部分的請求都分發到性能較高的機器上。

可是Ribbon內置的負載均衡策略都不支持Nacos的權重,因此咱們就須要自定義實現一個支持Nacos權重配置的負載均衡策略。好在Nacos Client已經內置了負載均衡的能力,因此實現起來也比較簡單,代碼以下:

package com.zj.node.contentcenter.configuration;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;

/**
 * 支持Nacos權重配置的負載均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private  NacosDiscoveryProperties discoveryProperties;

    /**
     * 讀取配置文件,並初始化NacosWeightedRule
     *
     * @param iClientConfig iClientConfig
     */
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        log.debug("lb = {}", loadBalancer);

        // 須要請求的微服務名稱
        String name = loadBalancer.getName();
        // 獲取服務發現的相關API
        NamingService namingService = discoveryProperties.namingServiceInstance();

        try {
            // 調用該方法時nacos client會自動經過基於權重的負載均衡算法選取一個實例
            Instance instance = namingService.selectOneHealthyInstance(name);
            log.info("選擇的實例是:instance = {}", instance);

            return new NacosServer(instance);
        } catch (NacosException e) {
            return null;
        }
    }
}

而後在配置文件中配置一下就可使用該負載均衡策略了:

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosWeightedRule

思考:既然Nacos Client已經有負載均衡的能力,Spring Cloud Alibaba爲何還要去整合Ribbon呢?

我的認爲,這主要是爲了符合Spring Cloud標準。Spring Cloud Commons有個子項目 spring-cloud-loadbalancer ,該項目制定了標準,用來適配各類客戶端負載均衡器(雖然目前實現只有Ribbon,但Hoxton就會有替代的實現了)。

Spring Cloud Alibaba遵循了這一標準,因此整合了Ribbon,而沒有去使用Nacos Client提供的負載均衡能力。


同一集羣優先調用

Spring Cloud Alibaba之服務發現組件 - Nacos一文中已經介紹過集羣的概念以及做用,這裏就再也不贅述,加上上一小節中已經介紹過如何自定義負載均衡策略了,因此這裏再也不囉嗦而是直接上代碼,實現代碼以下:

package com.zj.node.contentcenter.configuration;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.core.Balancer;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

/**
 * 實現同一集羣優先調用並基於隨機權重的負載均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        // 獲取配置文件中所配置的集羣名稱
        String clusterName = discoveryProperties.getClusterName();
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        // 獲取須要請求的微服務名稱
        String serviceId = loadBalancer.getName();
        // 獲取服務發現的相關API
        NamingService namingService = discoveryProperties.namingServiceInstance();

        try {
            // 獲取該微服務的全部健康實例
            List<Instance> instances = namingService.selectInstances(serviceId, true);
            // 過濾出相同集羣下的全部實例
            List<Instance> sameClusterInstances = instances.stream()
                    .filter(i -> Objects.equals(i.getClusterName(), clusterName))
                    .collect(Collectors.toList());

            // 相同集羣下沒有實例則須要使用其餘集羣下的實例
            List<Instance> instancesToBeChosen;
            if (CollectionUtils.isEmpty(sameClusterInstances)) {
                instancesToBeChosen = instances;
                log.warn("發生跨集羣調用,name = {}, clusterName = {}, instances = {}",
                        serviceId, clusterName, instances);
            } else {
                instancesToBeChosen = sameClusterInstances;
            }

            // 基於隨機權重的負載均衡算法,從實例列表中選取一個實例
            Instance instance = ExtendBalancer.getHost(instancesToBeChosen);
            log.info("選擇的實例是:port = {}, instance = {}", instance.getPort(), instance);

            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("獲取實例發生異常", e);
            return null;
        }
    }
}

class ExtendBalancer extends Balancer {

    /**
     * 因爲Balancer類裏的getHostByRandomWeight方法是protected的,
     * 因此經過這種繼承的方式來實現調用,該方法基於隨機權重的負載均衡算法,選取一個實例
     */
    static Instance getHost(List<Instance> hosts) {
        return getHostByRandomWeight(hosts);
    }
}

一樣的,想要使用該負載均衡策略的話,在配置文件中配置一下便可:

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.zj.node.contentcenter.configuration.NacosSameClusterWeightedRule

基於元數據的版本控制

在以上兩個小節咱們實現了基於Nacos權重的負載均衡策略及同一集羣下優先調用的負載均衡策略,但在實際項目中,可能會面臨多版本共存的問題,即一個微服務擁有不一樣版本的實例,而且這些不一樣版本的實例之間多是互不兼容的。例如微服務A的v1版本實例沒法調用微服務B的v2版本實例,只可以調用微服務B的v1版本實例。

而Nacos中的元數據就比較適合解決這種版本控制的問題,至於元數據的概念及配置方式已經在Spring Cloud Alibaba之服務發現組件 - Nacos一文中介紹過,這裏主要介紹一下如何經過Ribbon去實現基於元數據的版本控制。

舉個例子,線上有兩個微服務,一個做爲服務提供者一個做爲服務消費者,它們都有不一樣版本的實例,以下:

  • 服務提供者有兩個版本:v一、v2
  • 服務消費者也有兩個版本:v一、v2

v1和v2是不兼容的。服務消費者v1只能調用服務提供者v1;消費者v2只能調用提供者v2。如何實現呢?下面咱們來圍繞該場景,實現微服務之間的版本控制。

綜上,咱們須要實現的主要有兩點:

  • 優先選擇同集羣下,符合metadata的實例
  • 若是同集羣下沒有符合metadata的實例,就選擇其餘集羣下符合metadata的實例

首先咱們得在配置文件中配置元數據,元數據就是一堆的描述信息,以k - v形式進行配置,以下:

spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server的地址
        server-addr: 127.0.0.1:8848
        # 配置元數據
        metadata: 
          # 當前實例版本
          version: v1
          # 容許調用的提供者實例的版本
          target-version: v1

而後就能夠寫代碼了,和以前同樣,也是經過負載均衡策略實現,具體代碼以下:

package com.zj.node.contentcenter.configuration;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.alibaba.nacos.client.utils.StringUtils;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.DynamicServerListLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;

import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 基於元數據的版本控制負載均衡策略
 *
 * @author 01
 * @date 2019-07-27
 **/
@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties discoveryProperties;

    private static final String TARGET_VERSION = "target-version";
    private static final String VERSION = "version";

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        // do nothing
    }

    @Override
    public Server choose(Object key) {
        // 獲取配置文件中所配置的集羣名稱
        String clusterName = discoveryProperties.getClusterName();
        // 獲取配置文件中所配置的元數據
        String targetVersion = discoveryProperties.getMetadata().get(TARGET_VERSION);

        DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
        // 須要請求的微服務名稱
        String serviceId = loadBalancer.getName();
        // 獲取該微服務的全部健康實例
        List<Instance> instances = getInstances(serviceId);

        List<Instance> metadataMatchInstances = instances;
        // 若是配置了版本映射,那麼表明只調用元數據匹配的實例
        if (StringUtils.isNotBlank(targetVersion)) {
            // 過濾與版本元數據相匹配的實例,以實現版本控制
            metadataMatchInstances = filter(instances,
                    i -> Objects.equals(targetVersion, i.getMetadata().get(VERSION)));

            if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                log.warn("未找到元數據匹配的目標實例!請檢查配置。targetVersion = {}, instance = {}",
                        targetVersion, instances);
                return null;
            }
        }

        List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
        // 若是配置了集羣名稱,需篩選同集羣下元數據匹配的實例
        if (StringUtils.isNotBlank(clusterName)) {
            // 過濾出相同集羣下的全部實例
            clusterMetadataMatchInstances = filter(metadataMatchInstances,
                    i -> Objects.equals(clusterName, i.getClusterName()));

            if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                clusterMetadataMatchInstances = metadataMatchInstances;
                log.warn("發生跨集羣調用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
            }
        }

        // 基於隨機權重的負載均衡算法,選取其中一個實例
        Instance instance = ExtendBalancer.getHost(clusterMetadataMatchInstances);

        return new NacosServer(instance);
    }

    /**
     * 經過過濾規則過濾實例列表
     */
    private List<Instance> filter(List<Instance> instances, Predicate<Instance> predicate) {
        return instances.stream()
                .filter(predicate)
                .collect(Collectors.toList());
    }

    private List<Instance> getInstances(String serviceId) {
        // 獲取服務發現的相關API
        NamingService namingService = discoveryProperties.namingServiceInstance();
        try {
            // 獲取該微服務的全部健康實例
            return namingService.selectInstances(serviceId, true);
        } catch (NacosException e) {
            log.error("發生異常", e);
            return Collections.emptyList();
        }
    }
}

class ExtendBalancer extends Balancer {
    /**
     * 因爲Balancer類裏的getHostByRandomWeight方法是protected的,
     * 因此經過這種繼承的方式來實現調用,該方法基於隨機權重的負載均衡算法,選取一個實例
     */
    static Instance getHost(List<Instance> hosts) {
        return getHostByRandomWeight(hosts);
    }
}
相關文章
相關標籤/搜索