Spring Cloud Alibaba gateway ribbon 自定義負載均衡規則

上一篇介紹了,ribbon的組件。本篇要本身寫一個灰度方案。其實就是一個很簡單的思惟擴散。前端

需求

前端header請求攜帶version字段。路由服務根據version去須要對應版本的服務集合,進行或輪詢或hash或權重的負載。請求路由到服務上,若是還要調用下游服務,也按照version規則去路由下游服務器。前端未攜帶版本按照後端服務最高version版本進行路由。git

分析若是本身動手寫一個灰度方案。須要考慮的因素有幾點?github

  • 服務對應的版本。key(版本號):value(對應版本號的服務集合)
  • 對應版本號的服務集合須要從新排序。
  • 重寫負載均衡規則,就是ribbon的IRule方法。按照咱們想要的負載規則去路由咱們的請求

解決方案:spring

  • 利用註冊中心的metadata屬性元數據,讓服務攜帶版本信息。
  • 拿到要請求的服務集合。spring cloud Alibaba nacos NamingService接口根據服務名稱獲取全部服務List集合,若是你使用的spring cloud 版本可使用 ILoadBalancer 對象獲取全部的服務集合
  • Instance服務裏面攜帶了,服務註冊到註冊中心的自定義版本信息
  • 重寫IRule負載規則。按照需求轉發請求。

來寫一下網關層的實現。
gateway負載規則有一個攔截器segmentfault

建立負載規則的類信息GrayscaleProperties後端

public class GrayscaleProperties implements Serializable {
    private String version;
    private String serverName;
    private String serverGroup;
    private String active;
    private double weight = 1.0D;
}

由於gateway的特殊性LoadBalancerClientFilter過濾器主要解析lb:// 爲前綴的路由規則,在經過LoadBalancerClient#choose(String) 方法獲取到須要的服務實例,從而實現負載均衡。在這裏咱們要寫本身的負載均衡就須要從新須要重寫LoadBalancerClientFilter 過濾器
LoadBalancerClientFilter 介紹:次過濾器做用在url以lb開頭的路由,而後利用loadBalancer來獲取服務實例,構造目標requestUrl,設置到GATEWAY_REQUEST_URL_ATTR屬性中,供NettyRoutingFilter使用。服務器

GatewayLoadBalancerClientAutoConfiguration 在初始化會檢測@ConditionalOnBean(LoadBalancerClient.class) 是否存在,若是存在就會加載LoadBalancerClientFilter負載過濾器負載均衡

如下是源碼dom

@Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
        String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
        //判斷url 前綴 如不是lb開頭的就進行下一個過濾器
        if (url == null || (!"lb".equals(url.getScheme()) && !"lb".equals(schemePrefix))) {
            return chain.filter(exchange);
        }
        //根據網關的原始網址。替換exchange url爲 http://IP:PORT/path 路徑的url
        //preserve the original url
        addOriginalRequestUrl(exchange, url);
    
        log.trace("LoadBalancerClientFilter url before: " + url);
        // 這裏呢會進行調用真正的負載均衡
        final ServiceInstance instance = choose(exchange);

        if (instance == null) {
            String msg = "Unable to find instance for " + url.getHost();
            if(properties.isUse404()) {
                throw new FourOFourNotFoundException(msg);
            }
            throw new NotFoundException(msg);
        }

        URI uri = exchange.getRequest().getURI();

        // if the `lb:<scheme>` mechanism was used, use `<scheme>` as the default,
        // if the loadbalancer doesn't provide one.
        String overrideScheme = instance.isSecure() ? "https" : "http";
        if (schemePrefix != null) {
            overrideScheme = url.getScheme();
        }

        URI requestUrl = loadBalancer.reconstructURI(new DelegatingServiceInstance(instance, overrideScheme), uri);

        log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
        exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
        return chain.filter(exchange);
    }
    。。。。
    // 由於注入了ribbon 會使用ribbon 進行負載均衡規則進行負載
    protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }

若是單單定製了 IRule 的實現類 Server choose(Object key) 方法裏面的 key值就是一個默認值。就不知道轉發到那個服務。因此要進行重寫LoadBalancerClientFilter 這個類的 protected ServiceInstance choose(ServerWebExchange exchange) 進行key的賦值操做ide

public class GatewayLoadBalancerClientFilter extends LoadBalancerClientFilter {

