微服务网关Gateway
1 Gateway简介
1.1 简介
- Spring Cloud Gateway是Spring官方基于Spring5.0、SpringBoot2.0和Project Reactor等技术开发的网关,旨在为微服务框架提供一种简单而有效的统一的API路由管理方式,统一访问接口。Spring Cloud Gateway作为Spring Cloud生态体系中的网关,目标是替代Netflix的Zuul,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全、监控/埋点和限流等等。它是基于Netty的响应式开发模式。
1.2 核心概念
- 1️⃣路由(route):路由是网关最基础的部分,路由信息由一个ID,一个目的URL、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
- 2️⃣断言(Predicate):Java8中的断言函数,Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自http Request中的任何信息,比如请求头和参数等。
- 3️⃣过滤器(Filter):一个标准的Spring WebFilter,Spring Cloud Gateway中的Filter分为两种类型:Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。
2 入门案例
2.1 入门案例
2.1.1 创建工程并导入依赖
- 修改部分:
<!--
Spring Cloud Gateway使用的web框架是webflux,和SpringMVC不兼容。引入的限流组件是Hystrix。Redis底层不再使用jedis,而是lettuce。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
- 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_demo</artifactId>
<groupId>org.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway_server7007</artifactId>
<dependencies>
<!--
Spring Cloud Gateway使用的web框架是webflux,和SpringMVC不兼容。引入的限流组件是Hystrix。Redis底层不再使用jedis,而是lettuce。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
</project>
2.1.2 配置启动类
- 启动类:
package com.sunxiaping.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author 许大仙
* @version 1.0
* @since 2020-10-05 19:22
*/
@SpringBootApplication
public class Gateway7007Application {
public static void main(String[] args) {
SpringApplication.run(Gateway7007Application.class, args);
}
}
2.1.3 编写配置文件
- application.yml
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Path=/product/**
上面配置的意思是,配置了一个id为product-service的路由规则,当访问网关请求地址以product开头时,会自动转发到http://localhost:9004这个地址。启动项目后,如果访问的路径是http://localhost:7007/product/findById/1,就会自动转发到http://localhost:9004/product/findById/1这个地址。
2.2 路由规则
- Spring Cloud Gateway的功能很强大,上面我们只是使用了predicates进行了简单的条件匹配,其实Spring Cloud Gateway帮准我们内置了很多Predicates功能。在Spring Cloud Gateway中Spring利用Predicate的特性实现了各种路由匹配规则,有通过header、请求参数等不同的条件来作为条件匹配到对应的路由。
- 示例:在某个时间之前允许转发
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Before=2020-11-11T00:00:00+08:00[Asia/Shanghai] # 在2020-11-11T00:00:00之前允许访问
- 示例:在某个时间之后允许转发
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- After=2020-11-11T00:00:00+08:00[Asia/Shanghai] # 在2020-11-11T00:00:00之后允许访问
- 示例:在某个时间段内允许转发
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Between=2018-11-11T00:00:00+08:00[Asia/Shanghai],2020-11-11T00:00:00+08:00[Asia/Shanghai]
- 示例:通过Cookie匹配
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
# 如果cookie的名称是abc,cookie的值是根据下面的正则表达式匹配
- Cookie=abc,admin # Cookie的name,正则表达式
- 示例:通过Header属性匹配
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Header=X-Request-Id, \d+ # Header头名称,正则表达式
- 示例:通过Host匹配
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Host=**.jd.com # http://surveys.jd.com/和http://passport.jd.com/等都可以匹配
- 示例:通过请求方式匹配
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Method=GET #GET请求匹配
- 示例:根据请求路径匹配
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Path=/foo/{segment} # http://localhost:7007/foo/a等都可匹配
- 示例:根据请求参数匹配
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Query=smile # http://localhost:7007?simle=abc,只要请求中包含smile参数即可
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- Query=smile,abc # http://localhost:7007?simle=abc,请求中包含smile参数且smile的参数值是abc
- 示例:根据请求的IP地址进行匹配
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
uri: http://localhost:9004 # 路由到微服务的uri
predicates: # 断言(判断条件)
- RemoteAddr=192.168.1.1/24
2.3 动态路由
2.3.1 概述
- 和Zuul网关类似,在Spring Cloud Gateway中也支持动态路由:即自动从注册中心获取服务列表并访问。
2.3.2 配置动态路由
- 1️⃣添加Eureka-client的依赖:
- 修改部分:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_demo</artifactId>
<groupId>org.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway_server7007</artifactId>
<dependencies>
<!--
Spring Cloud Gateway使用的web框架是webflux,和SpringMVC不兼容。引入的限流组件是Hystrix。Redis底层不再使用jedis,而是lettuce。
-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
- 2️⃣配置启动类:
package com.sunxiaping.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
/**
* @author 许大仙
* @version 1.0
* @since 2020-10-05 19:22
*/
@SpringBootApplication
@EnableEurekaClient
public class Gateway7007Application {
public static void main(String[] args) {
SpringApplication.run(Gateway7007Application.class, args);
}
}
- 3️⃣编写配置文件:
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
- Path=/product/**
# 配置 eureka
eureka:
instance:
# 主机名称:服务名称修改,其实就是向eureka server中注册的实例id
instance-id: api-gateway-server:${server.port}
# 显示IP信息
prefer-ip-address: true
client:
service-url: # 此处修改为 Eureka Server的集群地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
2.4 重写转发路径
-
在Spring Cloud Gateway中,路由转发是直接将匹配的路由Path直接拼接到映射路径URI之后,那么在微服务开发中往往并不方便。这里可以使用RewritePath机制来进行路径重写。
-
示例:
-
1️⃣修改application.yml,将匹配路径改为
/product-service/**
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
# - Path=/product/**
- Path:/product-service/**
重启网关,我们在浏览器中访问http://localhost:7007/product-service/product/findById/1,会抛出404。这是因为路由转发规则默认转发的路径是http://localhost:9004/product-service/product/findById/1,商品微服务中没有对应的路径。
- 2️⃣修改application.yml,添加RewritePath重写转发规则。
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
# - Path=/product/**
- Path=/product-service/**
filters: # 配置路由过滤器 http://localhost:7007/product-service/product/findById/1 --> http://localhost:7007/product/findById/1
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器
此时我们访问http://localhost:7007/product-service/product/findById/1,会自动转发到http://localhost:9004/product/findById/1。
2.5 微服务名称转发
-
Spring Cloud Gateway支持根据微服务名称进行自动转发。只需要修改application.yml配置文件即可(需要和Eureka整合)。
-
示例:
-
1️⃣修改部分:
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 微服务名称以小写形式呈现
- 2️⃣完整部分:
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 微服务名称以小写形式呈现
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
# - Path=/product/**
- Path=/product-service/**
filters: # 配置路由过滤器 http://localhost:7007/product-service/product/findById/1 --> http://localhost:7007/product/findById/1
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器
# 配置 eureka
eureka:
instance:
# 主机名称:服务名称修改,其实就是向eureka server中注册的实例id
instance-id: api-gateway-server:${server.port}
# 显示IP信息
prefer-ip-address: true
client:
service-url: # 此处修改为 Eureka Server的集群地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
logging:
level:
org.springframework.cloud.gateway: trace
org.springframework.http.server.reactive: debug
org.springframework.web.reactive: debug
reactor.ipc.netty: debug
3 过滤器
3.1 概述
- Spring Cloud Gateway除了具备请求路由功能之外,也支持对请求的过滤。和Zuul网关类似,也是通过过滤器的形式来实现的。
3.2 过滤器基础
3.2.1 过滤器的生命周期
- Spring Cloud Gateway的Filter的生命周期不像Zuul那么丰富,只有两个:"pre"和"post"。
- PRE:这种过滤器在请求被路由之前调用。我们可以利用这种过滤器实现身份认证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可以用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等等。
3.2.2 过滤器的类型
- Spring Cloud Gateway的Filter从作用范围可以分为两种:GatewayFilter和GlobalFilter。
- 1️⃣GatewayFilter:应用到当个路由或者一个分组的路由上。
- 2️⃣GlobalFilter:应用到所有的路由上。
3.3 局部过滤器
- 局部过滤器(GatewayFilter),是针对单个路由的过滤器。可以对访问的URL过滤,进行切面处理。在Spring Cloud Gateway中通过GatewayFilter的形式内置了很多不同类型的局部过滤器。
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名 称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand的名 称 |
FallbackHeaders | 为fallbackUri的请求头中添加具 体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个 preserveHostHeader=true的属 性,路由过滤器会检查该属性以 决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令 牌桶 | keyResolver、 rateLimiter、 statusCode、 denyEmptyKey、 emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的 url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的 一系列Header | 默认就会启用,可以通 过配置指定仅删除哪些 Header |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以 及重写后路径的正则表 达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正 则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行 WebSession::save操作 | 无 |
secureHeaders | 为原始响应添加一系列起安全作 用的响应头 | 无,支持修改这些安全 响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后 的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是 数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的 路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、 methods、series |
RequestSize | 设置允许接收最大请求包的大 小。如果请求包大小超过设置的 值,则返回 413 Payload Too Large | 请求包大小,单位为字 节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
每个过滤器工厂都对应一个实体类,并且这些类的名称必须以GatewayFilterFactory结尾,这是Spring Cloud Gateway的一个约定,例如AddRequestHeader对一个的实体类为AddRequestHeaderGatewayFilterFactory。
3.4 全局过滤器
- 全局过滤器(GlobalFilter)作用于所有路由,Spring Cloud Gateway定义了Global Filter接口,用户可以自定义实现自己的Global Filter。通过全局过滤器可以实现对权限的统一校验,安全性校验等功能,并且全局过滤器也是程序员使用比较多的过滤器。
- Spring Cloud Gateway内部也是通过一些列的内置全局过滤器对整个路由转发进行处理,如下图所示:
4 统一鉴权
4.1 概述
- 内置的过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己去编写过滤器来实现的,那么我们通过代码的形式自定义一个过滤器,去完成统一的权限校验。
4.2 鉴权逻辑
- 开发中的鉴权逻辑:
- 1️⃣当客户端第一次请求服务的时候,服务端对用户进行信息认证(登录)。
- 2️⃣认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证。
- 3️⃣以后每次请求,客户端都携带认证的token。
- 4️⃣服务daunt对token进行解密,判断是否有效。
- 如上图所示,对于验证用户是否已经登录授权的过程可以在网关层统一校验。校验的标准就是请求中是否携带token凭证以及token的正确性。
4.3 代码实现
- LoginFilter.java
package com.sunxiaping.gateway.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 自定义全局过滤器
*
* @author 许大仙
* @version 1.0
* @since 2020-10-06 20:24
*/
@Component
public class LoginFilter implements GlobalFilter, Ordered {
/**
* 执行过滤器中的业务逻辑
* 对请求参数中的access-token进行判断,如果存在,则代表认证成功,否则,认证失败。
*
* @param exchange 相当于请求和响应的上下文对象
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String token = request.getHeaders().getFirst("access-token");
if (StringUtils.isEmpty(token)) {
//设置Http的状态码
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
//请求结束
return exchange.getResponse().setComplete();
}
//如果存在,继续执行
return chain.filter(exchange);
}
/**
* 指定过滤器的执行顺序,返回值越小,优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 1;
}
}
5 网关限流
5.1 常见的限流算法
5.1.1 计数器
- 计数器限流算法是最简单的一种限流实现方式。
- 其本质是通过维护一个单位时间内的计数器,每次请求计数器+1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。
5.1.2 漏桶算法
- 漏桶算法可以很好的限制容量池的大小,从而防止流量暴增。
- 漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。
- 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
- 为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增加时可以存多少水(burst),另一个是桶漏洞的大小(rate)。
5.1.3 令牌桶算法
- 令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程序的突发调用。
- 在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数量达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置QPS为100,那么限流器初始化完成1秒后,桶中就已经有100个令牌了,这时服务可能还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定速率执行。
5.2 基于Filter的限流
5.2.1 概述
- Spring Cloud Gateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory实现。在过滤器工厂中是通过Redis和Lua脚本结合的方式进行流量控制。
5.2.2 准备工作
- Redis。
5.2.3 导入Redis的reactive依赖
- 修改部分:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
- 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_demo</artifactId>
<groupId>org.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway_server7007</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
</dependencies>
</project>
5.2.4 修改配置文件
- application.yml
server:
port: 7007
spring:
application:
name: api-gateway-server
# ----------修改部分------------
redis:
host: 192.168.1.57
port: 6379
database: 1
# ----------修改部分------------
# 配置 Spring Cloud Gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 微服务名称以小写形式呈现
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
# - Path=/product/**
- Path=/product-service/**
filters: # 配置路由过滤器
# ----------修改部分------------
- name: RequestRateLimiter # 使用的限流过滤器是Spring Cloud Gateway提供的
args:
# 使用SpEL从容器中获取对象
key-resolver: '#{@pathKeyResolver}'
# 令牌桶每秒填充平均速率
redis-rate-limiter.replenishRate: 1
# 令牌桶的上限
redis-rate-limiter.burstCapacity: 3
# ----------修改部分------------
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器
# 配置 eureka
eureka:
instance:
# 主机名称:服务名称修改,其实就是向eureka server中注册的实例id
instance-id: api-gateway-server:${server.port}
# 显示IP信息
prefer-ip-address: true
client:
service-url: # 此处修改为 Eureka Server的集群地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
5.2.5 配置Redis中key的解析器
- keyResolverConfig.java
package com.sunxiaping.gateway.config;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;
/**
* 配置Redis中Key的解析器
*
* @author 许大仙
* @version 1.0
* @since 2020-10-22 09:53
*/
@Configuration
public class KeyResolverConfig {
/**
* 基于请求路径的限流
*
* @return
*/
//@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
/**
* 基于请求参数的限流
* 请求/abc?userId=1
*
* @return
*/
//@Bean
public KeyResolver useKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
/**
* 基于请求IP地址的限流
*
* @return
*/
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst("x-Forwarded-For"));
}
}
5.2.6 总结
- Spring Cloud Gateway目前提供的限流还是比较简单的,在实际开发中我们的限流策略会有很多种情况,比如:对不同接口的限流,被限流后的友好提示,这些可以通过自定义RedisRateLimiter来实现自己的限流策略。
5.3 基于Sentinel的限流
5.3.1 概述
- Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
5.3.2 导入相关jar包的Maven坐标
- 修改部分:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
- 完整部分:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>spring_cloud_demo</artifactId>
<groupId>org.sunxiaping</groupId>
<version>1.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>api_gateway_server7007</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
</dependencies>
</project>
5.3.3 修改配置文件
- application.yml
server:
port: 7007
spring:
application:
name: api-gateway-server
# 配置 Spring Cloud Gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
lower-case-service-id: true # 微服务名称以小写形式呈现
routes:
# 配置路由: 路由id,路由到微服务的uri,断言(判断条件)
- id: product-service # 路由id
# uri: http://localhost:9004
uri: lb://service-product # 路由到微服务的uri。 lb://xxx,lb代表从注册中心获取服务列表,xxx代表需要转发的微服务的名称
predicates: # 断言(判断条件)
# - Path=/product/**
- Path=/product-service/**
filters: # 配置路由过滤器
- RewritePath=/product-service/(?<segment>.*), /$\{segment} # 路径重写的过滤器
# 配置 eureka
eureka:
instance:
# 主机名称:服务名称修改,其实就是向eureka server中注册的实例id
instance-id: api-gateway-server:${server.port}
# 显示IP信息
prefer-ip-address: true
client:
service-url: # 此处修改为 Eureka Server的集群地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
5.3.4 配置Sentinel限流
- GatewayConfig.java
package com.sunxiaping.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem;
import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.result.view.ViewResolver;
import javax.annotation.PostConstruct;
import java.util.*;
/**
* Sentinel限流的配置
*/
@Configuration
public class GatewayConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public GatewayConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
/**
* 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(this.viewResolvers, this.serverCodecConfigurer);
}
/**
* 配置限流过滤器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
/**
* 配置初始化的限流参数
* 用于指定资源的限流规则:
* 1.资源名称(路由id)
* 2.配置统计时间窗口
* 3.配置限流阈值
*/
@PostConstruct
public void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product-service") //资源名称
.setCount(1) // 限流阈值
.setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒
);
//配置自定义API分组
rules.add(new GatewayFlowRule("product_api").setCount(1).setIntervalSec(1));
rules.add(new GatewayFlowRule("order_api").setCount(1).setIntervalSec(1));
GatewayRuleManager.loadRules(rules);
}
/**
* 自定义异常提示
*/
@PostConstruct
public void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
Map<String, Object> map = new HashMap<>();
map.put("code", "001");
map.put("message", "对不起,接口限流了");
return ServerResponse
.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(map));
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
/**
* 自定义API限流分组
* 1.定义分组
* 2.对小组配置限流规则
*/
@PostConstruct
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
this.add(new ApiPathPredicateItem().setPattern("/product-service/product/**").
setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
ApiDefinition api2 = new ApiDefinition("order_api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
this.add(new ApiPathPredicateItem().setPattern("/order-service/order"));
}});
definitions.add(api1);
definitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
}
6 网关高可用
- 高可用HA是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用的最大的风险和敌人,应该尽量在系统设计的过程中避免单点。
- 方法论上,高可用保证的原则是“集群化”,或者叫做“冗余”。只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他备份能够顶上。
- 我们实际使用Spring Cloud Gateway的方式如上图所示,不同的客户端使用不同的负载将请求分发到后端的Gateway,Gateway再通过HTTP调用后端服务,最后对外输出。因此为了保证Gateway的高可用性,前端可以同时启动多个Gateway实例进行负载,在Gateway的前端使用Nginx或者F5进行负载转发以达到高可用性。