介绍

        在单体应用程序架构下,客户端(Web或移动端)通过向服务端发起一次网络调用来获取数据。负载均衡器将请求路由给N个相同的应用程序实例中的一个。然后应用程序会查询各种数据库表处理业务逻辑,并将响应返回给客户端。

        微服务架构下,单体应用被切割成多个微服务,如果将所有的微服务直接对外暴露,势必会出现安全方面的各种问题。 客户端可以直接向每个微服务发送请求,其问题主要包括: 

  • 客户端需求和每个微服务暴露的细粒度API不匹配。 
  • 部分服务使用的协议不是Web友好协议。可能使用Thrift二进制RPC,也可能使用AMQP消息传递协议。 
  • 微服务难以重构。如果合并两个服务,或者将一个服务拆分成两个或更多服务,这类重构非常困难。 

        针对如上问题,一个常用的解决方案是使用API网关。API网关自身也是一个服务,并且是后端服务的唯一入口。从面向对象设计的角度看,它与外观模式类似。API网关封装了系统内部架构,为每个客户端提供一个定制的API。除此之外,它还可以负责身份验证、监控、负载均衡、限流、降级与应用检测等功能。

Spring Cloud——服务网关_Spring Cloud

Spring Cloud Zuul

简介

        Zuul 作为微服务系统的网关组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。

        Zuul 作为网关组件,在微服务架构中有着非常重要的作用,主要体现在以下 6 个方面。 

  • Zuul、Ribbon 以及 Eureka 相结合,可以实现智能路由和负载均衡的功能,Zuul 能够将请求流量按某种策略分发到集群状态的多个服务实例。 
  • 网关将所有服务的 API 接口统一聚合,并统一对外暴露。外界系统调用 API 接口时, 都是由网关对外暴露的 API 接口,外界系统不需要知道微服务系统中各服务相互调 用的复杂性。微服务系统也保护了其内部微服务单元的 API 接口,防止其被外界直接调用,导致服务的敏感信息对外暴露。 
  •  网关服务可以做用户身份认证和权限认证,防止非法请求操作 API 接口,对服务器起到保护作用。 
  • 网关可以实现监控功能,实时日志输出,对请求进行记录。 
  • 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级。 
  • API 接口从内部服务分离出来,方便做测试。

Zuul 的工作原理

        Zuul 是通过 Servlet 来实现的,Zuul 通过自定义的 ZuulServlet(类似于 Spring MVC 的DispatcServlet)来对请求进行控制。Zuul 的核心是一系列过滤器,可以在 Http 请求的发起和响应返回期间执行一系列的过滤器。Zuul 包括以下 4 种过滤器。 

  • PRE 过滤器:它是在请求路由到具体的服务之前执行的,这种类型的过滤器可以做 安全验证,例如身份验证、参数验证等。 
  • ROUTING 过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用 Http Client 进行网络请求。 
  • POST 过滤器:它是在请求已被路由到微服务后执行的。一般情况下,用作收集统计信息、指标,以及将响应传输到客户端。
  • ERROR 过滤器:它是在其他过滤器发生错误时执行的。

         Zuul 采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接相互通信,而是通 过 RequestContext 对象来共享数据,每个请求都会创建一个 RequestContext 对象。Zuul 过滤器具有以下关键特性。 

  • Type(类型):Zuul 过滤器的类型,这个类型决定了过滤器在请求的哪个阶段起作用, 例如 Pre、Post 阶段等。 
  • Execution Order(执行顺序):规定了过滤器的执行顺序,Order 的值越小,越先执行。 
  • Criteria(标准):Filter 执行所需的条件。 
  • Action(行动):如果符合执行条件,则执行 Action(即逻辑代码)。

Zuul 请求的生命周期

      当一个客户端 Request 请求进入 Zuul 网关服务时,网关先进入“pre filter”,进行一系列 的验证、操作或者判断。然后交给“routing filter”进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由“post filter”进行处理,该类型的处理器处理完之后,将 Response 信息返回给客户端。

Spring Cloud——服务网关_Spring Cloud_02

       ZuulServlet 是 Zuul 的核心 Servlet。ZuulServlet 的作用是初始化 ZuulFilter,并编排这些 ZuulFilter 的执行顺序。该类中有一个 service()方法,执行了过滤器执行的逻辑。

