spring cloud gateway 之 动态路由改造

目录

  1. gateway本地文件常规路由配置

  2. 本地文件配置对业务造成的痛点

  3. 动态路由改造

1 gateway本地文件常规路由配置

我们先大致看下gateway中的常规概念

  • Route(路由):路由是网关的基本单元,由ID、URI、一组Predicate、一组Filter组成,根据Predicate进行匹配转发。

  • Predicate(谓语、断言):路由转发的判断条件,目前SpringCloud Gateway支持多种方式,常见如:PathQueryMethodHeader等。

  • Filter(过滤器):过滤器是路由转发请求时所经过的过滤逻辑,可用于修改请求、响应内容。

     

整体架构:

图片

我们本地文件配置路由信息的时候都是application.yml中配置这样一段内容

 

 spring:    cloud:    gateway:      routes:        - id: authWsdl          uri: lb://demo-auth          predicates:            - Path=/demo/authenticationIf          filters:            - StripPrefix=1            - name: RequestRateLimiter              args:                # 令牌桶每秒填充平均速率                redis-rate-limiter.replenishRate: 100                # 令牌桶的上限                redis-rate-limiter.burstCapacity: 200                # 使用SpEL表达式从Spring容器中获取Bean对象                key-resolver: "#{@pathKeyResolver}"

 

gateway就可以转发到demo-auth认证微服务上了。至于其他的断言方式配置就不一一展现了,网上一搜一大把。

 

2 本地文件配置对业务造成的痛点

这中文件配置在实际生产环境中对业务有什么影响么?

 

我们网关作为业务的最前沿,如果经常更改配置文件重启会造成业务系统可用率大大降低,因为网关一单重启所有业务都不可用。而且对于流量大的公司来说重启网关将是灾难性的。

 

但是我们实际生产环境中可能后端服务经常变动,频繁的发布版本,或者灰度发布,虽然我们的服务都是注册到注册中心的,但是从业务下线到业务上线,这个时间段网关感知能力不是实时的,再次期间部分请求是失败的。

 

如果我们后端服务增加或者减少了 同样需要改网关配置文件,然后重启生效。那我们有没有办法吧网关的路由配置搞成动态的,让网关直接去数据库中动态读取呢?这样我们就可以减少网关的重启此处,降低系统失败率。

 

于是我们翻开spring cloud gateway的源码发现如下几个核心类:

​​​​​​​

org.springframework.cloud.gateway.route.RouteDefinitionWriterorg.springframework.cloud.gateway.route.RouteDefinitionLocatororg.springframework.cloud.gateway.route.RouteDefinitionRepositoryorg.springframework.cloud.gateway.route.InMemoryRouteDefinitionRepository

见名之意,一看着就是个mapper层的东西。原来啊gateway默认是走的内存,启动的时候回把配置文件解析到内存中。而RouteDefinitionRepository这个就是mapper的接口,那就好办了,接下来我们自己搞下,有点长耐心看下。

 

3 动态路由改造

我们的设想是这样

  1. 通过实现RouteDefinitionRepository接口来干扰路由信息的存储、读取等操作。

     

  2. 抽象出来一个GatewayRouteRepository接口,支持扩展多种存储方式。

     

  3. 开发一个管理后台对数据源头进行更新操作,后台提供可视化的页面,避免操作人员操作失误(漏写信息,错写单词等等)。

     

  4. 通过zk实现监听机制,因为我们实际生产环境中网关也是集群部署,我们需要更新完路由配置信息后所有网关实例都可以感知到,然后去数据源端重新拉取最新路由信息。

 

ok既然思路有了那我们就开始吧,创建MyRouteDefinitionRepository来实现:

   org.springframework.cloud.gateway.route.RouteDefinitionRepository

该接口有3个方法:​​​​​​​

//全量获取路由信息Flux<RouteDefinition> getRouteDefinitions() //保存路由信息 Mono<Void> save(Mono<RouteDefinition> route) //删除路由配置信息根据路由idMono<Void> delete(Mono<String> routeId)

 

我们先看内存实现的方式源码​​​​​​​

    private final Map<String, RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());
    public InMemoryRouteDefinitionRepository() {    }
    public Mono<Void> save(Mono<RouteDefinition> route) {        return route.flatMap((r) -> {            if (StringUtils.isEmpty(r.getId())) {                return Mono.error(new IllegalArgumentException("id may not be empty"));            } else {                this.routes.put(r.getId(), r);                return Mono.empty();            }        });    }
    public Mono<Void> delete(Mono<String> routeId) {        return routeId.flatMap((id) -> {            if (this.routes.containsKey(id)) {                this.routes.remove(id);                return Mono.empty();            } else {                return Mono.defer(() -> {                    return Mono.error(new NotFoundException("RouteDefinition not found: " + routeId));                });            }        });    }
    public Flux<RouteDefinition> getRouteDefinitions() {        return Flux.fromIterable(this.routes.values());    }

 

那就简单了 源代码粘过来,然后把

private final Map<String,RouteDefinition> routes = Collections.synchronizedMap(new LinkedHashMap());

 

