SpringCloud系列——SpringCloud Gateway 网关(六)

664 阅读13分钟

什么是服务网关?

API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的企业级应用防火墙
API 网关是一个服务器,是系统对外的唯一入口。
API 网关封装了系统内部架构,为每个客户端提供定制的 API。

image.png

为什么需要服务网关?

统一管理平台

举个例子: 报警电话 110, 打火警电话 119, 急救电话 120, 交通事故电话 122, 高速公路报警电话 12122, 森林防火报警电话12119等等 你突然发现路边变压器坏了, 你想通知有关部分, 但你不知道到底是哪个部门和什么电话号码, 所以你晕了 接着有个叫 Gateway 的人搞了个类似 114 查号平台, 你只需要打电话给 114 , 告知他你有什么需求, 114会跟你的要求转播到相应的平台 而这个 114 平台就类似于 Gateway API 网关, 而你就是客户端, Gateway会根据你的要求做出甄别、回应或者转发给相应的微服务(110 119等)

API网关出现的原因就跟上面的例子略有不同,但总体相似。随着微服务架构的出现,不同的微服务一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:

  • 客户端请求多个微服务,各个服务ip不一样,增加客户端复杂度。
  • 存在跨域,在一定场景下处理相对复杂。
  • 认证复杂,每个服务都需要独立认证。
  • 与微服务耦合太强,微服务变更,客户端需要变更

image.png

当然不管有没有 API 网关,后端微服务都可以通过 API 很好地支持客户端的访问, 如下图:
image.png

但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入 API 网关也有一系列的好处:

  • 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
  • 聚合后台服务,节省流量,提高性能,提升用户体验
  • 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能

隔离外部访问与内部系统

所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能

API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能。通常,网关也是提供REST/HTTP的访问API。

常见的API网关主要提供以下的功能:

  • 路由功能:路由是微服务网关的核心能力。通过路由功能微服务网关可以将请求转发到目标微服务。在微服务架构中,网关可以结合注册中心的动态服务发现,实现对后端服务的发现,调用方只需要知道网关对外暴露的服务API就可以透明地访问后端微服务。
  • 负载均衡:API网关结合负载均衡技术,利用Eureka或者Consul等服务发现工具,通过轮询、指定权重、IP地址哈希等机制实现下游服务的负载均衡。
  • 统一鉴权:一般而言,无论对内网还是外网的接口都需要做用户身份认证,而用户认证在一些规模较大的系统中都会采用统一的单点登录(Single Sign On)系统,如果每个微服务都要对接单点登录系统,那么显然比较浪费资源且开发效率低。API网关是统一管理安全性的绝佳场所,可以将认证的部分抽取到网关层,微服务系统无须关注认证的逻辑,只关注自身业务即可。
  • 协议转换:API网关的一大作用在于构建异构系统,API网关作为单一入口,通过协议转换整合后台基于REST、AMQP、Dubbo等不同风格和实现技术的微服务,面向Web Mobile、开放平台等特定客户端提供统一服务。
  • 指标监控:网关可以统计后端服务的请求次数,并且可以实时地更新当前的流量健康状态,可以对URL粒度的服务进行延迟统计,也可以使用Hystrix Dashboard查看后端服务的流量状态及是否有熔断发生。
  • 限流熔断:在某些场景下需要控制客户端的访问次数和访问频率,一些高并发系统有时还会有限流的需求。在网关上可以配置一个阈值,当请求数超过阈值时就直接返回错误而不继续访问后台服务。当出现流量洪峰或者后端服务出现延迟或故障时,网关能够主动进行熔断,保护后端服务,并保持前端用户体验良好。
  • 黑白名单:微服务网关可以使用系统黑名单,过滤HTTP请求特征,拦截异常客户端的请求,例如DDoS攻击等侵蚀带宽或资源迫使服务中断等行为,可以在网关层面进行拦截过滤。比较常见的拦截策略是根据IP地址增加黑名单。在存在鉴权管理的路由服务中可以通过设置白名单跳过鉴权管理而直接访问后端服务资源。
  • 灰度发布:微服务网关可以根据HTTP请求中的特殊标记和后端服务列表元数据标识进行流量控制,实现在用户无感知的情况下完成灰度发布。
  • 流量染色:和灰度发布的原理相似,网关可以根据HTTP请求的Host、Head、Agent等标识对请求进行染色,有了网关的流量染色功能,我们可以对服务后续的调用链路进行跟踪,对服务延迟及服务运行状况进行进一步的链路分析。
  • 文档中心:网关结合Swagger,可以将后端的微服务暴露给网关,网关作为统一的入口给接口的使用方提供查看后端服务的API规范,不需要知道每一个后端微服务的Swagger地址,这样网关起到了对后端API聚合的效果。
  • 日志审计:微服务网关可以作为统一的日志记录和收集器,对服务URL粒度的日志请求信息和响应信息进行拦截。

