原文网址:Spring Cloud Gateway--动态路由--方案/对比_IT利刃出鞘的博客-CSDN博客
简介
说明
本文介绍Spring Cloud Gateway动态路由的方案。
为什么需要动态路由
场景1:开发环境:提高调试效率
在开发软件(例如Idea)上打断点调试是最快的调试方法。如果是与前端联调,前端一般将请求地址设置为网关,这样会负载均衡到不同的机器上,不能指定到自己的电脑。
如果有了动态路由,那么可以指定某个url直接路由到自己电脑。
场景2:线上环境:可以快速将请求切到某个服务器
如果没有动态路由,代码上线时如果出了问题,需要回滚代码,重新部署,很慢。
如果有了动态路由,代码上线时,只更新部分实例,然后将流量切过去,如果有问题,立马切到其他未更新的实例即可。
1.静态路由
所谓静态路由,就是指API网关启动前,通过配置文件或者代码的方式,静态的配置好API之间的路由关系,此后不需要二次维护,大多数的内部API网关适用于这种方式。
方法1:配置文件
本质上是修改application.yml文件,相关修改方法,官网已经有详尽的描述了,如需帮助可参考官方文档。本文仅举例其中一种,一看便知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17spring: # 网关配置 cloud: gateway: routes: - id: payment_routh #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 #uri: lb://cloud-payment-service #匹配后提供服务的路由地址 predicates: - Path=/payment/get/** # 断言,路径相匹配的进行路由 - id: payment_routh2 #payment_route #路由的ID,没有固定规则但要求唯一,建议配合服务名 uri: http://localhost:8001 #匹配后提供服务的路由地址 #uri: lb://cloud-payment-service #匹配后提供服务的路由地址 predicates: - Path=/payment/lb/** # 断言,路径相匹配的进行路由
方式2:用代码配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// static imports from GatewayFilters and RoutePredicates @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) { return builder.routes() .route(r -> r.host("**.abc.org").and().path("/image/png") .filters(f -> f.addResponseHeader("X-TestHeader", "foobar")) .uri("http://httpbin.org:80") ) .route(r -> r.path("/image/webp") .filters(f -> f.addResponseHeader("X-AnotherHeader", "baz")) .uri("http://httpbin.org:80") .metadata("key", "value") ) .route(r -> r.order(-1) .host("**.throttle.org").and().path("/get") .filters(f -> f.filter(throttle.apply(1, 1, 10, TimeUnit.SECONDS))) .uri("http://httpbin.org:80") .metadata("key", "value") ) .build(); }
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
3
4
5
6
7
8
9
10{ "id": "first_route", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/first"} }], "filters": [], "uri": "https://www.uri-destination.org", "order": 0 }]
删除路由
使用 DELETE 请求到 /gateway/routes/{id_route_to_delete}即可完成删除路由。
3.自由扩展动态路由
上述自由度有限。
- 基于服务注册发现的Spring Cloud DiscoveryClient,需要全部服务在Spring Cloud家族体系下,一旦有外部路由关系,会将思维负载化。
- Actuator API是一种外部API调用,虽然能够解决90%以上的问题,但是对于高度定制化的需求,频繁定制增删改查路由的API,难免会有bug,甚至修改时会造成服务的瞬时不可用。
基于上述问题,为何不尝试使用代码的方式解决问题?Spring Cloud Gateway的源码非常优秀,可以有多种方式让我们实现接口,完成一切我们想要的,于是想出了如下两种思路:
- 思路一:底层修改,扩展Spring Cloud Gateway底层路由加载机制
- 思路二:动态修改,请求进来时,以GlobalFilter的方式动态修改路由地址
方案1:底层修改
底层修改,就是通过一定机制,将Spring Cloud Gateway运行态时保存的路由关系,通过实现、继承加载自定义类的方式,对其进行动态路由修改,每当路由有变化时,再触发一次动态的修改。
因此,这种思路需要两种保障: 1. 监听机制 2. 实现自定义路由的核心类
Spring Cloud Gateway 核心加载机制如图所示:
大体上来讲,我们有两种修改思路:
- 从 RouteDefinitonLocator阶段下手
- 从RouteLoacator阶段下手
法1:RouteLocator 全量更新
首先,实现ApplicationEventPublisherAware接口,实现路由的动态监听。
1
2
3
4
5
6
7
8
9
10
11
12
13
14@Component public class GatewayRoutesRefresher implements ApplicationEventPublisherAware { private ApplicationEventPublisher publisher; @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } public void refreshRoutes() { publisher.publishEvent(new RefreshRoutesEvent(this)); } }
然后实现RouteLocator,一次性刷新全量的API信息,实现动态加载。
apiRepository是自定义的API Repository,可来源于Redis、数据库、Zookeeper等,保存着API的信息及PATH(getRequestPath)、目标地址(getRoutePath)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51@Component public class RefreshRouteLocator implements RouteLocator { private static Logger log = LoggerFactory.getLogger(RefreshRouteLocator.class); private Flux<Route> route; private RouteLocatorBuilder builder; private RouteLocatorBuilder.Builder routesBuilder; /** * 自定义的API Repository,可来源于Redis、数据库、Zookeeper等,保存着API的信息及PATH、目标地址 */ @Autowired private APIRepository apiRepository; @Autowired GatewayRoutesRefresher gatewayRoutesRefresher; public RefreshRouteLocator(RouteLocatorBuilder builder) { this.builder = builder; clearRoutes(); } public void clearRoutes() { routesBuilder = builder.routes(); } /** * 配置完成后,调用本方法构建路由和刷新路由表 */ public void buildRoutes() { clearRoutes(); if (routesBuilder != null) { apiRepository.getAll().parallelStream().forEach(service ->{ String serviceId = service.getServiceId(); APIInfo serviceDefinition = apiRepository.get(serviceId); if (serviceDefinition == null) { log.error("无此服务配置信息:" + serviceId); } URI uri = UriComponentsBuilder.fromHttpUrl(serviceDefinition.getRoutePath()).build().toUri(); routesBuilder.route(serviceId, r -> r.path(serviceDefinition.getRequestPath()).uri(uri)); }); this.route = routesBuilder.build().getRoutes(); } gatewayRoutesRefresher.refreshRoutes(); } @Override public Flux<Route> getRoutes() { return route; } }
最后,在需要刷新时,可调用buildRoutes(),重新构建全量路由,完成!
1
2
3
4
5
6
7@Autowired private RefreshRouteLocator refreshableRoutesLocator; // ...... public void 需要刷新路由时的方法() { refreshableRoutesLocator.buildRoutes(); }
法2:RouteDefinitionRepository 全量更新
RouteDefinitionRepository方法与方法一类似,RouteDefinitionLocator 是路由定义定位器的顶级接口,它的主要作用就是读取路由的配置信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56@Component public class FileRouteDefinitionRepository implements RouteDefinitionRepository, ApplicationEventPublisherAware { private static final Logger LOGGER = LoggerFactory.getLogger(FileRouteDefinitionRepository.class); private ApplicationEventPublisher publisher; private List<RouteDefinition> routeDefinitionList = new ArrayList<>(); @Value("${gateway.route.config.file}") private String file; @Override public void setApplicationEventPublisher(ApplicationEventPublisher publisher) { this.publisher = publisher; } @PostConstruct public void init() { load(); } /** * 监听事件刷新配置 */ @EventListener public void listenEvent(RouteConfigRefreshEvent event) { load(); this.publisher.publishEvent(new RefreshRoutesEvent(this)); } /** * 加载 */ private void load() { try { String jsonStr = Files.lines(Paths.get(file)).collect(Collectors.joining()); routeDefinitionList = JSON.parseArray(jsonStr, RouteDefinition.class); LOGGER.info("路由配置已加载,加载条数:{}", routeDefinitionList.size()); } catch (Exception e) { LOGGER.error("从文件加载路由配置异常", e); } } @Override public Mono<Void> save(Mono<RouteDefinition> route) { return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation"))); } @Override public Mono<Void> delete(Mono<String> routeId) { return Mono.defer(() -> Mono.error(new NotFoundException("Unsupported operation"))); } @Override public Flux<RouteDefinition> getRouteDefinitions() { return Flux.fromIterable(routeDefinitionList); } }
法3:RouteDefinitionWriter 增量更新
上述的方式,都是通过构建全量API,更新API达到路由关系的全量更新,但似乎操作风险大了点,如果想一条一条的增量更新,除了Actuator API,还有没有其它方式呢?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public interface RouteDefinitionWriter { /** * 保存路由配置 * * @param route 路由配置 * @return Mono<Void> */ Mono<Void> save(Mono<RouteDefinition> route); /** * 删除路由配置 * * @param routeId 路由编号 * @return Mono<Void> */ Mono<Void> delete(Mono<String> routeId); }
RouteDefinitionWriter 接口定义了保存save与删除delete两个路由方法。可以通过Autowired后调用这两个方法,调用修改路由关系,例子如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63/** * redis 保存路由信息,优先级比配置文件高 * @author Zou.LiPing */ @Component @RequiredArgsConstructor @Slf4j(topic = "RedisRouteDefinitionWriter") public class RedisRouteDefinitionWriter implements RouteDefinitionRepository, ApplicationEventPublisherAware { private final RedisUtils redisUtils; private ApplicationEventPublisher publisher; @Override public Mono<Void> save(Mono<RouteDefinition> route) { return route.flatMap(r -> { RouteDefinitionVo vo = new RouteDefinitionVo(); BeanUtils.copyProperties(r, vo); log.info("保存路由信息{}", vo); redisUtils.hPut(CacheConstants.ROUTE_KEY,r.getId(), JSON.toJSONString(vo)); refreshRoutes(); return Mono.empty(); }); } @Override public Mono<Void> delete(Mono<String> routeId) { routeId.subscribe(id -> { log.info("删除路由信息={}", id); redisUtils.hDelete(CacheConstants.ROUTE_KEY, id); refreshRoutes(); }); return Mono.empty(); } private void refreshRoutes() { this.publisher.publishEvent(new RefreshRoutesEvent(this)); } /** * 动态路由入口 * @return Flux<RouteDefinition> */ @Override public Flux<RouteDefinition> getRouteDefinitions() { List<RouteDefinition> definitionList = new ArrayList<>(); Map<Object, Object> objectMap = redisUtils.hGetAll(CacheConstants.ROUTE_KEY); if (Objects.nonNull(objectMap)) { for (Map.Entry<Object, Object> objectObjectEntry : objectMap.entrySet()) { RouteDefinition routeDefinition = JSON.parseObject(objectObjectEntry.getValue().toString(),RouteDefinition.class); definitionList.add(routeDefinition); } } log.info("redis 中路由定义条数: {}, definitionList={}", definitionList.size(), definitionList); return Flux.fromIterable(definitionList); } @Override public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) { this.publisher = applicationEventPublisher; } }
方案2:动态修改
通过修改底层的方式,应该是比较优选的方案,但也有其弊端,就是灵活度不够。
如果相同的API,但需根据不同的业务逻辑,如租户ID等标识路由到不同的位置,那种方案似乎就无法解决了。
这个时候,我们可以自己实现一个GlobalFilter,来实现在请求进来后,动态的修改路由目标地址。
这种方式,可能损失一定的效率,但可以拥有更高的灵活度。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78package com.knife.gateway.dynamic; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; import java.net.URI; import java.util.Map; import java.util.Objects; /** * 动态路由 */ @Slf4j @Component public class Router4jFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest originalRequest = exchange.getRequest(); // 可获得所有请求参数 // Map<String, String> cachedRequestBody = exchange // .getAttribute(ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR); //获取域名+端口后的path String rawPath = originalRequest.getURI().getRawPath(); // todo 从redis中取出所有url,然后用rawPath去匹配 String host = "localhost"; int port = 9012; URI originUri = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR); URI newUri = UriComponentsBuilder.fromUri(originUri) .host(host) .port(port) .build() .toUri(); //重新封装request对象 ServerHttpRequest newRequest = originalRequest.mutate().uri(newUri).build(); // NettyRoutingFilter 最终从GATEWAY_REQUEST_URL_ATTR 取出uri对象进行http请求 // 所以这里要将新的对象覆盖进去 exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, newUri); return chain.filter(exchange.mutate().request(newRequest).build()); // 也可以加回调方法 // return chain.filter(exchange.mutate().request(newRequest).build()) // .then(Mono.fromRunnable(() -> { // //请求完成回调方法 可以在此完成计算请求耗时等操作 // })); } /** * 这里不能用@Order,必须实现Ordered接口 * 值必须大于10150。原因:Gateway有自己的过滤器,两个比较重要的如下: * RouteToRequestUrlFilter:将根据Route将网关请求转为真实的请求。order = 10000 * ReactiveLoadBalancerClientFilter:负载均衡。order = 10150 */ @Override public int getOrder() { return 15000; } }
动态路由方案对比
方法 | 优点 | 缺点 |
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 多种思路实现动态路由 - 简书
最后
以上就是斯文泥猴桃最近收集整理的关于Spring Cloud Gateway--动态路由--方案/对比简介为什么需要动态路由1.静态路由2.原生动态路由3.自由扩展动态路由动态路由方案对比其他网址的全部内容,更多相关Spring内容请搜索靠谱客的其他文章。
发表评论 取消回复