github 代码地址: https://github.com/guzhangyu/learn-spring-cloud/tree/master/spring-cloud-gateway
目录
1.3.2 SpringCloud Gateway的处理流程
1.4 SpringCloud Gateway 路由配置方式
1.5 详解:SpringCloud Gateway 匹配规则
1.1 SpringCloud Gateway简介
SpringCloud Gateway 是 Spring Cloud的一个全新项目,该项目是基于Spring 5.0, Spring Boot 2.0 和 Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的api路由管理方式。
为了提升性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了Reactor模式通信框架Netty。Spring Cloud Gateway的目标,不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
Spring Cloud Gateway底层使用了高性能的通信框架Netty。
1.2 SpringCloud Gateway特征
(1) 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.9
(2) 集成 Hystrix断路器
(3) 集成 Spring Cloud DiscoveryClient
(4) Predicates 和 Filters 作用于特定路由,易于编写的Predicates 和 Filters
(5) 具备一些网关的高级功能:动态路由、限流、路径重写
简单说一下上文中的几个术语
(1) Filter(过滤器):
可以使用它拦截和修改请求,并且对上游的响应进行二次处理。过滤器为 org.springframework.cloud.gateway.filter.GatewayFilter类的实例
(2) Route(路由)
网关配置的基本组成模块,一个Route模块由一个ID,一个目标URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配,目标URI会被访问。
(3) Predicate(断言)
这是一个Java 8 的Predicate,可以使用它来匹配来自HTTP请求的任何内容,例如headers或参数。断言的输入类型是一个ServletWebExchange。
1.3 SpringCloud Gateway 和架构
Webflux的响应式编程不仅仅是编程风格的改变,而且对一系列的著名框架,都提供了响应式访问的开发包,如Netty、Redis等。
SpringCloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
1.3.1 Webflux模型
Webflux模式替换了旧的Servlet线程模型,用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程中进行处理,大大提高了Loop线程的利用率。官方结构图:
Webflux虽然可以兼容多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟Netty是目前业界认可的最高性能的通信框架。而Webflux的Loop线程,正好就是著名的Reactor模型IO处理模型的Reactor线程,如果使用的是高性能的通信框架Netty,这就是Netty的EventLoop线程。
1.3.2 SpringCloud Gateway的处理流程
客户端向Spring Cloud Gateway发出请求,然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前或之后执行业务逻辑。
1.4 SpringCloud Gateway 路由配置方式
1.4.1 基础uri的路由配置方式
如果请求的目标地址,是单个的URI资源路径,配置文件示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
-id: url-proxy-1
uri: https://blog.csdn.net
predicates:
-Path=/csdn
各字段含义如下:
id: 我们自定义的路由id,保持唯一
uri: 目标服务地址
predicates: 路由条件,Predicates接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非)。
上面这段配置的意思是,配置了一个id为url-proxy-1的URI代理规则,路由的规则是:当访问地址 http://localhost:8080/csdn/1.jsp时,会路由到上游地址 https://blog.csdn.net/1.jsp。
1.4.2 基于代码的路由配置方式
转发功能同样可以通过代码来实现,我们可以在启动类 GatewayApplication中添加方法 customRouteLocator()来定制转发规则。
package com.learn.springcloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
/**
* @Author zhangyugu
* @Date 2020/9/17 5:52 上午
* @Version 1.0
*/
@EnableDiscoveryClient
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/csdn")
.uri("https://blog.csdn.net"))
.build();
}
}
我们在yaml配置文件中注销掉相关路由的配置,重启服务,访问链接: http://localhost:8080/csdn,可以看到和上面一样的页面,证明我们测试成功。
1.4.3 和注册中心相结合的路由配置方式
在uri的schema协议部分为自定义的lb:类型,表示从微服务注册中心订阅服务,并且进行服务的路由。一个典型的示例如下:
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: lb-route
uri: lb://nacos-provider-route
predicates:
- Path=/nacos-provider/**
nacos:
server-addr: localhost:8848
discovery:
server-addr: localhost:8848
注册中心相结合的路由配置方式,与单个uri的路由配置,区别其实很小,仅仅在于uri的schema协议不同。单个uri的地址的schema协议,一般为http或者https协议。
1.5 详解:SpringCloud Gateway 匹配规则
Spring Cloud Gateway的功能很强大,我们仅仅通过Predicates的设计就可以看出来,前面我们只是使用了predicates进行了简单的条件匹配,其实Spring Cloud Gateway帮我们内置了很多Predicates功能。
Spring Cloud Gateway 是通过 Spring WebFlux的HandlerMapping作为底层支持来匹配到转发路由,Spring Cloud Gateway内置了很多Predicates工厂,这些Predicates工厂通过不同的HTTP请求参数来匹配,多个Predicates工厂可以组合使用。
1.5.1 Predicates 断言条件介绍
Predicate 来源于 Java 8,是Java 8中引入的一个函数,Predicate接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来讲Predicate组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在Spring Cloud Gateway中Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
1.5.2 通过请求参数匹配
Query Route Predicate 支持传入两个参数,一个是属性名,一个为属性值,属性值可以是正则表达式。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Query=smilem pu.
这样只有当请求中包含smile属性并且参数值是以pu开头的长度为三位的字符串才会进行匹配和路由。
1.5.3 通过Header属性匹配
Header Route Predicate 和 Cookie Route Predicate一样,也是接收2个参数,一个header中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Header=X-Request-Id, \d+
使用 curl 测试,命令行输入:
curl http://localhost:8080 -H "X-Request-Id:88"
则返回页面html证明匹配成功。
1.5.4 通过Cookie匹配
Cookie Route Predicate 可以接收两个参数,一个是Cookie name,一个是正则表达式,路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配则不执行。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Cookie=sessionId, test
使用curl 测试,命令行输入:
curl http://localhost:8080 --cookie "sessionId=test" 则会返回页面代码
1.5.5 通过Host匹配
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.baidu.com
使用curl 测试,命令行输入:
curl http://localhost:8080 -H "Host: www.baidu.com"
curl http://localhost:8080 -H "Host: md.baidu.com"
经测试以上两种host均可匹配到host_route路由,去掉Host参数则会报404参数。
1.5.6 通过请求方式匹配
可以通过是POST、GET、PUT、DELETE等不同的请求方式来路由。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Method=GET
使用curl测试,命令行输入:
curl http://localhost:8080 测试返回页面代码,证明匹配到路由。我们再以POST的方式请求测试。
curl -X POST http://localhost:8080 返回404没有找到,证明没有匹配上路由。
1.5.7 通过请求路径匹配
Path Route Predicate 接收一个匹配路径的参数来判断是否走路由。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Path=/foo/{segment}
如果请求路径符合要求,则此路由将匹配,例如:/foo/1 或者 /foo/bar。
使用 curl 测试,命令行输入:
curl http://localhost:8080/foo/1
curl http://localhost:8080/fao/1
经过测试第一条命令可以正确获取到页面返回值,第二条命令报404,证明路由是通过指定路由来匹配。
1.5.8 通过请求ip地址进行匹配
Predicate 也支持通过设置某个ip区间号段的请求才会路由,RemoteAddr Route Predicate接收cidr符号(IPv4 或 IPv6)字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中192.168.0.1 是ip地址,16是子网掩码)。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.10/24
可以将此地址设置为本机的ip地址进行测试。
curl localhost:8080 如果请求的远程地址是 192.168.1.10 ,则此路由将匹配。
1.5.9 组合使用
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.10/24
- Path=/foo/{segment}
- Method=GET
- Host=**.baidu.com
- Cookie=sessionId, test
- Header=X-Request-Id, \d+
- Query=smile, pu.
各种Predicates同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。
1.6 SpringCloud Gateway高级功能
1.6.1 实现熔断降级
为什么要实现熔断降级?
在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免地会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。
为什么在网关上请求失败需要快速返回给客户端?
因为当一个客户端请求发生故障时,这个请求会一直堆积在网关上,网关上堆积多了就会给网关乃至整个服务造成巨大的压力,甚至整个服务宕掉。因此要对一些服务和页面进行有策略的降级,以此缓解服务器资源的压力,以保证核心业务的正常运行,同时也保持了大部分客户得到正确的响应,所以需要网关上请求失败然后快速返回给客户端。
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: rate_limit_route
uri: http://localhost:8080
order: 0
predicates:
- Path=/test/**
filters:
- StripPrefix=1
- name: Hystrix
args:
name: fallbackCmdA
fallbackUri: forward:/fallbackA
hystrix:
command:
fallbackCmdA:
execution:
isolation:
thread:
timeoutInMilliseconds:
5000
这里的配置,使用了两个过滤器:
(1) 过滤器 StripPrefix,作用是把请求路径的最前面n个部分截取掉。
StripPrefix=1就代表截取路径的个数为1,比如前端过来请求 /test/good/1/view,匹配成功后,路由到后端的请求路径就会变成 http://localhost:8888/good/1/view。
(2) 过滤器 Hystrix,作用是通过Hystrix进行熔断降级。
当上游的请求,进入Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间。
fallbackUri配置的降级地址的代码如下:
package com.learn.springcloud.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author zhangyugu
* @Date 2020/9/19 5:28 上午
* @Version 1.0
*/
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服务暂时不可用");
return response;
}
}
1.6.2 分布式限流
从某种意义上说,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则就选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断地进行,如果桶中令牌数达到上限,就丢弃令牌。
所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没有启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
在Spring Cloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关的最基本功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用于Redis内的通过执行lua脚本实现了令牌桶的方式。具体的实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中:
首先在工程的pom.xml文件中引入gateway的起步依赖和redis的reactive依赖,代码如下:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: rate_limit_redis
uri: http://httpbin.org:80/get
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
filters:
- name: RequestRateLimiter
args:
key-resolver: '#{@userKeyResolver}'
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 3
redis:
host: localhost
port: 6379
database: 0
上面的配置文件,指定程序的端口为8081,配置了redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器需要配置三个参数:
- burstCapacity,令牌桶总量
- replenishRate,令牌桶每秒填充平均速率
- key-resolver,用于限流的键的解析器的Bean对象的名字。它使用Spel表达式根据#{@beanName}从Spring容器中获取Bean对象。
这里根据用户id限流,请求路径中必须携带userId参数
@Bean
KeyResolver userKeyResolver() {
return exchange -> {
return Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
};
}