当前位置:   article > 正文

Spring Cloud Gateway--动态路由--方案/对比_springcloud 动态 getway

springcloud 动态 getway

原文网址:Spring Cloud Gateway--动态路由--方案/对比_IT利刃出鞘的博客-CSDN博客

简介

说明

        本文介绍Spring Cloud Gateway动态路由的方案。

为什么需要动态路由

场景1:开发环境:提高调试效率

        在开发软件(例如Idea)上打断点调试是最快的调试方法。如果是与前端联调,前端一般将请求地址设置为网关,这样会负载均衡到不同的机器上,不能指定到自己的电脑。

        如果有了动态路由,那么可以指定某个url直接路由到自己电脑。

场景2:线上环境:可以快速将请求切到某个服务器

        如果没有动态路由,代码上线时如果出了问题,需要回滚代码,重新部署,很慢。

如果有了动态路由,代码上线时,只更新部分实例,然后将流量切过去,如果有问题,立马切到其他未更新的实例即可。

1.静态路由

所谓静态路由,就是指API网关启动前,通过配置文件或者代码的方式,静态的配置好API之间的路由关系,此后不需要二次维护,大多数的内部API网关适用于这种方式。

方法1:配置文件

        本质上是修改application.yml文件,相关修改方法,官网已经有详尽的描述了,如需帮助可参考官方文档。本文仅举例其中一种,一看便知。

  1. spring:
  2. # 网关配置
  3. cloud:
  4. gateway:
  5. routes:
  6. - id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
  7. uri: http://localhost:8001 #匹配后提供服务的路由地址
  8. #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
  9. predicates:
  10. - Path=/payment/get/** # 断言,路径相匹配的进行路由
  11. - id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名
  12. uri: http://localhost:8001 #匹配后提供服务的路由地址
  13. #uri: lb://cloud-payment-service #匹配后提供服务的路由地址
  14. predicates:
  15. - Path=/payment/lb/** # 断言,路径相匹配的进行路由

方式2:用代码配置

  1. // static imports from GatewayFilters and RoutePredicates
  2. @Bean
  3. public RouteLocator customRouteLocator(RouteLocatorBuilder builder,
  4. ThrottleGatewayFilterFactory throttle) {
  5. return builder.routes()
  6. .route(r -> r.host("**.abc.org").and().path("/image/png")
  7. .filters(f ->
  8. f.addResponseHeader("X-TestHeader", "foobar"))
  9. .uri("http://httpbin.org:80")
  10. )
  11. .route(r -> r.path("/image/webp")
  12. .filters(f ->
  13. f.addResponseHeader("X-AnotherHeader", "baz"))
  14. .uri("http://httpbin.org:80")
  15. .metadata("key", "value")
  16. )
  17. .route(r -> r.order(-1)
  18. .host("**.throttle.org").and().path("/get")
  19. .filters(f -> f.filter(throttle.apply(1,
  20. 1,
  21. 10,
  22. TimeUnit.SECONDS)))
  23. .uri("http://httpbin.org:80")
  24. .metadata("key", "value")
  25. )
  26. .build();
  27. }

2.原生动态路由

1. Spring Cloud DiscoveryClient

        Spring Cloud原生支持服务自动发现并且注册到路由之中,通过在application.properties中设置spring.cloud.gateway.discovery.locator.enabled=true ,同时确保 DiscoveryClient 的实体 (Nacos,Netflix Eureka, Consul, 或 Zookeeper) 已经生效,即可完成服务的自动发现及注册。

2. Actuator API

创建路由

        创建一个路由关系,需要使用 POST请求到/gateway/routes/{id_route_to_create} 。请求内容为JSON请求体,请求内容参考如下。

  1. {
  2. "id": "first_route",
  3. "predicates": [{
  4. "name": "Path",
  5. "args": {"_genkey_0":"/first"}
  6. }],
  7. "filters": [],
  8. "uri": "https://www.uri-destination.org",
  9. "order": 0
  10. }]

删除路由

 使用 DELETE 请求到 /gateway/routes/{id_route_to_delete}即可完成删除路由。

3.自由扩展动态路由

        上述自由度有限。

  1. 基于服务注册发现的Spring Cloud DiscoveryClient,需要全部服务在Spring Cloud家族体系下,一旦有外部路由关系,会将思维负载化。
  2. Actuator API是一种外部API调用,虽然能够解决90%以上的问题,但是对于高度定制化的需求,频繁定制增删改查路由的API,难免会有bug,甚至修改时会造成服务的瞬时不可用。

基于上述问题,为何不尝试使用代码的方式解决问题?Spring Cloud Gateway的源码非常优秀,可以有多种方式让我们实现接口,完成一切我们想要的,于是想出了如下两种思路:

  1. 思路一:底层修改,扩展Spring Cloud Gateway底层路由加载机制
  2. 思路二:动态修改,请求进来时,以GlobalFilter的方式动态修改路由地址

方案1:底层修改

        底层修改,就是通过一定机制,将Spring Cloud Gateway运行态时保存的路由关系,通过实现、继承加载自定义类的方式,对其进行动态路由修改,每当路由有变化时,再触发一次动态的修改。

        因此,这种思路需要两种保障: 1. 监听机制 2. 实现自定义路由的核心类

        Spring Cloud Gateway 核心加载机制如图所示:

大体上来讲,我们有两种修改思路:

  1. 从 RouteDefinitonLocator阶段下手
  2. 从RouteLoacator阶段下手

法1:RouteLocator 全量更新

首先,实现ApplicationEventPublisherAware接口,实现路由的动态监听。

  1. @Component
  2. public class GatewayRoutesRefresher implements ApplicationEventPublisherAware {
  3. private ApplicationEventPublisher publisher;
  4. @Override
  5. public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
  6. this.publisher = applicationEventPublisher;
  7. }
  8. public void refreshRoutes() {
  9. publisher.publishEvent(new RefreshRoutesEvent(this));
  10. }
  11. }

        然后实现RouteLocator,一次性刷新全量的API信息,实现动态加载。

        apiRepository是自定义的API Repository,可来源于Redis、数据库、Zookeeper等,保存着API的信息及PATH(getRequestPath)、目标地址(getRoutePath)。

  1. @Component
  2. public class RefreshRouteLocator implements RouteLocator {
  3. private static Logger log = LoggerFactory.getLogger(RefreshRouteLocator.class);
  4. private Flux<Route> route;
  5. private RouteLocatorBuilder builder;
  6. private RouteLocatorBuilder.Builder routesBuilder;
  7. /**
  8. * 自定义的API Repository,可来源于Redis、数据库、Zookeeper等,保存着API的信息及PATH、目标地址
  9. */
  10. @Autowired
  11. private APIRepository apiRepository;
  12. @Autowired
  13. GatewayRoutesRefresher gatewayRoutesRefresher;
  14. public RefreshRouteLocator(RouteLocatorBuilder builder) {
  15. this.builder = builder;
  16. clearRoutes();
  17. }
  18. public void clearRoutes() {
  19. routesBuilder = builder.routes();
  20. }
  21. /**
  22. * 配置完成后,调用本方法构建路由和刷新路由表
  23. */
  24. public void buildRoutes() {
  25. clearRoutes();
  26. if (routesBuilder != null) {
  27. apiRepository.getAll().parallelStream().forEach(service ->{
  28. String serviceId = service.getServiceId();
  29. APIInfo serviceDefinition = apiRepository.get(serviceId);
  30. if (serviceDefinition == null) {
  31. log.error("无此服务配置信息:" + serviceId);
  32. }
  33. URI uri = UriComponentsBuilder.fromHttpUrl(serviceDefinition.getRoutePath()).build().toUri();
  34. routesBuilder.route(serviceId, r -> r.path(serviceDefinition.getRequestPath()).uri(uri));
  35. });
  36. this.route = routesBuilder.build().getRoutes();
  37. }
  38. gatewayRoutesRefresher.refreshRoutes();
  39. }
  40. @Override
  41. public Flux<Route> getRoutes() {
  42. return route;
  43. }
  44. }

