SpringCloud gateway (史上最全)

疯狂创客圈 Java 分布式聊天室【 亿级流量】实战系列之 -25【 博客园 总入口html

前言

### 前言前端

疯狂创客圈(笔者尼恩建立的高并发研习社群)Springcloud 高并发系列文章,将为你们介绍三个版本的 高并发秒杀:java

1、版本1 :springcloud + zookeeper 秒杀react

2、版本2 :springcloud + redis 分布式锁秒杀程序员

3、版本3 :springcloud + Nginx + Lua 高性能版本秒杀web

以及有关Springcloud 几篇核心、重要的文章面试

1、Springcloud 配置, 史上最全 一文全懂正则表达式

2、Springcloud 中 SpringBoot 配置全集 , 收藏版redis

3、Feign Ribbon Hystrix 三者关系 , 史上最全 深度解析算法

4、SpringCloud gateway 详解 , 史上最全

本文为《SpringCloud gateway 详解》篇,为你们解读若是作到使用SpringCloud gateway

1.1 SpringCloud Gateway 简介

SpringCloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。

SpringCloud Gateway 做为 Spring Cloud 生态系统中的网关,目标是替代 Zuul,在Spring Cloud 2.0以上版本中,没有对新版本的Zuul 2.0以上最新高性能版本进行集成,仍然仍是使用的Zuul 2.0以前的非Reactor模式的老版本。而为了提高网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通讯框架Netty。

Spring Cloud Gateway 的目标,不只提供统一的路由方式,而且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

提早声明:Spring Cloud Gateway 底层使用了高性能的通讯框架Netty

1.2 SpringCloud Gateway 特征

SpringCloud官方,对SpringCloud Gateway 特征介绍以下:

(1)基于 Spring Framework 5,Project Reactor 和 Spring Boot 2.0

(2)集成 Hystrix 断路器

(3)集成 Spring Cloud DiscoveryClient

(4)Predicates 和 Filters 做用于特定路由,易于编写的 Predicates 和 Filters

(5)具有一些网关的高级功能:动态路由、限流、路径重写

从以上的特征来讲,和Zuul的特征差异不大。SpringCloud Gateway和Zuul主要的区别,仍是在底层的通讯框架上。

简单说明一下上文中的三个术语:

1Filter(过滤器)

和Zuul的过滤器在概念上相似,可使用它拦截和修改请求,而且对上游的响应,进行二次处理。过滤器为org.springframework.cloud.gateway.filter.GatewayFilter类的实例。

(2)Route(路由):

网关配置的基本组成模块,和Zuul的路由配置模块相似。一个Route模块由一个 ID,一个目标 URI,一组断言和一组过滤器定义。若是断言为真,则路由匹配,目标URI会被访问。

3Predicate(断言)

这是一个 Java 8 的 Predicate,可使用它来匹配来自 HTTP 请求的任何内容,例如 headers 或参数。断言的输入类型是一个 ServerWebExchange。

1.3 SpringCloud Gateway和架构

Spring在2017年下半年迎来了Webflux,Webflux的出现填补了Spring在响应式编程上的空白,Webflux的响应式编程不只仅是编程风格的改变,并且对于一系列的著名框架,都提供了响应式访问的开发包,好比Netty、Redis等等。

SpringCloud Gateway 使用的Webflux中的reactor-netty响应式编程组件,底层使用了Netty通信框架。
在这里插入图片描述

1.3.1 SpringCloud Zuul的IO模型

Springcloud中所集成的Zuul版本,采用的是Tomcat容器,使用的是传统的Servlet IO处理模型。

你们知道,servlet由servlet container进行生命周期管理。container启动时构造servlet对象并调用servlet init()进行初始化;container关闭时调用servlet destory()销毁servlet;container运行时接受请求,并为每一个请求分配一个线程(通常从线程池中获取空闲线程)而后调用service()。

弊端:servlet是一个简单的网络IO模型,当请求进入servlet container时,servlet container就会为其绑定一个线程,在并发不高的场景下这种模型是适用的,可是一旦并发上升,线程数量就会上涨,而线程资源代价是昂贵的(上线文切换,内存消耗大)严重影响请求的处理时间。在一些简单的业务场景下,不但愿为每一个request分配一个线程,只须要1个或几个线程就能应对极大并发的请求,这种业务场景下servlet模型没有优点。
在这里插入图片描述

