1 概述

1.1 为什么需要微服务网关

不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护

image.png
如果让客户端直接与各个微服务通讯,可能会有很多问题

  • 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
  • 在某些场景下存在跨域请求问题
  • 加大身份认证的难度,每个微服务需要独立认证

因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可。

  • 简化开发
  • 易于监控
  • 易于认证
  • 减少了客户端与各个微服务之间的交互次数

image.png

1.2 服务网关概念

1.2.1 什么是微服务网关

API网关是一个服务器,是系统对外的唯一入口。API网关封装了系统内部架构,为每个客户端提供一个定制的API。API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务

1.2.2 应用场景

网关具有的职责,如身份验证、监控、负载均衡、缓存、请求分片与管理、静态响应处理。当然,最主要的职责还是与“外界联系

1.3 常见API网关实现方式

  1. kong
  2. zuul
  3. traefik
  4. Spring Cloud Gateway
  5. Nginx+lua实现

1.4 基于Nginx的网关实现

nginx是一款自由的、开源的、高性能的http服务器和反向代理器。同时也是一个imap、pop3、smtp代理服务器。nginx可以作为一个http服务器进行网站的发布处理,另外nginx可以作为反向代理进行负载均衡的实现

1.4.1 正向/反向代理

正向代理:它代理的是客户端,代客户端发出请求
反向代理:它代理的是服务端,代服务端接收请求

1.4.2 配置nginx的请求转发

  1. location /api-order {
  2. proxy_pass http://127.0.0.1:9001/;
  3. }
  4. location /api-product {
  5. proxy_pass http://127.0.0.1:9002/;
  6. }

2 微服务网关zuul

image.png

2.1 简介

ZUUL是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器,这些过滤器可以完成以下功能:

  • 动态路由:动态将请求路由到不同后端集群
  • 压力测试:逐渐增加指向集群的流量,以了解性能
  • 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
  • 静态响应处理:边缘位置进行响应,避免转发到内部集群
  • 身份认证和安全: 识别每一个资源的验证要求,并拒绝那些不符的请求。Spring Cloud对Zuul进行了整合和增强。

image.png
在Zuul中, 整个请求的过程是这样的,首先将请求给zuulservlet处理,zuulservlet中有一个zuulRunner对象,该对象中初始化了RequestContext:作为存储整个请求的一些数据,并被所有的zuulfilter共享。zuulRunner中还有 FilterProcessor,FilterProcessor作为执行所有的zuulfilter的管理器。FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载。有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器,最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器。执行完这些过滤器,最终将请求的结果返回给客户端。

2.2 搭建zuul网关服务器

依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
  <version>2.1.0.RELEASE</version>
</dependency>

启动类添加注解@EnableZuulProxy

@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
public class ZuulServerApplication {
  public static void main(String[] args) {
      SpringApplication.run(ZuulServerApplication.class, args);
  }
}

配置文件 application.yml

server:
    port: 8080 #服务端口
spring:
    application:
        name: api-gateway #指定服务名

2.3 Zuul中的路由转发

最直观的理解:“路由”是指根据请求URL,将请求分配到对应的处理程序。在微服务体系中,Zuul负责接收所有的请求。根据不同的URL匹配规则,将不同的请求转发到不同的微服务处理。

