SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化

SpringCloud2.x 的权重路由和灰度控制

 

前言

在学习istio的过程中,发现istio的权重控制和灰度控制实在太好用了,虽然istio现在用在生产中还是不太成熟,但是可以吸取相关的优点来整合到现有的springcloud中.

springcloud已经是个成熟的框架了,其有ribbon组件负责提供负载均衡,还有gateway组件进行路由转发,但是使用的时候还是有不方便的地方,比如ribbon没有基于权重的轮询算法,这个就很蛋疼了,因为我们部署的机子可能每个性能都不一样,即使用的云服务,但也不能每个云服务器都是一样的,所以我们需要有一种权重来控制流量的方向(不是限流哦!);还有就是gateway的路由的动态配置,我们是希望能够存储在redis或者mysql中,所有也是需要自己进行一定的开发才行.

想要实现的效果如下

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第1张图片

 

实践

那么我这次的目的就是让我们的springcloud有带权重的轮询功能和灰度控制的路由, 以及网关路由的redis持久化功能,首先开始开发轮询.

 

 

自定义ribbon规则

首先负载均衡功能在springcloud的ribbon组件提供,那么我们肯定要对其进行自定义的扩展,最主要的就是IRule接口了,其实我们简单的开发只要实现IRule接口就行了,但是我这里因为还要做灰度控制的功能,所以我实现的抽象类是PredicateBasedRule, 这个类可以很方便的结合Predicate来进行灰度判定. 主要的代码如下

// 一个抽象类,提供Predicate
public abstract class DiscoveryEnabledRule extends PredicateBasedRule {
    
    private final DiscoveryEnabledPredicate predicate;
    
    public DiscoveryEnabledRule(DiscoveryEnabledPredicate discoveryEnabledPredicate) {
        this.predicate =discoveryEnabledPredicate;
    }
    
 
    @Override
    public AbstractServerPredicate getPredicate() {
        return predicate;
    }

}
// 这里主要介绍和nacos的整合时的规则
public class NacosMetadataAwareRule extends DiscoveryEnabledRule {

    public NacosMetadataAwareRule() {
        super(new NacosMetadataAwarePredicate());
    }
}

 

public abstract class DiscoveryEnabledPredicate extends AbstractServerPredicate {

    @Override
    public boolean apply(@Nullable PredicateKey input) {
        return input != null
                && apply(input.getServer());
    }

    protected abstract boolean apply(Server server);
    
}

public class NacosMetadataAwarePredicate extends DiscoveryEnabledPredicate {
    //带权重的负载均衡算法
    WeightRoundRobin weightRoundRobin = new NacosWeightRoundRobin();
    
    /**
     * 这里主要是进行各种筛选, 暂时是通过metainfo中的version来进行灰度删选
     */
    @Override
    protected boolean apply(Server server) {
        
        final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
        final Set> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
        final Map metadata = ((NacosServer)server).getMetadata();
        System.out.println("-------- 需要的meta信息 --------");
        attributes.forEach(e-> System.out.println(e.getKey()+":"+e.getValue()));
        System.out.println("-------- 存在的meta信息 --------"+server.getPort());
        metadata.forEach((k,v)-> System.out.println(k + ":" + v));
        return metadata.entrySet().containsAll(attributes);
    }
    
    @Override
    public Optional chooseRoundRobinAfterFiltering(List servers, Object loadBalancerKey) {
        List eligible = getEligibleServers(servers, loadBalancerKey);
        if (eligible.size() == 0) {
            return Optional.absent();
        }
        //根据权重轮询获取一个服务器
        return Optional.of(weightRoundRobin.choose(eligible));
    }
    
   
}

 

以上两个类就是核心的ribbon的自定义规则实现,然后就是写个自动装配的类就可以了

@Configuration
@AutoConfigureBefore(RibbonClientConfiguration.class)
@ConditionalOnProperty(value = "ribbon.filter.metadata.enabled", matchIfMissing = true)
public class RibbonDiscoveryRuleAutoConfiguration {

    
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public DiscoveryEnabledRule metadataAwareRuleEureka() {
        DiscoveryEnabledRule metadataAwareRule = new EurekaMetadataAwareRule();
        return metadataAwareRule;
    }
    
    
    
    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnClass(NacosServer.class)
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public DiscoveryEnabledRule metadataAwareRuleNacos() {
        DiscoveryEnabledRule metadataAwareRule = new NacosMetadataAwareRule();
        return metadataAwareRule;
    }
}

这样的话,一个带权重的负载均衡的ribbon的starter就可以说是完成了

 

 

自定义gateway的路由持久化和全局过滤器

网关这边主要是对路由做一个持久化,然后在整一个过滤器用于从请求中获取version传递给ribbon

 

持久化实现

@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
    
    public static final String GATEWAY_ROUTES = "geteway_routes";
    
    @Resource
    private StringRedisTemplate redisTemplate;
    
    @Override
    public Flux getRouteDefinitions() {
        List routeDefinitions = new ArrayList<>();
        redisTemplate.opsForHash().values(GATEWAY_ROUTES)
                .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(), RouteDefinition.class)));
        return Flux.fromIterable(routeDefinitions);
    }
    
    @Override
    public Mono save(Mono route) {
        return route.flatMap(r -> {
            redisTemplate.opsForHash().put(GATEWAY_ROUTES, r.getId(), JSON.toJSONString(r));
            return Mono.empty();
        });
    }
    
    @Override
    public Mono delete(Mono routeId) {
        return routeId.flatMap(id -> {
            if (redisTemplate.opsForHash().get(GATEWAY_ROUTES, id) != null) {
                redisTemplate.opsForHash().delete(GATEWAY_ROUTES, id);
                return Mono.empty();
            }
            return Mono.defer(() -> Mono.error(new NotFoundException("RouteDefinition not found: " + routeId)));
        });
    }
    
}