改成我们自己的GatewayRouteRepository接口即可,这是我们的GatewayRouteRepository:​​​​​​​

public interface GatewayRouteRepository {
    void saveData(String id,String data) throws Exception ;
    void remove(String id) throws Exception ;
    List<RouteDefinition> getAllList();
}

 

我们实现了2中存储方式一种是redis 、一种是zk 为什么实现这2中呢,我们希望网关中引入的组件越少越好,这样可以降低网关的问题率,提高可用度,本身我们就通过redis在网关层做了限流操作,通过zk实现lvs自动负载到网实例(网关注册到zk,agent监听zk临时节点信息动态更改lvs配置)又设想通过zk动态通知所有实例刷新路由故此实现了这两种。

 

其他朋友也可以通过实现GatewayRouteRepository来适配适合你们自己的存储方式。

 

我们分别看下这2中实现​​​​​​​

public class RedisGateRouteRespository implements GatewayRouteRepository {
    private Logger log= LoggerFactory.getLogger(RedisGateRouteRespository.class);
    @Autowired    private StringRedisTemplateTest redisTemplate;
    @Override    public void saveData(String id, String data) throws Exception {        redisTemplate.hset(GateWayContext.ROUTEPATH,id,data);    }
    @Override    public void remove(String id) throws Exception {        redisTemplate.hdel(GateWayContext.ROUTEPATH,id);    }

    @Override    public List<RouteDefinition> getAllList() {        Set<String> keys = redisTemplate.hkeys(GateWayContext.ROUTEPATH);        List<RouteDefinition> routeDefinitions=new ArrayList<>();        keys.forEach(key->{            GatewayRouteDefinition gatewayRouteDefinition =                    JSON.parseObject(                            (String) redisTemplate.hget(GateWayContext.ROUTEPATH, key),                            GatewayRouteDefinition.class);            log.info("缓存信息:{}",gatewayRouteDefinition.toString());            routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));        });        return routeDefinitions;    }
}
​​​​​​​
public class ZookeeperRepository implements GatewayRouteRepository {
    @Autowired    private CuratorFramework zk;
    @Override    public void saveData(String id,String data) throws Exception {        if(null == zk.checkExists().forPath(GateWayContext.ROUTEPATH + "/" + id)) {            zk.create().creatingParentsIfNeeded().                    withMode(CreateMode.PERSISTENT)                    .forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());        }else{            zk.setData().forPath(GateWayContext.ROUTEPATH + "/" + id, data.getBytes());        }    }
    @Override    public void remove(String id) throws Exception {        zk.delete().forPath(id);    }
    @Override    public List<RouteDefinition> getAllList() {        List<RouteDefinition> routeDefinitions=new ArrayList<>();        try {            List<String> strings = zk.getChildren().forPath(GateWayContext.ROUTEPATH);            strings.forEach(path->{                try {                    String s = new String(zk.getData().forPath(GateWayContext.ROUTEPATH + "/" + path));                    GatewayRouteDefinition gatewayRouteDefinition = JSON.parseObject(s, GatewayRouteDefinition.class);                    routeDefinitions.add(RouteDefintionAssemble.assembleRouteDefinition(gatewayRouteDefinition));                } catch (Exception e) {                    e.printStackTrace();                }            });        } catch (Exception e) {            e.printStackTrace();        }        return routeDefinitions;    }

 

这2中实现代码很简单,就不做过多阐述了。我们来看下如何同时网关刷新路由信息。还是的看下源码是怎么做的,再次打开源码发现了一个event包如图

图片

哈哈这里有个RefreshRoutesEvent,一看这就是个通知刷新路由的event,那源码是否是通过spring事件机制刷新的路由的呢?我们来debug下看下调用链。

 

果然是呀,我们向网关增加一个路由然后发布refreshRoutesEvent事件,果然调用到我们自己的gatRouteDefinitions方法中取重新过去了全量路由信息

 

图片

 

那我们来看下我们如何用zk实现监听​

public class ZookeeperEventListener {    @Autowired    private NacosDiscoveryProperties nacosDiscoveryProperties;
    private Logger  log= LoggerFactory.getLogger(ZookeeperEventListener.class);
    @Autowired    private CuratorFramework zk;
    public ZookeeperEventListener(){        log.info("开始实例化 zk类");    }
    @Autowired    private MyGateWayApplicationEvent applicationEvent;
    @PostConstruct    public void init() throws Exception {        register();        listenerRoute();    }
   private void listenerRoute() throws Exception {        PathChildrenCache pathChildrenCache = new PathChildrenCache(zk, GateWayContext.ROUTEPATH,true);
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {            @Override            public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception  {                log.info("监听到zk发生变化,对应类型为:{}",event.getType());                //如果发现节点变更,清楚当前路由,从新加载                PathChildrenCacheEvent.Type type = event.getType();                if(type.equals(PathChildrenCacheEvent.Type.CHILD_UPDATED)){                    applicationEvent.publish();                }            }        });       pathChildrenCache.start();
    }
    private void register() throws Exception {        log.info("开始向zk注册");        String ip = nacosDiscoveryProperties.getIp();        zk.create().creatingParentsIfNeeded()                .withMode(CreateMode.EPHEMERAL)                .forPath(GateWayContext.GATEWAYREGISTERPATH+ip+":31081");    }
}
public class MyGateWayApplicationEvent implements ApplicationEventPublisherAware {
    private Logger log= LoggerFactory.getLogger(MyGateWayApplicationEvent.class);
    private ApplicationEventPublisher applicationEventPublisher;
    @Override    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {        this.applicationEventPublisher=applicationEventPublisher;    }
    public void publish(){        log.info("监听到zk的route节点发生变更,开始发布刷新事件");        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));    }}

