说明

Gateway网关是我们服务的守门神,所有微服务的统一入口。Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

核心功能

  • 请求路由
  • 集成 Hystrix 断路器
  • 权限控制
  • 限流

加入网关后的服务结构:
SpringCloud_Gategory - 图1
路由:gateway加入后,一切请求都必须先经过gateway,因此gateway就必须根据某种规则,把请求转发到某个微服务,这个过程叫做路由。
权限控制:请求经过路由时,我们可以判断请求者是否有请求资格,如果没有则进行拦截。
限流:当请求流量过高时,在网关中按照下游的微服务能够接受的速度来放行请求,避免服务压力过大。

demo

基于前面对的 feign-demo 工程(SpringCloud_Feign.md 里有步骤)
新建 gateway-service
SpringCloud_Gategory - 图2
添加gateway依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>gateway-demo</artifactId>
  7. <groupId>com.it.learn</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>gateway-service</artifactId>
  12. <properties>
  13. <maven.compiler.source>8</maven.compiler.source>
  14. <maven.compiler.target>8</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.cloud</groupId>
  19. <artifactId>spring-cloud-starter-gateway</artifactId>
  20. </dependency>
  21. </dependencies>
  22. <build>
  23. <plugins>
  24. <plugin>
  25. <groupId>org.springframework.boot</groupId>
  26. <artifactId>spring-boot-maven-plugin</artifactId>
  27. </plugin>
  28. </plugins>
  29. </build>
  30. </project>

编写启动类

  1. package com.it.learn;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. @SpringBootApplication
  5. public class GatewayApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(GatewayApplication.class);
  8. }
  9. }

编写配置文件

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名

