spring cloud项目07:网关(Gateway)(2)
JAVA 8
spring boot 2.5.2
spring cloud 2020.0.3
---
授人以渔:
最新版本,下载下来,以便查阅。
更多版本的官方文档:
https://docs.spring.io/spring-cloud/docs/
没有PDF版本,把网页保存下来。
前文:spring cloud项目06:网关(Gateway)(1)
本文使用的项目:
主要路径:前端请求经过 external.gateway 转发到 adapter.web。在此过程中,会做一些试验。
external.gateway | 网关服务 | 端口 25001 |
adapter.web | web适配层应用 | 端口 21001 |
data.user | user数据层应用 | 端口 20001 |
eureka.server | Eureka注册中心 | 端口 10001 |
目录
使用RequestRateLimiterGatewayFilterFactory限流
前文 中,路由配置中的uri使用的是 http://localhost:21001,硬编码,而且uri无法配置多个(试验失败),实现不了负载均衡(LB,Load Balance),需要改进。
在S.C.微服务系统中,所有服务都可以注册,那么,网关服务是否可以注册呢?当然可以!
网关服务注册之后,即可使用注册中心的服务信息来访问 已注册的服务。
添加依赖包:
<!-- 注册到注册中心 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
添加配置(同其它微服务):
# Eureka客户端配置
eureka:
instance.prefer-ip-address: true
lease-renewal-interval-in-seconds: 15
lease-expiration-duration-in-seconds: 30
client:
service-url:
defaultZone: http://localhost:10001/eureka/
registry-fetch-interval-seconds: 20
# 提升日志级别,避免输出太多注册相关的正常日志
logging:
level:
com.netflix.discovery.DiscoveryClient: warn
spring:
application:
# 服务名
name: external.gateway
检查注册中心:网关服务已注册成功
检查 /actuator/gateway/globalfilters端点:来自博客园
和前文对比,多了一个 ReactiveLoadBalancerClientFilter,看起来是做 LB 的。
返回信息
{
"org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter@68b9834c": -2147482648,
"org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter@7da39774": 10000,
"org.springframework.cloud.gateway.filter.NettyRoutingFilter@7d7cac8": 2147483647,
"org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter@306f6f1d": 10150,
"org.springframework.cloud.gateway.filter.ForwardRoutingFilter@441b8382": 2147483647,
"org.lib.external.gateway.filters.TokenGlobalFilter@6f76c2cc": 0,
"org.springframework.cloud.gateway.filter.ForwardPathFilter@1df1ced0": 0,
"org.springframework.cloud.gateway.filter.WebsocketRoutingFilter@5349b246": 2147483646,
"org.springframework.cloud.gateway.filter.NettyWriteResponseFilter@6fc6deb7": -1,
"org.springframework.cloud.gateway.filter.GatewayMetricsFilter@32b0876c": 0,
"org.springframework.cloud.gateway.filter.RemoveCachedBodyFilter@367f0121": -2147483648
}
在上面的配置后,还是只能使用之前配置的路由。来自博客园
进一步:添加下面的配置后,可以无需手动添加路由即可访问注册中心 所有服务(不建议 生产环境使用)
# spring.cloud.gateway.discovery.locator.enabled=true 默认是 false
# 路由配置
gateway:
discovery:
locator:
enabled: true
lowerCaseServiceId: true
配置后,可以使用下面的链接访问 已注册的 adapter.web、data.user 服务的端点:
http://localhost:25001/adapter.web/user/get?id=1
http://localhost:25001/data.user/user/get?id=1
注,红色部分是 小写了的服务名。
此时,/actuator/gateway/routes 也发生了很大的变化,多了很多路由:来自博客园
响应
[
{
"predicate": "Paths: [/adapter.web/**], match trailing slash: true",
"metadata": {
"jmx.port": "59178",
"management.port": "21001"
},
"route_id": "ReactiveCompositeDiscoveryClient_ADAPTER.WEB",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /adapter.web/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://ADAPTER.WEB",
"order": 0
},
{
"predicate": "Paths: [/data.user/**], match trailing slash: true",
"metadata": {
"jmx.port": "59158",
"management.port": "20001"
},
"route_id": "ReactiveCompositeDiscoveryClient_DATA.USER",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /data.user/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://DATA.USER",
"order": 0
},
{
"predicate": "Paths: [/external.gateway/**], match trailing slash: true",
"metadata": {
"jmx.port": "53359",
"management.port": "25001"
},
"route_id": "ReactiveCompositeDiscoveryClient_EXTERNAL.GATEWAY",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[RewritePath /external.gateway/?(?<remaining>.*) = '/${remaining}'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]"
],
"uri": "lb://EXTERNAL.GATEWAY",
"order": 0
},
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]"
],
"uri": "http://localhost:21001",
"order": 0
}
]
甚至通过 其自身(/external.gateway/**) 来访问——不会死循环吗?!
还好,spring.cloud.gateway.discovery.locator.* 下还有很多配置(可以在 官文 查到):
负载均衡配置:
开启3个adapter.web服务,端口分别为:21001、21002、21003。来自博客园
去掉前面的spring.cloud.gateway.discovery.locator.* 的配置。
修改路由中的uri为下面的:lb://adapter.web
- id: route1
# 1)服务
# uri: http://localhost:21001
# 负载均衡访问服务 adapter.web
uri: lb://adapter.web
访问 /web/user/get?id=1,检查 3个adapter.web服务 是否均衡地收到并处理了请求:成功,均衡地处理了请求。
小结,
网关服务化后,可以很方便地实现负载均衡地访问代理服务。来自博客园
spring.cloud.gateway.discovery.locator.* 的最佳实践还有待探索,比如,根据前缀只允许访问服务中的部分请求,这就需要开发不同的断言、过滤器了吧。
限流,限制进入系统的流量。
限流的作用:1)防止流量突发使服务器过载;2)防止流量攻击。
常见限流维度:IP限流、请求URL限流、用户访问频次限流。(注:在使用微信公众平台接口时,还可以限制每个账号每小时、每天的调用次数等)
限流发生的位置:1)网关层(Nginx、Zuul、S.C.Gateway等),2)应用层。
本文介绍在S.C.Gateway中实现限流。
搜索:常见限流算法——计数器算法、漏桶算法、令牌桶算法
自定义pre类型的过滤器,可以实现需要的限流算法。来自博客园
在S.C.Gateway中,已经提供了一个 RequestRateLimiterGatewayFilterFactory,其使用 Redis和Lua脚本实现令牌桶算法进行限流。
@ConfigurationProperties("spring.cloud.gateway.filter.request-rate-limiter")
public class RequestRateLimiterGatewayFilterFactory
extends AbstractGatewayFilterFactory<RequestRateLimiterGatewayFilterFactory.Config> {
// ...
private final RateLimiter defaultRateLimiter;
private final KeyResolver defaultKeyResolver;
// ...
}
Lua脚本位置:
注,官文中的 The RequestRateLimiter GatewayFilter Factory 一节有它详细的介绍。来自博客园
6.10. The RequestRateLimiter GatewayFilter Factory
The RequestRateLimiter GatewayFilter factory uses a RateLimiter implementation to determine if the
current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by
default) is returned.
This filter takes an optional keyResolver parameter and parameters specific to the rate limiter
(described later in this section).
使用RequestRateLimiterGatewayFilterFactory限流
实现根据远程主机地址限流。
由于S.C.Gateway基于Netty,因此,需要引入reactive版本的redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置Redis:来自博客园
spring:
# Redis配置-限流使用
redis:
host: mylinux
port: 6379
建立KeyResolver类并注册到Spring容器:
# HostAddrKeyResolver.java
public class HostAddrKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
}
# AppConfig.java
@Configuration
public class APPConfig {
/**
* 限流的键解析器
* @author ben
* @date 2021-09-13 16:48:26 CST
* @return
*/
@Bean
public HostAddrKeyResolver hostAddrKeyResolver() {
return new HostAddrKeyResolver();
}
}
配置路由使用RequestRateLimiterGatewayFilterFactory:
配置方式和其它的不太一样,具体需要看看源码。
配置参数已在下面的注释中有说明(SpEL真的很重要,使用Spring时键值无处不在啊)!
# 过滤器配置
filters:
# 限流过滤器
- name: RequestRateLimiter
args:
# 用于限流的键的解析器的Bean对象的名称——SpEL表达式
# 默认有一个 PrincipalNameKeyResolver类,下面的hostAddrKeyResolver 需要自行实现
key-resolver: '#{@hostAddrKeyResolver}'
# 令牌桶每秒的平均填充速率
redis-rate-limiter.replenishRate: 1
# 令牌桶总量
redis-rate-limiter.burstCapacity: 3
测试限流效果:来自博客园
1)Postman:快速点击(要足够快),以此触发限流机制
测试期间发现响应的状态为:429 Too Many Requests,此时触发了限流规则。
2)Apache JMeter:配置多个线程快速访问
在Redis中,限流的数据是怎么保存的呢?检查下。来自博客园
redis-cli检查
# 多了两个key
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x04set1"
2) "request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens"
3) "\xac\xed\x00\x05t\x00\x05test3"
4) "request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp"
5) "\xac\xed\x00\x05t\x00\x05test1"
127.0.0.1:6379>
# 生存期很短
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
(integer) 4
127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.tokens
"2"
127.0.0.1:6379> get request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
"1631535157"
127.0.0.1:6379> ttl request_rate_limiter.{0:0:0:0:0:0:0:1}.timestamp
(integer) 4
小结,
就这样,在S.C.Gateway中把 限流 用起来了。
上面的用法很简单,真实的限流则有各种各样的规则,比如,服务器弹性部署时,网关怎么弹性更改配置呢?更改配置文件吗?这个时候就需要编程来实现了。
Gateway中有限流,底层应用是否也要有限流呢?两者如何互补?
Gateway中的令牌桶限流算法的实现原理是怎样的?那个Lua脚本是怎么写的?都需要继续探索的。
先读一遍官文才好。
在前面的示例中,网关的配置都是在 配置文件中完成的。
是否可以通过编程来实现路由配置呢?
配置文件配置 和 编程配置,两种方式的优缺点分别是什么?来自博客园
编程配置路由方式:使用Spring容器中routeLocatorBuilder Bean生成一个RouteLocator Bean即可。
默认下,已经有 routeDefinitionRouteLocator、cachedCompositeRouteLocator 两个Bean了,是用来做什么的呢?
示例代码:用各种方式,配置了 3个路由
package org.lib.external.gateway.routes;
import java.util.function.Function;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.Buildable;
import org.springframework.cloud.gateway.route.builder.PredicateSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 应用路由配置(编程方式)
* @author ben
* @date 2021-09-13 20:47:31 CST
*/
@Configuration
public class AppRoutesConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder
.routes()
// 路由1
.route("routeP1", new Function<PredicateSpec, Buildable<Route>>() {
@Override
public Buildable<Route> apply(PredicateSpec t) {
return t.order(3)
.path("/user/**")
.filters(f->f.addResponseHeader("program-header", "routeP1"))
.uri("lb://adapter.web");
}
})
// 路由2
.route("routeP2", r->r.order(2)
.path("/routeP2/**")
.filters(f->f.addResponseHeader("program-header", "routeP2")
.retry(3)
.rewritePath("/routeP2/(?<segment>.*)", "/$\\{segment}"))
.uri("lb://adapter.web")
)
// 路由3
.route(r->r.order(-10)
.path("/routeP3/**")
.filters(f->f.addResponseHeader("program-header", "routeP3")
.rewritePath("/routeP3/(?<segment>.*)", "/$\\{segment}"))
.uri("lb://adapter.web")
)
.build();
}
}
访问/actuator/gateway/routes:按优先级 从高到低 展示了系统中的路由,其中,第二的route1 是 配置文件中的,看来可以共存。
[
{
"predicate": "Paths: [/routeP3/**], match trailing slash: true",
"route_id": "c48fc06f-380c-405d-abf4-791df2008e37",
"filters": [
"[[RewritePath /routeP3/(?<segment>.*) = '/${segment}'], order = 0]"
],
"uri": "lb://adapter.web",
"order": -10
},
{
"predicate": "(After: 2021-09-11T14:13:13+08:00[Asia/Shanghai] && Paths: [/web/**], match trailing slash: true)",
"route_id": "route1",
"filters": [
"[[AddResponseHeader X-Response-Default-Red = 'Default-Blue'], order = 1]",
"[[AddRequestHeader addHead = 'abc'], order = 1]",
"[[RequestTime logEnabled=true], order = 2]",
"[[RewritePath /web/(?<segment>.*) = '/${segment}'], order = 2]",
"[org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory$$Lambda$978/889546737@2cad0ced, order = 3]"
],
"uri": "lb://adapter.web",
"order": 0
},
{
"predicate": "Paths: [/routeP2/**], match trailing slash: true",
"route_id": "routeP2",
"filters": [
"[[AddRequestHeader program-header = '210913'], order = 0]",
"[[Retry routeId = 'routeP2', retries = 3, series = list[SERVER_ERROR], statuses = list[[empty]], methods = list[GET], exceptions = list[IOException, TimeoutException]], order = 0]",
"[[RewritePath /routeP2/(?<segment>.*) = '/${segment}'], order = 0]"
],
"uri": "lb://adapter.web",
"order": 2
},
{
"predicate": "Paths: [/user/**], match trailing slash: true",
"route_id": "routeP1",
"filters": [
"[[AddRequestHeader program-header = '210913'], order = 0]"
],
"uri": "lb://adapter.web",
"order": 3
}
]
测试使用4个路由访问 web适配层应用:都能成功获取数据
http://localhost:25001/user/get?id=1
http://localhost:25001/web/user/get?id=1
http://localhost:25001/routeP2/user/get?id=1
http://localhost:25001/routeP3/user/get?id=1
小结:
编程添加路由,策略有变化,需要重启服务:确定。来自博客园
配置文件中添加路由。策略有变化,是否不需要重启服务?更新配置即可?TODO
对了,上面说的配置文件更新,是指存放于外部的配置文件(S.C.Config)更改后,是否可以更新到 正在运行的 网关服务?
哪些路由需要使用 编程添加,哪些通过 配置文件添加?
还是再看看官文吧,最权威的。来自博客园
》》》全文完《《《
1、《深入理解Spring Cloud与微服务构建》
2019年9月第2版,作者:方志朋
2、