Spring Cloud Gateway是Spring Cloud的一个全新项目,基于Spring 5.0+Spring Boot 2.0和Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
Spring Cloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
Spring Cloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,如:安全、监控/指标、和限流。
Spring Cloud Gateway底层使用了高性能的通信框架Netty。
从以上的特征来说,和Zuul的特征区别不大。SpringCloud Gateway和Zuul主要的区别,还是在**底层的通信框架**上。
Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由,目标URI会被访问。
Predicate(断言):这是一个java 8的Predicate,可以使用它来匹配来自HTTP请求的任何内容,如:请求头和请求参数。断言的输入类型是一个ServerWebExchange。
Filter(过滤器):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者后对请求进行修改。
总结:web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。predicate就是匹配条件,而filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标URI,就可以实现具体的路由了。
Spring的Webflux的响应式编程不仅仅是编程风格的改变,而且对于一系列的著名框架,都提供了响应式访问的开发包,比如Netty、Redis等。
SpringCloud Gateway使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通讯框架。
SpringCloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接收请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。
弊端:servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,但是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上下文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势。
所以SpringCloud Zuul是基于servlet之上的一个阻塞式处理模型,即Spring实现了处理所有request请求的一个servlet(DispatcherServlet),并由该servleet阻塞式处理。因此SpringCloud Zuul无法摆脱servlet模型的弊端。虽然Zuul 2.0开始使用了Netty,并且有了大规模Zuul 2.0集群部署的成熟案例。但是,SpringCloud官方已经没有集成该版本的计划了。
Webflux模型替换了旧的Servlet线程模型。用少量的线程处理request和response io操作,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是非常灵活的,用户可以将业务中阻塞的操作提交到响应式框架的work线程中执行,而不阻塞的操作依然可以在Loop线程中进行处理,大大提高了Loop线程的利用率。
Webflux虽然可以兼容 多个底层的通信框架,但是一般情况下,底层使用的还是Netty,毕竟Netty是目前业界认可的最高新能的通信框架。而Webflux的Loop线程,正好就是著名的Reactor模式IO处理模型的Reactor线程,如果使用的是高性能的通信框架Netty,者就是Netty的EventLoop线程。
客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链来将请求发送到实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(post)执行业务逻辑。
如果请求的目标地址,是单个的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为url-proxy-1的URI代理规则,路由的规则为:当访问地址http://localhost:8080/csdn/1.jsp时,会路由到上游地址https://blog.csdn.net/1.jsp
转发功能同样可以通过代码来实现,可以在启动类GateWayApplication中添加方法customRouteLocator()来定制转发规则。
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("path_route", r -> r.path("/csdn")
.uri("https://blog.csdn.net"))
.build();
}
在uri的schema协议部分为自定义的lb:类型,表示从微服务注册中心(如Eureka),并且进行服务的路由。
server:
port: 8084
spring:
cloud:
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务进行路由
gateway:
routes:
- id: seckill-provider-route #路由的ID,没有固定规则但要求唯一,建议配合服务名
uri: lb://seckill-provider #匹配后提供服务的路由地址
predicates:
- Path=/seckill-provider/** #断言,路径相匹配的进行路由
- id: message-provider-route
uri: lb://message-provider
predicates:
- Path=/message-provider/**
application:
name: cloud-gateway
eureka:
instance:
prefer-ip-address: true
client:
service-url:
defaultZone: http://localhost:8888/eureka/
注册中心相结合的路由配置方式,与单个URI的路由配置,区别其实很小,仅仅在于URI的schema协议不同。单个URI的地址的schema协议,一般为http或者https协议。
SpringCloud Gateway是通过Spring WegFlux的HandlerMapping作为底层支持来匹配到转发路由,Spring Cloud Gateway内置了很多Predicates工厂,这些Predicates工厂通过不同的HTTP请求参数来匹配,多个Predicates工厂可以组合使用。
Predicate来源于Java 8,是Java 8中引入的一个函数,Predicate接收一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
在Spring Cloud Gateway中Spring利用Predicate的特性实现了各种路由匹配规则,有通过Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
SpringCloud内置的集中Predicate的实现,如下图:
Query Route Predicate支持传入两个参数,一个是属性名一个是属性值,属性值可以是正则表达式。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Query=smile
这样配置,只要请求中包含了smile属性的参数即可匹配路由。
使用curl测试,命令行输入:curl localhost:8080?smile=x&id=2
经过测试发现只要请求中带有smile参数就会匹配路由,不带smile参数则不会匹配路由。
还可以将Query的值以键值对的方式进行配置,这样在请求过来时对属性值和正则进行匹配,匹配上才会走路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Query=keep, pu.
这样只要当请求中包含keep属性并且参数值是以pu开头的长度为三位的字符串才会进行匹配和路由。
使用curl测试,命令行输入: curl localhost:8080?keep=pub
测试可以返回页面代码,将keep的属性值改为pubx再次访问就会报404,证明路由需要匹配正则表达式才会进行路由。
Header Route Predicate和Cookie Route Predicate一样,也是接收2个参数,一个header中属性名和一个正则表达式,这个属性值和正则表达式匹配则执行。
server:
port: 8080
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”
则返回页面代码证明匹配成功。将参数-H “X-Request-Id:88”改为-H “X-Request-Id:spring”再次执行时返回404证明没有匹配。
Cookie Route Predicate 可以接受两个参数,一个是Cookie name,一个是正则表达式,路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
server:
port: 8080
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”
则会返回页面代码,如果去掉–cookie “sessionId=test”,后台会报404错误。
Host Route Predicate接收一组参数,一组匹配的域名列表,这个模板是一个ant分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。
server:
port: 8080
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错误。
可以通过POST、GET、PUT、DELETE等不同的请求方式来进行路由。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Method=GET
使用curl测试,命令行输入:
测试返回页面代码,证明匹配到路由,再以POST的方式请求测试。
curl -x POST http://localhost:8080
返回404没有找到,证明没有匹配上路由。
Path Route Predicate接收一个匹配路径的参数来判断是否走路由
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: http://ityouknow.com
order: 0
predicates:
- Path=/foo/{segment}
如果请求路径符合要求,则此路由将匹配。
使用curl测试,命令行输入:
curl http://localhost:8080/foo/1
curl http://localhost:8080/foo/xx
curl http://localhost:8080/boo/xx
经过测试第一和第二条命令可以正常获取页面返回值,最后一个命令报404,证明路由是通过指定路由来匹配。
Predicate也支持通过设置某个ip区间号段的请求才会路由,RemoteAddr Route Predicate接收cidr符号(IPv4或IPv6)字符串的列表(最小大小为1),如:192.168.0.1/16(其中192.168.0.1是IP地址,16是子网掩码)。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- RemoteAddr=192.168.1.1/24
可以将此地址设置为本机的ip地址进行测试。
curl localhost:8080
如果请求的远程地址是192.168.1.10,则此路由将匹配。
server:
port: 8080
spring:
application:
name: api-gateway
cloud:
gateway:
routes:
- id: gateway-service
uri: https://www.baidu.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各种Predicates同时存在于同一个路由时,请求必须同时满足所有条件才被这个路由匹配。
一个请求满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。
在分布式系统中,网关作为流量的入口,因此会有大量的请求进入网关,向其他服务发起调用,其他服务不可避免的会出现调用失败(超时,异常),失败时不能让请求堆积在网关上,需要快速失败并返回给客户端,想要实现这个要求,就必须在网关上做熔断、降级操作。
server.port: 8082
spring:
application:
name: gateway
redis:
host: localhost
port: 6379
password: 123456
cloud:
gateway:
routes:
- id: rateLimit_route
uri: http://localhost:8000
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)过滤器Strprefix,作用是去掉请求路径的最前面n个部分截取掉。
StriPrefix=1就代表截取路径的个数为1,比如前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view
2)过滤器Hystrix,作用是通过Hystrix进行熔断降级
当上游的请求,进入了Hystrix熔断机制时,就会调用fallbackUri配置的降级地址。需要注意的是,还需要单独设置Hystrix的commandKey的超时时间。
fallbackUri配置的降级地址的代码如下:
@RestController
public class FallbackController {
@GetMapping("/fallbackA")
public Response fallbackA() {
Response response = new Response();
response.setCode("100");
response.setMessage("服务暂时不可用");
return response;
}
}
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的频率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行。否则选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中就已经有100个令牌了,这是服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以只用桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率进行。
在SpringCloud Gateway中,有Filter过滤器,因此可以在“pre”类型的Filter中自行实现上述三种过滤器。但是限流作为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFacotry这个类,适用于Redis内的通过执行Lua脚本实现令牌桶的方式。具体实现逻辑RequestRateLimiterGatewayFilterFactory类中,lua脚本在如下图所示的文件夹中: