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

相关文章
相关标签/搜索