因此Springcloud Zuul 是基于servlet之上的一个阻塞式处理模型,即spring实现了处理全部request请求的一个servlet(DispatcherServlet),并由该servlet阻塞式处理处理。因此Springcloud Zuul没法摆脱servlet模型的弊端。虽然Zuul 2.0开始,使用了Netty,而且已经有了大规模Zuul 2.0集群部署的成熟案例,可是,Springcloud官方已经没有集成改版本的计划了。

1.3.2 Webflux模型

Webflux模式替换了旧的Servlet线程模型。用少许的线程处理request和response io操做,这些线程称为Loop线程,而业务交给响应式编程框架处理,响应式编程是很是灵活的,用户能够将业务中阻塞的操做提交到响应式框架的work线程中执行,而不阻塞的操做依然能够在Loop线程中进行处理,大大提升了Loop线程的利用率。官方结构图:

在这里插入图片描述

Webflux虽然能够兼容多个底层的通讯框架,可是通常状况下,底层使用的仍是Netty,毕竟,Netty是目前业界承认的最高性能的通讯框架。而Webflux的Loop线程,正好就是著名的Reactor 模式IO处理模型的Reactor线程,若是使用的是高性能的通讯框架Netty,这就是Netty的EventLoop线程。

关于Reactor线程模型,和Netty通讯框架的知识,是Java程序员的重要、必备的内功,个中的原理,具体请参见尼恩编著的《Netty、Zookeeper、Redis高并发实战》一书,这里不作过多的赘述。

1.3.3 Spring Cloud Gateway的处理流程

客户端向 Spring Cloud Gateway 发出请求。而后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再经过指定的过滤器链来将请求发送到咱们实际的服务执行业务逻辑,而后返回。过滤器之间用虚线分开是由于过滤器可能会在发送代理请求以前(“pre”)或以后(“post”)执行业务逻辑。
在这里插入图片描述

1.4 Spring Cloud Gateway路由配置方式

1.4.1 基础URI一种路由配置方式

若是请求的目标地址,是单个的URI资源路径,配置文件示例以下:

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        -id: url-proxy-1
          uri: https://blog.csdn.net
          predicates:
            -Path=/csdn

各字段含义以下:

id:咱们自定义的路由 ID,保持惟一

uri:目标服务地址

predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其余复杂的逻辑(好比:与,或,非)。

上面这段配置的意思是,配置了一个 id 为 url-proxy-1的URI代理规则,路由的规则为:当访问地址http://localhost:8080/csdn/1.jsp时,会路由到上游地址https://blog.csdn.net/1.jsp。

1.4.2 基于代码的路由配置方式

转发功能一样能够经过代码来实现,咱们能够在启动类 GateWayApplication 中添加方法 customRouteLocator() 来定制转发规则。

package com.springcloud.gateway;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
 
@SpringBootApplication
public class GatewayApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
 
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("path_route", r -> r.path("/csdn")
                        .uri("https://blog.csdn.net"))
                .build();
    }
 
}

咱们在yaml配置文件中注销掉相关路由的配置,重启服务,访问连接:http://localhost:8080/ csdn, 能够看到和上面同样的页面,证实咱们测试成功。

上面两个示例中 uri 都是指向了个人CSDN博客,在实际项目使用中能够将 uri 指向对外提供服务的项目地址,统一对外输出接口。

1.4.3 和注册中心相结合的路由配置方式

在uri的schema协议部分为自定义的lb:类型,表示从微服务注册中心(如Eureka)订阅服务,而且进行服务的路由。

一个典型的示例以下:

server:
  port: 8084