//ZuulServlet类中的service方法
public void service() throws ServletException, IOException {
	try {
		try {
			preRoute();
		} catch (ZuulException e) {
			error(e);
			postRoute();
			return;
		}
		try {
			route();
		} catch (ZuulException e) {
			error(e);
			postRoute();
			return;
		}
		try {
			postRoute();
		} catch (ZuulException e) {
			error(e);
			return;
		}
	} catch (Throwable e) {
		error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
	} finally {
		RequestContext.getCurrentContext().unset();
	}
}复制代码

       首先执行 preRoute()方法,这个方法执行的是 PRE 类型的过滤器的逻辑。如果执行这个方法时出错了,那么会执行 error(e)和 postRoute()。然后执行 route()方法, 该方法是执行 ROUTING 类型过滤器的逻辑。最后执行 postRoute(),该方法执行了 POST 类型过滤器的逻辑。

Zuul 的常见使用方式

       Zuul 是采用了类似于 Spring MVC 的 DispatchServlet 来实现的,采用的是异步阻塞模型, 所以性能比 Ngnix 差。由于 Zuul 和其他 Netflix 组件可以相互配合、无缝集成,Zuul 很容易 就能实现负载均衡、智能路由和熔断器等功能。在大多数情况下,Zuul 都是以集群的形式存 在的。由于 Zuul 的横向扩展能力非常好,所以当负载过高时,可以通过添加实例来解决性 能瓶颈。

       一种常见的使用方式是对不同的渠道使用不同的 Zuul 来进行路由,例如移动端共用一个 Zuul 网关实例,Web 端用另一个 Zuul 网关实例,其他的客户端用另外一个 Zuul 实例进行路由。 这种不同的渠道用不同 Zuul 实例的架构。

Spring Cloud——服务网关_服务网关_03
另外一种常见的集群是通过 Ngnix 和 Zuul 相互结合来做负载均衡。暴露在最外面的是 Ngnix 主从双热备进行 Keepalive,Ngnix 经过某种路由策略,将请求路由转发到 Zuul 集群上, Zuul 最终将请求分发到具体的服务上。

Spring Cloud——服务网关_Spring Cloud_04|

Spring Cloud Gateway

简介

        Spring Cloud Gateway 基于Spring Boot2、Spring WebFlux和Project Reactor,是Spring Cloud的全新项目。Spring Cloud Gateway 旨在提供一种简单而有效的途径来转发请求,并为它们提供横切关注点,例如:安全性、监控/指标和弹性。

       Spring Cloud Gateway具有如下特征:

  • 支持动态路由;
  • 支持内置到Spring Handler映射中的路由匹配;
  • 支持基于HTTP请求的路由匹配(Path、Method、Header、Host等);
  • 过滤器作用于匹配的路由;
  • 过滤器可以修改下游HTTP请求和HTTP响应(增加/修改头部、增加/修改请求参数、改写请求路径等);
  • 通过API或配置驱动;
  • 支持Spring Cloud DiscoveryClient配置路由,与服务发现与注册配合使用。 

        在Finchley正式版之前,Spring Cloud推荐的网关是Netflix提供的Zuul。与Zuul相比,Spring Cloud Gateway使用非阻塞API。Spring Cloud Gateway 还支持WebSocket,并且与Spring紧密集成,拥有更好的开发体验。Zuul基于Servlet2.5,使用阻塞架构,它不支持任何长连接,如WebSocket。Zuul的设计模式和Nginx较像,每次I/O操作都是从工作线程中选择一个执行,请求线程被阻塞直到工作线程完成,但是差别是Nginx用C++实现,Zuul用Java实现,而JVM本身会有第一次加载较慢的情况,使得Zuul的性能相对较差。Zuul已经发布了Zuul2.x,基于Netty、非阻塞、支持长连接,但Spring Cloud目前还没有整合。Zuul2.x的性能肯定会较Zuul1.x有较大提升。在性能方面,根据官方提供的基准(benchmark)测试,SpringCloud Gateway的RPS(每秒请求数)是Zuul的1.6倍。综合来说,Spring Cloud Gateway在提供的功能和实际性能方面,表现都很优异。

路由配置

spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: https://example.org
        predicates:
          - Path=/red/{segment},/blue/{segment}
        filters:
           StripPrefix=1#跳过前缀复制代码

id:自定义路由ID,保持唯一。 

uri:目标服务地址,支持普通URI及lb://应用注册服务名称,后者表示从注册中心获取集群服务地址。 

predicates:路由条件,根据匹配的结果决定是否执行该请求路由。

filters:过滤规则,包含pre和post过滤。其中StripPrefix=1,表示Gateway根据该配置的值去掉URL路径中的部分前缀(这里去掉一个前缀,即在转发的目标URI中去掉gateway)。

Cookie匹配路由

判断请求中携带的Cookie是否匹配配置的规则,配置如下

spring:
  cloud:
    gateway:
      routes:
      - id: cookie_route
        uri: https://example.org
        predicates:
        - Cookie=chocolate, ch.p复制代码

Header匹配路由

判断请求中Header头消息对应的name和value与Predicate配置的值是否匹配,value也是正则匹配形式。

spring:
  cloud:
    gateway:
      routes:
      - id: header_route
        uri: https://example.org
        predicates:
        - Header=X-Request-Id, \d+复制代码

       该配置中会匹配请求中Header头中的name=X-Request-Id,并且value会根据正则表达式匹配\d+,也就是匹配1个以上的数字。

Host匹配路由

       HTTP请求会携带一个Host字段,这个字段表示请求的服务器网址。就是匹配请求中的Host字段进行路由。

spring:
  cloud:
    gateway:
      routes:
      - id: host_route
        uri: https://example.org
        predicates:
        - Host=**.somehost.org,**.anotherhost.org复制代码

        Host可以配置一个列表,列表中的每个元素通过,分隔。在上述配置中,当前请求中Host的值符合**.somehost.com,**anotherhost.com时,才会将请求路由到https://example.org,比如www.somehost.com、test.somehost.com都符合该规则。

请求方法匹配路

会根据HTTP请求的Method属性来匹配以实现路由,配置如下。

spring:
  cloud:
    gateway:
      routes:
      - id: method_route
        uri: https://example.org
        predicates:
        - Method=GET,POST复制代码

该配置表示,如果HTTP请求的方法是GET或POST,都会路由到https://example.org。

请求路径匹配路由

请求路径匹配路由是比较常见的路由匹配规则,配置如下。

spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: https://example.org
        predicates:
        - Path=/red/{segment},/blue/{segment}复制代码

       ${segment}是一种比较特殊的占位符,/*表示单层路径匹配,/**表示多层路径匹配。上述配置规则中,匹配请求的URI为/red/*、/blue/*时,才会转发到https://example.org。

Websocket路由

websocket路由配置

spring:
  cloud:
    gateway:
      routes:
      # SockJS route
      - id: websocket_sockjs_route
        uri: http://localhost:3001
        predicates:
        - Path=/websocket/info/**
      # Normal Websocket route
      - id: websocket_route
        uri: ws://localhost:3001
        predicates:
        - Path=/websocket/**复制代码

    Filter过滤器

        Filter分为Pre类型的过滤器和Post类型的过滤器。 

  • Pre类型的过滤器在请求转发到后端微服务之前执行,在Pre类型过滤器链中可以做鉴权、限流等操作。 
  • Post类型的过滤器在请求执行完之后、将结果返回给客户端之前执行。

        在Spring Cloud Gateway中内置了很多Filter,Filter有两种实现,分别是GatewayFilter和GlobalFilter。GlobalFilter会应用到所有的路由上,而GatewayFilter只会应用到单个路由或者一个分组的路由上。

AddRequestParameterFilter

该过滤器的功能是对所有匹配的请求添加一个查询参数。

spring:
  cloud:
    gateway:
      routes:
      - id: add_request_parameter_route
        uri: https://example.org
        filters:
        - AddRequestParameter=red, blue复制代码

在上面这段配置中,会对所有请求增加red=blue这个参数。

AddResponseHeadeFilter

该过滤器会对所有匹配的请求,在返回结果给客户端之前,在Header中添加相应的数据。

spring:
  cloud:
    gateway:
      routes:
      - id: add_request_header_route
        uri: https://example.org
        predicates:
        - Path=/red/{segment}
        filters:
        - AddRequestHeader=X-Request-Red, Blue复制代码

在上面这段配置中,会在Response中添加Header头,key=X-Response-Red,Value=Blue。

RetryGatewayFilter

该过滤器为请求重试过滤器,当后端服务不可用时,网关会根据配置参数来发起重试请求。

spring:
  cloud:
    gateway:
      routes:
      - id: retry_test
        uri: http://localhost:8080/flakey
        predicates:
        - Host=*.retry.com
        filters:
        - name: Retry
          args:
            retries: 3
            statuses: 503
            methods: GET,POST
            backoff:
              firstBackoff: 10ms
              maxBackoff: 50ms
              factor: 2
              basedOnPreviousValue: false复制代码

    RetryGatewayFilter提供4个参数来控制重试请求,参数说明如下。 

  • retries:请求重试次数,默认值是3。 
  • status:HTTP请求返回的状态码,针对指定状态码进行重试,比如,在上述配置中,当服务端返回的状态码是503时,才会发起重试,此处可以配置多个状态码。
  • methods:指定HTTP请求中哪些方法类型需要进行重试,默认值是GET。
  • series:配置错误码段,表示符合某段状态码才发起重试,默认值是SERVER_ERROR(5),表示5xx段的状态码都会发起重试。如果series配置了错误码段,但是status没有配置,则仍然会匹配series445/468GatewayFilter进行重试。

工作原理

基本概念:

  • 路由(Route):它是网关的基本组件,由ID、目标URI、Predicate集合、Filter集合组成。
  • 谓语(Predicate):它是Java8中引入的函数式接口,提供了断言的功能。它可以匹配HTTP请求中的任何内容。如果Predicate的聚合判断结果为true,则意味着该请求会被当前Router进行转发。 
  • 过滤器(Filter):为请求提供前置和后置的过滤。

    具体步骤如下: 

  1. 请求发送到网关,DispatcherHandler是HTTP请求的中央分发器,将请求匹配到相应的HandlerMapping。 
  2. 请求与处理器之间有一个映射关系,网关将会对请求进行路由,handler 此处会匹配到RoutePredicateHandlerMapping,以匹配请求所对应的Route。 
  3. 随后到达网关的Web处理器,该WebHandler代理了一系列网关过滤器和全局过滤器的实例,如对请求或者响应的头部进行处理(增加或者移除某个头部)。
  4. 最后,转发到具体的代理服务。 这里比较重要的功能点是路由的过滤和路由的定位,Spring Cloud Gateway提供了非常丰富的路由过滤器和路由断言。

Spring Cloud——服务网关_服务网关_05

网关限流

         限流的主要目的是通过限制并发访问数或者限制一个时间窗口内允许处理的请求数量来保护系统,一旦达到限制数量则对当前请求进行处理采取对应的拒绝策略,比如跳转到错误页面拒绝请求、进入排队系统、降级等。 从本质上来说,限流的主要作用是损失一部分用户的可用性,为大部分用户提供稳定可靠的服务。比如系统当前能够处理的并发数是10万,如果此时来了12万用户,那么限流机制会保证为10万用户提供正常服务。

       在实际开发过程中,限流几乎无处不在: 

  • 在Nginx层添加限流模块限制平均访问速度。

  • 通过设置数据库连接池、线程池的大小来限制总的并发数。

  • 通过Guava提供的Ratelimiter限制接口的访问速度。

  • TCP通信协议中的流量整形。

限流实现算法

       要实现限流,最重要的就是限流的算法。

计数器算法

          一种比较简单的限流实现算法,在指定周期内累加访问次数,当访问次数达到设定的闽值时,触发限流策略,当进入下一个时间周期时进行访问次数的清零。 这种算法可以用在短信发送的频次限制上,比如限制同一个用户一分钟之内触发短信发送的次数。

         限定了每一分钟能够处理的总的请求数为100,在第一个一分钟内,一共请求了60次。接着到第二个一分钟,counter又从0开始计数,在一分半钟时,已经达到了最大限流的阈值,这个时候后续的所有请求都会被拒绝。

Spring Cloud——服务网关_Spring Cloud_06

       这种算法存在一个临界问题,在第一分钟的0:58和第二分钟的1:02这个时间段内,分别出现了100个请求,整体来看就会出现4秒内总的请求量达到200,超出了设置的闽值。

Spring Cloud——服务网关_Spring Cloud_07

滑动窗口算法

       为了解决计数器算法带来的临界问题,所以引入了滑动窗口算法。滑动窗口是一种流量控制技术,在TCP网络通信协议中,就采用了滑动窗口算法来解决网络拥塞的情况。 

        简单来说,滑动窗口算法的原理是在固定窗口中分割出多个小时间窗口,分别在每个小时间窗口中记录访问次数,然后根据时间将窗口往前滑动并删除过期的小时间窗口。最终只需要统计滑动窗口范围内的所有小时间窗口总的计数即可。 

       我们将一分钟拆分为4个小时间窗口,每个小时间窗口最多能够处理25个请求。并且通过虚线框表示滑动窗口的大小(当前窗口的大小是2,也就是在这个窗口内最多能够处理50个请求)。同时滑动窗口会随着时间往前移动,比如前面15s结束之后,窗口会滑动到15s~45s这个范围,然后在新的窗口中重新统计数据。这种方式很好地解决了固定窗口算法的临界值问题。 Sentinel就是采用滑动窗口算法来实现限流的。

Spring Cloud——服务网关_服务网关_08

令牌桶限流算法

       令牌桶是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。对于每一个请求,都需要从令牌桶中获得一个令牌,如果没有获得令牌,则需要触发限流策略。

       系统会以一个恒定速度(rtokens/sec)往固定容量的令牌桶中放入令牌,如果此时有客户端请求过来,则需要先从令牌桶中拿到令牌以获得访问资格。

Spring Cloud——服务网关_Spring Cloud_09
假设令牌生成速度是每秒10个,也就等同于QPS=10,此时在请求获取令牌的时候,会存在三种情况: 

  • 请求速度大于令牌生成速度:那么令牌会很快被取完,后续再进来的请求会被限流。
  • 请求速度等于令牌生成速度:此时流量处于平稳状态。 
  • 请求速度小于令牌生成速度:说明此时系统的并发数并不高,请求能被正常处理。

由于令牌桶有固定的大小,当请求速度小于令牌生成速度时,令牌桶会被填满。所以令牌桶能够处理突发流量,也就是在短时间内新增的流量系统能够正常处理,这是令牌桶的特性。

漏桶限流算法

        漏桶限流算法的主要作用是控制数据注入网络的速度,平滑网络上的突发流量。 漏桶限流算法的原理,在漏桶算法内部同样维护一个容器,这个容器会以恒定速度出水,不管上面的水流速度多快,漏桶水滴的流出速度始终保持不变。实际上消息中间件就使用了漏桶限流的思想,不管生产者的请求量有多大,消息的处理能力取决于消费者。

Spring Cloud——服务网关_Spring Cloud_10

       在漏桶限流算法中,存在以下几种可能的情况:

  • 请求速度大于漏桶流出水滴的速度:也就是请求数超出当前服务所能处理的极限,将会触发限流策略。
  • 请求速度小于或者等于漏桶流出水滴的速度,也就是服务端的处理能力正好满足客户端的请求量,将正常执行。 

        漏桶限流算法和令牌桶限流算法的实现原理相差不大,最大的区别是漏桶无法处理短时间内的突发流量,漏桶限流算法是一种恒定速度的限流算法。

Sentinel限流

       Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。

从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:

  • route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
  • 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组

具体与Spring Cloud Gateway 的集成可以参考官方文档

Sentinel限流原理

  • 通过GatewayRuleManager加载网关限流规则GatewayFlowRule时,无论是否针对请求属性进行限流,Sentinel底层都会将网关流控规则GatewayFlowRule转化为热点参数规则ParamFlowRule存储在GatewayFlowManager中,与正常的热点参数规则相互隔离。在转化时,Sentinel会根据请求属性配置,为网关流控规则设置参数索引(idx),并添加到生成的热点参数规则中。
  • 在外部请求进入API网关时,会先经过SentinelGatewayFilter,在该过滤器中依次进行Route ID/API分组匹配、请求属性解析和参数组装。 
  • Sentinel根据配置的网关限流规则来解析请求属性,并依照参数索引顺序组装参数数组,最终传入SphU.entry(name,args)中。
  • 在Sentinel API Gateway Adapter Common模块中在Slot Chain中添加了一个GatewayFlowSlot,专门用来处理网关限流规则的检查。
  • 如果当前限流规则并没有指定限流参数,则Sentinel会在参数的最后一个位置置入一个预设的常量$D,最终实现普通限流。

Spring Cloud——服务网关_Spring Cloud_11

Gateway限流

         在Gateway中限流的实现基于内置的限流过滤器配置实现, RequestRateLimiter该过滤器会对访问到当前网关的所有请求执行限流过滤,如果被限流,默认情况下会响应HTTP 429-TooMany Requests。默认提供了RedisRateLimiter的限流实现,它采用令牌桶算法来实现限流功能。

spring:
  cloud:
    gateway:
      routes:
      - id: requestratelimiter_route
        uri: https://example.org
        filters:
        - name: RequestRateLimiter
          args:
            redis-rate-limiter.replenishRate: 10
            redis-rate-limiter.burstCapacity: 20
            redis-rate-limiter.requestedTokens: 1复制代码

      redis-rate-limiter过滤器有两个配置属性: 

  • replenishRate:令牌桶中令牌的填充速度,代表允许每秒执行的请求数。
  • burstCapacity:令牌桶的容量,也就是令牌桶最多能够容纳的令牌数。表示每秒用户最大能够执行的请求数量。