最后,在需要刷新时,可调用buildRoutes(),重新构建全量路由,完成!

  1. @Autowired
  2. private RefreshRouteLocator refreshableRoutesLocator;
  3. // ......
  4. public void 需要刷新路由时的方法() {
  5. refreshableRoutesLocator.buildRoutes();
  6. }

法2:RouteDefinitionRepository 全量更新

        RouteDefinitionRepository方法与方法一类似,RouteDefinitionLocator 是路由定义定位器的顶级接口,它的主要作用就是读取路由的配置信息。

  1. @Component
  2. public class FileRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware {
  3. private static final Logger LOGGER = LoggerFactory.getLogger(FileRouteDefinitionRepository.class);
  4. private ApplicationEventPublisher publisher;
  5. private List<RouteDefinition> routeDefinitionList = new ArrayList<>();
  6. @Value("${gateway.route.config.file}")
  7. private String file;
  8. @Override
  9. public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
  10. this.publisher = publisher;
  11. }
  12. @PostConstruct
  13. public void init() {
  14. load();
  15. }
  16. /**
  17. * 监听事件刷新配置
  18. */
  19. @EventListener
  20. public void listenEvent(RouteConfigRefreshEvent event) {
  21. load();
  22. this.publisher.publishEvent(new RefreshRoutesEvent(this));
  23. }
  24. /**
  25. * 加载
  26. */
  27. private void load() {
  28. try {
  29. String jsonStr = Files.lines(Paths.get(file)).collect(Collectors.joining());
  30. routeDefinitionList = JSON.parseArray(jsonStr, RouteDefinition.class);
  31. LOGGER.info("路由配置已加载,加载条数:{}", routeDefinitionList.size());
  32. } catch (Exception e) {
  33. LOGGER.error("从文件加载路由配置异常", e);
  34. }
  35. }
  36. @Override
  37. public Mono<Void> save(Mono<RouteDefinition> route) {
  38. return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
  39. }
  40. @Override
  41. public Mono<Void> delete(Mono<String> routeId) {
  42. return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation")));
  43. }
  44. @Override
  45. public Flux<RouteDefinition> getRouteDefinitions() {
  46. return Flux.fromIterable(routeDefinitionList);
  47. }
  48. }