zuul:
    routes:
        product-service: # 这里是路由id,随意写
      path: /product-service/** # 这里是映射路径,这里将所有请求前缀为 /product-service/的请求,转发到http://127.0.0.1:9002 处理
      url: http://127.0.0.1:9002 # 映射路径对应的实际url地址
      sensitiveHeaders: #默认zuul会屏蔽cookie,cookie不会传到下游服务,这里设置为空则取消默认的黑名单,如果设置了具体的头信息则不会传到下游服务

配置好zuul路由之后启动服务,在浏览器中输入http://localhost:8080/product-service/xxxx即可访问订单微服务

2.3.1 面向服务的路由

Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置

依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类配置注解@EnableDiscoveryClient

@SpringBootApplication
@EnableZuulProxy // 开启Zuul的网关功能
@EnableDiscoveryClient
public class ZuulServerApplication {
  public static void main(String[] args) {
      SpringApplication.run(ZuulServerApplication.class, args);
  }
}

添加eureka配置

eureka:
    client:
        serviceUrl:
            defaultZone: http://127.0.0.1:8761/eureka/
            registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
    instance:
    preferIpAddress: true
    ip-address: 127.0.0.1

修改映射配置

zuul:
  routes:
    product-service: #路由id,随便写
      path: /product-service/** #映射路径  #localhost:8080/product-service/sxxssds
      serviceId: shop-service-product #配置转发的微服务的服务名称

2.3.2 简化的路由设置

zuul:
    routes:
      shop-service-product:/product-service/**

2.3.3 默认的路由规则

默认情况下,一切服务的映射路径就是服务名本身

zuul:
    routes:
      shop-service-product:/shop-serviceproduct/**

2.4 zuul中的过滤器

zuul的核心功能:

  • 对请求的路由
    • 路由功能负责将外部请求转发到具体的为服务实例上,实现实现外部访问统一入口的基础
  • 对请求的过滤
    • 负责对请求的处理过程的干预,是实现请求校验、服务聚合功能的基础

2.4.1 zuulfilter简介

Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景

  1. PRE:此过滤器在请求路由之前调用。可以历次实现身份验证、在集群中选择请求的微服务、记录调试信息等
  2. ROUTING:此过滤器将请求路由到微服务,用于构建反送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务
  3. POST:此过滤器在路由到微服务以后执行,用来为响应添加标准的http header 、收集统计信息和指标、将响应从微服务发送给客户端
  4. ERROR:在其他阶段发生错误是执行该过滤器
package com.example.demo.filter;

import com.netflix.zuul.IZuulFilter;
import com.netflix.zuul.exception.ZuulException;

public abstract class ZuulFilter implements IZuulFilter {

    abstract public String filterType();

    abstract  public  int filterOrder();

    @Override
    public boolean shouldFilter() {
        return false;
    }

    @Override
    public Object run() throws ZuulException {
        return null;
    }
}
  1. shouldFilter:返回一个boolean值,判断该过滤器是否需要执行
  2. run:过滤器的具体业务逻辑
  3. filterType:返回字符串,代表过滤器的类型
    1. pre:请求在被路由之前执行
    2. routing:在路由请求是调用
    3. post:在routing和error过滤器之后调用
    4. error:处理请求时发生错误调用
  4. filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高

2.4.2 生命周期

image.png

  1. 正常流程:请求到达首先会经过pre类型过滤器,而后到达routing类型进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器,而后返回响应
  2. 异常流程:
    1. 整个过程中,pre或者routing过滤器出现异常,都会直接进入error过滤器,等error处理完毕后,会将请求交给post过滤器,最后返回给用户
    2. error出现异常,也会进入post过滤器,而后返回
    3. post过滤器出现异常,会转跳到error,但是与pre和routing不同的时,请求不会到达post过滤器
  3. 不同过滤器的场景:
    1. 请求鉴权:一般放在pre类型,如果没有发现访问权限,直接就拦截了
    2. 异常处理:一般会放在error或post过滤器中结合处理
    3. 服务调用时长统计:pre和post结合使用

2.4.3 自定义过滤器

模拟登录校验
基本逻辑:如果请求中有access-token参数,则认为请求有效,放行

package com.example.demo.filter;

import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

@Component
public class LoginFilter extends ZuulFilter{

    @Override
    public String filterType() {
//        登录验证,"pre"
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
//        设置顺序为1
        return 1;
    }

    @Override
    public boolean shouldFilter() {
//        返回true,代表过滤器生效
        return true;
    }

    //    run为过滤的方法
    @Override
    public Object run() throws ZuulException {

//        登录权限验证的逻辑
//        获取zuul提供的请求上下文
        RequestContext context = RequestContext.getCurrentContext();
//        从上下文中获取request对象
        HttpServletRequest request = context.getRequest();
//        从请求中获取token
        String token = request.getParameter("access-token");
//        判断
        if (token==null||"".equals(token.trim())){
//            没有token,登录校验失败,拦截
            context.setSendZuulResponse(false);
//            返回401状态吗,也可以考虑重定向登录页
            context.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }

        return null;
    }
}

RequestContext:用在过滤器之间传递消息。他的数据保存在每个请求的ThreadLocal中。他用于存储请求路由到哪里、错误、HttpServletRequest、HttpServletResponse都存储在RequestContext中。

2.6 zuul网关存在的问题

  1. 性能问题
    1. zuul1.x版本本质上就是一个同步servlet,采用多线程阻塞模型进行请求转发
    2. 每一个请求,servlet容器要为该线程请求分配一个县城专门负责处理这个请求,直到响应返回客户端这个线程才会被释放返回容器线程池。后台调用耗时,此线程容易堵塞。
  2. 不支持长连接,如websocket

2.7 zuul网管替换方案

  • zuul2.x版本
  • SpringCloud Gateway

3 微服务网关Gateway

3.1 简介

3.1.1 含义

Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。

Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式

3.1.2 核心概念

image.png
路由:路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组filter组成。如果断言为真,则说明请求url和配置的路由匹配
断言:允许开发者去定义匹配来自http request中的任何信息,比如请求头和参数等
过滤器:网关中的过滤器分两种类型,一是gateway filter 和global filter。过滤器可以对请求和响应进行处理

3.2 案例

3.2.1 案例

依赖

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
  <version>3.0.5</version>
</dependency>

注意SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容。引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce。

application.yml

server:
    port:8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: product-service  #自定义路由,保持唯一
          uri: http://127.0.0.1:9002  #目标服务地址
          predicates: #路由条件,predicate接受一个输入参数,返回一个布尔值结果。
            - Path=/product/**

配置一个id为product-service的路由规则,当访问网关请求地址以product开头时,会自动转发到地址ttp://127.0.0.1:9002 。配置完成启动项目即可在浏览器访问进行测试。例如访问:http://localhost:8080/product/1

3.2.2 路由规则

image.png

#路由断言之后匹配
spring:
    cloud:
        gateway:
            routes:
                - id: after_route
                uri: https://xxxx.com
                #路由断言之前匹配
                predicates:
                - After=xxxxx
#路由断言之前匹配
spring:
    cloud:
        gateway:
            routes:
                - id: before_route
                uri: https://xxxxxx.com
                predicates:
                - Before=xxxxxxx
#路由断言之间
spring:
    cloud:
        gateway:
        routes:
            - id: between_route
            uri: https://xxxx.com
            predicates:
            - Between=xxxx,xxxx
#路由断言Cookie匹配,此predicate匹配给定名称(chocolate)和正则表达式(ch.p)
spring:
    cloud:
        gateway:
            routes:
                - id: cookie_route
                uri: https://xxxx.com
                predicates:
                - Cookie=chocolate, ch.p
#路由断言Header匹配,header名称匹配X-Request-Id,且正则表达式匹配\d+
spring:
    cloud:
        gateway:
            routes:
                - id: header_route
                uri: https://xxxx.com
                predicates:
                - Header=X-Request-Id,\d+
#路由断言匹配Host匹配,匹配下面Host主机列表,**代表可变参数
spring:
    cloud:
        gateway:
            routes:
                - id: host_route
                uri: https://xxxx.com
                predicates:
                - Host=**.somehost.org,**.anotherhost.org
#路由断言Method匹配,匹配的是请求的HTTP方法
spring:
    cloud:
        gateway:
            routes:
                - id: method_route
                uri: https://xxxx.com
                predicates:
                - Method=GET
#路由断言匹配,{segment}为可变参数
spring:
    cloud:
        gateway:
            routes:
                - id: host_route
                uri: https://xxxx.com
                predicates:
                - Path=/foo/{segment},/bar/{segment}
#路由断言Query匹配,将请求的参数param(baz)进行匹配,也可以进行regexp正则表达式匹配 (参数包含foo,并且foo的值匹配ba.)
spring:
    cloud:
        gateway:
            routes:
                - id: query_route
                uri: https://xxxx.com
                predicates:
                - Query=baz 或 Query=foo,ba.
#路由断言RemoteAddr匹配,将匹配192.168.1.1~192.168.1.254之间的ip地址,其中24为子网掩码位数即255.255.255.0
spring:
    cloud:
        gateway:
            routes:
                - id: remoteaddr_route
                uri: https://example.org
                predicates:
                - RemoteAddr=192.168.1.1/24

3.2.3 动态路由

动态路由即自动的从注册中心获取服务列表并访问
依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

application.yml

server:
    port: 8080 #服务端口
spring:
  application:
      name: api-gateway #指定服务名
cloud:
  gateway:
    routes:
      - id: product-service
      uri: lb://shop-service-product
      predicates:
      - Path=/product/**
eureka:
  client:
      serviceUrl:
          defaultZone: http://127.0.0.1:8761/eureka/
          registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
  instance:
      preferIpAddress: true
      ip-address: 127.0.0.1

uri:以lb://开头,lb代表从注册中心获取服务,后面接需要转发的服务名称

3.2.4 重写转发路径

server:
    port: 8080 #服务端口
spring:
  application:
      name: api-gateway #指定服务名
cloud:
  gateway:
    routes:
      - id: product-service
      uri: lb://shop-service-product
      predicates:
      - Path=/product/**
      filters:
            - RewritePath=/product-service/(?<segment>.*), /$\{segment}

通过RewritePath配置重写转发的url,将/product-service/(?.*),重写为{segment},然后转发到订单微服务。比如在网页上请求http://localhost:8080/product-service/product,此时会将请求转发到http://127.0.0.1:9002/product/1( 值得注意的是在yml文档中 $ 要写成 $\ )

3.3 过滤器

3.3.1 过滤器基础

生命周期

  • pre:请求被路由之前调用
  • post:在路由到服务器以后执行

过滤器类型

  • GatewayFilter:应用到单个路由或者一个分组的路由上
  • GlobalFilter:应用到所有路由上

3.3.2 局部过滤器

针对单个路由的过滤器。可以对访问的url过滤,进行切面处理。
通过GatewayFilter的形式内置了很对不同类型的局部过滤器

每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory 结尾

3.3.3 全局过滤器

作用于所有路由,gateway定义了global filter接口,用户可以自定义实现自己的global filter
通过全局过滤器可以实现对权限的统一校验,安全性验证等功能

image.png

3.4 统一鉴权

3.4.1 鉴权逻辑

  1. 当客户第一次请求服务时,服务端对用户进行信息认证(登录)
  2. 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
  3. 以后每次请求,客户端都携带认证的token
  4. 服务器对token进行解密,判断是否有效

image.png

验证用户已经登录鉴权的过程可以再网管层统一检验,
检验的便准是请求中是否携带token凭证以及token的正确性

3.4.2 代码实现

校验所有请求参数中是否包括“token”,如果不包含请求参数则不转发路由,否则执行正常的逻辑

package com.example.demo.filter;

import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

public class AuthorizeFilter implements GlobalFilter, Ordered {

    private static Logger logger = LoggerFactory.getLogger(AuthorizeFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String token = exchange.getRequest().getQueryParams().getFirst("token");
        if (StringUtils.isBlank(token)){
            logger.info("token is empty....");
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }

        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }
}
  1. 自定义全局过滤器需要实现 GlobalFilter, Ordered接口
  2. filter方法中完成过滤器的逻辑判断
  3. getOrder方法中指定过滤器的优先级,返回值越大级别越低
  4. ServerWebExchange相当于当前请求和响应上下文,存放着重要请求、响应属性、请求实例和响应实例
  5. chain.filter()继续向下游执行

3.5 网管限流

3.5.1 常见的网管限流算法

计算器

计算机限流算法本质上是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阙值,则之后的请求都被拒绝,直到单位时间过去,再将计数器重置为0

漏桶算法

可以很好的限制容量池的大小,从而防止流量暴增
漏桶可以看作是一个带有常量服务时间的但服务器队列,如果漏桶溢出,那么数据包被丢弃。
漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形

为了更好的控制流量,漏桶算法需要通过两个变量进行控制

  • 桶的大小
  • 水桶漏洞的大小

令牌桶算法

是对漏桶算法的改进
漏桶算法能限制请求调用的速率,令牌算法能够显示调用的平均速率的同时还允许一定程度的突发调用

存在一个桶,以一定的速率往桶中存放一定数量的令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌或直接拒绝

3.5.2 基于filter的限流

gateway提供了基于令牌桶的限流支持
给予内置过滤器工厂RequestRateLimiterGatewayFilterFactory 实现
在过滤器中通过redis和lua脚本结合的方式进行流量控制

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

application.yml

server:
    port: 8080

spring:
    application:
        name: api-gateway #指定服务名
    cloud:
        gateway:
            routes:
                - id: product-service
                uri: lb://shop-service-product
                filters:
                    - RewritePath=/order-service/(?<segment>.*),/$\{segment}
                    - name: RequestRateLimiter
                      args:
                          # 用于限流的键的解析器的bean对象的名字。使用SpEL从spring容器中获取对象
                          key-resolver: '#{@pathKeyResolver}'
                          # 令牌桶每秒填充平均速率
                          redis-rate-limiter.replenishRate: 1
                          # 令牌桶的总容量
                          redis-rate-limiter.burstCapacity: 3

    redis:
        host: localhost
        port: 6379

配置RequestRateLimiter的限流过滤器:

package com.example.demo.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;

@Configuration
public class KeyResolverConfiguration {

//    基于请求路径的限流
    @Bean
    public KeyResolver pathKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getPath().toString());
    }

//    基于请求ip地址的限流
    @Bean
    public KeyResolver ipKeyResolver(){
        return exchange -> Mono.just(exchange.getRequest().getHeaders().getFirst("X-Forwarded-For"));
    }

//    基于用户的限流
    @Bean
    public KeyResolver userKeyResolver(){
        return  exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
    }

}

当达到令牌桶的总容量3时,其他的请求会返回429错误
可以使用redis的monitor监听redis的执行过程。redis会有对应的数据

3.5.3 基于Sentinel的限流

Sentinel支持对gateway、zuul等主流的api网管进行限流

Sentinel提供两种资源维度的限流:

  1. route维度:即在spring配置文件中配置的路由条目,资源名为对应的routeld
  2. 自定义api维度:用户可以利用Sentinel提供的api来自定义一些api分组

Sentinel1.6引入了Sentinel API Gateway Adapter Cammon模块,此模块中包含网关限流的规则和自定义API的实体和管理逻辑

  1. GatewayFlowRule:网管限流规则
  2. ApiDefinition:用户自定义的API定义分组

依赖

<dependency>
  <groupId>com.alibaba.csp</groupId>
  <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
  <version>1.8.2</version>
</dependency>

编写配置类

package com.example.demo.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.GlobalFilter;
import org.springframework.context.annotation.Bean;
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.*;

/**
 * sentinel限流的配置
 */
//@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;
    }

    /**
     * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 配置限流过滤器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     *  用于指定资源的限流规则.
     *      1.资源名称 (路由id)
     *      2.配置统计时间
     *      3.配置限流阈值
     */
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        rules.add(new GatewayFlowRule("product_api") //资源名称
                .setCount(1) //限流阙值
                .setIntervalSec(1)  //统计时间窗口,单位秒,默认1
        );

        GatewayRuleManager.loadRules(rules);
    }



}

application.yml

server:
    port: 8080

spring:
    application:
        name: api-gateway #指定服务名
    cloud:
        gateway:
            routes:
                - id: order-service
                uri: lb://shop-service-product
                predicates:
                - Path=/order-service/**
                filters:
                - RewritePath=/order-service/(?<segment>.*), /$\{segment}
    redis:
        host: localhost
        port: 6379
        database: 0

在一秒内多次访问http://localhost:8080/order-service/order/buy/1就可以看到限流启作用了。
页面显示的是Blocked by Sentinel: FlowException

自定义异常提示

可以再GatewayCallbackManager注册回调
setBlockHandler:注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为blockRequestHandler

package com.example.demo.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.GlobalFilter;
import org.springframework.context.annotation.Bean;
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.*;

/**
 * sentinel限流的配置
 */
//@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;
    }

    /**
     * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 配置限流过滤器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     *  用于指定资源的限流规则.
     *      1.资源名称 (路由id)
     *      2.配置统计时间
     *      3.配置限流阈值
     */
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        rules.add(new GatewayFlowRule("product_api") //资源名称
                .setCount(1) //限流阙值
                .setIntervalSec(1)  //统计时间窗口,单位秒,默认1
        );

        GatewayRuleManager.loadRules(rules);
    }


    /**
     * 自定义限流处理器
     */
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockHandler = new BlockRequestHandler() {
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map map = new HashMap();
                map.put("code",001);
                map.put("message","不好意思,限流啦");
                return ServerResponse.status(HttpStatus.OK)
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(BodyInserters.fromObject(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockHandler);
    }


}

参数限流

package com.example.demo.config;

import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants;
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 org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Bean;
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.*;

/**
 * sentinel限流的配置
 */
//@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;
    }

    /**
     * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 配置限流过滤器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     *  用于指定资源的限流规则.
     *      1.资源名称 (路由id)
     *      2.配置统计时间
     *      3.配置限流阈值
     */
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        rules.add(new GatewayFlowRule("product_api") //资源名称
                .setCount(1) //限流阙值
                .setIntervalSec(1)  //统计时间窗口,单位秒,默认1
                .setParamItem(new GatewayParamFlowItem().setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("id"))
        );

        GatewayRuleManager.loadRules(rules);
    }



}

通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url获取参数。
setFieldName指定参数名称

自定义api分组

package com.example.demo.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.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.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.GlobalFilter;
import org.springframework.context.annotation.Bean;
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.*;

/**
 * sentinel限流的配置
 */
//@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;
    }

    /**
     * 配置限流的异常处理器:SentinelGatewayBlockExceptionHandler
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
        return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
    }

    /**
     * 配置限流过滤器
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public GlobalFilter sentinelGatewayFilter() {
        return new SentinelGatewayFilter();
    }

    /**
     * 配置初始化的限流参数
     *  用于指定资源的限流规则.
     *      1.资源名称 (路由id)
     *      2.配置统计时间
     *      3.配置限流阈值
     */
    @PostConstruct
    public void initGatewayRules() {
        Set<GatewayFlowRule> rules = new HashSet<>();

        rules.add(new GatewayFlowRule("product_api") //资源名称
                .setCount(1) //限流阙值
                .setIntervalSec(1)  //统计时间窗口,单位秒,默认1
                .setParamItem(new GatewayParamFlowItem().setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM).setFieldName("id"))
        );

        GatewayRuleManager.loadRules(rules);
    }


    /**
     * 自定义限流处理器
     */
    @PostConstruct
    public void initBlockHandlers() {
        BlockRequestHandler blockHandler = new BlockRequestHandler() {
            public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
                Map map = new HashMap();
                map.put("code",001);
                map.put("message","不好意思,限流啦");
                return ServerResponse.status(HttpStatus.OK)
                        .contentType(MediaType.APPLICATION_JSON_UTF8)
                        .body(BodyInserters.fromObject(map));
            }
        };
        GatewayCallbackManager.setBlockHandler(blockHandler);
    }

    /**
     * 自定义API限流分组
     *      1.定义分组
     *      2.对小组配置限流规则
     */
    @PostConstruct
    private void initCustomizedApis() {
        Set<ApiDefinition> definitions = new HashSet<>();
        ApiDefinition api1 = new ApiDefinition("product_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/product-service/product/**"). //以/product-service
                            // /product/开都的所有url
                            setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX));
                }});
        ApiDefinition api2 = new ApiDefinition("order_api")
                .setPredicateItems(new HashSet<ApiPredicateItem>() {{
                    add(new ApiPathPredicateItem().setPattern("/order-service/order")); //完全匹配/order-service/order 的url
                }});
        definitions.add(api1);
        definitions.add(api2);
        GatewayApiDefinitionManager.loadApiDefinitions(definitions);
    }


}