image.png

什么是Spring Cloud 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 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。

image.png

上图可以很直观的看出来 Gateway 大体上的设计思路

客户端的请求通过**Gateway的Predicates**进行匹配,如果判断为 **false**则跳到下一个**Route**,如果为**true**则通过一系列的**Filter**链路处理后将请求转发给微服务

另外,我们还能看出Gateway底层使用的是 Netty 进行通讯, 而 Netty 底层使用的是 event loop 模型
我们来解了解面试常问道的 event loop是什么?和 Reactor线程模型是什么?

什么是 event loop 模型?

EventLoop 这个概念其实并不是 Netty 独有的,它是一种事件等待和处理的程序模型,可以解决多线程资源消耗高的问题。例如 Node.js 就采用了 EventLoop 的运行机制,不仅占用资源低,而且能够支撑了大规模的流量访问。
下图展示了 EventLoop 通用的运行模式。每当事件发生时,应用程序都会将产生的事件放入事件队列当中,然后 EventLoop 会轮询从队列中取出事件执行或者将事件分发给相应的事件监听者执行。事件执行的方式通常分为立即执行、延后执行、定期执行几种。

触发事件和事件完成之间是有事件间隔的,所以触发事件时会将事件存入 事件队列 中,然后交给 EventLoop 线程 轮询,等到 Worker 线程执行完毕事件后将告知 Event Loop 将 执行结果+Callback 返回给 主线程,主线程执行 Callback

image.png

什么是Reactor线程模型?

Reactor 模式是基于事件驱动的,它会监听事件的发生,当监听到事件发生后,根据多路复用策略,将事件分发给相应的处理器处理。

核心组件:

  • Handle(Event):用于表示事件。
  • Event Demultiplexer:事件分离器,用于同步等待事件的发生。
  • Reactor:反应器,用于监听和分发事件。内部会调用 Event Demultiplexer 来同步等待事件的发生,然后将事件交由 Event Handler 处理。
  • Event Handler:事件处理器,用于处理事件。

SpringCloud Gateway核心流程

image.png
核心概念:

  1. Gateway Client 向 Spring Cloud Gateway 发送请求
  2. 请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文
  3. 然后网关的上下文会传递到 DispatcherHandler ,它负责将请求分发给 RoutePredicateHandlerMapping
  4. RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用
  5. 如果过断言成功,由FilteringWebHandler 创建过滤器链并调用
  6. 通过特定于请求的 Fliter 链运行请求,Filter 被虚线分隔的原因是Filter可以在发送代理请求之前(pre)和之后(post)运行逻辑
  7. 执行所有pre过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
  8. 处理完毕之后将 Response 返回到 Gateway 客户端

Filter过滤器:

  • Filter在pre类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换等。
  • Filter在post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等

动画.gif

代码实例

github切换到 consul 分支

image.png

image.png

新建 payment8007 项目

新建一个新的项目: cloud-providerconsul-payment8007

<dependencies>
    <!--SpringCloud consul-server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    </dependency>
    <!-- SpringBoot整合Web组件 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!--日常通用jar包配置-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
server:
  port: 8007

spring:
  application:
    name: consul-provider-payment
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}

新建项目gateway9527

创建项目cloud-gateway-gateway9527

<dependencies>
  <!--gateway-->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
  </dependency>
  <!--SpringCloud consul-server -->
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
  </dependency>
  <!-- 注意这里不能导入 web,因为 gataway已经有webflux了 -->
  <!--        <dependency>-->
  <!--            <groupId>org.springframework.boot</groupId>-->
  <!--            <artifactId>spring-boot-starter-web</artifactId>-->
  <!--        </dependency>-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>
  <dependency>
    <groupId>com.zhazha.springcloud</groupId>
    <artifactId>cloud-api-commons</artifactId>
    <version>1.0-SNAPSHOT</version>
  </dependency>
  <!-- 引入自己定义的api通用包,可以使用Payment支付Entity --><!--一般基础配置类-->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

注意需要将 spring-boot-starter-web包注释掉,然后添加 spring-cloud-starter-gateway

server:
  port: 9527