    public GatewayLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
        super(loadBalancer, properties);
    }
    @Override
    protected ServiceInstance choose(ServerWebExchange exchange) {

        if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
            RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer;
            HttpHeaders headers = exchange.getRequest().getHeaders();
            String version = headers.getFirst( GrayscaleConstant.GRAYSCALE_VERSION );
            String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
            GrayscaleProperties build = GrayscaleProperties.builder().version( version ).serverName( serviceId ).build();
            //這裏使用服務ID 和 version 作爲選擇服務實例的key
            //TODO 這裏也能夠根據實際業務狀況作本身的對象封裝
            return client.choose(serviceId,build);
        }
        return super.choose(exchange);
    }
}

自定義gateway灰度負載規則

@Slf4j
public class GrayscaleLoadBalancerRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //留空
    }

    /**
     * gateway 特殊性。須要設置key值內容知道你要轉發的服務名稱 key已經在filter內設置了key值。
     * @param key
     * @return
     */
    @Override
    public Server choose(Object key) {

        try {
            GrayscaleProperties grayscale = (GrayscaleProperties) key;
            String version = grayscale.getVersion();
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
            List<Instance> instances = namingService.selectInstances(grayscale.getServerName(), true);
                        
            if (CollectionUtils.isEmpty(instances)) {
                log.warn("no instance in service {}", grayscale.getServerName());
                return null;
            } else {
                List<Instance> instancesToChoose = buildVersion(instances,version);
                //進行cluster-name分組篩選
                // TODO 思考若是cluster-name 節點所有掛掉。是否是能夠請求其餘的分組的服務?能夠根據狀況在定製一份規則出來
                if (StringUtils.isNotBlank(clusterName)) {
                    List<Instance> sameClusterInstances = (List)instancesToChoose.stream().filter((instancex) -> {
                        return Objects.equals(clusterName, instancex.getClusterName());
                    }).collect(Collectors.toList());
                    if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                        instancesToChoose = sameClusterInstances;
                    } else {
                        log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{grayscale.getServerName(), clusterName, instances});
                    }
                }
                //按nacos權重獲取。這個是NacosRule的代碼copy 過來 沒有本身實現權重隨機。這個權重是nacos控制檯服務的權重設置
                                // 若是業務上有本身特殊的業務。能夠本身定製規則,黑白名單,用戶是不是灰度用戶,測試帳號。等等一些自定義設置
                Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
                return new NacosServer(instance);
            }
        } catch (Exception var9) {
            log.warn("NacosRule error", var9);
            return null;
        }
    }
}

以上就是gateway的定製負載規則。

啓動三個cloud-discovery-client服務

file

對應版本一、二、3

而後postman進行接口請求 http://localhost:9000/client/client/user/service/save header 裏面添加 version 字段。分別請求對應的版本服務。

gateway 路由所有請求到了對應版本的路由服務上。

服務於服務間的版本請求。

其實和gateway 原理同樣,只不過少了gateway 攔截器這一層。
建立本身的AbstractGrayscalLoadBalancerRule 繼承AbstractLoadBalancerRule 抽象類,這個抽象類封裝了一些咱們須要用到的方法。

/**
 * @Author: xlr
 * @Date: Created in 1:03 PM 2019/11/24
 */
@Slf4j
@Data
public abstract class AbstractGrayscalLoadBalancerRule extends AbstractLoadBalancerRule {

    /**
     * asc 正序 反之desc 倒敘
     */
    protected boolean asc = true;

    /**
     * 篩選想要的值
     * @param instances
     * @param version
     * @return
     */
    protected List <Instance> buildVersion(List<Instance> instances,String version){
        //進行按版本分組排序
        Map<String,List<Instance>> versionMap = getInstanceByScreen(instances);
        if(versionMap.isEmpty()){
            log.warn("no instance in service {}", version);
        }
        //若是version 未傳值使用最低版本服務
        if(StringUtils.isBlank( version )){
            if(isAsc()){
                version = getFirst( versionMap.keySet() );
            }else {
                version = getLast( versionMap.keySet() );
            }
        }

        List <Instance> instanceList = versionMap.get( version );

        return instanceList;
    }

    /**
     * 根據version 組裝一個map key value  對應 version List<Instance>
     * @param instances
     * @return
     */
    protected Map<String,List<Instance>> getInstanceByScreen(List<Instance> instances){

        Map<String,List<Instance>> versionMap = new HashMap<>( instances.size() );
        instances.stream().forEach( instance -> {
            String version = instance.getMetadata().get( GrayscaleConstant.GRAYSCALE_VERSION );
            List <Instance> versions = versionMap.get( version );
            if(versions == null){
                versions = new ArrayList<>(  );
            }
            versions.add( instance );
            versionMap.put( version,versions );
        } );
        return versionMap;
    }

