Spring Cloud Zuul API 网关服务

API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade 模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由负载均衡校验过滤等功能之外, 还需要更多能力比如与服务治理框架的结合、请求转发时的熔断机制服务聚合等一系列高级功能。Spring Cloud 中了提供了基于 Netflix Zuul 实现的 API 网关组件 Spring Cloud Zuul。

对于服务实例的维护间题,Spring Cloud Zuul 通过与 SpringCloud Eureka 进行整合,将自身注册为Eureka 服务治理下的应用,同时从 Eureka 中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。

对于路由规则的维护问题,Zuul 默认会以服务名作为 ContextPath 的方式来创建路由映射,大部分情况下,这样的默认设置已经可以实现大部分的路由需求,除了一些特殊情况(比如兼容一些旧的 URL)还需要做一些特别的配置。

对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在 API 网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。

Spring Cloud Zuul 提供了一套过滤器机制,它可以很好地支持这样的任务。开发者可以通过使用 Zuul 来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,否者就返回错误提示。 通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。

微服务架构虽然可以将开发单元拆分得更为细致,有效降低了开发难度,但是它所引出的各种问题如果处理不当会成为实施过程中的不稳定因素,甚至掩盖掉原本实施微服务带来的优势。所以,在微服务架构的实施方案中,API网关服务的使用几乎成为必然的选择。

API 网关对微服务架构的重要性

  • 作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
  • 与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发
  • 实现接口权限校验与微服务业务逻辑的解耦
  • 通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。

请求路由

传统路由方式

传统路由的映射方式 API 网关根据请求的 URL 路径找到最匹配的 path 表达式,直接转发给该表达式对应的 url 或对应 serviceId 下配置的实例地址,以实现外部请求的路由。

在不依赖服务发现机制的情况下,通过在配置文件中具体指定每个路由表达式与服务实例的映射关系来实现 API 网关对外部请求的路由。

传统路由的配置方式并不友好,它需要运维人员花费大量的时间来维护各个路由 path 与 url 的关系 。

面向服务的路由

通过面向服务的路由配置方式,不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的 path 与serviceld(服务名) 的映射组合,使得维护工作变得非常简单。 这归功于 Spring Cloud Eureka 的服务发现机制,它使得 API 网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射实例的维护问题。

可以直接将 API 网关也看作 Eureka 服务治理下的一个普通微服务应用。它除了会将自己注册到 Eureka 服务注册中心上之外,也会从注册中心获取所有服务以及它们的实例清单。所以在 Eureka 的帮助下,API 网关服务本身就已经维护了系统中所有 serviceId 与实例地址的映射关系。当有外部请求到达 API 网关的时候,根据请求的 URL 路径找到最佳匹配的 path 规则,API 网关就可以知道要将该请求路由到哪个具体的 serviceId上去。由于在 API 网关中已经知道 serviceId 对应服务实例的地址清单,只需要通过 Ribbon 的负载均衡策略,直接在这些清单中选择一个具体的实例进行转发就能完成路由工作。

服务路由的默认规则

默认情况下,为 Spring Cloud Zuul 构建的 API 网关服务引入 Spring Cloud Eureka 之后,Zuul 为 Eureka 的每个服务都自动创建一个默认路由规则,这些默认规则的 path 会使用 serviceId 配置的服务名作为请求前缀。

路径匹配

不论是使用传统路由的配置方式还是服务路由的配置方式,都需要为每个路由规则定义匹配表达式,也就是 path参数。

当使用通配符的时候,经常会碰到这样的问题:一个 URL 路径可能会被多个不同路由的表达式匹配上。Zuul 的路由匹配算法中,在使用路由规则匹配请求路径的时候是通过线性遍历的方式,在请求路径获取到第一个匹配的路由规则之后就返回并结束匹配过程。 所以当存在多个匹配的路由规则时,匹配结果完全取决于路由规则的保存顺序。路由规则通过 LinkedHashMap 保存,也就是说,路由规则的保存是有序的,而内容的加载是通过遍历配置文件中路由规则依次加入的。由于 properties 的配置内容无法保证有序,所以当出现这种情况的时候,为了保证 路由的优先顺序,需要使用 YAML 文件来配置,以实现有序的路由规则。

