API网关是一个服务器,是系统的唯一入口。 从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。它可能还具有其它职责,如身份验证、监控、负载均衡、缓存、协议转换、限流熔断、静态响应处理。
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。
微服务网关作为微服务后端服务的统一入口,它可以统筹管理后端服务,主要分为数据平面和控制平面:
数据平面主要功能是接入用户的HTTP请求和微服务被拆分后的聚合。使用微服务网关统一对外暴露后端服务的API和契约,路由和过滤功能正是网关的核心能力模块。另外,微服务网关可以实现拦截机制和专注跨横切面的功能,包括协议转换、安全认证、熔断限流、灰度发布、日志管理、流量监控等。
控制平面主要功能是对后端服务做统一的管控和配置管理。例如,可以控制网关的弹性伸缩;可以统一下发配置;可以对网关服务添加标签;可以在微服务网关上通过配置Swagger功能统一将后端服务的API契约暴露给使用方,完成文档服务,提高工作效率和降低沟通成本。
路由功能:路由是微服务网关的核心能力。通过路由功能微服务网关可以将请求转发到目标微服务。在微服务架构中,网关可以结合注册中心的动态服务发现,实现对后端服务的发现,调用方只需要知道网关对外暴露的服务API就可以透明地访问后端微服务。
负载均衡:API网关结合负载均衡技术,利用Eureka或者Consul等服务发现工具,通过轮询、指定权重、IP地址哈希等机制实现下游服务的负载均衡。
统一鉴权:一般而言,无论对内网还是外网的接口都需要做用户身份认证,而用户认证在一些规模较大的系统中都会采用统一的单点登录(Single Sign On)系统,如果每个微服务都要对接单点登录系统,那么显然比较浪费资源且开发效率低。API网关是统一管理安全性的绝佳场所,可以将认证的部分抽取到网关层,微服务系统无须关注认证的逻辑,只关注自身业务即可。
协议转换:API网关的一大作用在于构建异构系统,API网关作为单一入口,通过协议转换整合后台基于REST、AMQP、Dubbo等不同风格和实现技术的微服务,面向Web Mobile、开放平台等特定客户端提供统一服务。
指标监控:网关可以统计后端服务的请求次数,并且可以实时地更新当前的流量健康状态,可以对URL粒度的服务进行延迟统计,也可以使用Hystrix Dashboard查看后端服务的流量状态及是否有熔断发生。
限流熔断:在某些场景下需要控制客户端的访问次数和访问频率,一些高并发系统有时还会有限流的需求。在网关上可以配置一个阈值,当请求数超过阈值时就直接返回错误而不继续访问后台服务。当出现流量洪峰或者后端服务出现延迟或故障时,网关能够主动进行熔断,保护后端服务,并保持前端用户体验良好。
黑白名单:微服务网关可以使用系统黑名单,过滤HTTP请求特征,拦截异常客户端的请求,例如DDoS攻击等侵蚀带宽或资源迫使服务中断等行为,可以在网关层面进行拦截过滤。比较常见的拦截策略是根据IP地址增加黑名单。在存在鉴权管理的路由服务中可以通过设置白名单跳过鉴权管理而直接访问后端服务资源。
灰度发布:微服务网关可以根据HTTP请求中的特殊标记和后端服务列表元数据标识进行流量控制,实现在用户无感知的情况下完成灰度发布。
流量染色:和灰度发布的原理相似,网关可以根据HTTP请求的Host、Head、Agent等标识对请求进行染色,有了网关的流量染色功能,我们可以对服务后续的调用链路进行跟踪,对服务延迟及服务运行状况进行进一步的链路分析。
文档中心:网关结合Swagger,可以将后端的微服务暴露给网关,网关作为统一的入口给接口的使用方提供查看后端服务的API规范,不需要知道每一个后端微服务的Swagger地址,这样网关起到了对后端API聚合的效果。
日志审计:微服务网关可以作为统一的日志记录和收集器,对服务URL粒度的日志请求信息和响应信息进行拦截。
Nginx是一个高性能的HTTP和反向代理服务器。Nginx一方面可以做反向代理,另外一方面可以做静态资源服务器,接口使用Lua动态语言可以完成灵活的定制功能。
Nginx适合做门户网关,是作为整个全局的网关,对外的处于最外层的那种;而Gateway属于业务网关,主要用来对应不同的客户端提供服务,用于聚合业务。各个微服务独立部署,职责单一,对外提供服务的时候需要有一个东西把业务聚合起来。
Gateway可以实现熔断、重试等功能,这是 Nginx不具备的。
Kong是一款基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,由Mashape公司开源的API Gateway项目。Kong是基于NGINX和Apache Cassandra或PostgreSQL构建的,能提供易于使用的RESTful API来操作和配置API管理系统,所以它可以水平扩展多个Kong服务器,通过前置的负载均衡配置把请求均匀地分发到各个Server,来应对大批量的网络请求。
优点:基于Nginx所以在性能和稳定性上都没有问题。Kong作为一款商业软件,在Ngin上做了很扩展工作,而且还有很多付费的商业插件。Kong本身也有付费的企业版,其中包括技术支持、使用培训服务以及API分析插件。
缺点:如果你使用Spring Cloud,Kong如何结合目前已有的服务治理体系?
Traefik 是一个为了让部署微服务更加便捷而诞生的现代HTTP反向代理、负载均衡工具。它支持多种后台 (Docker, Swarm, Kubernetes, Marathon, Mesos, Consul, Etcd, Zookeeper, BoltDB, Rest API, file…) 来自动化、动态的应用它的配置文件设置。
Zuul 是 Netflix 开源的一个API网关组件,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。社区活跃,融合于 SpringCloud 完整生态,是构建微服务体系前置网关服务的最佳选型之一(不过2.0已经闭源)
官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
传统的Web框架,比如说: Struts2,SpringMVC等都是基于Servlet APl与Servlet容器基础之上运行的。但是在Servlet3.1之后有了异步非阻塞的支持。而WebFlux是一个典型非阻塞异步的框架,它的核心是基于Reactor的相关API实现的。相对于传统的web框架来说,它可以运行在诸如Netty,Undertow及支持Servlet3.1的容器上。非阻塞式+函数式编程(Spring 5必须让你使用Java 8)。
Spring WebFlux是Spring 5.0 引入的新的响应式框架,区别于Spring MVC,它不需要依赖Servlet APl,它是完全异步非阻塞的,并且基于Reactor来实现响应式流规范。
基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0
集成断路器
集成 Spring Cloud DiscoveryClient
Predicates 和 Filters 作用于特定路由,易于编写的 Predicates 和 Filters
具备一些网关的高级功能:动态路由、限流、路径重写
路径重写
限流
动态路由
路由(Route)
路由是网关最基础的部分,它由一个 ID,一个目标 URI,一组断言和一组过滤器定义。如果断言为真,则路由匹配。
断言(Predicate)
Java8 中的断言函数。Spring Cloud Gateway 中的断言函数输入类型是 Spring 5.0 框架中 的 ServerWebExchange。Spring Cloud Gateway 中的断言函数允许开发者去定义匹配来自于 Http Request 中的任 何信息,比如请求头和参数等。
过滤器(Filter)
一个标准的 Spring Web Filter。Spring Cloud Gateway 中的 Filter 分为两种类型,分别是 Gateway Filter 和 Global Filter。过滤器将会对请求和响应进行处理。
客户端向Spring Cloud Gateway
发出请求。然后在Gateway Handler Mapping
中找到与请求相匹配的路由,将其发送到GatewayWeb Handler
。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(“pre”)或之后(“post”)执行业务逻辑。Filter在"pre"类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等,在"post"类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。
可以参考:SpringCloud-2020.0.3版本简单入门
这里我注册中心使用了Nacos,需要自行下载并启动,默认端口号是8848,网关使用springcloud gateway。搭建聚合模块demo,一个消费者模块和一个网关模块,请求开源从网关转发到消费者模块。多模块搭建开源参考SpringBoot聚合项目创建、打包与多环境
创建springboot项目,保留pom.xml
文件
<modelVersion>4.0.0modelVersion>
<groupId>com.examplegroupId>
<artifactId>cloud-gatewayartifactId>
<version>0.0.1-SNAPSHOTversion>
<packaging>pompackaging>
<name>cloud-gatewayname>
<description>cloud-gatewaydescription>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
<modules>
<module>consumermodule>
<module>gatewaymodule>
modules>
<dependencies>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.80version>
dependency>
dependencies>
新建springboot项目,引入依赖,这里注意控制网关和nacos版本,很容易因为版本不对而冲突
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>cloud-gatewayartifactId>
<groupId>com.examplegroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<artifactId>consumerartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>consumername>
<description>consumerdescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.3.12.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<version>2.3.12.RELEASEversion>
<scope>testscope>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<version>2.2.7.RELEASEversion>
dependency>
dependencies>
配置application.yml文件
配置application.yml文件
server:
port: 8888
spring:
application:
name: consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
最后编写简单的controller
@RestController
public class HelloController {
@GetMapping("hello")
public String hello(){
return "world";
}
}
新建springboot项目,引入依赖
<modelVersion>4.0.0modelVersion>
<parent>
<artifactId>cloud-gatewayartifactId>
<groupId>com.examplegroupId>
<version>0.0.1-SNAPSHOTversion>
parent>
<artifactId>gatewayartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>gatewayname>
<description>gatewaydescription>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
<version>2.2.3.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
<version>2.2.7.RELEASEversion>
dependency>
dependencies>
配置application.yml文件
server:
port: 8889
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: hello
uri: lb://consumer
predicates:
- Path=/** # 断言:路径相匹配的进行路由
nacos:
discovery:
server-addr: localhost:8848
启动后发现浏览器访问http://localhost:8888/hello
和http://localhost:8889/hello
,都能成功访问,即代表网关搭建成功
Spring Cloud Gateway
是通过 Spring WebFlux
的 HandlerMapping
做为底层支持来匹配到转发路由,Spring Cloud Gateway
内置了很多 Predicates
工厂,这些 Predicates
工厂通过不同的 HTTP 请求参数来匹配,多个 Predicates
工厂可以组合使用。Predicate
来源于 Java 8,是 Java 8 中引入的一个函数,Predicate
接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate
组合成其他复杂的逻辑(比如:与,或,非)。可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: hello
uri: lb://consumer
predicates:
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
# - Before=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
# - Between=2021-02-23T14:20:00.000+08:00[Asia/Shanghai], 2021-02-24T14:20:00.000+08:00[Asia/Shanghai]
#指定Cookie正则匹配指定值
predicates:
- Cookie=cookie,china
#指定Header正则匹配指定值,内容必须是数字
predicates:
- Header=X-Request-Id,\d+
# - Header=X-Request-Id
#请求Host匹配指定值
predicates:
- Host=**.somehost.org,**.anotherhost.org
#请求Method匹配指定请求方式
predicates:
- Method=GET,POST
predicates:
#请求包含某参数
- Query=green
#请求包含某参数并且参数值匹配正则表达式
# - Query=red, gree.
#远程地址匹配
predicates:
- RemoteAddr=192.168.1.1/24
# 断言:路径相匹配的进行路由
predicates:
- Path=/system/**
在开发或者测试的时候,或者线上发布,线上服务多版本控制的时候,需要对服务提供权重路由,最常见的使用就是,一个服务有两个版本,旧版本V1,新版本v2。在线上灰度的时候,需要通过网关动态实时推送,路由权重信息。比如80%的流量走服务v1版本,20%的流量走服务v2版本。
predicates:
- Weight=group1, 2
在yml配置文件说明,访问http://localhost:8889/hello
即可转发
#第一种:ws(websocket)方式: uri: ws://localhost:8888
#第二种:http方式: uri: http://localhost:8888/
#第三种:lb(注册中心中服务名字)方式: uri: lb://consumer
spring:
cloud:
gateway:
routes:
# 路由id,没有固定规则,建议配合服务名
- id: consumer
# 匹配后提供服务的路由地址
# 需要注意的是uri的协议为lb,表示启用Gateway的负载均衡功能。lb://serviceName是spring cloud gateway在微服务中自动为我们创建的负载均衡uri。
uri: lb://consumer
predicates:
# 断言:路径相匹配的进行路由
- Path=/**
注入RouteLocator的Bean
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
// 第一个参数是路由的唯一id
routes.route("consumer",
r -> r.path("/hello")
.uri("http://localhost:8888/hello")).build();
return routes.build();
}
}
可以通过服务名进行转发,无需配置routes也可以转发,访问http://localhost:8889/consumer/hello
即可转发
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
#开启根据微服务名称自动转发
enabled: true
#小写
lower-case-service-id: true
Spring Cloud Gateway根据作用范围划分为GatewayFilter
和GlobalFilter
,二者区别如下
**GatewayFilter **:网关过滤器,需要通过spring.cloud.routes.filters
配置在具体路由下,只作用在当前路由上或通过spring.cloud.default-filters
配置在全局,作用在所有路由上。
**GlobalFilter **:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter
包装成GatewayFilterChain
可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
网关过滤器官网详解
网关过滤器用于拦截并链式处理 Web 请求,可以实现横切与应用无关的需求,比如:安全、访问超时的设置等。修改传入的 HTTP 请求或传出 HTTP 响应。SpringCloud Gateway 包含许多内置的网关过滤器工厂一共有 22 个,包括头部过滤器、 路径类过滤器、Hystrix 过滤器和重写请求 URL 的过滤器, 还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、Body、Status、Session、Redirect、Retry、RateLimiter 和Hystrix。
RewritePath 网关过滤器工厂采用路径正则表达式参数和替换参数,使用Java 正则表达式来灵活地重写请求路径。下面将http://localhost:8889/hello1/hello
重写为http://localhost:8888/hello
routes:
- id: hello
uri: http://localhost:8888
predicates:
- Path=/hello1/**
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
filters:
- RewritePath=/hello1/?(?>.*), /$\{segment}
PrefixPath 网关过滤器工厂为匹配的URI添加指定前缀,即在uri路径前加上我们自己的路径然后请求给下游服务
filters:
- PrefixPath=/mypath
StripPrefix网关过滤器工厂采用一个参数 StripPrefix,该参数表示在将请求发送到下游之前从请求中剥离的路径个数,比如下面http://localhost:8889/hello1/test/hello1/hello
请求将变成http://localhost:8888/hello
routes:
- id: hello
uri: http://localhost:8888
predicates:
- Path=/hello1/**
- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
filters:
- StripPrefix=2
- RewritePath=/hello1/?(?>.*), /$\{segment}
SetPath网关过滤器工厂采用路径模板参数。它提供了一种通过允许模板化路径段来操作请求路径的简单方法,使用了SpringFramework 中的uri模板,允许多个匹配段。下面http://localhost:8889/hello1/hello
请求将变成http://localhost:8888/hello
routes:
- id: hello
uri: http://localhost:8888
predicates:
- Path=/hello1/{segment}
filters:
- SetPath=/{segment}
AddRequestParameter网关过滤器工厂会将指定参数添加至匹配到的下游请求中。
#请求添加red=blue给下游
filters:
- AddRequestParameter=red, blue
SetStatus网关过滤器工厂采用单个状态参数,它必须是有效的Spring HttpStatus。它可以是整数404或枚举NOT_FOUND的字符串表示。
filters:
#任何情况下,响应的HTTP状态都将设置为401
- SetStatus=401
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter
包装成 GatewayFilterChain
可识别的过滤器,它是请求业务以及路由的 URI 转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
官网自定义过滤器
这里有两种方式配置,一种是实现GatewayFilter
接口,一种是继承AbstractGatewayFilterFactory
,分别作用于路由bean类配置和yml配置上
这种自定义网关过滤器需要实现以下两个接口 : GatewayFilter
, Ordered
public class CustomGatewayFilter 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;
}
}
然后在路由bean类上添加filter,最后访问即可通过我们自己实现的过滤器
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
// 第一个参数是路由的唯一id
// http://localhost:9527/guonei => http://news.baidu.com/guonei
routes.route("consumer",
r -> r.path("/hello")
.uri("http://localhost:8888/hello")
.filter(new CustomGatewayFilter()))
.build();
return routes.build();
}
}
这里演示一个黑白名单过滤
首先创建IgnoreWhiteProperties
白名单配置类
//动态刷新类
@RefreshScope
@Configuration
@ConfigurationProperties(prefix = "ignore")
public class IgnoreWhiteProperties
{
/**
* 放行白名单配置,网关不校验此处的白名单
*/
private List<String> whites = new ArrayList<>();
public List<String> getWhites()
{
return whites;
}
public void setWhites(List<String> whites)
{
this.whites = whites;
}
}
配置自定义局部过滤器BlackListUrlFilter
,主要是继承AbstractGatewayFilterFactory
类
@Component
public class BlackListUrlFilter extends AbstractGatewayFilterFactory<BlackListUrlFilter.Config> {
@Autowired
IgnoreWhiteProperties ignoreWhiteProperties;
@Override
public GatewayFilter apply(Config config) {
System.out.println("这是我自定义的局部过滤器");
System.out.println(config);
return (exchange, chain) -> {
String url = exchange.getRequest().getURI().getPath();
// 跳过不需要验证的路径,即白名单
PathMatcher pathMatcher = new AntPathMatcher();
for (String s:ignoreWhiteProperties.getWhites()) {
if (pathMatcher.match(s,url)) {
return chain.filter(exchange);
}
}
if (config.matchBlacklist(url)) {
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return exchange.getResponse().writeWith(
Mono.just(response.bufferFactory().wrap(JSON.toJSONBytes("请求地址不允许访问"))));
}
return chain.filter(exchange);
};
}
public BlackListUrlFilter()
{
super(Config.class);
}
public static class Config {
private List<String> blacklistUrl;
private final List<Pattern> blacklistUrlPattern = new ArrayList<>();
public boolean matchBlacklist(String url) {
return !blacklistUrlPattern.isEmpty() && blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
}
public List<String> getBlacklistUrl() {
return blacklistUrl;
}
public void setBlacklistUrl(List<String> blacklistUrl) {
this.blacklistUrl = blacklistUrl;
this.blacklistUrlPattern.clear();
this.blacklistUrl.forEach(url -> {
// 取消正则的贪婪模式
this.blacklistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
最后配置一下配置文件
# 不校验白名单
ignore:
whites:
- /hello1/*
- /auth/login
- /*/v2/api-docs
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
#开启根据微服务名称自动转发
enabled: true
lower-case-service-id: true
#注意这里的过滤器有先后顺序
routes:
- id: hello
uri: http://localhost:8888
predicates:
- Path=/hello1/{segment}
filters:
# 也可以简写,但必须要带有xxxGatewayFilterFactory,比如RSAGatewayFilterFactory,只需要写RSA
#- RSA
- SetPath=/{segment}
- name: BlackListUrlFilter
args:
blacklistUrl:
# 黑名单
- /user/list
nacos:
discovery:
server-addr: localhost:8881
自定义网关过滤器需要实现以下两个接口: GatewayFilter
, Ordered
,配置完后全局生效
@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤器执行业务
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("custom global filter");
// 继续向下执行
return chain.filter(exchange);
}
/**
* 过滤器执行顺序,数值越小,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
举例网关鉴权,这里可以增加jwt校验,如果通过就继续,否则不让请求通过
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(AuthFilter.class);
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain
chain) {
//String url = exchange.getRequest().getURI().getPath();
//忽略以下url请求
//if(url.indexOf("/login") >= 0){
// return chain.filter(exchange);
// }
String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
logger.info( "token is empty ..." );
ServerHttpResponse response = exchange.getResponse();
// 响应类型
response.getHeaders().add("Content-Type", "application/json; charset=utf-8");
// 响应状态码,HTTP 401 错误代表用户没有访问权限
response.setStatusCode(HttpStatus.UNAUTHORIZED);
// 响应内容
// 可以自定义全局返回类
// String message = JSON.toJSONString(xxx);
String message = "{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(buffer));
// 也可以直接简单点返回,这样就没有返回消息
// exchange.getResponse().setStatusCode( HttpStatus.UNAUTHORIZED );
// return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
配置网关跨域,当然如果使用了 nginx
等配置代理来解决跨域,则可以不需要添加跨域支持
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
exposedHeaders: "Content-Disposition,Content-Type,Cache-Control"
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮:
用户增长过快(好事)
因为某个热点事件(微博热搜)
竞争对象爬虫
恶意的请求
常见的限流算法有:
计数器算法
漏桶(Leaky Bucket)算法
令牌桶(Token Bucket)算法
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器counter,每当一个请求过来的时候,counter就加1,如果counter 的值大于100并且该请求与第一个请求的间隔时间还在1分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于1分钟,重置counter重新计数。
但是计数器算法存在资源浪费问题,并不是最优算法
漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
漏桶算法主要用途在于保护它人(服务),假设入水量很大,而出水量较慢,则会造成网关的资源堆积可能导致网关瘫痪。而目标服务可能是可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务那边的资源浪费。漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终保持不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就会丢弃。
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
Spring Cloud Gateway 内部使用的就是该算法,大概描述如下:
所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
根据限流大小,设置按照一定的速率往桶里添加令牌;
桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到令牌这些请求会瞬时被处理调用目标服务
限流官网文档
Spring Cloud Gateway 官方提供了RequestRateLimiterGatewayFilterFactory
过滤器工厂,使用 Redis 和Lua 脚本实现了令牌桶的方式。 具体实现逻辑在RequestRateLimiterGatewayFilterFactory
类中, Lua 脚本在gateway依赖中
首先需要配置好redis环境,然后在pom.xml中添加相关依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
<version>2.3.12.RELEASEversion>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.11.1version>
dependency>
配置application.yml,添加相关配置
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
#开启根据微服务名称自动转发
enabled: true
lower-case-service-id: true
routes:
- id: hello
uri: http://localhost:8888
predicates:
- Path=/hello1/{segment}
filters:
- SetPath=/{segment}
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
redis:
#连接超时时间
timeout: 10000
# Redis服务器地址
host: localhost
#Redis服务器端口
port: 6379
#Redis服务器密码
password :
#选择哪个库,默认0库
database : 1
lettuce:
pool:
#最大连接数,默认8
max-active: 1024
# 最大连接阻塞等待时间,单位豪秒,默认-1
max-wait: 10000
#最大空闲连接,默认8
max-idle: 200
#最小空闲连接,默认0
min-idle: 5
URI限流
配置限流过滤器和限流过滤器引用的 bean 对象
参数限流
配置限流过滤器和限流过滤器引用的 bean 对象
IP 限流
配置限流过滤器和限流过滤器引用的 bean 对象
三种限流方式不能同时存在,选定一种,并在yml配置文件配置好key-resolver
即可
@Configuration
public class KeyResolverConfig {
/**
* 根据路径限流
*/
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
}
/**
* 根据参数限流
*/
//@Bean
public KeyResolver parameterKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
/**
* 根据 IP 限流
*/
//@Bean
public KeyResolver ipKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
网关限流介绍文档
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。注意:被调用的微服务必须接入sentinel。
Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:
GatewayFlowRule
:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
ApiDefinition
:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api
,请求 path 模式为 /foo/**
和 /baz/**
的都归到 my_api
这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
单独使用添加 sentinel gateway adapter
依赖即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway
依赖来让 spring-cloud-alibaba-sentinel-gateway
模块里的 Spring Cloud Gateway
自动化配置类生效。同时请将 spring.cloud.sentinel.filter.enabled
配置项置为 false(若网关流控控制台上看到了 URL 资源,就是此配置项没有置为 false)。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-spring-cloud-gateway-adapterartifactId>
<version>1.8.4version>
dependency>
配置application.yml
server:
port: 8889
spring:
application:
name: gateway
cloud:
gateway:
routes:
- id: consumer
uri: http://localhost:8888
predicates:
- Path=/hello1/{segment}
filters:
- SetPath=/{segment}
nacos:
discovery:
server-addr: localhost:8881
最后使用时只需注入对应的 SentinelGatewayFilter
实例以及 SentinelGatewayBlockExceptionHandler
实例即可,用户还可以通过 GatewayRuleManager.loadRules(rules)
手动加载网关规则
/*** 限流规则配置类 */
@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() {
//初始化自定义的API
initCustomizedApis();
//初始化网关限流规则(代码中配置流控规则,一般在控制台配置)
initGatewayRules();
}
/**
* 这个api分组一定要配置,否则限流不起效果
*/
private void initCustomizedApis() {
Set<ApiDefinition> definitions = new HashSet<>();
// 设置资源保护名
ApiDefinition api = new ApiDefinition("consumer-service-api")
.setPredicateItems(new HashSet<ApiPredicateItem>() {{
// 设置匹配路径,只有配置了的路径才能出发限流
add(new ApiPathPredicateItem().setPattern("/hello1/**")
.setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
}});
definitions.add(api);
GatewayApiDefinitionManager.loadApiDefinitions(definitions);
}
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
//resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称。
//count:限流阈值
//intervalSec:统计时间窗口,单位是秒,默认是 1 秒,这里是60s内最多只接受2个请求
rules.add(new GatewayFlowRule("consumer-service-api")
.setCount(2)
.setIntervalSec(60)
);
// 加载网关规则
GatewayRuleManager.loadRules(rules);
}
}
启动成功后访问几次页面即可发现Blocked by Sentinel: ParamFlowException
,同时我们还可以在 cmd
下执行这个命令来查看实时的统计信息curl http://localhost:8719/cnode?id=gateway
thread: 代表当前处理该资源的线程数;
pass: 代表一秒内到来到的请求;
blocked: 代表一秒内被流量控制的请求数量;
success: 代表一秒内成功处理完的请求;
total: 代表到一秒内到来的请求以及被阻止的请求总和;
RT: 代表一秒内该资源的平均响应时间;
1m-pass: 则是一分钟内到来的请求;
1m-block: 则是一分钟内被阻止的请求;
1m-all: 则是一分钟内到来的请求和被阻止的请求的总和;
exception: 则是一秒内业务本身异常的总和。
除此之外,无论触发了限流、熔断降级还是系统保护,它们的秒级拦截详情日志都在 ${user_home}/logs/csp/sentinel-block.log
里。如果没有发生拦截,则该日志不会出现。日志格式如下:
新建文件SentinelFallbackHandler
public class SentinelFallbackHandler implements WebExceptionHandler {
private Mono<Void> writeResponse(ServerResponse response, ServerWebExchange exchange) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] datas = "{\"code\":429, \"msg\":\"请求超过最大数,请稍后再试\"}".getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = serverHttpResponse.bufferFactory().wrap(datas);
return serverHttpResponse.writeWith(Mono.just(buffer));
}
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
if (!BlockException.isBlockException(ex)) {
return Mono.error(ex);
}
return handleBlockedRequest(exchange, ex).flatMap(response -> writeResponse(response, exchange));
}
private Mono<ServerResponse> handleBlockedRequest(ServerWebExchange exchange, Throwable throwable) {
return GatewayCallbackManager.getBlockHandler().handleRequest(exchange, throwable);
}
}
最后在GatewayConfiguration
配置好自定义的 SentinelFallbackHandler
注入到 GatewayConfiguration
中,注意要注释掉默认的限流异常处理器,或者放在其上面位置
/**
* 自定义异常处理器
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelFallbackHandler sentinelGatewayExceptionHandler() {
return new SentinelFallbackHandler();
}
直接在GatewayConfiguration
类中定义
@PostConstruct
public void doInit() {
//初始化自定义的API
initCustomizedApis();
//初始化网关限流规则(代码中配置流控规则,一般在控制台配置)
initGatewayRules();
//自定义限流异常处理器
initBlockRequestHandler();
}
private void initBlockRequestHandler() {
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
HashMap<String, String> result = new HashMap<>();
result.put("code",String.valueOf(HttpStatus.TOO_MANY_REQUESTS.value()));
result.put("msg", HttpStatus.TOO_MANY_REQUESTS.getReasonPhrase());
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(result));
}
};
//设置自定义异常处理器
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
Sentinel 1.6.3 引入了网关流控控制台的支持,用户可以直接在 Sentinel 控制台上查看 API Gateway 实时的 route 和自定义 API 分组监控,管理网关规则和 API 分组配置。在 API Gateway 端,用户只需要在原有启动参数的基础上添加如下启动参数即可标记应用为 API Gateway 类型
注:通过 Spring Cloud Alibaba Sentinel 自动接入的 API Gateway 整合则无需此参数(在VM option添加,不过测试发现这个版本需要添加)
-Dcsp.sentinel.app.type=1
流控原理如下
首先需要换依赖,同时启动sentinel的jar包以及nacos
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2021.1version>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
<version>2021.1version>
dependency>
其次修改相应配置文件
spring:
application:
name: gateway
cloud:
sentinel:
transport:
dashboard: localhost:8080
port: 8719
# 如果需要配置Sentinel全局异常处理,可以添加以下配置
scg:
fallback:
mode: response # 重定向(redirect) 或者 响应(response)
# redirect: # mode 为 redirect 时,设置重定向URL
response-status: 200 # 响应状态码
response-body: "{code: 500, msg: '服务器压力山大,请稍后再试!'}" # 响应内容体
最后我们访问sentinel:http://localhost:8080/#/dashboard
,即可在网页设置限流等规则,还可以自定义 API 分组的监控(推荐)
一个请求过来,首先经过 Nginx 的一层负载,到达网关,然后由网关负载到真实后端,若后端有问题,网关会进行重试访问,多次访问后仍返回失败,可以通过熔断或服务降级立即返回结果。而且,由于是负载均衡,网关重试时不一定会访问到出错的后端。
参考文章:
微服务网关选型:5种主流 API 网关
GateWay实战
微服务系列:服务网关 Spring Cloud Gateway 入门