没啥好讲的就是增删改查,只不过是通过Reactor的响应式模型进行处理.

自定义过滤器

@Component
public class WeightRoundRibonFilter implements GlobalFilter, Ordered {
    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        
         System.out.println(Thread.currentThread().getName());
        String version = exchange.getRequest().getHeaders().getFirst("version");
        if (StringUtils.isNotBlank(version)) {
            RibbonFilterContext currentContext = RibbonFilterContextHolder.getCurrentContext();
            currentContext.add("version", version);
        }
        Mono mono = chain.filter(exchange).subscriberContext(ctx -> ctx.put("version", version));
        return mono;
        
    }
    
    @Override
    public int getOrder() {
        return 10000;  //这里要注意loadBalanceClientFilter的order是10100,这里要比他小一点
    }
}

gateway的配置

server:
  port: 13000

spring:
  application:
    name: gateway
  cloud:
    gateway:
      discovery:
        locator:
          enabled: false  #表明gateway开启服务注册和发现的功能,并且spring cloud gateway自动根据服务发现为每一个服务创建了一个router,这个router将以服务名开头的请求路径转发到对应的服务。
          lowerCaseServiceId: true   #是将请求路径上的服务名配置为小写(因为服务注册的时候,向注册中心注册时将服务名转成大写的了),比如以/service-hi/*的请求路径被路由转发到服务名为service-hi的服务上。
          filters:
            - StripPrefix=1
      routes:
      - id: client2
        uri: lb://client2
        predicates:
          - Path=/client2/**
        filters:
          - StripPrefix=1


    nacos:
      config:
        server-addr: 127.0.0.1:8848
      discovery:
        server-addr: 127.0.0.1:8848

首先是关闭本身的自动路由配置,然后手动配置serviceId为client2的路由

 

测试

首先看一下实例注册情况,直接在nacos控制台上看

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第2张图片

测试权重的情况

这里直接在浏览器上访问地址

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第3张图片

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第4张图片

可以看到是1:2的比例

 

灰度测试

使用postman或者别的,在请求头上加上version: client2-v2, 那么在请求时就会固定路由到metainfo中存在版本client2-v2的实例, 通过这个功能,就是可以让v1的请求走v1实例,就能实现灰度的功能

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第5张图片

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第6张图片

 

路由持久化测试

首先在redis添加路由

SpringCloud2.x 的权重路由和灰度控制,以及gateway的路由持久化_第7张图片

直接从actuator的http://localhost:13000/actuator/gateway/routes 路径就能看到已经从redis中读取出来并配置了

// 20190630140142
// http://localhost:13000/actuator/gateway/routes

[
  {
    "route_id": "id",
    "route_definition": {
      "id": "id",
      "predicates": [
        {
          "name": "Path",
          "args": {
            "pattern": "/jd"
          }
        }
      ],
      "filters": [
        {
          "name": "AddRequestHeader",
          "args": {
            "_genkey_0": "header",
            "_genkey_1": "addHeader"
          }
        },
        {
          "name": "AddRequestParameter",
          "args": {
            "_genkey_0": "param",
            "_genkey_1": "addParam"
          }
        }
      ],
      "uri": "http://127.0.0.1:8888/header",
      "order": 0
    },
    "order": 0
  },
  {
    "route_id": "client2",
    "route_definition": {
      "id": "client2",
      "predicates": [
        {
          "name": "Path",
          "args": {
            "_genkey_0": "/client2/**"
          }
        }
      ],
      "filters": [
        {
          "name": "StripPrefix",
          "args": {
            "_genkey_0": "1"
          }
        }
      ],
      "uri": "lb://client2",
      "order": 0
    },
    "order": 0
  }
]

 

 

问题

  • 在使用Hystrix时使用Threadlocal会有问题,这时需要将ThreadLocal替换成HystrixRequestVariableDefault来传递.
  • 在使用springcloud-gateway时,因为gateway使用的也是响应式,所以担心ThreadLocal也会出现问题, 不过好在过滤器的执行都是在一个线程中,那么就不用考虑这个问题.
  • 不应该在loadBalanceClientFilter之前加一个过滤器,应该直接继承它,然后在查找Instance后就把threadlocal清除,否则需要在其之后在加一个过滤器去清除,因为不像拦截器一样可以前后都拦截

 

 

TODO

  • 全链路的灰度控制:  只要实现FeignRequestInterceptor,RestTemplateInterceptor传递版本号,HttpRequestInterceptor接受并存到Threadlocal
  • 以为nacos自带元信息的动态修改,而eureka是不支持的,所以eureka的metainfo动态修改 需要额外开发, eureka提供的修改metadata的接口为  (/eureka/apps/{appID}/{instanceID}/metadata?key=value)
  • 在loadBalanceClientFilter之后加个拦截器进行删除threadlocal

 

 

说明

文章的代码已上传github (https://github.com/cdy1996/sample-springcloud/tree/nacos-weight-route) 代码部分参考自其他的出处.

 

 

 

你可能感兴趣的:(redis,springcloud,nacos)