SpringCloud Gateway 在不重启网关服务的前提下,实现添加服务路由零配置升级

2022-08-30 14:54:41 浏览数 (1)

点击上方“猿芯”,选择“设为星标

后台回复"1024",有份惊喜送给面试的你

本文将分四部分讲解:

  1. SpringCloud Gateway 实现动态路由必要性
  2. SpringCloud Gateway 动态路由源码解析
  3. SpringCloud Gateway 动态路由配置实现方式
  4. SpringCloud Gateway 动态路由配置注意的事项

SpringCloud Gateway 实现动态路由必要性

在实际的生产环境中,如果采用了微服务架构,每次功能迭代发版上线,经常会遇到需要在网关,添加路由配置,如 zuul

代码语言:javascript复制
zuul:
 ignored-services: '*'
 routes:
    ddc:
      path: /ddc/**
      serviceId: portal-ddc
    pcm:
      path: /pcm/**
      serviceId: portal-pcm

由于采用的是 yml 配置文件添加路由,所以每次都需要在修改配置文件后,再重启网关服务,会造成全网停服的情况,给用户带来了很大的不便。

所以我们需要实现在不重启网关服务的前提下实现添加服务路由零配置升级

SpringCloud Gateway 动态路由源码解析

查看 Spring Cloud Gateway 官网,不幸的是 Gateway 并没有提供类似于 Nacos 控制台配置管理页面给开发者来管理服务的路由信息。

于是笔者翻阅 Gateway 路由相关源码,其内部是提供了路由 CRUD 相关 API 接口的。

GatewayControllerEndpoint 端点

Gateway 通过 GatewayControllerEndpoint 暴露路由 Endpoint 端点进行 CRUD 操作

接下来利用 Postman (据说还有个 Postwomen)进行路由 CRUD 操作。

  • 添加路由:actuator/gateway/routes/{id}
  • 删除路由:actuator/gateway/routes/{id}
  • 查询单条路由:actuator/gateway/routes/{id}
  • 查询所有路由:actuator/gateway/routes

另外,如果想访问 GatewayControllerEndpoint 端点中的方法,需要在 Gateway 添加 spring-boot-starter-actuator 依赖

代码语言:javascript复制
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

并在 yml 配置文件中暴露所有端点。

代码语言:javascript复制
management:
 endpoints:
  web:
   exposure: 
    include: "*"

打开浏览器输入 actuator 地址:http://localhost:8080/actuator/,如果找到 Gateway 端点信息:http://localhost:8080/actuator/gateway,说明可以通过 GatewayControllerEndpoint 进行 CRUD 操作了

SpringCloud Gateway 动态路由配置实现方式

除了使用 GatewayControllerEndpoint 可以配置路由之外还可以利用 RouteLocatorBuilder 通过代码构建服务路由。

代码语言:javascript复制
 @SpringBootApplication
 public class DemogatewayApplication {

 @Bean
 public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
   return builder.routes()
     .route("path_route", r -> r.path("/get")
     .uri("http://httpbin.org"))
     .route("host_route", r -> r.host("*.myhost.org")
     .uri("http://httpbin.org"))
     .route("rewrite_route", r -> r.host("*.rewrite.org")
     .filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
     .uri("http://httpbin.org"))
     .route("hystrix_route", r -> r.host("*.hystrix.org")
     .filters(f -> f.hystrix(c -> c.setName("slowcmd")))
     .uri("http://httpbin.org"))
     .route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
     .filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
     .uri("http://httpbin.org"))
     .route("limit_route", r -> r
     .host("*.limited.org").and().path("/anything/**")
     .filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
     .uri("http://httpbin.org"))
     .build();
  }
}

另外,如果不嫌麻烦,可以利用 RouteDefinitionWriter 自定义实现类进行路由保存删除操作。

代码语言:javascript复制
public interface RouteDefinitionWriter {
 Mono<Void> save(Mono<RouteDefinition> route);
 Mono<Void> delete(Mono<String> routeId);
}

默认情况下,Spring Cloud Gateway 使用内存方式(HashMap)存储路由信息。

其实现逻辑在 InMemoryRouteDefinitionRepository 类中,类图如下:

通过查看类图,我们知道 InMemoryRouteDefinitionRepositoryRouteDefinitionWriter 的一个实现类。

这里给我们一个很大启发,是否可以利用 RouteDefinitionWriter 自定义实现类,把路由信息存储到 mysqlredis 或者 mongo 等数据库呢?

答案是可以的。

例如,我们利用 Redis 缓存路由信息,只需在 RouteDefinitionWriter 实现类 RedisRouteDefinitionRepository 中添加 redisTemplate 注解,进行路由信息的 CRUD 操作。

代码语言:javascript复制
@Component
public class RedisRouteDefinitionRepository implements RouteDefinitionRepository {
 public static final String GW_ROUTES = "apis_gateway_routes";
 
  @Autowired
  private StringRedisTemplate redisTemplate;
  
  @Override
  public Flux<RouteDefinition> getRouteDefinitions() {
   List<RouteDefinition> routeDefinitions = new ArrayList<>();
   redisTemplate.opsForHash().values(GW_ROUTES).stream()
  .forEach(routeDefinition -> routeDefinitions.add(JSON.parseObject(routeDefinition.toString(),  RouteDefinition.class)));
   return Flux.fromIterable(routeDefinitions);
  }
  
  @Override
  public Mono<Void> save(Mono<RouteDefinition> route) {
    RouteDefinition definition = new RouteDefinition();
    definition.setId("id");
    URI uri = UriComponentsBuilder.fromHttpUrl("lb://consumer-service").build().toUri();
    definition.setUri(uri);
    PredicateDefinition predicate = new PredicateDefinition();
    predicate.setName("Path");
    Map<String, String> predicateArgs = new HashMap<>();
    predicateArgs.put("pattern", "/consumer/**");
    predicate.setArgs(predicateArgs);
    definition.setPredicates(Arrays.asList(predicate));
    FilterDefinition filter = new FilterDefinition();
    filter.setName("StripPrefix");
    Map<String, String> filterArgs = new HashMap<>();
    filterArgs.put("_genkey_0", "1");
    filter.setArgs(filterArgs);
    definition.setFilters(Arrays.asList(filter));
    redisTemplate.opsForHash().put(GW_ROUTES, "routeKey", JSON.toJSONString(definition));
    return null;
  }
  
  @Override
  public Mono<Void> delete(Mono<String> routeId) {
   return null;
  }
}

提供 REST 对外接口,对路由进行 CRUD 操作,最后,每次完成 save 或者 delete 删除,然后发一个 RefreshRoutesEvent 事件,通知 Gateway 更新路由信息。

代码语言:javascript复制
@RestController
@RequestMapping("/routes")
public class RouteController implements ApplicationEventPublisherAware {

  @Autowired
  private RedisRouteDefinitionRepository routeDefinitionWriter;
  
  private ApplicationEventPublisher publisher;

  @Override
  public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
   this.publisher = publisher;
  }
  
  @PostMapping
  public String addRoute(@RequestBody RouteDefinition definition) {
   routeDefinitionWriter.save(Mono.just(definition)).subscribe();
   this.publisher.publishEvent(new RefreshRoutesEvent(this));
   return "0";
  }
  
  @GetMapping("/{id}")
  public String delete(@PathVariable String id) {
    this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
    this.publisher.publishEvent(new RefreshRoutesEvent(this));
    return "0";
  }
}

如果自定义 RouteDefinitionWriter 的实现类,就会替换 InMemoryRouteDefinitionRepository从而当 rest 接口发送 RefreshRoutesEvent 刷新路由事件后, CachingRouteDefinitionLocator 刷新 Gateway 节点的路由缓存信息。

SpringCloud Gateway 动态路由配置注意的事项

在实际的生产环境中,Gateway网关一般是多实例部署,那么基于 InMemoryRouteDefinitionRepository 存储路由信息,并不合适。

因为每次通过 Gatewayrest 接口只会更新某个 Gateway 节点路由信息,并不能同步到其他节点。

这就解释为什么要用 redis 或则其他数据库存储路由信息的原因了。

这样当 Gateway 节点灰度重启或者在 Gateway 内置定时 job 刷新时,就可以通过 RedisRouteDefinitionRepositorygetRouteDefinitions 方法 从 redis 缓存获取路由信息呢。

往期推荐

  1. 肝九千字长文 | MyBatis-Plus 码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
  2. 用 MHA 做 MySQL 读写分离,频繁爆发线上生产事故后,泪奔分享 Druid 连接池参数优化实战
  3. 微服务架构下,解决数据库跨库查询的一些思路
  4. 一文读懂阿里大中台、小前台战略

作者简介:猿芯,一枚简单的北漂程序员。喜欢用简单的文字记录工作与生活中的点点滴滴,愿与你一起分享程序员灵魂深处真正的内心独白。我的微信号:WooolaDunzung,公众号【猿芯】输入 1024 ,有份面试惊喜送给你哦。

< END >

【猿芯】

微信扫描二维码,关注我的公众号。

原创不易,莫要干想,如果觉得有点用的话,动动你的发财之手,一键三连击:分享、点赞、在看,你们的鼓励是我写作更多优质文章的最强动力 ^_^

分享、点赞、在看,3连3连!

0 人点赞