Spring Cloud Alibaba gateway ribbon 自定义负载均衡规则

∥☆過路亽.° 提交于 2019-12-10 22:52:39

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

上一篇介绍了,ribbon的组件。本篇要自己写一个灰度方案。其实就是一个很简单的思维扩散。

需求

前端header请求携带version字段。路由服务根据version去需要对应版本的服务集合,进行或轮询或hash或权重的负载。请求路由到服务上,如果还要调用下游服务,也按照version规则去路由下游服务器。前端未携带版本按照后端服务最高version版本进行路由。

分析如果自己动手写一个灰度方案。需要考虑的因素有几点?

  • 服务对应的版本。key(版本号):value(对应版本号的服务集合)
  • 对应版本号的服务集合需要重新排序。
  • 重写负载均衡规则,就是ribbon的IRule方法。按照我们想要的负载规则去路由我们的请求

解决方案:

  • 利用注册中心的metadata属性元数据,让服务携带版本信息。
  • 拿到要请求的服务集合。spring cloud Alibaba nacos NamingService接口根据服务名称获取所有服务List集合,如果你使用的spring cloud 版本可以使用 ILoadBalancer 对象获取所有的服务集合
  • Instance服务里面携带了,服务注册到注册中心的自定义版本信息
  • 重写IRule负载规则。按照需求转发请求。

来写一下网关层的实现。 gateway负载规则有一个拦截器

创建负载规则的类信息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负载过滤器

以下是源码

@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的赋值操作

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

对应版本1、2、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 注解方式使用

如何喜欢可以关注分享本公众号。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。转载请附带公众号二维码

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!