只是这里是全量刷新,其实我们可以自己编写事件Event来根据路由id更新固定的某个路由信息,发布的时候吧路由id携带过去即可(需要自定义事件RefreshRoutesEventById,自己实现监听刷新逻辑,我们这里就不实现了有兴趣的朋友可以自己尝试下)。

 

我们管理后台那边只需把数据在源端更新后再更新zk的固定节点即可,这样所有网关实例均可马上感知到,不过实际代码debug中发现spring cloud gateway会定时去全量刷新。所以这部分看个人需求,如果想要立刻生效的可以采用我这种实现方式。

 

我这里为了避免后台项目需要依赖spring cloud gateway 包所以存储的model采用自定义的,这样的话gateway哪里就需要转换下。​​​​​​​

public class RouteDefintionAssemble {
    //把传递进来的参数转换成路由对象    public static  RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {        RouteDefinition definition = new RouteDefinition();        definition.setId(gwdefinition.getId());        definition.setOrder(gwdefinition.getOrder());
        //设置断言        List<PredicateDefinition> pdList=new ArrayList<>();        List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();        for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {            PredicateDefinition predicate = new PredicateDefinition();            predicate.setArgs(gpDefinition.getArgs());            predicate.setName(gpDefinition.getName());            pdList.add(predicate);        }        definition.setPredicates(pdList);
        //设置过滤器        List<FilterDefinition> filters = new ArrayList();        List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();        for(GatewayFilterDefinition filterDefinition : gatewayFilters){            FilterDefinition filter = new FilterDefinition();            filter.setName(filterDefinition.getName());            filter.setArgs(filterDefinition.getArgs());            filters.add(filter);        }        definition.setFilters(filters);        URI uri = null;        if(gwdefinition.getUri().startsWith("http")){            uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();        }else{            // uri为 lb://consumer-service 时使用下面的方法            uri = URI.create(gwdefinition.getUri());        }        definition.setUri(uri);        return definition;    }}

这是我自己实现的3个model ​​​​​​​

public class GatewayFilterDefinition {
    //过滤器名称    private String name;
    // 路由规则    private Map<String,String> args=new LinkedHashMap<>();
    public String getName() {        return name;    }
    public void setName(String name) {        this.name = name;    }
    public Map<String, String> getArgs() {        return args;    }
    public void setArgs(Map<String, String> args) {        this.args = args;    }}
​​​​​​
public class GatewayRouteDefinition {
    //路由id    private String id;
    //路由断言集合配置    private List<GatewayPredicateDefinition> predicates=new ArrayList<>();
    //路由过滤器集合配置    private List<GatewayFilterDefinition> filters=new ArrayList<>();
    //转发目标uri    private String uri;
    //执行顺序    private int order=0;
    public String getId() {        return id;    }
    public void setId(String id) {        this.id = id;    }
    public List<GatewayPredicateDefinition> getPredicates() {        return predicates;    }
    public void setPredicates(List<GatewayPredicateDefinition> predicates) {        this.predicates = predicates;    }
    public List<GatewayFilterDefinition> getFilters() {        return filters;    }
    public void setFilters(List<GatewayFilterDefinition> filters) {        this.filters = filters;    }
    public String getUri() {        return uri;    }
    public void setUri(String uri) {        this.uri = uri;    }
    public int getOrder() {        return order;    }
    public void setOrder(int order) {        this.order = order;    }    @Override    public String toString() {        return JSON.toJSONStringWithDateFormat(this,"yyyy-MM-dd HH:mm:ss");    }}

​​​​​​​​​​​​​​

public class GatewayPredicateDefinition {
    //断言对应的name    private String  name;
    //配置 断言规则    private Map<String,String> args=new LinkedHashMap<>();
    public String getName() {        return name;    }
    public void setName(String name) {        this.name = name;    }
    public Map<String, String> getArgs() {        return args;    }
    public void setArgs(Map<String, String> args) {        this.args = args;    }}

 

总结

通过以上改造我们的网关就可以不重启的情况下,动态更改路由信息了,还可以基于此功能实现灰度发布,动态增加、减少转发功能,后端服务上线下也可以做到最业务0影响,上述代码已经上线1月目前运行稳定,如果有需要改进的地方望朋友们踊跃指出。

 

spring这个家族 真的很伟大,所有产品都那么优秀,高度抽象,提供各种各样的接口来供我们扩展,非常值得我们去学习。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值