    /**
     * 獲取第一個值
     * @param keys
     * @return
     */
    protected String getFirst(Set<String> keys){
        List <String> list = sortVersion( keys );
        return list.get( 0 );
    }

    /**
     * 獲取最後一個值
     * @param keys
     * @return
     */
    protected String getLast(Set <String> keys){
        List <String> list = sortVersion( keys );
        return list.get( list.size()-1 );
    }

    /**
     * 根據版本排序
     * @param keys
     * @return
     */
    protected List<String > sortVersion(Set <String> keys){
        List<String > list = new ArrayList <>( keys );
        Collections.sort(list);
        return list;
    }
}

建立實現類GrayscaleLoadBalancerRule 繼承本身定義的抽象類AbstractGrayscalLoadBalancerRule

/**
 * fegin 負載均衡。在獲取到咱們想設置的對象以後,咱們還能夠設置 服務、用戶、角色等各個維度的黑白名單,限制、轉發、等策略,具體的使用場景還得須要結合工做中的實際使用場景。
 * 這裏只是提供一個簡單的思路。但願看到這個註釋的人。可以有觸類旁通的能力,定製本身的規則。
 * @Author: xlr
 * @Date: Created in 12:19 PM 2019/11/24
 */
@Slf4j
public class GrayscaleLoadBalancerRule extends AbstractGrayscalLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //留空
    }
    /**
     * gateway 特殊性。須要設置key值內容知道你要轉發的服務名稱。
     * @param key
     * @return
     */
    @Override
    public Server choose(Object key) {
        log.info("GrayscaleLoadBalancerRule 執行 choose方法 ,參數 key: {}",key);
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer)this.getLoadBalancer();
            String name = loadBalancer.getName();
            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();
            List<Instance> instances = namingService.selectInstances(name, true);

            if (CollectionUtils.isEmpty(instances)) {
                log.warn("no instance in service {}", name);
                return null;
            } else {
                List<Instance> instancesToChoose = null;

                String version = (String) ThreadLocalUtils.getKey( GrayscaleConstant.GRAYSCALE_VERSION );

                List <Instance> instanceList = buildVersion( instances,version );

                if (StringUtils.isNotBlank(clusterName)) {
                    List<Instance> sameClusterInstances = (List)instanceList.stream().filter((instancex) -> {
                        return Objects.equals(clusterName, instancex.getClusterName());
                    }).collect(Collectors.toList());
                    if (!CollectionUtils.isEmpty(sameClusterInstances)) {
                        instancesToChoose = sameClusterInstances;
                    } else {
                        log.warn("A cross-cluster call occurs,name = {}, clusterName = {}, instance = {}", new Object[]{name, clusterName, instanceList});
                    }
                }

                Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);
                return new NacosServer(instance);
            }
        } catch (Exception var9) {
            log.warn("NacosRule error", var9);
            return null;
        }
    }
}

分別在client、server的啓動類上,聲明自定義的IRule

@Bean
    IRule rule(){
        return new GrayscaleLoadBalancerRule();
    }

在啓動三個server服務進行負載均衡。繼續的測試效果。就不在貼圖了。有興趣的小夥伴們能夠本身嘗試寫一下。

這裏在多說一點,注意bean對象父子上下文。若是有沒接觸過這個的能夠度娘一下這個知識點。

思考

企業定製路由規則,在根據gateway提供的謂詞、斷言、過濾器這幾個要素組合,
定製企業本身想要的路由規則。到此時這樣gateway纔是企業真正想要的路由功能。

往期資料、參考資料

Sentinel 官方文檔地址

摘自參考 spring cloud 官方文檔

Spring Cloud alibaba 官網地址

示例代碼地址

往期地址 spring cloud alibaba 地址

spring cloud alibaba 簡介

Spring Cloud Alibaba (nacos 註冊中心搭建)

Spring Cloud Alibaba 使用nacos 註冊中心

Spring Cloud Alibaba nacos 配置中心使用

spring cloud 網關服務

Spring Cloud zuul網關服務 一

Spring Cloud 網關服務 zuul 二

Spring Cloud 網關服務 zuul 三 動態路由

Spring Cloud alibaba網關 sentinel zuul 四 限流熔斷

Spring Cloud gateway 網關服務 一

Spring Cloud gateway 網關服務二 斷言、過濾器

Spring Cloud gateway 三 自定義過濾器GatewayFilter

Spring Cloud gateway 網關四 動態路由

Spring Cloud gateway 五 Sentinel整合

Spring Cloud gateway 六 Sentinel nacos存儲動態刷新

Spring Cloud gateway 七 Sentinel 註解方式使用

如何喜歡能夠關注分享本公衆號。
file

相關文章
相關標籤/搜索