spring:
  cloud:
    gateway:
      routes:
      -id: seckill-provider-route
        uri: lb://seckill-provider
        predicates:
        - Path=/seckill-provider/**

      -id: message-provider-route
        uri: lb://message-provider
        predicates:
        -Path=/message-provider/**

application:
  name: cloud-gateway

eureka:
  instance:
    prefer-ip-address: true
  client:
    service-url:
      defaultZone: http://localhost:8888/eureka/

注册中心相结合的路由配置方式,与单个URI的路由配置,区别其实很小,仅仅在于URI的schema协议不一样。单个URI的地址的schema协议,通常为http或者https协议。

1.5 详解:SpringCloud Gateway 匹配规则

Spring Cloud Gateway 的功能很强大,咱们仅仅经过 Predicates 的设计就能够看出来,前面咱们只是使用了 predicates 进行了简单的条件匹配,其实 Spring Cloud Gataway 帮咱们内置了不少 Predicates 功能。

Spring Cloud Gateway 是经过 Spring WebFlux 的 HandlerMapping 作为底层支持来匹配到转发路由,Spring Cloud Gateway 内置了不少 Predicates 工厂,这些 Predicates 工厂经过不一样的 HTTP 请求参数来匹配,多个 Predicates 工厂能够组合使用。

1.5.1 Predicate 断言条件介绍

Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其余复杂的逻辑(好比:与,或,非)。能够用于接口请求参数校验、判断新老数据是否有变化须要进行更新操做。

在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各类路由匹配规则,有经过 Header、请求参数等不一样的条件来进行做为条件匹配到对应的路由。网上有一张图总结了 Spring Cloud 内置的几种 Predicate 的实现。
在这里插入图片描述
[
说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来咱们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。

1.5.2 经过请求参数匹配

Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值能够是正则表达式。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            -Query=smile

这样配置,只要请求中包含 smile 属性的参数便可匹配路由。

使用 curl 测试,命令行输入:

curl localhost:8080?smile=x&id=2

通过测试发现只要请求汇总带有 smile 参数即会匹配路由,不带 smile 参数则不会匹配。

还能够将 Query 的值以键值对的方式进行配置,这样在请求过来时会对属性值和正则进行匹配,匹配上才会走路由。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            -Query=keep, pu.

这样只要当请求中包含 keep 属性而且参数值是以 pu 开头的长度为三位的字符串才会进行匹配和路由。

使用 curl 测试,命令行输入:

curl localhost:8080?keep=pub

测试能够返回页面代码,将 keep 的属性值改成 pubx 再次访问就会报 404,证实路由须要匹配正则表达式才会进行路由。

1.5.3 经过 Header 属性匹配

Header Route Predicate 和 Cookie Route Predicate 同样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            - Header=X-Request-Id, \d+

使用 curl 测试,命令行输入:

curl http://localhost:8080 -H "X-Request-Id:88"

则返回页面代码证实匹配成功。将参数-H "X-Request-Id:88"改成-H "X-Request-Id:spring"再次执行时返回404证实没有匹配。

Cookie Route Predicate 能够接收两个参数,一个是 Cookie name ,一个是正则表达式,路由规则会经过获取对应的 Cookie name 值和正则表达式去匹配,若是匹配上就会执行路由,若是没有匹配上则不执行。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            - Cookie=sessionId, test

使用 curl 测试,命令行输入:

curl http://localhost:8080 --cookie "sessionId=test"

则会返回页面代码,若是去掉--cookie "sessionId=test",后台汇报 404 错误。

1.5.5 经过 Host 匹配

Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号做为分隔符。它经过参数中的主机地址做为匹配规则。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            - Host=**.baidu.com

使用 curl 测试,命令行输入:

curl http://localhost:8080 -H "Host: www.baidu.com"

curl http://localhost:8080 -H "Host: md.baidu.com"

经测试以上两种 host 都可匹配到 host_route 路由,去掉 host 参数则会报 404 错误。

1.5.6 经过请求方式匹配

能够经过是 POST、GET、PUT、DELETE 等不一样的请求方式来进行路由。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            - Method=GET

使用 curl 测试,命令行输入:

# curl 默认是以 GET 的方式去请求

curl http://localhost:8080

测试返回页面代码,证实匹配到路由,咱们再以 POST 的方式请求测试。

# curl 默认是以 GET 的方式去请求

curl -X POST http://localhost:8080

返回 404 没有找到,证实没有匹配上路由

1.5.7 经过请求路径匹配

Path Route Predicate 接收一个匹配路径的参数来判断是否走路由。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        -id: gateway-service

​          uri: http://ityouknow.com

​          order: 0

​          predicates:

​            -Path=/foo/{segment}

若是请求路径符合要求,则此路由将匹配,例如:/foo/1 或者 /foo/bar。

使用 curl 测试,命令行输入:

curl http://localhost:8080/foo/1

curl http://localhost:8080/foo/xx

curl http://localhost:8080/boo/xx

通过测试第一和第二条命令能够正常获取到页面返回值,最后一个命令报404,证实路由是经过指定路由来匹配。

1.5.8 经过请求 ip 地址进行匹配

Predicate 也支持经过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。

server:

  port: 8080

spring:

  application:

​    name: api-gateway

  cloud:

​    gateway:

​      routes:

​        - id: gateway-service

​          uri: https://www.baidu.com

​          order: 0

​          predicates:

​            - RemoteAddr=192.168.1.1/24

能够将此地址设置为本机的 ip 地址进行测试。

curl localhost:8080

若是请求的远程地址是 192.168.1.10,则此路由将匹配。

1.5.10 组合使用

server:
  port: 8080
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: gateway-service
          uri: https://www.baidu.com
          order: 0
          predicates:
            - Host=**.foo.org
            - Path=/headers
            - Method=GET
            - Header=X-Request-Id, \d+
            - Query=foo, ba.
            - Query=baz
            - Cookie=chocolate, ch.p

各类 Predicates 同时存在于同一个路由时,请求必须同时知足全部的条件才被这个路由匹配。

一个请求知足多个路由的断言条件时,请求只会被首个成功匹配的路由转发

1.6 Springcloud gateway 高级功能

1.6.1 实现熔断降级

为何要实现熔断降级?

在分布式系统中,网关做为流量的入口,所以会有大量的请求进入网关,向其余服务发起调用,其余服务不可避免的会出现调用失败(超时、异常),失败时不能让请求堆积在网关上,须要快速失败并返回给客户端,想要实现这个要求,就必须在网关上作熔断、降级操做。

为何在网关上请求失败须要快速返回给客户端?

由于当一个客户端请求发生故障的时候,这个请求会一直堆积在网关上,固然只有一个这种请求,网关确定没有问题(若是一个请求就能形成整个系统瘫痪,那这个系统能够下架了),可是网关上堆积多了就会给网关乃至整个服务都形成巨大的压力,甚至整个服务宕掉。所以要对一些服务和页面进行有策略的降级,以此缓解服务器资源的的压力,以保证核心业务的正常运行,同时也保持了客户和大部分客户的获得正确的相应,因此须要网关上请求失败须要快速返回给客户端。

server.port: 8082

spring:
  application:
    name: gateway
  redis:
      host: localhost
      port: 6379
      password: 123456
  cloud:
    gateway:
      routes:
        - id: rateLimit_route
          uri: http://localhost:8000
          order: 0
          predicates:
            - Path=/test/**
          filters:
            - StripPrefix=1
            - name: Hystrix
              args:
                name: fallbackCmdA
                fallbackUri: forward:/fallbackA

  hystrix.command.fallbackCmdA.execution.isolation.thread.timeoutInMilliseconds: 5000

这里的配置,使用了两个过滤器:

(1)过滤器StripPrefix,做用是去掉请求路径的最前面n个部分截取掉。

StripPrefix=1就表明截取路径的个数为1,好比前端过来请求/test/good/1/view,匹配成功后,路由到后端的请求路径就会变成http://localhost:8888/good/1/view。

(2)过滤器Hystrix,做用是经过Hystrix进行熔断降级

当上游的请求,进入了Hystrix熔断降级机制时,就会调用fallbackUri配置的降级地址。须要注意的是,还须要单独设置Hystrix的commandKey的超时时间

fallbackUri配置的降级地址的代码以下:

package org.gateway.controller;

import org.gateway.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class FallbackController {

    @GetMapping("/fallbackA")
    public Response fallbackA() {
        Response response = new Response();
        response.setCode("100");
        response.setMessage("服务暂时不可用");
        return response;
    }
}

1.6.2 分布式限流

从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法可以限制请求调用的速率,而令牌桶算法可以在限制调用的平均速率的同时还容许必定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以必定的速率往桶中放令牌。每次请求调用须要先获取令牌,只有拿到令牌,才有机会继续执行,不然选择选择等待可用的令牌、或者直接拒绝。放令牌这个动做是持续不断的进行,若是桶中令牌数达到上限,就丢弃令牌,因此就存在这种状况,桶中一直有大量的可用令牌,这时进来的请求就能够直接拿到令牌执行,好比设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没彻底启动好,等启动完成对外提供服务时,该限流器能够抵挡瞬时的100个请求。因此,只有桶中没有令牌时,请求才会进行等待,最后至关于以必定的速率执行。
在这里插入图片描述

在Spring Cloud Gateway中,有Filter过滤器,所以能够在“pre”类型的Filter中自行实现上述三种过滤器。可是限流做为网关最基本的功能,Spring Cloud Gateway官方就提供了RequestRateLimiterGatewayFilterFactory这个类,适用在Redis内的经过执行Lua脚本实现了令牌桶的方式。具体实现逻辑在RequestRateLimiterGatewayFilterFactory类中,lua脚本在以下图所示的文件夹中:
在这里插入图片描述

首先在工程的pom文件中引入gateway的起步依赖和redis的reactive依赖,代码以下:

配置以下:

server:
  port: 8081
spring:
  cloud:
    gateway:
      routes:
      - id: limit_route
        uri: http://httpbin.org:80/get
        predicates:
        - After=2017-01-20T17:42:47.789-07:00[America/Denver]
        filters:
        - name: RequestRateLimiter
          args:
            key-resolver: '#{@userKeyResolver}'
            redis-rate-limiter.replenishRate: 1
            redis-rate-limiter.burstCapacity: 3
  application:
    name: cloud-gateway
  redis:
    host: localhost
    port: 6379
    database: 0

在上面的配置文件,指定程序的端口为8081,配置了 redis的信息,并配置了RequestRateLimiter的限流过滤器,该过滤器须要配置三个参数:

  • burstCapacity,令牌桶总容量。

  • replenishRate,令牌桶每秒填充平均速率。

  • key-resolver,用于限流的键的解析器的 Bean 对象的名字。它使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

这里根据用户ID限流,请求路径中必须携带userId参数

@Bean

KeyResolver userKeyResolver() {
  return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}

KeyResolver须要实现resolve方法,好比根据userid进行限流,则须要用userid去判断。实现完KeyResolver以后,须要将这个类的Bean注册到Ioc容器中。

若是须要根据IP限流,定义的获取限流Key的bean为:

@Bean
public KeyResolver ipKeyResolver() {
  return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

经过exchange对象能够获取到请求信息,这边用了HostName,若是你想根据用户来作限流的话这边能够获取当前请求的用户ID或者用户名就能够了,好比:

若是须要根据接口的URI进行限流,则须要获取请求地址的uri做为限流key,定义的Bean对象为:

@Bean
KeyResolver apiKeyResolver() {
  return exchange -> Mono.just(exchange.getRequest().getPath().value());
}

经过exchange对象能够获取到请求信息,这边用了HostName,若是你想根据用户来作限流的话这边能够获取当前请求的用户ID或者用户名就能够了,好比:

若是须要根据接口的URI进行限流,则须要获取请求地址的uri做为限流key,定义的Bean对象为:

@Bean
KeyResolver apiKeyResolver() {
  return exchange -> Mono.just(exchange.getRequest().getPath().value());
}

最后,介绍一下疯狂创客圈:疯狂创客圈,一个Java 高并发研习社群博客园 总入口

疯狂创客圈,倾力推出:面试必备 + 面试必备 + 面试必备 的基础原理+实战 书籍 《Netty Zookeeper Redis 高并发实战

img


疯狂创客圈 Java 死磕系列

  • Java (Netty) 聊天程序【 亿级流量】实战 开源项目实战

  • Netty 源码、原理、JAVA NIO 原理
  • Java 面试题 一网打尽
  • 疯狂创客圈 【 博客园 总入口 】