文章目录
- 1. 概述
- 2. SpringCloud Gateway与Zuul的区别
- 3. Gateway原理
- 4. gateway的三大核心概念
- 5. Route(路由)
- 6. 过滤器
- 7. 自定义全局过滤器之权限验证玩法
- 8. 网关限流玩法
- 9. 结合Sentinel实现限流
- 10 .遇到的问题
- 11. 调整netty参数
1. 概述
SpringCloudGateway官网,变化很大,以实际为准
Gateway是在Spring生态系统之上构建的API网关服务,基于Spring5SpringBoot2和ProjectReactor等技术。
Gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等
SpringCloud Gateway是Spring Cloud的一个全新项目,基于Spring5.0+Spring Boot20和 Project Reactor等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的API路由管理方式。
SpringCloud Gateway作为SpringCloud生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然还是使用的Zuul1.x非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。
SpringCloud Gateway的目标提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
一句话就是: SpringCloud Gateway使用的是Webflux中的reator-netty响应式编程组件,底层使用了Netty通讯框架
1.1 作用
反向代理
鉴权
流量控制
熔断
日志监控
1.2 微服务架构中网关在哪里?
1.3 为什么选择gateway?
zuul2.0一直跳票迟迟不发布,一方面因为Zuul1.0已经进入了维护阶段,而且Gateway是SpringCloud团队研发的,是亲儿子产品,值得信赖,而且很多功能Zuul都没有用起来也非常的简单便捷。
Gateway是基于异步非阳塞模型上进行开发的,性能方面不需要担心。虽然Netflix早就发布了最新的 Zuul 2.x. 但SpringCloud貌似没有整合计划。而且Netflix相关组件都宣布进入维护期;不知前景如何?
SpringCloud Gateway具有如下特性:
3.1 基于Spring Framework 5, Project Reactor 和 Spring Boot 2.0进行构建;动态路由:能够匹配任何请求属性;
3.2 可以对路由指定Predicate(断言)和Filter(过滤器);集成Hystrix的断路器功能;
集成Spring Cloud 服务发现功能;
3.3 易于编写的Predicate(断言)和Filter(过滤器);请求限流功能;支持路径重写。
2. SpringCloud Gateway与Zuul的区别
在SpringCloud Finchley正式版之前,SpringCloud 推荐的网关是Netflix提供的Zuul1.x,是一个基于阻塞1/O的API;
Zuul1x基于Servlet2.5使用阻塞架构,它不支持任何长连接(如WebSocket)Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择1个执行,请求线程被阻塞到工作线程完成,但是差别是Nginx用C++实现,Zuul 用Java 实现,而JVM 本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。
Zuul2.x理念更先进,想基于Netty非阻塞和支持长连接,但SpringCloud目前还没有整合。Zuul2.x的性能较Zuul 1.x有较大提升。在性能方面,根据官方提供的基准测试,Spring Cloud Gateway 的 RPS(每秒请求数)是Zuul的1.6倍。但是最后2.x 夭折了;
Spring Cloud Gateway建立在Spring Framework5、 Project Reactor和Spring Boot2之上,使用非阻塞APl。
Spring Cloud Gateway还支持WebSocket,并且与Spring紧密集成拥有更好的开发体验;
2.1 Zuul1.x模型
Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。
servlet由servlet container进行生命周期管理。
container启动时构造servlet对象并调用servletinit()进行初始化;
container运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service()。 container关闭时调用servlet destory0销毁servlet;
上述模式的缺点:
servlet是一个简单的网络IO模型,当请求进入servlet container时,servletcontainer就会为其绑定一个线程,在并发不高的场景下这种模型是适用的。但是一旦高并发(比如抽风用jemeter压,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单业务场景下,不希望为每个request分配一个线程,只需要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优势
所以Zuul1.X是基于servlet之上的一个阻塞式处理模型
,即spring实现了处理所有request请求的一个servlet(DispatcherServlet)并由该servlet阻塞式处理处理。所以Springcloud Zuul无法摆脱servlet模型的弊端;
2.2 Gateway模型
Gateway是基于Spring WebFlux
webFlux是什么?
特性一 :
异步非阻塞
众所周知,SpringMVC是同步阻塞的IO模型,资源浪费相对来说比较严重,当我们在处理一个比较耗时的任务时,例如:上传一个比较大的文件,首先,服务器的线程一直在等待接收文件,在这期间它就像个傻子一样等在那儿(放学别走),什么都干不了,好不容易等到文件来了并且接收完毕,我们又要将文件写入磁盘,在这写入的过程中,这根线程又再次懵bi了,又要等到文件写完才能去干其它的事情。这一前一后的等待,不浪费资源么?
没错,Spring WebFlux就是来解决这问题的,Spring WebFlux可以做到异步非阻塞。还是上面那上传文件的例子,Spring WebFlux是这样做的:线程发现文件还没准备好,就先去做其它事情,当文件准备好之后,通知这根线程来处理,当接收完毕写入磁盘的时候(根据具体情况选择是否做异步非阻塞),写入完毕后通知这根线程再来处理(异步非阻塞情况下)。特性二:
响应式(reactive)函数编程
如果你觉得java8的lambda写起来很爽,那么,你会再次喜欢上Spring WebFlux,因为它支持函数式编程,得益于对于reactive-stream的支持(通过reactor框架来实现的);特性三:
不再拘束于Servlet容器
以前,我们的应用都运行于Servlet容器之中,例如我们大家最为熟悉的Tomcat, Jetty…等等。而现在Spring WebFlux不仅能运行于传统的Servlet容器中(前提是容器要支持Servlet3.1,因为非阻塞IO是使用了Servlet3.1的特性),还能运行在支持NIO的Netty和Undertow中。
3. Gateway原理
摘自官网: https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
这里补充一句,其实最好的老师还是官网,多去看看就好
上图翻译: 客户端向Spring Cloud Gateway
发出请求,再由网关处理程序 Gateway Handler Mapping
映射确定与请求相匹配的路由,将其发送到网关Web理程序 Gateway Web Handler
.该处理程序通过指定的过滤器链将请求发送到我们实际的服务执行业务逻辑,然后返回。过途器由虚钱分隔的原因是,过滤器可以在发送代理请求之前和之后运行逻辑.所有pre
过滤器逻辑均被执行。然后发出代理请求,发出代理请求后,将运行post
过滤器逻辑,
4. gateway的三大核心概念
路由(Route)
:路由是网关最基础的部分,路由信息由lD、目标URI、一组断言和一组过滤器组成。如果断言路由为真,则说明请求的URI和配置匹配。断言(Predicate)
:Java8中的断言函数。Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange.Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自于Http Request中的任何信息,比如请求头和参数等。过滤器(Fiter)
:一个标准的Spring Web Filter,Spring Cloud Gateway 中的Filter分为两种类型,分别是Gateway Filter和Global Filter.过滤器将会对请求和响应进行处理。
5. Route(路由)
5.1 路由之 Path
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/wql01/** # 匹配对应Url的请求,将匹配的请求追加在目标URI之后
当在浏览器输入: http://localhost:8888/wql01/01 时候发现是以wql01开头,则会被断言为true,则 会将/wql01/01拼接到uri后面,
也就是最终会走 http://localhost:7777/wql01/01 的逻辑,这就是 Path 路由的规则
5.2 路由之 Query
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 # - Query=token # 匹配请求参数中包含token的请求 - Query=token, abc. #匹配请求参数中包含token并且参数满足正则表达式 abc. 的请求
Query=token
eg: http://localhost:8888/wql01/01?token=123Query=token, abc.
eg: http://localhost:8888/wql01/01?token=abc1
5.3 路由之 Method
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Method=GET # 匹配任意Get请求
5.4 路由之 Datetime
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 # 匹配中国上海时间2021-02-02 20:20:20 之后的请求 - After=2021-02-02T20:20:20.000+08:00[Asia/Shanghai]
After表示请求在这时间之后,同样还有before between 规则就是相应的范围;
5.5 路由之 RemoteAddr
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - RemoteAddr=192.168.43.156/0 # 匹配远程地址请求是RemoteAddr的请求,0表示子网掩码
使用ipconfig看下你本机ipv4地址,不管是无线网的还是以太网的,比如我的是
如果我现在使用http://localhost:8888/wql01/01 去访问是访问不到的,因为我设定了远程地址是192.168.43.156,所以必须使用
http://192.168.43.156:8888/wql01/01 才能访问
5.6 路由之 Header
配置如下yaml
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: client01-7777 # 路由 Id ,唯一 uri: http://localhost:7777/ # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Header=x-Request-Id, \d+ # 匹配请求头包含X-Request-Id 并且匹配正则表达式 \d+ 的请求
跟Query 用法类似,只不过Header是根据头部判断;
5.7 路由之 动态路由
动态路由,即服务发现的路由,也就是面向服务的路由,SpringCloudGateway与Eureka整合开发,根据serviceId自动从注册中心获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改Gateway的路由配置
服务方的服务名为 CLIENT01,这个可以在注册中心页面查看
测试:
5.8 路由之 服务名称转发
动态路由解决的问题是 uri写死的问题,那么如果现在有100个服务,是不是我们要在配置文件里面写100组路由,如下图所示,这是一组代表Client01服务的配置,100个不同的服务写100组,维护麻烦,但是Spring里面其实很多帮我们做好了约定的操作,如果我们遵守约定,就可以很简洁的解决我们的烦恼
如下图所示,如果我们遵守约定,那么会变得特别简洁
准备了两个服务,一个client01 一个client02
如果我们想调用client01的服务,只需要发送 http://localhost:8888/client01/wql01/01 即可,与之前不同的是我们需要在加上调用方的服务名
如果我们想调用client02的服务,只需要发送 http://localhost:8888/client02/wql01/01 即可
这样就节省了大量的配置书写;
6. 过滤器
Spring Cloud Gateway 根据作用范围划分为GatewayFilter
和GlobalFilter
,二者区别如下:
GatewayFilter
:网关过滤器
需要通过spring.cloud.routes.filters配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters 配置在全局,作用在所有路由上。GlobalFilter
:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter
包装成GatewayFilterChain
可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
6.1 网关过滤器
网关过滤器用于拦截并链式处理 Web请求,可以实现横切与应用无关的需求,比如:安全、访问超时的设置等。修改传入的HTTP请求或传出HTTP响应。
Spring Cloud Gateway包含许多内置的网关过滤器工厂一共有22个,包括头部过滤器、路径类过滤器、Hystrix过滤器和重写请求URL的过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、Body,Status,Session,Redirect、Retry、RateLimiter 等
大概列举一下:
- Header 头部过滤器
- AddRequestHeader GatewayFilter Factory
- RemoveRequestHeader GatewayFilter Factory
- AddResponseHeader GatewayFilter Factory
- RemoveResponseHeader GatewayFilter Factory
- SetRequestHeader GatewayFilter Factory
- SetResponseHeader GatewayFilter Factory
- PreserveHostHeader GatewayFilter Factory
- SecureHeaders GatewayFilter Factory
- Parameter 请求参数过滤器
- AddRequestParameter GatewayFilter Factory
- Path 路径过滤器
- PrefixPath GatewayFilter Factory
- StripPrefix GatewayFilter Factory
- SetPath GatewayFilter Factory
- RewritePath GatewayFilter Factory
- Body 请求(响应)体过滤器
- ModifyRequestBody GatewayFilter Factory
- ModifyResponseBody GatewayFilter Factory
- Status 状态过滤器
- SetStatus GatewayFilter Factory
- Session 会话过滤器
- SaveSession GatewayFilter Factory
- Redirect 重定向过滤器
- RedirectTo GatewayFilter Factory
- Retry 重试过滤器
- Retry GatewayFilter Factory
- RateLimiter 限流过滤器
- RequestRateLimiter GatewayFilter Factory
每一个过滤器都对应一个工厂类,其实在官网每一种过滤器官网都给出了相应的案例教学,下面简单说几种常用的过滤器,素材摘自官网
6.1.1 RewritePath GatewayFilter Factory
重写Url:
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api-gateway/** # 匹配对应Url的请求 filters: # 网关过滤器 - RewritePath=/api-gateway/?(?<segment>.*), /$\{segment}
图中 filters
表示网关过滤器
比如我们发出 http://localhost:8888/api-gateway/wql01/01 请求 ,到时候/api-gateway/wql01/01会被替换成/wql01/01,比如下图所示
6.1.2 PrefixPathGatewayFilterFactory
PrefixPath 网关过滤器工厂为匹配的URI添加指定前缀
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/** # 匹配对应Url的请求 filters: # 网关过滤器 # 将 /01 重写为 /wql01/01 - PrefixPath=/wql01
比如 /01 会自动加上前缀/wql01 变成 /wql01/01
6.1.3 StripPrefixGatewayFilterFactory
StripPrefixGatewayFilterFactory 网关过滤器工厂采用一个参数StripPrefix,该参数表示在请求发送到下游之前从请求中剥离的路径个数.
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/** # 匹配对应Url的请求 filters: # 网关过滤器 # 将 /api/wql01/01 重写为 /wql01/01 - StripPrefix=1 # 1表示1个/符号
比如 /api/wql01/01 会自动截取掉前面第一个/符号的 变成/wql01/01
6.1.4 SetPathGatewayFilterFactory
SetPath 网关过滤器工厂采用路径模板参数,它提供了一种通过允许模板化路径段来操作请求路径的简单方法,使用了SpringFramework中的Uri模板,允许多个匹配段
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 将 /api/wql01/01 重写为 /wql01/01 - SetPath=/product/{segment}
/api/wql01/01 经过重新设置路径之后变成了/wql01/01
6.1.5 Parameter 参数过滤器
AddRequestParameter 网关过滤器工厂会将指定参数添加至匹配到的下游请求中.
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 将 /api/wql01/01 重写为 /wql01/01 - RewritePath=/api(?<segment>/?.*),$\{segment} # 过滤器是可以组合使用的,下面这个表示在请求参数里面添加flag=1 - AddRequestParameter=flag,1
当执行http://localhost:8888/api/wql01/01 请求的时候,会将/api/wql01/01 改成 /wql01/01 然后加上flag参数
6.1.6 Status 状态过滤器
SetStatus 网关过滤器工厂采用单个状态参数,它必须是有效的Spring HttpStatus ,它可以是整数404或者枚举NOT_FOUND的字符串表示;
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 将 /api/wql01/01 重写为 /wql01/01 - RewritePath=/api(?<segment>/?.*),$\{segment} # 过滤器是可以组合使用的,表示任何情况下,响应的Http 状态都将设置为404 - SetStatus=404 # 404 或者对应的枚举 NOT_FOUND
其实就是改变状态码
6.2 全局过滤器
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过 GatewayfilterAdapter 包装成GatewayfilterChain可识别的过滤器,它是请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
6.3 自定义过滤器
6.3.1 自定义网关过滤器
如果spring提供的网关过滤器满足不了你的需求,你可以自定义网关过滤器
主要实现两个接口: GatewayFilter
, Ordered
Ordered是排序用的,数字越小越靠前
- 创建自定义网关过滤器类
package com.wql.gatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 自定义网关过滤器 * * @author wql * @date 2021/12/28 23:23 */ public class DiyGatewayFilter implements GatewayFilter, Ordered { /** * 过滤器业务逻辑 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("自定义网关过滤器被执行"); // 继续往下执行 return chain.filter(exchange); } /** * 过滤器执行顺序,数值越小,优先级越高 */ @Override public int getOrder() { return 0; } }
- 将自定义网关过滤器通过配置类注册上去
package com.wql.config; import com.wql.gatewayFilter.DiyGatewayFilter; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 网关路由配置类 * * @author wql * @date 2021/12/28 23:30 */ @Configuration public class GatewayRouteConfiguration { @Bean public RouteLocator routeLocator(RouteLocatorBuilder builder) { return builder.routes().route(r ->r //断言,判断条件 .path("/wql01/**") // 目标uri,路由到微服务的地址 .uri("lb://CLIENT01") // 自定义网关过滤器 .filter(new DiyGatewayFilter()) // 路由Id 唯一标识 .id("CLIENT01") ).build(); } }
6.3.2 自定义全局过滤器
自定义全局过滤器需要实现以下两个接口: GlobalFilter , Ordered 通过全局过滤器可以实现权限校验,安全性验证等功能;
- 创建自定义全局过滤器类
package com.wql.gatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 自定义全局过滤器 * * @author wql * @date 2021/12/28 23:23 */ @Component public class DiyGlobalFilter implements GlobalFilter, Ordered { /** * 过滤器业务逻辑 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("自定义全局过滤器被执行"); // 继续往下执行 return chain.filter(exchange); } /** * 过滤器执行顺序,数值越小,优先级越高 */ @Override public int getOrder() { return 0; } }
全局过滤器无需注册路由,直接加上一个@Component注解即可生效
7. 自定义全局过滤器之权限验证玩法
package com.wql.gatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; /** * 自定义全局过滤器 * * @author wql * @date 2021/12/28 23:23 */ @Component public class DiyGlobalFilter implements GlobalFilter, Ordered { /** * 过滤器业务逻辑 */ @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求参数 getFirst并不是代表获取第一个参数,这只是一种获取参数的写法而已 String token = exchange.getRequest().getQueryParams().getFirst("token"); //业务逻辑处理 if (null==token) { System.out.println("token为空"); ServerHttpResponse response = exchange.getResponse(); //响应类型 response.getHeaders().add("Content-Type","application/json;charset=utf-8"); //响应状态码 Http 401 代表用户没有访问权限 response.setStatusCode(HttpStatus.UNAUTHORIZED); //响应内容 String message = "{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}"; DataBuffer buffer = response.bufferFactory().wrap(message.getBytes()); //请求结束,不在继续向下请求 return response.writeWith(Mono.just(buffer)); } System.out.println("token is ok"); return chain.filter(exchange); } /** * 过滤器执行顺序,数值越小,优先级越高 */ @Override public int getOrder() { return 0; } }
8. 网关限流玩法
为什么需要限流?
比如Web服务、对外AP1,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(好事)
- 因为某个热点事件(微调热烫)
- 竞争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知通什么时候会有10倍基至20倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。
8.1 常见的限流算法
计算器算法
计数器算法是限流算法里最能单也是最容易实现的一种算法,比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个,那么我们可以这么做:在一开始的时候,我们可以设置一个计数端 counter,每当一个请求过来的时候,counter 就加1,如果counter的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于1分钟,重置counter 重新计数,具体算法的示意函如下:
这个算法虽然简单,但是有一个十分致命的漏洞,就是临界问题:
从上图中我们可以看到,假设有一个恶意用户,他在0:59时,瞬间发送了100个请求,并且1:00又瞬间发送了100个请求,那么其实这个用户在1秒里面,瞬间发送了200个请求,我们刚才规定的是1分钟最多100个请求,也就是每秒种最多1.7个请求,用户通过在时间窗口的重置节点处突发清求,可以瞬间超过我们的速幸限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
还有资料浪费的问题存在,我们的预期想法是希望100个请求可以均匀分散在这一分钟内,假设305以内我们就请求上限了,那么剩余的半分钟服务器就会处于闲置状态,比如下图:漏桶算法( Leaky Bucket) 算法
漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率.
漏桶算法是基于队列实现的
漏桶算法的劣势在于,加入水流出的速率是1s一个,如果此时来1s进来1万个请求,那么我需要1万秒才能处理完,请求堆积在桶里,超过桶的容量还会丢失大量请求,造成资源损失和浪费,同时也对网关造成很大的压力
令牌桶(Token Bucket) 算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌,算法中存在一种机制,以一定的速率往桶中放令牌,每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌致达到上限,就丢弃令牌。
场景大概是这样的:桶中一直有大量的可用令牌,这时进来的请求可以直接拿到令牌执行,比如设置QPS为100/s,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
Spring Cloud Gateway 内部使用的就是该算法,大概描述如下:
- 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
- 根据限流大小,设置按照一定的速率往桶里添加令牌;
- 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被委弃或者拒绝;
- 请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到今牌这些请求会瞬时被处理调用目标服务。
8.2 Gateway限流
Spring Cloud Gateway 官方提供了 RequestRateLimiterGatewayFilterFactory
过滤器工厂,使用Redis
和Lua
脚本实现了令牌桶的方式
8.2.1 Url限流规则
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@pathKeyResolver}" # 使用SpEL 表达式按名称引用bean,其中 pathKeyResolver是bean对象Id
yaml当中的pathKeyResolver 对应的是下面bean的名字:
package com.wql.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; /** * @author wql * @date 2021/12/29 23:47 */ @Configuration public class KeyResolverConfiguration { @Bean public KeyResolver pathKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getURI().getPath()); } }
8.2.2 params参数限流规则
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@paramsKeyResolver}" # 使用SpEL 表达式按名称引用bean,其中 paramsKeyResolver是bean对象Id
其实就是将bean的名字改成params即可,但是不能同时配置多个限流规则,不然会报错
package com.wql.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; /** * @author wql * @date 2021/12/29 23:47 */ @Configuration public class KeyResolverConfiguration { // @Bean // public KeyResolver pathKeyResolver(){ // return exchange -> Mono.just(exchange.getRequest().getURI().getPath()); // } @Bean public KeyResolver paramsKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userName")); } }
8.2.3 ip 限流规则
server: port: 8888 spring: application: name: gateway cloud: gateway: routes: - id: CLIENT01 # 路由 Id ,唯一 uri: lb://CLIENT01 # 目标 URI 路由到微服务的地址 predicates: # 断言,判断条件 - Path=/api/wql01/{segment} # 匹配对应Url的请求 filters: # 网关过滤器 # 限流过滤器 - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 1 # 令牌每秒填充速率 redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量 key-resolver: "#{@ipKeyResolver}" # 使用SpEL 表达式按名称引用bean,其中 ipKeyResolver是bean对象Id
同理,其实就是改变bean的对象
package com.wql.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; /** * @author wql * @date 2021/12/29 23:47 */ @Configuration public class KeyResolverConfiguration { // @Bean // public KeyResolver pathKeyResolver(){ // return exchange -> Mono.just(exchange.getRequest().getURI().getPath()); // } // @Bean // public KeyResolver paramsKeyResolver(){ // return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userName")); // } @Bean public KeyResolver paramsKeyResolver(){ return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }
8.3 限流之后默认提示 429 Too Many Request
重写
import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory; import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver; import org.springframework.cloud.gateway.filter.ratelimit.RateLimiter; import org.springframework.cloud.gateway.route.Route; import org.springframework.cloud.gateway.support.ServerWebExchangeUtils; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.Objects; /** * 针对限流操作 做的响应,默认响应码是 429 Too Many Request * * @author wangqinglong01 */ @Slf4j @Component public class GatewayRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory { private final RateLimiter defaultRateLimiter; private final KeyResolver defaultKeyResolver; public GatewayRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) { super(defaultRateLimiter, defaultKeyResolver); this.defaultRateLimiter = defaultRateLimiter; this.defaultKeyResolver = defaultKeyResolver; } @Override public GatewayFilter apply(Config config) { KeyResolver resolver = getOrDefault(config.getKeyResolver(), defaultKeyResolver); RateLimiter<Object> limiter = getOrDefault(config.getRateLimiter(), defaultRateLimiter); return (exchange, chain) -> resolver.resolve(exchange).flatMap(key -> { Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); String routeId = route.getId(); String finalRouteId = routeId; return limiter.isAllowed(routeId, key).flatMap(response -> { for (Map.Entry<String, String> header : response.getHeaders().entrySet()) { exchange.getResponse().getHeaders().add(header.getKey(), header.getValue()); } if (response.isAllowed()) { return chain.filter(exchange); } log.warn("已限流: {}", finalRouteId); ServerHttpResponse httpResponse = exchange.getResponse(); //修改code为500 httpResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); if (!httpResponse.getHeaders().containsKey("Content-Type")) { httpResponse.getHeaders().add("Content-Type", "application/json"); } //此处无法触发全局异常处理,手动返回 DataBuffer buffer = httpResponse.bufferFactory().wrap(("{\n" + " \"code\": \"1414\"," + " \"message\": \"服务器限流\"," + " \"data\": \"Server throttling\"," + " \"success\": false" + "}").getBytes(StandardCharsets.UTF_8)); return httpResponse.writeWith(Mono.just(buffer)); }); }); } private <T> T getOrDefault(T configValue, T defaultValue) { return Objects.nonNull(configValue) ? configValue : defaultValue; } }
然后将yaml里面的配置修改一下
- id: CLIENT01 uri: lb://CLIENT01 predicates: - Path=/** filters: - name: GatewayRequestRateLimiter #此处修改 args: key-resolver: '#{@keyResolver}' redis-rate-limiter.replenishRate: 1 redis-rate-limiter.burstCapacity: 3
9. 结合Sentinel实现限流
官方地址: https://github.com/alibaba/Sentinel/wiki/%E7%BD%91%E5%85%B3%E9%99%90%E6%B5%81
同时官网也提供了demo便于参考
官网链接: https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
- maven依赖:
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
- yaml当中配置一下
然后写配置类:
package com.wql.config; 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.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GatewayFilter; 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.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.HashSet; import java.util.List; @Configuration public class GatewayConfiguration { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; /** * 构造器 * * @param viewResolversProvider * @param serverCodecConfigurer */ public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } /** * 限流异常处理器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } /** * 限流过滤器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } /*--------------------------上面这些是官网的------------------------------*/ @PostConstruct public void doInit() { initGatewayRules(); } /** * 网关限流规则 */ private void initGatewayRules() { HashSet<GatewayFlowRule> rules = new HashSet<>(); /** * resource: 资源名称 可以是网关中的route名称或者用户自定义的API分组名称 * count: 限流阈值 * intervalSec: 统计时间窗口,单位是秒,默认是1s */ rules.add(new GatewayFlowRule("order-service").setCount(3).setIntervalSec(60)); //加载网关限流规则 GatewayRuleManager.loadRules(rules); } }
官网的配置类demo如下:
/* * Copyright 1999-2019 Alibaba Group Holding Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.wql.config.guanwang; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.PostConstruct; 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.GatewayParamFlowItem; 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.exception.SentinelGatewayBlockExceptionHandler; import com.alibaba.csp.sentinel.slots.block.RuleConstant; 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.codec.ServerCodecConfigurer; import org.springframework.web.reactive.result.view.ViewResolver; /** * @author Eric Zhao */ @Configuration public class GatewayConfiguration { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } @Bean @Order(-1) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } @PostConstruct public void doInit() { initCustomizedApis(); initGatewayRules(); } private void initCustomizedApis() { Set<ApiDefinition> definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition("some_customized_api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ add(new ApiPathPredicateItem().setPattern("/ahas")); add(new ApiPathPredicateItem().setPattern("/product/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); ApiDefinition api2 = new ApiDefinition("another_customized_api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ add(new ApiPathPredicateItem().setPattern("/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); definitions.add(api1); definitions.add(api2); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } private void initGatewayRules() { Set<GatewayFlowRule> rules = new HashSet<>(); rules.add(new GatewayFlowRule("aliyun_route") .setCount(10) .setIntervalSec(1) ); rules.add(new GatewayFlowRule("aliyun_route") .setCount(2) .setIntervalSec(2) .setBurst(2) .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_CLIENT_IP) ) ); rules.add(new GatewayFlowRule("httpbin_route") .setCount(10) .setIntervalSec(1) .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER) .setMaxQueueingTimeoutMs(600) .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_HEADER) .setFieldName("X-Sentinel-Flag") ) ); rules.add(new GatewayFlowRule("httpbin_route") .setCount(1) .setIntervalSec(1) .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) .setFieldName("pa") ) ); rules.add(new GatewayFlowRule("httpbin_route") .setCount(2) .setIntervalSec(30) .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) .setFieldName("type") .setPattern("warn") .setMatchStrategy(SentinelGatewayConstants.PARAM_MATCH_STRATEGY_CONTAINS) ) ); rules.add(new GatewayFlowRule("some_customized_api") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_CUSTOM_API_NAME) .setCount(5) .setIntervalSec(1) .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) .setFieldName("pn") ) ); GatewayRuleManager.loadRules(rules); } }
9.1 自定义限流异常处理器
package com.wql.config; 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.GatewayFilter; 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 org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @Configuration public class GatewayConfiguration { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; /** * 构造器 * * @param viewResolversProvider * @param serverCodecConfigurer */ public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } /** * 限流异常处理器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } /** * 限流过滤器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } /*--------------------------上面这些是官网的------------------------------*/ @PostConstruct public void doInit() { initGatewayRules(); initBlockHandle(); } /** * 网关限流规则 */ private void initGatewayRules() { HashSet<GatewayFlowRule> rules = new HashSet<>(); /** * resource: 资源名称 可以是网关中的route名称或者用户自定义的API分组名称 * count: 限流阈值 * intervalSec: 统计时间窗口,单位是秒,默认是1s */ rules.add(new GatewayFlowRule("order-service").setCount(3).setIntervalSec(60)); //加载网关限流规则 GatewayRuleManager.loadRules(rules); } /** * 自定义限流异常处理器 */ private void initBlockHandle() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { HashMap<String, String> result = new HashMap<>(); result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value())); result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase()); result.put("route", "order-service"); return ServerResponse .status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
9.2 限流分组
package com.wql.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.GatewayFilter; 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 org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.*; @Configuration public class GatewayConfiguration { private final List<ViewResolver> viewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; /** * 构造器 * * @param viewResolversProvider * @param serverCodecConfigurer */ public GatewayConfiguration(ObjectProvider<List<ViewResolver>> viewResolversProvider, ServerCodecConfigurer serverCodecConfigurer) { this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList); this.serverCodecConfigurer = serverCodecConfigurer; } /** * 限流异常处理器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() { // Register the block exception handler for Spring Cloud Gateway. return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer); } /** * 限流过滤器 * * @return */ @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public GlobalFilter sentinelGatewayFilter() { return new SentinelGatewayFilter(); } /*--------------------------上面这些是官网的------------------------------*/ @PostConstruct public void doInit() { initGatewayRules(); initBlockHandle(); } /** * 网关限流规则 */ private void initGatewayRules() { HashSet<GatewayFlowRule> rules = new HashSet<>(); /** * resource: 资源名称 可以是网关中的route名称或者用户自定义的API分组名称 * count: 限流阈值 * intervalSec: 统计时间窗口,单位是秒,默认是1s */ /*------------限流分组---------------------*/ rules.add(new GatewayFlowRule("product-api").setCount(3).setIntervalSec(60)); rules.add(new GatewayFlowRule("order-api").setCount(6).setIntervalSec(60)); /*---------------------------------------*/ //加载网关限流规则 GatewayRuleManager.loadRules(rules); //加载限流分组 initCustomizedApis(); } private void initCustomizedApis() { Set<ApiDefinition> definitions = new HashSet<>(); //product-api 组 ApiDefinition api1 = new ApiDefinition("product-api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ // 匹配 /product-service/product 以及其他子路径的所有请求 add(new ApiPathPredicateItem().setPattern("/product-service/product/**") .setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); //order-api 组 ApiDefinition api2 = new ApiDefinition("order-api") .setPredicateItems(new HashSet<ApiPredicateItem>() {{ // 只匹配 /order-service/order/index add(new ApiPathPredicateItem().setPattern("/order-service/order/index")); }}); definitions.add(api1); definitions.add(api2); //加载限流分组 GatewayApiDefinitionManager.loadApiDefinitions(definitions); } /** * 自定义限流异常处理器 */ private void initBlockHandle() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { HashMap<String, String> result = new HashMap<>(); result.put("code", String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value())); result.put("message", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase()); result.put("route", "order-service"); return ServerResponse .status(HttpStatus.TOO_MANY_REQUESTS) .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(result)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } }
10 .遇到的问题
- gateway本项目当中,如果断言的路径和controller的路径一致,则失效;
- gateway依赖应把内部web的module去掉
- spring 约定过滤器类名"xxx"+GatewayFilterFactory,其中"xxx"为配置中的name;
11. 调整netty参数
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.reactive.ReactorResourceFactory; import reactor.netty.ReactorNetty; /** * Netty参数配置 * * @author wql * @date 2022/9/14 17:45 */ @Configuration public class NettyConfig { @Bean public ReactorResourceFactory reactorClientResourceFactory() { // 配置线程组 System.setProperty(ReactorNetty.IO_SELECT_COUNT, "1"); // 这里工作线程数为2-4倍都可以 int ioWorkerCount = Math.max(Runtime.getRuntime().availableProcessors() * 3, 4); System.setProperty(ReactorNetty.IO_WORKER_COUNT, String.valueOf(ioWorkerCount)); System.setProperty(ReactorNetty.POOL_LEASING_STRATEGY, "lifo"); return new ReactorResourceFactory(); } }