1、API网关引言
Nacos、Ribbon、OpenFeign等组件解决微服务间的服务调用问题,但对于用户端从外侧访问微服务时,如何有效管理微服务,如何暴露服务给用户呢?
将每一个微服务的接口直接暴露给用户(错误做法):
- 所有 API 接口对外直接暴露给用户端是不安全和不可控的,用户可能越权访问不属于它的功能
- 后台服务可能采用不同的通信方式(restful/RPC等),不同的接入方式让用户端接入困难。
- 不同微服务可能有相同的处理逻辑,直接暴露服务给用户很难做到统一的前置处理,如访问前对用户鉴权,就必须将鉴权代码分散到每个服务模块中,随着服务数量增加代码将难以维护。
通过 API 网关为微服务访问提供统一的访问入口
作用:
- API 网关是用户端访问 API 的唯一入口,从用户的角度来说只需关注 API 网关暴露哪些接口,后端服务的处理细节用户不需要知道。从这方面讲,将用户端与微服务的具体实现进行了解耦。
- 针对所有请求进行统一鉴权、熔断、限流、日志等前置处理,让微服务专注自己的业务。
- 统一调用风格,通常 API 网关对外提供 RESTful 风格 URL 接口。用户传入请求后,由 API 网关负责转换为后端服务需要的 RESTful、RPC、WebService 等方式,大幅度简化用户的接入难度。
2、Gateway的使用
2.1、Gateway的首次使用
a. 新建一个Spring Boot项目模块,并引入依赖
注意:gateway-server模块一定是单独的模块,gateway依赖和spring-boot-starter-web依赖有冲突
<!-- 引入gateway starter依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
b. 配置application.yml
# 配置服务端口为8080 server: port: 8080 # 配置gateway spring: cloud: gateway: #配置路由规则 routes: # id是路由的唯一标识名 - id: order-service-route # uri 匹配后的路由地址 uri: http://localhost:18082 #断言,也就是匹配规则 predicates: - Path=/orders/** #配置日志,方便在控制台查看输出效果 logging: level: root: debug
c. 启动order-service,并配置端口号为18082
d.启动gate-server,访问
访问localhost:8080/order-service的URI(controller路径),即可路由到localhost:18082/order-service的URI(controller路径)
2.2、集成Nacos注册中心
由于配置路由地址的方式因路由地址是静态的,不能支持服务实例数目的扩展和伸缩。因此Gateway集成注册中心,从注册中心获取动态的服务地址列表。
a. 启动多个order-service实例,并将其注册到注册中心
spring: cloud: nacos: discovery: server-addr: 192.168.136.137:8848 username: nacos password: nacos namespace: ... group: dev
b. gateway-server模块添加nacos-discovery的起步依赖
<!-- 引入nacos starter依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
c. 配置gateway-server
spring: cloud: nacos: discovery: server-addr: 192.168.136.137:8848 username: nacos password: nacos namespace: ... group: dev #是否注册本服务到nacos,默认true。只订阅发现服务而不需要被别的服务发现可设置为false register-enabled: false gateway: routes: # id是路由的唯一标识名 - id: order-service-route # lb代表负载均衡LoadBalance(使用服务id代替静态路由地址) uri: lb://order-service #断言,也就是匹配规则 predicates: - Path=/orders/**
d.Gateway的负载均衡(默认使用ribbon的轮询策略)
order-service: ribbon: #默认是轮询,这里修改为随机 NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
e.启动gate-server,访问
访问localhost:8080/order-service的URI(controller路径),即可路由到localhost:18082/order-service的URI(controller路径)
2.3 Gateway自动集成Nacos实现路由转发
Gateway中配置路由规则的数量会随着微服务的数量而改变,当数量多时逐一手动配置就显得繁琐,此时可以使用Gateway的自动路由转发功能。
spring: cloud: nacos: discovery: server-addr: 192.168.136.137:8848 username: nacos password: nacos namespace: ... group: dev #是否注册本服务到nacos,默认true。只订阅发现服务而不需要被别的服务发现可设置为false register-enabled: false gateway: #自动路由转发 discovery: locator: enabled: true
这是一个自动项,允许 Gateway 自动实现后端微服务路由转发
注:访问地址由原先的直接访问URI(controller的路径)————>先说明访问的微服务id,再访问URI
#http://网关IP:端口/注册到nacos的微服务id/URI http://localhost:8080/order-service/orders/101
3、Gateway配置详解
Gateway 网关三个关键名词:路由(Route)、谓词(Predicate)、过滤器(Filter)。
- 路由(Route)是指一个完整的网关地址映射与处理过程。一个完整的路由包含两部分配置:谓词与过滤器。
- 谓词(Predicate)决定前端应用发来的请求要被转发到哪个微服务上。
- 过滤器(Filter)决定了转发过程中请求、响应数据被网关如何加工处理。
完整的路由配置格式:
spring: gateway: routes: - id: xxx #路由规则id uri: lb://微服务id #路由转发至哪个微服务 #具体的谓词 predicates: #具体的过滤器 filters:
3.1 Predicate谓词
Spring Cloud Gateway内置了很多的Predicate,不同的Predicate匹配HTTP请求的不同属性(匹配路径、匹配时间、匹配Cookie、匹配头部信息)
常用谓词
- Before/After/Between :在指定时点 前/后/内 路由规则生效。
predicates: #注意日期必须满足ZonedDateTime的形式 # - After=2023-01-01T00:00:00.000+08:00[Asia/Shanghai] # - Before=2024-01-01T00:00:00.000+08:00[Asia/Shanghai] - Between=2023-01-01T00:00:00.000+08:00[Asia/Shanghai],2024-01-01T00:00:00.000+08:00[Asia/Shanghai]
- Header 代表包含指定请求头,且请求头参数值与规则匹配时生效。
predicates: #value部分是正则表达式,\d+表示1个以上的数字 - Header=X-Request-Id,\d+
- Host 代表请求头中Host字段值匹配指定地址时生效。
predicates: - Host=**.baizhi.com,**.baizhiedu.com
- Method 代表要求 HTTP 方法符合规定时生效。
predicates: - Method=GET,POST
- Path 代表 URI 符合映射规则时生效。
predicates: # /order/{segment} 匹配后,可以segment当一个变量后续在Filter中可以获取到 # /orders/** 匹配所有以 /orders/开头的 - Path=/orders/**,/order/{segment}
3.2 Filter过滤器
过滤器(Filter)可以对请求或响应的数据进行额外处理,在Spring Cloud Gateway中内置了很多Filter实现。Filter分为局部过滤GatewayFilter和全局过滤GlobalFilter。
常用的内置GatewayFilter:
- AddRequestParameter 是对所有匹配的请求添加一个查询参数。
filters: #在请求参数中追加foo=bar - AddRequestParameter=foo,bar
- AddResponseHeader 会对所有匹配的请求,在返回结果给客户端之前,在 Header 中添加响应的数据。
#在Response中添加Header头,key=X-Response,Value=Blue。 filters: - AddResponseHeader=X-Response,Blue
- Retry 为重试过滤器,当后端服务不可用时,网关会根据配置参数来发起重试请求。
filters: #涉及过滤器参数时,采用name-args的完整写法 - name: Retry #name是内置的过滤器名 args: #参数部分使用args说明 retries: 3 #重试次数 status: 503 #返回指定错误状态码,才发起重试 methods: GET,DELETE #指定哪些请求方式需要重试 series: CLIENT_ERROR,SERVER_ERROR #和status类似,series表示错误码段 client_error是4xx,server_error是5xx
常用的内置GlobalFilter:
- LoadBalancerClientFilter 配合注册中心实现服务的自动发现和负载均衡。
- NettyRoutingFilter 使用基于Netty实现的HttpClient请求后端服务,完成服务转发。
- GatewayMetricsFilter 启动网关指标,需要添加
spring-boot-starter-actuator
依赖并进行相关配置pom.xml
<!-- 引入actuator starter依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
application.yml
management: endpoint: gateway: enabled: true endpoints: web: exposure: include: "*"
访问gatewaymetric端口
# 如果没有数据,请先访问一次网关的接口 http://localhost:8080/actuator/metrics/gateway.requests
3.3 处理跨域问题
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': #处理所有请求路径 allowedOrigins: "*" allowedHeaders: "*" allowCredentials: true allowedMethods: "*" # 使用过滤器去重响应头 default-filters: - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials,RETAIN_FIRST
3.4 路径重写
Gateway的路由在匹配请求后,默认将匹配到的uri拼接到目标地址后
即 http://网关ip:port/uri 会转发到 http://服务ip:port/uri
但有时前端发起的请求地址和要匹配的地址可能并不一致,比如说前端发起请求时在路径上添加/api前缀,这就需要在网关进行路径重写。
例:将http://localhost:8080/api/orders/1——>http://localhost:8081/orders/1
方式一:spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service/ predicates: #路径需要修改 - Path=/api/orders/** filters: # 使用RewritePath过滤器处理请求地址 - RewritePath=/api/?(?<segment>.*),/$\{segment}
方式二:
spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service/ predicates: #路径需要修改 - Path=/api/orders/** filters: # 使用StripPrefix过滤器,忽略掉第一层前缀进行转发 - StripPrefix=1
方式三:
spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service/ predicates: #路径需要修改 - Path=/api/orders/** filters: #使用StripPrefix过滤器,忽略掉前2层前缀进行转发 /api/orders/1 ==> /1 - StripPrefix=2 #转发前使用PrefixPath再补上orders前缀 /1 ==> /orders/1 - PrefixPath=/orders
3.5 路由的优先级
随着微服务数目的增加,网关中路由的配置也会逐渐增多,有时候会出现一个请求路径多个路由规则都匹配的情况,这个时候就需要保证路由的优先级
routes: - id: ab uri: lb://user-service/ #定义路由优先级,order值越小优先级越高 order: 2 predicates: - Path=/a/b/** filters: - RewritePath=/a/b/?(?<segment>.*),/users/$\{segment} - id: abc uri: lb://order-service/ order: 1 predicates: - Path=/a/b/c/** filters: - RewritePath=/a/b/c/?(?<segment>.*),/orders/$\{segment}
4、自定义Gateway过滤器
a.自定义全局过滤器
- 编码:实现GlobalFilter接口,并@Component配置为bean
@Component public class MyGlobalFilter implements GlobalFilter { //exchange:包含了请求和响应 chain:过滤器链 @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { System.out.println("MyGlobalFilter.filter before..."); long begin=System.currentTimeMillis()); Mono<Void> mono = chain.filter(exchange).doFinally(signalType->{ System.out.println("MyGlobalFilter.filter after..."); long endTime = System.currentTimeMillis(); System.out.println("duration filter time: "+(endTime-startTime)+"ms"); }); return mono; } }
b.自定义网关过滤器
- 编码:继承AbstractGatewayFilterFactory
//注意:类名必须以GatewayFilterFactory结尾 @Component public class MyAddRequestHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<MyAddRequestHeaderGatewayFilterFactory.MyAddRequestHeaderConfig> { //一定要调用父类的有参构造方法,传入配置类的类型 public MyAddRequestHeaderGatewayFilterFactory() { super(MyAddRequestHeaderConfig.class); } @Override //config对应着用户使用自定义网关过滤器提供的配置 public GatewayFilter apply(MyAddRequestHeaderConfig config) { return new GatewayFilter() { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //获取配置中的参数值 String headerName = config.getHeaderName(); String headerValue = config.getHeaderValue(); //修改request和exchange ServerHttpRequest request = exchange.getRequest().mutate().header(headerName, headerValue).build(); ServerWebExchange webExchange = exchange.mutate().request(request).build(); return chain.filter(webExchange); } }; } //用来自定义过滤器配置参数 public static class MyAddRequestHeaderConfig { private String headerName; private String headerValue; public String getHeaderName() { return headerName; } public void setHeaderName(String headerName) { this.headerName = headerName; } public String getHeaderValue() { return headerValue; } public void setHeaderValue(String headerValue) { this.headerValue = headerValue; } } }
- 配置:在指定路由中使用
spring: cloud: gateway: routes: - id: order-service-route uri: lb://order-service/ predicates: - Path=/orders/** filters: # 网关过滤器名就是类名去掉GatewayFilterFactory后缀 - name: MyAddRequestHeader args: headerName: X-Request headerValue: xxx
5、集成Sentinel限流
从Sentinel 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组
5.1、基本使用
- pom.xml引入依赖
<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>
- application.yml增加sentinel配置(连接sentinel的控制台,并设置gateway限流的响应),并启动微服务
spring: gateway: routes: - id: xxx #路由规则id uri: lb://微服务id #路由转发至哪个微服务 #具体的谓词 predicates: #具体的过滤器 filters: cloud: sentinel: transport: dashboard: localhost:9191 scg: fallback: # 模式 response redirect mode: response response-status: 429 content-type: application/json response-body: "对不起,已经被限流了!!!"
- 在Dashboard中配置规则(对指定route_id进行限流配置)
说明:Burst size是指应对突发请求时额外允许的请求数目,我们配置为0。- 测试查看限流效果
5.2、限流规则持久化
- pom.xml新增 nacos-config和 sentinel-datasource-nacos 依赖
<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-datasource-nacos</artifactId> </dependency> <!-- 引入config--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
- bootstrap.yml添加nacos-config的配置(连接nacos配置中心)
spring: application: name: gateway-server profiles: #环境 active: dev cloud: nacos: config: file-extension: yml server-addr: 192.168.136.137:8848 username: nacos password: nacos namespace: ... group: dev cluster-name: HZ
- application.yml中增加 Nacos下载规则
spring: cloud: sentinel: transport: dashboard: localhost:9191 scg: fallback: # 模式 response redirect mode: response response-status: 429 content-type: application/json response-body: "对不起,已经被限流了!!!" datasource: flow: nacos: server-addr: 192.168.136.137:8848 dataId: ${spring.application.name}-gw-flow-rules namespace: ... groupId: dev rule-type: gw-flow #规则类型必须为gw-flow username: nacos password: nacos
- 在 Nacos 配置中心页面,新增 data-id 为gateway-server-gw-flow-rules 的配置项
[ { "resource":"",//资源名称,网关的 route/自定义API 分组名 "resourceMode":0,//规则是针对 API Gateway 的 route还是自定义API分组 "grade":1, //类型 0-线程 1-QPS "count":2, //超过2个QPS限流将被限流 "strategy":0, //限流策略: 0-直接 1-关联 2-链路 "controlBehavior":0, //控制行为: 0-快速失败 1-WarmUp 2-排队等待 "intervalSec":1,//统计时间窗口 "burst":0//应对突发请求额外允许数目 } ]
- 启动微服务,查看Sentinel Dashboard
5.3、限流异常处理
Spring Cloud Gateway,默认的限流处理逻辑是返回默认的流控文本
Blocked by Sentinel
,返回status code
为429 Too Many Requests
。您可以通过以下Spring配置项来配置限流后的处理策略。
spring.cloud.sentinel.scg.fallback.mode
:限流处理策略,目前支持跳转redirect
和自定义返回response
两种策略。spring.cloud.sentinel.scg.fallback.redirect
:限流之后的跳转URL,仅在mode=redirect的时候生效。spring.cloud.sentinel.scg.fallback.response-body
:限流之后的返回内容,仅在mode=response的时候生效。spring.cloud.sentinel.scg.fallback.response-status
:限流之后的返回status code
,仅在mode=response的时候生效。- 除此之外,您也可以在GatewayCallbackManager上通过setBlockHandler注册函数实现自定义的逻辑处理被限流的请求,对应接口为
BlockRequestHandler
。
- 自定义BlockRequestHandler,并配置为bean
@Component public class SentinelBlockRequestHandler implements BlockRequestHandler { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) { ObjectMapper objectMapper = new ObjectMapper(); Map<String, Object> map = new HashMap<>(); map.put("code", 429); map.put("message", "访问人数过多"); String json = ""; try { json = objectMapper.writeValueAsString(map); } catch (JsonProcessingException e) { e.printStackTrace(); } return ServerResponse .status(429) .contentType(MediaType.APPLICATION_JSON) .body(fromValue(json)); } }
- yaml中配置使用自定义的BlockRequestHandler
spring: cloud: sentinel: transport: dashboard: localhost:9191 scg: fallback: # 模式 只要不是response redirect2种,任意的字符串都表示使用自定义Handler mode: custom