1. 介绍
SpringCloud Gateway是Spring Cloud的一个全新项目,为了提高网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
特征:
* 基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
* 动态路由
* Predicates 和 Filters 作用于特定路由
* 集成 Hystrix 断路器
* 集成 Spring Cloud DiscoveryClient
* 易于编写的 Predicates 和 Filters
* 限流
* 路径重写
1.1 能干什么
- 路由转发:相当于高级版的nginx
- 权限认证:通过自定义filter实现
- 日志/流量监控:通过自定义filter实现
- 流量控制(限流):自带的RequestRateLimiterGatewayFilter
1.2 三大核心概念
1.2.1 Route(路由)
这是网关的基本构建块。它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
1.2.2 Predicate(断言)
这是一个 Java 8 的 Predicate。输入类型是一个 ServerWebExchange。我们可以使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。
说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。
示例:通过时间匹配
Predicate 支持设置一个时间,在请求进行转发的时候,可以通过判断在这个时间之前或者之后进行转发。比如我们现在设置只有在2019年1月1日才会转发到我的博客,在这之前不进行转发,我就可以这样配置:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- After=2019-01-01T00:00:00+08:00[Asia/Shanghai]
1.2.3 Filter(过滤)
这是org.springframework.cloud.gateway.filter.GatewayFilter的实例,我们可以使用它修改请求和响应。
1.3 工作流程
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。
2. 实操理论知识
2.1 路由配置方式
2.1.1 配置文件配置路由【推荐】
- 直接路由【不推荐】:
spring:
application:
name: cloud-gateway
cloud:
gateway:
routes:
- id: payment-route1 # 路由的ID,没有固定规则但要求唯一
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 路径断言匹配
- id: payment-route2
uri: http://localhost:8001
predicates:
- Path=/payment/lb/** # 路径断言匹配
- 通过服务名路由【推荐】:
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment-route1 # 路由的ID,没有固定规则但要求唯一
uri: lb://cloud-payment-service # 通过微服务名自动负载路由地址
predicates:
- Path=/payment/get/** # 路径断言匹配
- id: payment-route2
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** # 路径断言匹配
2.1.2 代码配置路由【不推荐】
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path-route-1", r -> r.path("/guonei").uri("https://news.baidu.com/guonei"))
.route("path-route-2", r -> r.path("/guoji").uri("https://news.baidu.com/guoji"))
.route("path-route-3", r -> r.path("/mil").uri("https://news.baidu.com/mil"))
.build();
}
}
2.2
3. 实战
3.1 项目介绍
该演示项目共4个服务:
- cloud-eureka-server: 服务管理及注册
- cloud-gateway:gateway网关服务
- cloud-payment-service:两个支付服务实例
需求:
- 实现网关路由转发
- 实现日志记录
- 实现权限认证
3.2 cloud-eureka-server
3.2.1 依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
3.2.2 启动文件
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
3.2.3 application.yml
server:
port: 8761
spring:
application:
name: cloud-eureka-server
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
fetch-registry: false
register-with-eureka: false
server:
response-cache-update-interval-ms: 10000
3.3 cloud-payment-service
3.3.1 依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-web
3.3.2 启动文件
@SpringBootApplication
@EnableEurekaClient
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
3.3.3 业务逻辑
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/lb")
public String getPaymentLB() {
return serverPort;
}
@GetMapping("/get/{id}")
public Payment getPaymentById(@PathVariable Long id) {
Payment payment = new Payment(id, "Jack", 100.0);
return payment;
}
}
3.3.4 application.yml
server:
port: 8001
spring:
application:
name: cloud-payment-service
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
register-with-eureka: true # 是否将自己作为服务注册到服务中心,为true(默认)时自动生效
fetch-registry: true # 是否进行检索服务,当设置为True(默认值)时,会进行服务检索,注册中心不负责检索服务。
3.4 cloud-gateway
3.4.1 依赖
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
3.4.2 启动文件
3.4.3 filter
- LogGatewayFilter
@Component
@Slf4j
public class LogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********invoke LogGatewayFilter***************");
InetSocketAddress remoteAddress = exchange.getRequest().getRemoteAddress();
log.info("IP:" + remoteAddress);
List userAgents = exchange.getRequest().getHeaders().get("User-Agent");
log.info("User-Agent:" + userAgents);
if (userAgents == null || userAgents.size() == 0) {
log.info("user from " + remoteAddress + " User-Agent is none");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // filter的顺序,值越小,优先级越高
}
}
- AuthGateWayFilter
@Component
@Slf4j
public class AuthGateWayFilter implements GlobalFilter, Ordered {
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("***********invoke AuthGateWayFilter***************");
List token = exchange.getRequest().getHeaders().get("X-Auth-Token");
if (token == null || token.size() == 0) {
log.info("X-Auth-Token in header is not found");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
byte[] bytes = "{\"msg\": \"X-Auth-Token in header is not found\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
exchange.getResponse().getHeaders().add("Content-Type","application/json");
return exchange.getResponse().writeWith(Flux.just(buffer));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
3.4.4 application.yml
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment-route1 # 路由的ID,没有固定规则但要求唯一
# uri: http://localhost:8001 # 匹配后提供服务的路由地址
uri: lb://cloud-payment-service # 通过微服务名自动负载路由地址
predicates:
- Path=/payment/get/** # 路径断言匹配
- id: payment-route2
# uri: http://localhost:8001 # 匹配后提供服务的路由地址
uri: lb://cloud-payment-service
predicates:
- Path=/payment/lb/** # 路径断言匹配
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
register-with-eureka: true # 是否将自己作为服务注册到服务中心,为true(默认)时自动生效
fetch-registry: true # 是否进行检索服务,当设置为True(默认值)时,会进行服务检索,注册中心不负责检索服务。
4. 实现限流
前SpringCloudGateway分布式限流官方提供的正是基于redis的实现。
4.1 添加依赖
org.springframework.cloud
spring-cloud-starter-gateway
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.boot
spring-boot-starter-data-redis-reactive
4.2 自定义keyResolver
@Configuration
public class GatewayConfig {
@Bean
@Primary
public KeyResolver urlAndHostAddressKeyResolver() {
//按URL+IP来限流
return exchange -> {
String url = exchange.getRequest().getPath().toString();
String hostAddress = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
String key = url + ":" + hostAddress;
System.out.println(key);
return Mono.just(key);
};
}
@Bean
public KeyResolver hostAddressKeyResolver() {
//按IP来限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
}
@Bean
public KeyResolver urlKeyResolver() {
//按URL限流,即以每秒内请求数按URL分组统计,超出限流的url请求都将返回429状态
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
}
4.3 application.yml
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment-route
uri: lb://cloud-payment-service
predicates:
- Path=/payment/** # 路径断言匹配
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@urlAndHostAddressKeyResolver}'
redis-rate-limiter.replenishRate: 2 # 生成令牌的速率:2/s
redis-rate-limiter.burstCapacity: 10 # 令牌桶的容量:10
redis: # 令牌存放的redis地址
host: 192.168.101.130
port: 6380
database: 0
eureka:
instance:
hostname: cloud-gateway-service
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
register-with-eureka: true # 是否将自己作为服务注册到服务中心,为true(默认)时自动生效
fetch-registry: true # 是否进行检索服务,当设置为True(默认值)时,会进行服务检索,注册中心不负责检索服务。
4.5 jemeter测试
发送20个请求,成功响应12个,8个响应426:Too Many Requests,符合设定的速率。
4.6 redis数据分析
192.168.101.130:6380> keys *
1) "request_rate_limiter.{/payment/lb:127.0.0.1}.timestamp"
2) "request_rate_limiter.{/payment/lb:127.0.0.1}.tokens"
我们构造的keyResolver实例生成的字符串,即存储在redis中,redis的key的构造方式是:==request_rate_limiter.{keyResolver}.tokens==,并且这两个key是有ttl,ttl时间公式为:
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
其定义在lua脚本中。
lua脚本存在于:spring-cloud-gateway-core-2.2.4.RELEASE.jar!/META-INF/scripts/request_rate_limiter.lua,其完整内容如下:
local tokens_key = KEYS[1]
local timestamp_key = KEYS[2]
--redis.log(redis.LOG_WARNING, "tokens_key " .. tokens_key)
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local fill_time = capacity/rate
local ttl = math.floor(fill_time*2)
--redis.log(redis.LOG_WARNING, "rate " .. ARGV[1])
--redis.log(redis.LOG_WARNING, "capacity " .. ARGV[2])
--redis.log(redis.LOG_WARNING, "now " .. ARGV[3])
--redis.log(redis.LOG_WARNING, "requested " .. ARGV[4])
--redis.log(redis.LOG_WARNING, "filltime " .. fill_time)
--redis.log(redis.LOG_WARNING, "ttl " .. ttl)
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
--redis.log(redis.LOG_WARNING, "last_tokens " .. last_tokens)
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
--redis.log(redis.LOG_WARNING, "last_refreshed " .. last_refreshed)
local delta = math.max(0, now-last_refreshed)
local filled_tokens = math.min(capacity, last_tokens+(delta*rate))
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
--redis.log(redis.LOG_WARNING, "delta " .. delta)
--redis.log(redis.LOG_WARNING, "filled_tokens " .. filled_tokens)
--redis.log(redis.LOG_WARNING, "allowed_num " .. allowed_num)
--redis.log(redis.LOG_WARNING, "new_tokens " .. new_tokens)
if ttl > 0 then
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
end
-- return { allowed_num, new_tokens, capacity, filled_tokens, requested, new_tokens }
return { allowed_num, new_tokens }
5. 面试题
5.1 限流算法有哪些?SpringCloud Gateway使用的限流算法是哪种?
常见的限流算法:
计数器算法:每秒记录请求处理数量。缺点:不平衡,突刺现象
漏桶算法:以一定速率处理桶中的请求,桶中请求存储有阈值。缺点:无法应对短时间的突发流量
令牌桶算法:往桶中以一定速率发放令牌(有阈值),处理一个请求需要从桶中获取一个令牌。
https://blog.csdn.net/forezp/article/details/85081162
5. 参考资料
https://www.bilibili.com/video/BV1rE411x7Hz?p=66
https://www.cnblogs.com/babycomeon/p/11161073.html
https://docs.spring.io/spring-cloud-gateway/docs/2.2.4.RELEASE/reference/html/#gateway-starter
https://blog.csdn.net/forezp/article/details/85081162