概述
Spring Cloud Gateway是Spring官方基于Spring 5.0、Spring Boot 2.0和Project Reactor等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的API路由管理方式,统一访问接口,Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替换Zuul,其不仅提供统一的路由方式,并基于Filter链的方式提供了网关基本的功能,例如:安全、监控\埋点、限流等。它是基于Netty的响应式开发模式
路由
2.1 核心概念
- 路由(route):路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组Filter组成,如果断言为真,则说明请求URL和配置的路由匹配。
- 断言(predicates):Java 8 中的断言函数Spring Cloud Gateway中的断言函数输入类型是Spring 5.0 框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息,比如请求头和参数等。
- 过滤器(filter):一个标准的Spring weFilter,Spring Cloud Gateway中的filter分为两种类型,分别为Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。
2.2 基础配置
2.2.1 搭建环境
(1)创建工程导入坐标
<!-- gateway 基于netty + webFlux实现;webFlux与Springmvc存在冲突-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
(2)配置启动类
@SpringBootApplication
public class ShopGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ShopGatewayApplication.class, args);
}
}
(3)编写配置文件
server:
port: 8089
spring:
application:
name: api-gateway
#配置gateway的路由
cloud:
gateway:
routes:
#路由ID\URI\断言
- id: product-api
uri: http://127.0.0.1:9001
predicates:
- Path=/product/**
2.2.2 路由规则(断言|路由条件)
- After (指定时间之后)
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
- Before (指定时间之前)
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
- Between (指定时间之间)
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
- Cookie (指定正则cookie)
predicates:
- Cookie=chocolate, ch.p
- Header (指定请求头正则)
predicates:
- Header=X-Request-Id, \d+
- Host (指定host)
predicates:
- Host=**.somehost.org,**.anotherhost.org
- Method (指定请求方式GET|POST)
predicates:
- Method=GET # 所有GET请求都将被路由
- Path (指定路径)
predicates:
# segment为Map类型变量
- Path=/foo/{segment},/bar/{segment}
# 也可以直接写出 **代表所有请求都通过
- Path=/consumer/**
- Query (指定参数|指定参数值和参数名)
predicates:
- Query=baz
- RemoteAddr (指定远程IP)
predicates:
- Query=foo, ba.
2.2.3 动态路由(面向服务的路由)
- 引入注册中心配置pom.xml
<!-- eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
- 配置启动类
@EnableEurekaClient //可以省略
@SpringBootApplication
public class ShopGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ShopGatewayApplication.class, args);
}
}
- 修改配置文件
spring:
application:
name: shop-gateway-server
#配置gateway的路由
cloud:
gateway:
routes:
#路由ID\URI\断言
- id: product-api
#uri: http://127.0.0.1:9001 #不使用注册中心
uri: lb://shop-product-server #使用注册中心;格式需满足 lb:// 注册中心获取微服务的名称
predicates:
- Path=/product/**
#eureka注册中心配置
eureka:
client:
service-url:
defaultZone: http://localhost:10000/eureka/
instance:
prefer-ip-address: true #使用IP地址注册
2.2.4 重写转发路径(filters过滤器)
spring:
application:
name: shop-gateway-server
#配置gateway的路由
cloud:
gateway:
routes:
#路由ID\URI\断言
- id: product-api
#uri: http://127.0.0.1:9001 #不使用注册中心
uri: lb://shop-product-server #使用注册中心;格式需满足 lb:// 注册中心获取微服务的名称
predicates:
- Path=/product-service/product/**
filters: #配置过滤器,实现路径重写转发
- RewritePath=/product-service/(?<segment>.*),/$\$(segment) #路径重写过滤器
2.2.5 微服务名称转发
spring:
application:
name: shop-gateway-server
#配置gateway的路由
cloud:
gateway:
routes:
#路由ID\URI\断言
- id: product-api
#uri: http://127.0.0.1:9001 #不使用注册中心
uri: lb://shop-product-server #使用注册中心;格式需满足 lb:// 注册中心获取微服务的名称
predicates:
- Path=/product-service/product/**
filters: #配置过滤器,实现路径重写转发
- RewritePath=/product-service/(?<segment>.*),/$\$(segment) #路径重写过滤器
#自动从注册中心获取服务 http://localhost:8089/shop-product-server/product/query
discovery:
locator:
enabled: true #开启根据服务名称自动转发
lower-case-service-id: true #微服务名称以小写呈现
过滤器
3.1 过滤器的生命周期
- PRE:这种过滤器在请求被路由之前调用,可以用于实现身份验证,集群中选择请求的微服务等
- POST:这种过滤器在路由到微服务以后执行,可以用于为响应添加标准的HTTP Header、收集统计信息和指标等
3.2 过滤器的类型
- GatewayFilter(局部过滤器):应用到单个路由或者一个分组的路由上
- AddRequestHeader:指定请求头
routes:
- id: add_request_header_route
uri: http://example.org
filters:
- AddRequestHeader=X-Request-Foo, Bar
- AddRequestParameter:指定参数值
routes:
- id: add_request_parameter_route
uri: http://example.org
filters:
- AddRequestParameter=foo, bar
- AddResponseHeader:指定响应头
routes:
- id: add_request_header_route
uri: http://example.org
filters:
- AddResponseHeader=X-Response-Foo, Bar
- PrefixPath:添加路径前缀
routes:
- id: prefixpath_route
uri: http://example.org
filters:
- PrefixPath=/mypath
- PreserveHostHeader:原始请求头
routes:
- id: preserve_host_route
uri: http://example.org
filters:
- PreserveHostHeader
- RemoveRequestHeader:删除请求头
routes:
- id: removerequestheader_route
uri: http://example.org
filters:
- RemoveRequestHeader=X-Request-Foo
- RemoveResponseHeader:删除响应头
routes:
- id: removeresponseheader_route
uri: http://example.org
filters:
- RemoveResponseHeader=X-Response-Foo
- RewriitePath:重写请求路径
routes:
- id: rewritepath_route
uri: http://example.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo(?<segment>/?.*), $\{segment}
- RewriteResponseHeader:重写响应头
#/42?user=ford&password=omg!what&flag=true 重写后
#/42?user=ford&password=***&flag=true
routes:
- id: rewriteresponseheader_route
uri: http://example.org
filters:
- RewriteResponseHeader=X-Response-Foo, , password=[^&]+, password=***
- SaveSession:转发前调用WebSession::save
routes:
- id: save_session
uri: http://example.org
predicates:
- Path=/foo/**
filters:
- SaveSession
- StripPrefix:去除的路径中 的节点数
#通过网关发出/name/bar/foo请求时
#向nameservice发出的请求将是http://nameservice/foo
routes:
- id: nameRoot
uri: http://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2
- RequestSize:限制请求大小
routes:
- id: request_size_route
uri: http://localhost:8080/upload
predicates:
- Path=/upload
filters:
- name: RequestSize
args:
maxSize: 5000000
- GlobalFilter(全局过滤器):应用到所有的路由上
通过全局过滤器实现权限认证操作
@Component
public class LoginFilter implements GlobalFilter, Ordered {
/**
* @Description 执行过滤器中的业务逻辑
* @Date 2021/6/30 14:58
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("全局认证过滤器");
String token = exchange.getRequest().getQueryParams().getFirst("token");
if(null == token){
System.out.println("没有认证");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete(); //请求结束
}
System.out.println("认证成功");
return chain.filter(exchange); //继续向下执行
}
/**
* @Description 指定过滤器的执行顺序,返回值越小,执行优先级越高
* @Date 2021/6/30 14:59
*/
@Override
public int getOrder() {
return 0;
}
}
网关限流
4.1 常见的限流算法
- 计数器算法
是最简单的一种限流算法,其本质是通过维护一个单位时间内的计数器,每次请求计数器+1,当单位时间内计数器累加到阈值,之后的请求都会被拒绝,直到单位时间结束,将计数器清零。 - 漏桶算法
可以很好的限制容量池的大小,从而防止流量暴增,漏桶可以看作一个带有常量时间的单服务器队列,如果漏桶溢出,那么数据包会被丢弃,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。 - 令牌桶算法
令牌桶算法是对漏桶算法的改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时允许一定程度的突发调用,在令牌桶算法中,存在一个桶,用来存放固定数量的令牌,算法中存在一种机制,以一定的速率往桶中放令牌,每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌,或者直接拒绝,放令牌这个动作是持续不断的进行,如果统中令牌达到上限,就丢弃令牌。
4.2 Filter限流
Spring Cloud Gateway官方提供了基于令牌桶的限流支持,基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory实现,在过滤器工厂中是通过Redis和Lua脚本结合的方式进行流量控制。
(1)环境搭建
- 导入redis的依赖
<!-- 监控依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- 集成Redis实现Filter限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
- 部署Redis
(2)修改配置文件
spring:
application:
name: shop-gateway-server
#配置Redis
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
#配置gateway的路由
cloud:
gateway:
routes:
#路由ID\URI\断言
- id: product-api
#uri: http://127.0.0.1:9001 #不使用注册中心
uri: lb://shop-product-server #使用注册中心;格式需满足 lb:// 注册中心获取微服务的名称
predicates:
- Path=/product-service/**
filters: #配置过滤器,实现路径重写转发
- RewritePath=/product-service/(?<segment>.*), /$\{segment} #路径重写过滤器
#配置限流
- name: RequestRateLimiter
args:
key-resolver: '#{@myKeyResolver}' #使用SpEL从容器获取对象
redis-rate-limiter.replenishRate: 1 #令牌桶每秒填充平均速率
redis-rate-limiter.burstCapacity: 3 #令牌桶上限
(3)编写自定义KeyResolver
/**
* @Description 基于请求路径限流
* 基于请求路径: /abc
* 基于请求IP: 127.0.0.1
* 基于参数:
*/
@Configuration
public class MyKeyResolverConfig {
@Bean
public KeyResolver myKeyResolver(){
//实现基于请求路径自定义的KeyResolver
return exchange -> Mono.just(exchange.getRequest().getPath().toString());
}
@Bean
public KeyResolver ipKeyResolver(){
//实现基于请求ip自定义的KeyResolver
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostString());
}
@Bean
public KeyResolver paramKeyResolver(){
//实现基于请求参数自定义的KeyResolver
return exchange -> Mono.just(exchange.getRequest().getQueryParams().toString());
}
}
4.3 Sentinel限流
4.3.1 Sentinel限流实现
Sentinel支持对Gateway|Zuul等主流API网关的限流,其提供了两种资源维度的限流。
- route:即在spring配置文件中配置的路由条目,资源名为对应的routeid
- 自定义API:利用Sentinel提供的API自定义一些API分组
Sentinel(1.6.0+)提供了网关限流规则和自定义API的实体和管理逻辑
- GatewayFlowRule:网关限流规则,针对API Gateway的场景定制的限流规则,可以针对不同route或自定义API分组进行限流,支持针对请求参数、Header、IP等进行定制化的限流
- ApiDefinition:用户自定义的API定义分组,可以看做是一些URL匹配的组合,比如我们可以定义一个API叫my_api,请求path模式为/foo/**和/bus/**的都归到my_api这个api分组下面,限流的时候可以针对这个自定义的api分组维度进行限流。
(1)环境搭建
- 导入Sentinel依赖
<!-- 集成Sentinel实现Sentinel限流 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
<version>1.8.1</version>
</dependency>
- 编写Sentinel配置类
@Configuration
public class MySentinelConfig {
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public MySentinelConfig(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler(){
return new SentinelGatewayBlockExceptionHandler(viewResolvers,serverCodecConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter(){
return new SentinelGatewayFilter();
}
@PostConstruct
private void initGatewayRules(){
Set<GatewayFlowRule> rules = new HashSet<>();
rules.add(new GatewayFlowRule("product-api") //资源名称,这里为路由router的ID
//路由模式
.setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID)
.setCount(1) //QPS即每秒钟允许的调用次数
.setIntervalSec(1)); //每隔多少时间统计一次汇总数据
GatewayRuleManager.loadRules(rules);
}
}
4.3.2 自定义异常提示
- setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为BlockRequestHandler
@PostConstruct
public void initBlockHandlers(){
//声明自定义异常信息
BlockRequestHandler handler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
Map<String,String> result = new HashMap<>();
result.put("code","403");
result.put("message","对不起,请稍后再试");
return ServerResponse.status(HttpStatus.OK)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(result));
}
};
GatewayCallbackManager.setBlockHandler(handler); //设置自定义异常信息
}
4.3.3 自定义分组限流
@PostConstruct
private void initGatewayRules(){
Set<GatewayFlowRule> rules = new HashSet<>();
//配置分组限流
rules.add(new GatewayFlowRule("product-api") //分组名称
.setCount(1) //QPS即每秒钟允许的调用次数
.setIntervalSec(1)); //每隔多少时间统计一次汇总数据
rules.add(new GatewayFlowRule("order-api") //分组名称
.setCount(1) //QPS即每秒钟允许的调用次数
.setIntervalSec(1)); //每隔多少时间统计一次汇总数据
GatewayRuleManager.loadRules(rules);
}
//自定义分组限流,基于API
@PostConstruct
private void initCustomizedApis(){
Set<ApiDefinition> apiDefinitions = new HashSet<>();
ApiDefinition api1 = new ApiDefinition("product-api") //分组名称
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem()
.setPattern("/product-service/product/**")); //配置匹配的连接API(模糊匹配)
}});
ApiDefinition api2 = new ApiDefinition("order-api")
.setPredicateItems(new HashSet<ApiPredicateItem>(){{
add(new ApiPathPredicateItem()
.setPattern("/product-service/order")); //配置匹配的连接API(完全匹配)
}});
apiDefinitions.add(api1);
apiDefinitions.add(api2);
GatewayApiDefinitionManager.loadApiDefinitions(apiDefinitions);
}
高可用
5.1 概述
该可用HA是分布式系统架构设计中必须考虑的因素之一,它通常指,通过设计减少系统不能提供服务的时间。众所周知,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在设计系统的过程中避免单点,方法论上,高可用保证的原则是“集群化”,或者交“冗余”,只有一个单点,挂了服务会受影响,如果有冗余备份,挂了还有其他backup能够顶上。
5.2 结合Nginx实现网关的高可用
- 配置启动多个网关
- 网关一配置
server:
port: 8001
- 网关二配置
server:
port: 8002
- 网关三配置
server:
port: 8003
- 配置Nginx.conf
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#网关集群配置
upstream gateway {
server 127.0.0.1:8001;
server 127.0.0.1:8002;
server 127.0.0.1:8003;
}
server {
listen 80;
server_name localhost;
#127.0.0.1
location / {
proxy_pass http://gateway
}
location / {
root html;
index index.html index.htm;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}