编写路由规则
我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址
本例中,我们将 /user/**开头的请求,代理到http://127.0.0.1:8081

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名
  6. cloud:
  7. gateway:
  8. routes:
  9. - id: user-service # 当前路由的唯一标识
  10. uri: http://127.0.0.1:8081 # 路由的目标微服务地址
  11. predicates: # 断言
  12. - Path=/user/** # 按照路径匹配的规则

启动测试
当我们访问:http://localhost:10010/user/1,符合`/user/**`的规则,因此请求被代理到http://localhost:8081/user/1
SpringCloud_Gategory - 图3

面向服务的路由

在刚才的路由规则中,我们把路径对应的服务地址写死了!如果同一服务有多个实例的话,这样做显然就不合理了。
我们应该根据服务的名称,去Eureka注册中心查找 服务对应的所有实例列表,并且对服务列表进行负载均衡才对!
添加 Eureka 客户端依赖

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  5. <parent>
  6. <artifactId>gateway-demo</artifactId>
  7. <groupId>com.it.learn</groupId>
  8. <version>1.0-SNAPSHOT</version>
  9. </parent>
  10. <modelVersion>4.0.0</modelVersion>
  11. <artifactId>gateway-service</artifactId>
  12. <properties>
  13. <maven.compiler.source>8</maven.compiler.source>
  14. <maven.compiler.target>8</maven.compiler.target>
  15. </properties>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.cloud</groupId>
  19. <artifactId>spring-cloud-starter-gateway</artifactId>
  20. </dependency>
  21. <dependency>
  22. <groupId>org.springframework.cloud</groupId>
  23. <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
  24. </dependency>
  25. </dependencies>
  26. <build>
  27. <plugins>
  28. <plugin>
  29. <groupId>org.springframework.boot</groupId>
  30. <artifactId>spring-boot-maven-plugin</artifactId>
  31. </plugin>
  32. </plugins>
  33. </build>
  34. </project>

添加 Eureka 配置

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名
  6. cloud:
  7. gateway:
  8. routes:
  9. - id: user-service # 当前路由的唯一标识
  10. uri: http://127.0.0.1:8081 # 路由的目标微服务地址
  11. predicates: # 断言
  12. - Path=/user/** # 按照路径匹配的规则
  13. eureka:
  14. client:
  15. service-url:
  16. defaultZone: http://127.0.0.1:10086/eureka

修改映射配置
因为已经有了Eureka客户端,我们可以从Eureka获取服务的地址信息,因此映射时无需指定IP地址,而是通过服务名称来访问,而且Zuul已经集成了Ribbon的负载均衡功能。
启动测试,访问 http://localhost:10010/user/5
SpringCloud_Gategory - 图4

局部过滤器

GatewayFilter Factories是Gateway中的局部过滤器工厂,作用于某个特定路由,允许以某种方式修改传入的HTTP请求或返回的HTTP响应。

添加请求头

AddRequestHeader GatewayFilter Factory,可以在请求中添加请求头,配置如下:

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名
  6. cloud:
  7. gateway:
  8. routes:
  9. - id: user-service # 当前路由的唯一标识
  10. uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
  11. predicates: # 断言
  12. - Path=/user/** # 按照路径匹配的规则
  13. filters: # 过滤项
  14. - AddRequestHeader=info, java is best~
  15. eureka:
  16. client:
  17. service-url:
  18. defaultZone: http://127.0.0.1:10086/eureka

重启后,再user-service的内部断点,查看请求头:
SpringCloud_Gategory - 图5

Hystrix

网关做请求路由转发,如果被调用的请求阻塞,需要通过Hystrix来做线程隔离和熔断,防止出现故障。
定义降级处理规则
可以通过default-filter来配置,会作用于所有的路由规则。

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名
  6. cloud:
  7. gateway:
  8. routes:
  9. - id: user-service # 当前路由的唯一标识
  10. uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
  11. predicates: # 断言
  12. - Path=/user/** # 按照路径匹配的规则
  13. filters: # 过滤项
  14. - AddRequestHeader=info, java is best~
  15. default-filters: # 默认过滤项
  16. - name: Hystrix # 指定过滤工厂名称
  17. args: # 指定过滤的参数
  18. name: fallbackcmd # hystrix的指令名
  19. fallbackUri: forward:/fallbackTest # 失败后的跳转路径
  20. eureka:
  21. client:
  22. service-url:
  23. defaultZone: http://127.0.0.1:10086/eureka
  24. hystrix:
  25. command:
  26. default:
  27. execution.isolation.thread.timeoutInMilliseconds: 1000 # 失败超时时长

定义降级的处理函数
定义一个controller,用来编写失败的处理逻辑:

  1. package com.it.learn.web;
  2. import org.springframework.web.bind.annotation.RequestMapping;
  3. import org.springframework.web.bind.annotation.RestController;
  4. import java.util.HashMap;
  5. import java.util.Map;
  6. @RestController
  7. public class FallbackController {
  8. @RequestMapping(value = "/fallbackTest")
  9. public Map<String, String> fallBackController() {
  10. Map<String, String> response = new HashMap<>();
  11. response.put("code", "502");
  12. response.put("msg", "服务超时");
  13. return response;
  14. }
  15. }

启动测试,故意在要访问的服务打断点,导致超时降级
SpringCloud_Gategory - 图6

路由前缀

我们之前用/user/**这样的映射路径代表user-service这个服务。因此请求user-service服务的一切路径要以/user/**开头
比如,访问:localhost:10010/user/2会被代理到:localhost:8081/user/2
现在,我们在user-service中的com.it.learn.controller中定义一个新的接口:

  1. package com.it.learn.controller;
  2. import org.springframework.web.bind.annotation.GetMapping;
  3. import org.springframework.web.bind.annotation.RequestMapping;
  4. import org.springframework.web.bind.annotation.RestController;
  5. @RestController
  6. @RequestMapping("address")
  7. public class AddressController {
  8. @GetMapping("me")
  9. public String myAddress(){
  10. return "上海市浦东新区鹤沙航城750弄汇诚佳苑";
  11. }
  12. }

这个接口的路径是/address/me,并不是以/user/开头。当访问:localhost:10010/address/me时,并不符合映射路径,因此会得到404.
无论是 /user/**还是/address/**都是user-service中的一个controller路径,都不能作为网关到user-service的映射路径。
因此我们需要定义一个额外的映射路径,例如:/user-service,配置如下:

  1. spring:
  2. cloud:
  3. gateway:
  4. routes:
  5. - id: user-service # 当前路由的唯一标识
  6. uri: lb://user-service # 路由的目标微服务,user-service:代表服务id
  7. predicates: # 断言
  8. - 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可以满足我们的需求。
我们修改刚才的路由配置:

  1. server:
  2. port: 10010 #服务端口
  3. spring:
  4. application:
  5. name: gateway-service #指定服务名
  6. cloud:
  7. gateway:
  8. default-filters: # 默认过滤项
  9. - StripPrefix=1 # 去除用作路由的1个前缀
  10. - name: Hystrix # 指定过滤工厂名称
  11. args: # 指定过滤的参数
  12. name: fallbackcmd # hystrix的指令名
  13. fallbackUri: forward:/fallbackTest # 失败后的跳转路径
  14. routes:
  15. - id: user-service # 当前路由的唯一标识
  16. uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
  17. predicates: # 断言
  18. - Path=/user-service/** # 按照路径匹配的规则
  19. filters: # 过滤项
  20. - AddRequestHeader=info, java is best~
  21. eureka:
  22. client:
  23. service-url:
  24. defaultZone: http://127.0.0.1:10086/eureka
  25. hystrix:
  26. command:
  27. default:
  28. 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)
试试看:
SpringCloud_Gategory - 图7
SpringCloud_Gategory - 图8

网关限流

网关除了请求路由、身份验证,还有一个非常重要的作用:请求限流。当系统面对高并发请求时,为了减少对业务处理服务的压力,需要在网关中对请求限流,按照一定的速率放行请求。
SpringCloud_Gategory - 图9
常见的限流算法包括:

  • 计数器算法
  • 漏桶算法
  • 令牌桶算法

SpringGateway中采用的是令牌桶算法,令牌桶算法原理:

  • 准备一个令牌桶,有固定容量,一般为服务并发上限
  • 按照固定速率,生成令牌并存入令牌桶,如果桶中令牌数达到上限,就丢弃令牌。
  • 每次请求调用需要先获取令牌,只有拿到令牌,才继续执行,否则选择选择等待或者直接拒绝。

SpringCloud_Gategory - 图10
SpringCloud_Gategory - 图11
当用户发送请求后,先执行过滤器,在过滤器中为当前用户生成一个令牌桶
令牌同的键为用户的id,令牌同的值为单位时间内访问的有效次数.
下一个单位时间,令牌桶重置
从令牌桶中获取令牌,如果有则访问目标服务,如果没有则等待或拒绝服务.
SpringCloudGateway是采用令牌桶算法,其令牌相关信息记录在redis中,因此我们需要安装redis,并引入Redis相关依赖。
引入redis有关依赖:

  1. <!--redis-->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
  5. </dependency>

配置过滤条件key:
Gateway会在Redis中记录令牌相关信息,我们可以自己定义令牌桶的规则,例如:

  • 给不同的请求URI路径设置不同令牌桶
  • 给不同的登录用户设置不同令牌桶
  • 给不同的请求IP地址设置不同令牌桶

Redis中的一个Key和Value对就是一个令牌桶。因此Key的生成规则就是桶的定义规则。SpringCloudGateway中key的生成规则定义在KeyResolver接口中:

  1. public interface KeyResolver {
  2. Mono<String> resolve(ServerWebExchange exchange);
  3. }

这个接口中的方法返回值就是给令牌桶生成的key。API说明:

  • Mono:是一个单元素容器,用来存放令牌桶的key
  • ServerWebExchange:上下文对象,可以理解为ServletContext,可以从中获取request、response、cookie等信息

比如上面的三种令牌桶规则,生成key的方式如下:

  • 给不同的请求URI路径设置不同令牌桶,示例代码:

    1. return Mono.just(exchange.getRequest().getURI().getPath());// 获取请求URI
  • 给不同的登录用户设置不同令牌桶

    1. return exchange.getPrincipal().map(Principal::getName);// 获取用户
  • 给不同的请求IP地址设置不同令牌桶

    1. return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());// 获取请求者IP

    这里我们选择最后一种,使用IP地址的令牌桶key。
    我们在com.it.learn.ratelimit中定义一个类,配置一个KeyResolver的Bean实例:

    1. package com.it.learn.ratelimit;
    2. import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
    3. import org.springframework.stereotype.Component;
    4. import org.springframework.web.server.ServerWebExchange;
    5. import reactor.core.publisher.Mono;
    6. @Component
    7. public class IpKeyResolver implements KeyResolver {
    8. @Override
    9. public Mono<String> resolve(ServerWebExchange exchange) {
    10. return Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
    11. }
    12. }

    配置桶参数:
    另外,令牌桶的参数需要通过yaml文件来配置,参数有2个

  • replenishRate:每秒钟生成令牌的速率,基本上就是每秒钟允许的最大请求数量

  • burstCapacity:令牌桶的容量,就是令牌桶中存放的最大的令牌的数量

完整配置如下:

  1. server:
  2. port: 10010 #服务端口
  3. # 配置redis数据源,如果访问的是本地的redis服务,且端口号是6379,且没有配置密码,那么就是0配置
  4. redis:
  5. port: 6379 # 默认6379
  6. database: 1 # 默认0
  7. spring:
  8. application:
  9. name: gateway-service #指定服务名
  10. cloud:
  11. gateway:
  12. default-filters: # 默认过滤项
  13. - name: RequestRateLimiter #请求数限流 名字不能随便写
  14. args:
  15. key-resolver: "#{@ipKeyResolver}" # 指定一个key生成器
  16. redis-rate-limiter.replenishRate: 2 # 生成令牌的速率 每秒生成2个令牌
  17. redis-rate-limiter.burstCapacity: 2 # 桶的容量
  18. - StripPrefix=1 # 截去路由的前缀(1个前缀)
  19. - name: Hystrix # 指定过滤工厂名称
  20. args: # 指定过滤的参数
  21. name: fallbackcmd # hystrix的指令名
  22. fallbackUri: forward:/fallbackTest # 失败后的跳转路径
  23. routes:
  24. - id: user-service # 当前路由的唯一标识
  25. uri: lb://user-service # 路由的目标微服务,lb:代表负载均衡,user-service:代表服务id
  26. predicates: # 断言
  27. - Path=/user-service/** # 按照路径匹配的规则
  28. filters: # 过滤项
  29. - AddRequestHeader=info, java is best~
  30. eureka:
  31. client:
  32. service-url:
  33. defaultZone: http://127.0.0.1:10086/eureka
  34. hystrix:
  35. command:
  36. default:
  37. 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:代表请求次数过多,触发限流了。
SpringCloud_Gategory - 图12
demo
gateway-demo