spring:
  application:
    name: cloud-gateway
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        #hostname: 127.0.0.1
        service-name: ${spring.application.name}
    gateway:
      routes:
        #将 payment-8007 8006 提供的服务隐藏起来,不暴露给客户端,只给客户端暴露 API 网关的地址 9527
        - id: provider_payment_consul_routh   #路由 id,没有固定规则,但唯一,建议与服务名对应
          uri: lb://consul-provider-payment
          predicates:
            - Path=/payment/consul/**    #断言,路径匹配 注意:Path 中 P 为大写
            - Method=GET #只能时 GET 请求时,才能访问

这里我们需要注意 uripredicates这两个配置

其中

  • predicates:下面的配置相当于 匹配条件
  • uri:相当于匹配成功后访问的域名
  • id:就是这段 uripredicates 的唯一属性,用于标识唯一性,不能重名

**最终访问的是: **uri + predicates.Path** 相当于 ****https://consul-provider-payment/payment/consul/**

注意,如果你在 uri中配置uri: https://www.baidu.com那么他就会访问到 百度

所以如果你这样配置:

@Configuration
public class GateWayConfig {

    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
        RouteLocatorBuilder.Builder routes = builder.routes();

        routes.route("path_route_zhazha",
                r -> r.path("/guonei")
                        .uri("https://www.baidu.com")).build();

        return routes.build();
    }

    @Bean
    public RouteLocator customRouteLocator2(RouteLocatorBuilder builder) {
        RouteLocatorBuilder.Builder routes = builder.routes();
        routes.route("path_route_zhazha2",
                r -> r.path("/guoji")
                        .uri("https://news.baidu.com")).build();
        return routes.build();
    }

}

中如果你访问[http://localhost:9527/guonei](http://localhost:9527/guonei)会导致:
image.png
如果你访问[https://www.baidu.com/guonei](https://www.baidu.com/guonei)相同的:
image.png

所以需要注意的是**Path**必须是**uri**域名必须存在的路径

Java Bean方式配置路由器

@SpringBootApplication
public class DemogatewayApplication {
	@Bean
	public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
		return builder.routes()
			.route("path_route", r -> r.path("/get")
				.uri("http://httpbin.org"))
			.route("host_route", r -> r.host("*.myhost.org")
				.uri("http://httpbin.org"))
			.route("rewrite_route", r -> r.host("*.rewrite.org")
				.filters(f -> f.rewritePath("/foo/(?<segment>.*)", "/${segment}"))
				.uri("http://httpbin.org"))
			.route("hystrix_route", r -> r.host("*.hystrix.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd")))
				.uri("http://httpbin.org"))
			.route("hystrix_fallback_route", r -> r.host("*.hystrixfallback.org")
				.filters(f -> f.hystrix(c -> c.setName("slowcmd").setFallbackUri("forward:/hystrixfallback")))
				.uri("http://httpbin.org"))
			.route("limit_route", r -> r
				.host("*.limited.org").and().path("/anything/**")
				.filters(f -> f.requestRateLimiter(c -> c.setRateLimiter(redisRateLimiter())))
				.uri("http://httpbin.org"))
			.build();
	}
}

Gateway组件中的配置路由详解

路由参数

routes:
  - id: router1
    uri: http://www.baidu.com
    predicates:
      - Path=/baidu/**
    filters:
      - StripPrefix=1

其中的id参数,是唯一的,如果多个路由的话,id也应该是不同的。

uri:该参数时用来指定匹配后的访问链接,如果匹配成功,那么就去访问百度了。
predicates:断言参数,这个请接着往下看。
filters:过滤器参数。

断言参数

断言参数predicates,是用来匹配路由规则的,比如本次例子中的Path=/baidu/**,意思就是匹配[http://localhost/baidu/*/](http://localhost/baidu/*/)的相关的链接。
断言参数也是Gateway中规则最多的了,下面我们细说一下断言有提供哪些匹配参数。

After

- After=2021-01-01
匹配在2021年一月一日时间之后发生的请求。

After=2019-09-24T16:30:00+08:00[Asia/Shanghai]

Before

- Before=2021-01-01
匹配在2021年一月一日时间之前发生的请求。

Between

- Before=2021-01-01,2021-01-02
匹配在2021年一月一日至2021年一月二日之间发生的请求。

Cookie

- Cookie=username, macro
cookie的设置,有两个参数,分别是name和regexp(Java正则),可以匹配到相应名称的Cookie名称,且与正则相匹配的Cookie值的链接。

username=macro

Header

- Header=X-Request-Id, \d+
Header同样也提供了两个参数,分别是name和regexp(Java正则),可以匹配相应类型的Url,比如127.0.0.1/demo/1,这样就可以进入上述规则。

Host

- Host=**.baidu.com
Host就比较好理解了,其参数就是匹配相应的ip,或者域名等信息的Url。

Method

- Method=GET
Method就更加熟悉了,GET、POST、PUT、DELETE等都是属于Method中的一类,上述就是匹配GET类的请求。

Path

- Path=/baidu/**
Path:我们最常用的,用于匹配URL相关路径。

Query

- Query=username
Query:查询条件,用于匹配查询条件是否存在abc条件。

curl http://localhost:9527/user?username=zhazha

RemoteAddr

- RemoteAddr=192.168.1.1/24

使用curl工具从192.168.1.1发起请求可以匹配该路由。

curl http://localhost:9201/user/1

Weight

利用路由权重来匹配对应的路由规则

spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: http://localhost:8006
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: http://localhost:8007
        predicates:
        - Weight=group1, 2

使用权重来路由相应请求,以下表示有80%的请求会被路由到localhost:8006,20%会被路由到localhost:8007。