说明
Gateway网关是我们服务的守门神,所有微服务的统一入口。Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
核心功能
- 请求路由
- 集成 Hystrix 断路器
- 权限控制
- 限流
加入网关后的服务结构:
路由:gateway加入后,一切请求都必须先经过gateway,因此gateway就必须根据某种规则,把请求转发到某个微服务,这个过程叫做路由。
权限控制:请求经过路由时,我们可以判断请求者是否有请求资格,如果没有则进行拦截。
限流:当请求流量过高时,在网关中按照下游的微服务能够接受的速度来放行请求,避免服务压力过大。
demo
基于前面对的 feign-demo
工程(SpringCloud_Feign.md
里有步骤)
新建 gateway-service
添加gateway依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>gateway-demo</artifactId>
<groupId>com.it.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写启动类
package com.it.learn;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class);
}
}
编写配置文件
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
编写路由规则
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址
本例中,我们将 /user/**
开头的请求,代理到http://127.0.0.1:8081
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: http://127.0.0.1:8081 # 路由的目标微服务地址
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
启动测试
当我们访问:http://localhost:10010/user/1,符合`/user/**`的规则,因此请求被代理到http://localhost:8081/user/1
面向服务的路由
在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,并且对服务列表进行负载均衡才对!
添加 Eureka 客户端依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>gateway-demo</artifactId>
<groupId>com.it.learn</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>gateway-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
添加 Eureka 配置
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: http://127.0.0.1:8081 # 路由的目标微服务地址
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
修改映射配置
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
启动测试,访问 http://localhost:10010/user/5
局部过滤器
GatewayFilter Factories是Gateway中的局部过滤器工厂,作用于某个特定路由,允许以某种方式修改传入的HTTP请求或返回的HTTP响应。
添加请求头
AddRequestHeader GatewayFilter Factory,可以在请求中添加请求头,配置如下:
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=info, java is best~
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
Hystrix
网关做请求路由转发,如果被调用的请求阻塞,需要通过Hystrix来做线程隔离和熔断,防止出现故障。
定义降级处理规则
可以通过default-filter来配置,会作用于所有的路由规则。
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=info, java is best~
default-filters: # 默认过滤项
- name: Hystrix # 指定过滤工厂名称
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
定义降级的处理函数
定义一个controller,用来编写失败的处理逻辑:
package com.it.learn.web;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
public class FallbackController {
@RequestMapping(value = "/fallbackTest")
public Map<String, String> fallBackController() {
Map<String, String> response = new HashMap<>();
response.put("code", "502");
response.put("msg", "服务超时");
return response;
}
}
路由前缀
我们之前用/user/**
这样的映射路径代表user-service
这个服务。因此请求user-service
服务的一切路径要以/user/**
开头
比如,访问:localhost:10010/user/2
会被代理到:localhost:8081/user/2
现在,我们在user-service
中的com.it.learn.controller
中定义一个新的接口:
package com.it.learn.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("address")
public class AddressController {
@GetMapping("me")
public String myAddress(){
return "上海市浦东新区鹤沙航城750弄汇诚佳苑";
}
}
这个接口的路径是/address/me
,并不是以/user/
开头。当访问:localhost:10010/address/me
时,并不符合映射路径,因此会得到404.
无论是 /user/**
还是/address/**
都是user-service
中的一个controller路径,都不能作为网关到user-service的映射路径。
因此我们需要定义一个额外的映射路径,例如:/user-service
,配置如下:
spring:
cloud:
gateway:
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,user-service:代表服务id
predicates: # 断言
- Path=/user-service/** # 按照路径匹配的规则
那么问题来了:
当我们访问:http://localhost:10010/user-service/user/1时,映射路径`/user-service`指向用户服务,会被代理到:[http://localhost:8081/user-service/user/1](http://localhost:8081/user-service/user/1).
当我们访问:http://localhost:10010/user-service/address/me时,映射路径`/user-service`指向用户服务,会被代理到:[http://localhost:8081/user-service/address/me](http://localhost:8081/user-service/address/me)
而在user-service
中,无论是/user-service/user/1
还是/user-service/address/me
都是错误的,因为多了一个/user-service
。
这个/user-service
是gateway中的映射路径,不应该被代理到微服务,怎办吧?
解决思路很简单,当我们访问http://localhost:10010/user-service/user/1时,网关利用`/user-service`这个映射路径匹配到了用户微服务,请求代理时,只要把`/user-service`这个映射路径去除不就可以了吗。
恰好有一个过滤器:StripPrefixFilterFactory可以满足我们的需求。
我们修改刚才的路由配置:
server:
port: 10010 #服务端口
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
default-filters: # 默认过滤项
- StripPrefix=1 # 去除用作路由的1个前缀
- name: Hystrix # 指定过滤工厂名称
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user-service/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=info, java is best~
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
此时,网关做路由的代理时,就不会把/user-service作为目标请求路径的一部分了。
也就是说,我们访问:http://localhost:10010/user-service/user/1,会代理到:[http://localhost:8081/user/1](http://localhost:8081/user/1)
我们访问:http://localhost:10010/user-service/address/me,会代理到:[http://localhost:8081/address/me](http://localhost:8081/address/me)
试试看:
网关限流
网关除了请求路由、身份验证,还有一个非常重要的作用:请求限流。当系统面对高并发请求时,为了减少对业务处理服务的压力,需要在网关中对请求限流,按照一定的速率放行请求。
常见的限流算法包括:
- 计数器算法
- 漏桶算法
- 令牌桶算法
SpringGateway中采用的是令牌桶算法,令牌桶算法原理:
- 准备一个令牌桶,有固定容量,一般为服务并发上限
- 按照固定速率,生成令牌并存入令牌桶,如果桶中令牌数达到上限,就丢弃令牌。
- 每次请求调用需要先获取令牌,只有拿到令牌,才继续执行,否则选择选择等待或者直接拒绝。
当用户发送请求后,先执行过滤器,在过滤器中为当前用户生成一个令牌桶
令牌同的键为用户的id,令牌同的值为单位时间内访问的有效次数.
下一个单位时间,令牌桶重置
从令牌桶中获取令牌,如果有则访问目标服务,如果没有则等待或拒绝服务.
SpringCloudGateway是采用令牌桶算法,其令牌相关信息记录在redis中,因此我们需要安装redis,并引入Redis相关依赖。
引入redis有关依赖:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
配置过滤条件key:
Gateway会在Redis中记录令牌相关信息,我们可以自己定义令牌桶的规则,例如:
- 给不同的请求URI路径设置不同令牌桶
- 给不同的登录用户设置不同令牌桶
- 给不同的请求IP地址设置不同令牌桶
Redis中的一个Key和Value对就是一个令牌桶。因此Key的生成规则就是桶的定义规则。SpringCloudGateway中key的生成规则定义在KeyResolver
接口中:
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
这个接口中的方法返回值就是给令牌桶生成的key。API说明:
- Mono:是一个单元素容器,用来存放令牌桶的key
- ServerWebExchange:上下文对象,可以理解为ServletContext,可以从中获取request、response、cookie等信息
比如上面的三种令牌桶规则,生成key的方式如下:
给不同的请求URI路径设置不同令牌桶,示例代码:
return Mono.just(exchange.getRequest().getURI().getPath());// 获取请求URI
给不同的登录用户设置不同令牌桶
return exchange.getPrincipal().map(Principal::getName);// 获取用户
给不同的请求IP地址设置不同令牌桶
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());// 获取请求者IP
这里我们选择最后一种,使用IP地址的令牌桶key。
我们在com.it.learn.ratelimit
中定义一个类,配置一个KeyResolver的Bean实例:package com.it.learn.ratelimit;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class IpKeyResolver implements KeyResolver {
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
配置桶参数:
另外,令牌桶的参数需要通过yaml文件来配置,参数有2个:replenishRate
:每秒钟生成令牌的速率,基本上就是每秒钟允许的最大请求数量burstCapacity
:令牌桶的容量,就是令牌桶中存放的最大的令牌的数量
完整配置如下:
server:
port: 10010 #服务端口
# 配置redis数据源,如果访问的是本地的redis服务,且端口号是6379,且没有配置密码,那么就是0配置
redis:
port: 6379 # 默认6379
database: 1 # 默认0
spring:
application:
name: gateway-service #指定服务名
cloud:
gateway:
default-filters: # 默认过滤项
- name: RequestRateLimiter #请求数限流 名字不能随便写
args:
key-resolver: "#{@ipKeyResolver}" # 指定一个key生成器
redis-rate-limiter.replenishRate: 2 # 生成令牌的速率 每秒生成2个令牌
redis-rate-limiter.burstCapacity: 2 # 桶的容量
- StripPrefix=1 # 截去路由的前缀(1个前缀)
- name: Hystrix # 指定过滤工厂名称
args: # 指定过滤的参数
name: fallbackcmd # hystrix的指令名
fallbackUri: forward:/fallbackTest # 失败后的跳转路径
routes:
- id: user-service # 当前路由的唯一标识
uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
predicates: # 断言
- Path=/user-service/** # 按照路径匹配的规则
filters: # 过滤项
- AddRequestHeader=info, java is best~
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
hystrix:
command:
default:
execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长
这里配置了一个过滤器:RequestRateLimiter,并设置了三个参数:
key-resolver
:"#{@ipKeyResolver}"
是SpEL表达式,写法是#{@bean的名称}
,ipKeyResolver就是我们定义的Bean名称redis-rate-limiter.replenishRate
:每秒钟生成令牌的速率redis-rate-limiter.burstCapacity
:令牌桶的容量
这样的限流配置可以达成的效果:
- 每一个IP地址,每秒钟最多发起2次请求
- 每秒钟超过2次请求,则返回429的异常状态码
测试
我们快速在浏览器多次访问http://localhost:10010/user-service/user/3,就会得到一个错误:
429:代表请求次数过多,触发限流了。
demo
gateway-demo