法3:RouteDefinitionWriter 增量更新

        上述的方式,都是通过构建全量API,更新API达到路由关系的全量更新,但似乎操作风险大了点,如果想一条一条的增量更新,除了Actuator API,还有没有其它方式呢?

  1. public interface RouteDefinitionWriter {
  2. /**
  3. * 保存路由配置
  4. *
  5. * @param route 路由配置
  6. * @return Mono<Void>
  7. */
  8. Mono<Void> save(Mono<RouteDefinition> route);
  9. /**
  10. * 删除路由配置
  11. *
  12. * @param routeId 路由编号
  13. * @return Mono<Void>
  14. */
  15. Mono<Void> delete(Mono<String> routeId);
  16. }

        RouteDefinitionWriter 接口定义了保存save与删除delete两个路由方法。可以通过Autowired后调用这两个方法,调用修改路由关系,例子如下。

  1. /**
  2. * redis 保存路由信息,优先级比配置文件高
  3. * @author Zou.LiPing
  4. */
  5. @Component
  6. @RequiredArgsConstructor
  7. @Slf4j(topic = "RedisRouteDefinitionWriter")
  8. public class RedisRouteDefinitionWriter implements RouteDefinitionRepository, ApplicationEventPublisherAware {
  9. private final RedisUtils redisUtils;
  10. private ApplicationEventPublisher publisher;
  11. @Override
  12. public Mono<Void> save(Mono<RouteDefinition> route) {
  13. return route.flatMap(r -> {
  14. RouteDefinitionVo vo = new RouteDefinitionVo();
  15. BeanUtils.copyProperties(r, vo);
  16. log.info("保存路由信息{}", vo);
  17. redisUtils.hPut(CacheConstants.ROUTE_KEY,r.getId(), JSON.toJSONString(vo));
  18. refreshRoutes();
  19. return Mono.empty();
  20. });
  21. }
  22. @Override
  23. public Mono<Void> delete(Mono<String> routeId) {
  24. routeId.subscribe(id -> {
  25. log.info("删除路由信息={}", id);
  26. redisUtils.hDelete(CacheConstants.ROUTE_KEY, id);
  27. refreshRoutes();
  28. });
  29. return Mono.empty();
  30. }
  31. private void refreshRoutes() {
  32. this.publisher.publishEvent(new RefreshRoutesEvent(this));
  33. }
  34. /**
  35. * 动态路由入口
  36. * @return Flux<RouteDefinition>
  37. */
  38. @Override
  39. public Flux<RouteDefinition> getRouteDefinitions() {
  40. List<RouteDefinition> definitionList = new ArrayList<>();
  41. Map<Object, Object> objectMap = redisUtils.hGetAll(CacheConstants.ROUTE_KEY);
  42. if (Objects.nonNull(objectMap)) {
  43. for (Map.Entry<Object, Object> objectObjectEntry : objectMap.entrySet()) {
  44. RouteDefinition routeDefinition = JSON.parseObject(objectObjectEntry.getValue().toString(),RouteDefinition.class);
  45. definitionList.add(routeDefinition);
  46. }
  47. }
  48. log.info("redis 中路由定义条数: {}, definitionList={}", definitionList.size(), definitionList);
  49. return Flux.fromIterable(definitionList);
  50. }
  51. @Override
  52. public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
  53. this.publisher = applicationEventPublisher;
  54. }
  55. }