:properties 的配置内容为什么无法保证有序?因为 Properties 继承自 Hashtable,而 Hashtable 是无序的,不保证进出顺序。

请求过滤

为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微 服务中去,冗余代码的出现是我们不希望看到的。

所以比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。

对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。 由于网关服务的加入,外部客户端访问系统已经有了统一入口, 既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度也得到了相应降低。Zuul 允许开发者在 API 网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,只需继承 ZuulFilter 抽象类并实现它定义的 4 个抽象函数即可。

Cookie 与头信息

默认情况下 Spring Cloud Zuul 在请求路由时,会过滤掉 HTTP 请求头信息中的一些敏感信息,防止它们被传递到下游的外部服务器。默认的敏感头信息通过 zuul.sensitiveHeaders 参数定义,包括 CookieSet-CookieAuthorization 三个属性。所以在开发 Web 项目时常用的 Cookie 在 Spring Cloud Zuul 网关中默认是不会传递 的,这就会引发 一个常见的问题:如果要将已经使用了 Spring Security、Shiro 等安全框架构建的 Web 应用通过Spring Cloud Zuul 构建的网关来进行路由时,由于 Cookie 信息无法传递,Web应用将无法实现登录和鉴权。可以通过指定路由的参数配置来解决这个问题。

不推荐通过设置全局参数为空来覆盖默认值的方法,虽然可以实现 Cookie 的传递,但是破坏了默认设置的用意。在微服务架构的 API 网关之内,对于无状态的 RESTful API 请求肯定是要远多于这些 Web 类应用请求的,甚至还有一些架构设计会将 Web 类应用和 App 客户端一样都归为 API 网关之外的客户端应用。

Hystrix 和 Ribbon 支持

spring-cloud-starter-zuul 依赖包含了 spring-cloud-starter-hystrix 和 spring-cloud-starter-ribbon 模块的依赖,所以 Zuul 天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。但要注意,当使用 path 与 url 的映射关系来配置路由规则的时候,对于路由转发的请求不会采用 HystrixCommand 来包装,所以这类路由请求没有线程隔离和断路器的保护,并且也不会有负载均衡的能力。因此在使用 Zuul 的时候尽量使用 path 和 serviceId 的组合来进行配置,这样不仅可以保证API网关的健壮稳定,也能用到 Ribbon 的客户端负载均衡功能。

过滤器

Zuul 包含请求路由和过滤两个功能,其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验服务聚合等功能的基础。

然而实际上,路由功能在真正运行时,它的路由映射请求转发都是由几个不同的过滤器完成的。其中路由映射主要通过 pre 类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由 route 类型的过滤器来完成,对 pre 类型过滤器获得的路由地址进行转发。

所以,过滤器可以说是 Zuul 实现API网关功能最为核心的部件,每一个进入 Zuul 的 HTTP 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。在 Spring Cloud Zuul 中实现的过滤器必须包含 4 个基本特征:过滤类型、执行顺序、执行条件、具体操作。 这些元素看起来非常熟悉,实际上它就是 ZuulFilter 接口中定义的 4 个抽象方法:

String filterType(); 
int filterOrder(); 
boolean shouldFilter();
Object run(); 

它们各自的含义与功能总结如下:

  • filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在 HTTP 请求过程中定义的各个阶段。在 Zuul 中默认定义了 4 种不同生命周期的过滤器类型:

    • pre: 可以在请求被路由之前调用。

    • routing: 在路由请求时被调用。

    • error: 处理请求时发生错误时被调用。

    • post: 在 routing 和 error 过滤器之后被调用。

  • filterOrder: 通过 int 值来定义过滤器的执行顺序,数值越小优先级越高

  • shouldFilter: 返回一个 boolean 值来判断该过滤器是否要执行。可以通过此方法来指定过滤器的有效范围。

  • run: 过滤器的具体逻辑。在该函数中可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

请求生命周期

Zuul 默认定义了 4 种不同的过滤器类型,它们覆盖了一个外部 HTTP 请求到达 API 网关, 直到返回请求结果的全部生命周期。下图描述了一个 HTTP 请求到达 API 网关之后在各种不同类型的过滤器之间流转的详细过程。

Spring Cloud Zuul API 网关服务_第1张图片

从上图中可以看到,当外部 HTTP 请求到达 API 网关服务的时候,首先它会进入第一个阶段 pre, 在这里它会被 pre 类型的过滤器进行处理,该类型过滤器的主要目的是在进行请求路由之前做一些前置加工,比如请求的校验等。

在完成了 pre 类型的过滤器处理之后,请求进入第二个阶段 routing,即路由请求转发阶段,请求将会被 routing类型过滤器处理。这里的具体处理内容就是将外部请求转发到具体服务实例上去的过程,当服务实例将请求结果都返回之后,routing 阶段完成,请求进入第三个阶段 post。

此时请求将会被 post 类型的过滤器处理,这些过滤器在处理的时候不仅可以获取到请求信息,还能获取到服务实例的返回信息,所以在 post 类型的过滤器中,可以对处理结果进行一些加工或转换等内容。

另外还有一个特殊的阶段 error, 该阶段只有在上述三个阶段中发生异常的时候才会触发,但是它的最后流向还是 post 类型的过滤器,因为它需要通过 post 过滤器将最终结果返回给请求客户端。

禁用过滤器

不论是核心过滤器还是自定义过滤器,只要在 API 网关应用中为它们创建了实例,默认情况下,它们都是启用状态的。如果有些过滤器我们不想使用了,如何禁用它们呢?

大多情况下初识 Zuul 的使用者第一反应就是通过重写shouldFilter 逻辑, 让它返回false, 这样该过滤器对于任何请求都不会被执行,基本实现了对过滤器的禁用。 但是对于自定义过滤器来说似乎是实现了过滤器不生效的功能,但是这样的做法缺乏灵活性。 由于直接要修改过滤器逻辑, 不得不重新编译程序,并且如果该过滤器在未来一段时间还有可能被启用的时候,那么就又得修改代码并编译程序。 同时对于核心过滤器来说,就更为麻烦,不得不获取源码来进行修改和编译。

实际上在 Zuul 中特别提供了一个参数来禁用指定的过滤器,该参数的配置格式如下:

zuul...disable=true

其中,代表过滤器的类名,如 AccessFilter; 代表过滤器类型,如 pre。 所以如果想要禁用过滤器, 只需要在 application.properties 配置文件中增加如下配置即可:

zuul.AccessFilter.pre.disable=true 

该参数配置除了可以对自定义的过滤器进行禁用配置之外,很多时候可以用它来禁用 Spring Cloud Zuul 中默认定义的核心过滤器。 这样就可以抛开 Spring Cloud Zuul自带的那套核心过滤器,实现一套更符合实际需求的处理机制。

动态加载

在微服务架构中,由于 API 网关服务担负着外部访问统一入口的重任,它同其他应用不同,任何关闭应用和重启应用的操作都会使系统对外服务停止,对于很多 7 * 24 小时服务的系统来说,这样的情况是绝对不被允许的。所以,作为最外部的网关,它必须具备动态更新内部逻辑的能力,比如动态修改路由规则、动态添加/删除过滤器等。 通过 Zuul 实现的 API 网关服务当然也具备了动态路由动态过滤器的能力。可以在不重启 API 网关服务的前提下,动态修改路由规则和添加或删除过滤器。下面介绍如何通过 Zuul 来实现动态 API 网关服务。

动态路由

通过之前对请求路由的详细介绍,可以发现对于路由规则的控制几乎都可以在配置文件 application.propertiesapppication.yaml中完成。既然这样,对于如何实现 Zuul 的动态路由,很自然地会将它与 SpringCloud Config动态刷新机制联系到一起。只需将 API 网关服务的配置文件通过 Spring Cloud Config 连接的 Git 仓库存储和管理,就能轻松实现动态刷新路由规则的功能。

动态过滤器

对于请求过滤器的动态加载与请求路由的动态加载在实现机制上会有所不同。 这个不难理解,请求路由通过配置文件就能实现,而请求过滤则都是通过编码实现。 所以对于实现请求过滤器的动态加载,需要借助于 JVM 实现的动态语言的帮助,比如 Groovy

API 网关服务的动态过滤器功能可以帮助增强 API 网关的持续服务能力,对于网关中的处理逻辑维护也变得更为灵 活,不仅可以动态地实现请求校验,还可以动态地实现对请求内容的干预。

参考:

《Spring Cloud 微服务实战》翟永超 著

你可能感兴趣的:(SpringCloud)