3.6 网关高可用

高可用是指通过设计减少系统不能提供服务的时间

不同的客户端使用不同的负载将请求分发到后端的gateway,gateway在已通过http调用后端服务,最后对外输出。

server:
    port: 8080

spring:
    application:
        name: api-gateway #指定服务名
    cloud:
        gateway:
            routes:
                - id: product-service
                uri: lb://shop-service-product
                predicates:
                - Path=/product-service/**
                filters:
                    - RewritePath=/product-service/(?<segment>.*),/$\{segment}
eureka:
  client:
      serviceUrl:
          defaultZone: http://eureka1:8761/eureka/
          registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
  instance:
      preferIpAddress: true
      ip-address: 127.0.0.1
server:
    port: 8081

spring:
    application:
        name: api-gateway #指定服务名
    cloud:
        gateway:
            routes:
                - id: product-service
                uri: lb://shop-service-product
                predicates:
                - Path=/product-service/**
                filters:
                    - RewritePath=/product-service/(?<segment>.*),/$\{segment}
eureka:
  client:
      serviceUrl:
          defaultZone: http://eureka1:8761/eureka/
          registry-fetch-interval-seconds: 5 # 获取服务列表的周期:5s
  instance:
      preferIpAddress: true
      ip-address: 127.0.0.1

nginx

#配置多台服务器(这里只在一台服务器上的不同端口)
upstream gateway {
  server 127.0.0.1:8081;
  server 127.0.0.1:8080;
}
#请求转向mysvr 定义的服务器列表
location / {
  proxy_pass http://gateway;
}

3.7 执行流程分析

image.png

  1. gateway客户端向spring cloud gateway发送请求
  2. 请求首先被HttpWebHandlerAdapter进行提取组装成网关上下文,然后网关上下文会传递到DispatcherHandler
  3. DispatcherHandler是所有请求的分发处理器,主要负责分发请求对应的处理器。比如请求分发到对应的RoutePredicateHandlerMapping(路由断言处理映射器)
  4. RoutePredicateHandlerMapping主要作用于路由查找,以及找到路由后返回对应的FilterWebHandler
  5. FilterWebHandler负责组装filter链并调用filter执行一系列的filter处理,然后把请求转到后端对应的代理服务器处理,处理完毕后由response返回到gateway客户端