方案2:动态修改

        通过修改底层的方式,应该是比较优选的方案,但也有其弊端,就是灵活度不够。

        如果相同的API,但需根据不同的业务逻辑,如租户ID等标识路由到不同的位置,那种方案似乎就无法解决了。

        这个时候,我们可以自己实现一个GlobalFilter,来实现在请求进来后,动态的修改路由目标地址。

        这种方式,可能损失一定的效率,但可以拥有更高的灵活度。

  1. package com.knife.gateway.dynamic;
  2. import lombok.extern.slf4j.Slf4j;
  3. import org.springframework.cloud.gateway.filter.GatewayFilterChain;
  4. import org.springframework.cloud.gateway.filter.GlobalFilter;
  5. import org.springframework.cloud.gateway.route.Route;
  6. import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
  7. import org.springframework.core.Ordered;
  8. import org.springframework.core.annotation.Order;
  9. import org.springframework.http.server.reactive.ServerHttpRequest;
  10. import org.springframework.stereotype.Component;
  11. import org.springframework.web.server.ServerWebExchange;
  12. import org.springframework.web.util.UriComponents;
  13. import org.springframework.web.util.UriComponentsBuilder;
  14. import reactor.core.publisher.Mono;
  15. import java.net.URI;
  16. import java.util.Map;
  17. import java.util.Objects;
  18. /**
  19. * 动态路由
  20. */
  21. @Slf4j
  22. @Component
  23. public class Router4jFilter implements GlobalFilter, Ordered {
  24. @Override
  25. public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
  26. ServerHttpRequest originalRequest = exchange.getRequest();
  27. // 可获得所有请求参数
  28. // Map<String, String> cachedRequestBody = exchange
  29. // .getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR);
  30. //获取域名+端口后的path
  31. String rawPath = originalRequest.getURI().getRawPath();
  32. // todo 从redis中取出所有url,然后用rawPath去匹配
  33. String host = "localhost";
  34. int port = 9012;
  35. URI originUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
  36. URI newUri = UriComponentsBuilder.fromUri(originUri)
  37. .host(host)
  38. .port(port)
  39. .build()
  40. .toUri();
  41. //重新封装request对象
  42. ServerHttpRequest newRequest = originalRequest.mutate().uri(newUri).build();
  43. // NettyRoutingFilter 最终从GATEWAY_REQUEST_URL_ATTR 取出uri对象进行http请求
  44. // 所以这里要将新的对象覆盖进去
  45. exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, newUri);
  46. return chain.filter(exchange.mutate().request(newRequest).build());
  47. // 也可以加回调方法
  48. // return chain.filter(exchange.mutate().request(newRequest).build())
  49. // .then(Mono.fromRunnable(() -> {
  50. // //请求完成回调方法 可以在此完成计算请求耗时等操作
  51. // }));
  52. }
  53. /**
  54. * 这里不能用@Order,必须实现Ordered接口
  55. * 值必须大于10150。原因:Gateway有自己的过滤器,两个比较重要的如下:
  56. * RouteToRequestUrlFilter:将根据Route将网关请求转为真实的请求。order = 10000
  57. * ReactiveLoadBalancerClientFilter:负载均衡。order = 10150
  58. */
  59. @Override
  60. public int getOrder() {
  61. return 15000;
  62. }
  63. }

动态路由方案对比

方法

优点

缺点

Spring Cloud DiscoveryClient

完全兼容DiscoveryClient,零编码,配置文件一句话

场景局限、自由度低

Actuator API

OpenAPI、Spring Cloud Gateway内部源码改变影响程度较小,不需要关注内部细节

没有修改、有操作风险、加载全量需外部请求大量次数API

底层更新 - 全量 (RouteDefinitonLocator、RouteLoacator)

只需考虑整体API路由关系、实现思路简单

全量修改万一存在BUG影响整体、效率浪费

底层更新 - 增量 (RouteDefinitionWriter)

效率较优

适用于单独修改新增的频繁的场景,有重复新增、删除的风险

动态更新 (GlobalFilter)

自由度超高

效率稍低、没有在底层或路由关系中修改、Acuator API无法查看实际路由关系、摒弃了Spring Cloud Gateway的优秀特性

其他网址

Spring Cloud Gateway 多种思路实现动态路由 - 简书

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/AllinToyou/article/detail/317421?site
推荐阅读
相关标签
  

闽ICP备14008679号