服务网关——Spring Cloud Zuul

(git上的源码:https://gitee.com/rain7564/spring_microservices_study/tree/master/fifth-spring-cloud-zuul)

对于服务网关是什么、有什么用?使用API Gateway这篇文章已经讲得很清楚了,这里就不再赘述。当然这只是翻译版,原版在这里:Building Microservices: Using an API Gateway

Spring Cloud和Netflix Zuul

Using an API Gateway一文中提到Netflix API Gateway其实就是Netflix Zuul。Zuul是一个服务网关,Spring Cloud融入Zuul后,使用Spring Cloud提供的注解很容易就能搭建一个API Gateway。Zuul提供了几个功能,包括:

  • 只用一个URL就能映射应用中所有服务的路由。但Zuul并不局限于单个URL,也可以定义多个路由入口,做到细粒度的路由映射。
  • 自定义过滤器对经过网关的所有请求进行过滤。服务网关的过滤器,可以实现对所有请求进行过滤,而不用在各个服务实现过滤器。

下面进入正题,服务网关的实现。

创建gateway-zuul服务

创建一个Zuul服务端,首先要创建一个Spring Boot项目,然后引入Zuul相关的启动依赖。

创建Spring boot项目并修改pom文件

创建一个空Spring Boot项目后,pom文件修改如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.study.microservice</groupId>
    <artifactId>gateway-zuul</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>gateway-zuul</name>
    <description>API Gateway</description>

    <parent>
        <groupId>cn.study.microservice</groupId>
        <artifactId>fifth-spring-cloud-zuul</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-zuul</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>
    </dependencies>

</project>

上面的xml文件,只引入了两个启动依赖,第一个是Zuul的启动依赖,该启动依赖除了包含Zuul的核心jar包外,还包括Hystrix、Ribbon、actuator等。第二个是后文介绍将Zuul服务托管在Eureka时会用到。

修改启动类

pom文件修改好之后,需要修改gateway-zuul服务的启动类,如下:

@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayZuulApplication.class, args);
    }
}

上面的代码实际上只加了一个注解—@EnableZuulProxy。

若在添加注解@EnableZuulProxy时是手动输入,且IDE开启自动补全功能,那么应该会看到另一个注解——@EnableZuulServer。如果使用该注解,虽然也会创建一个Zuul服务端,但不加载任何反向代理过滤器,不使用Eureka的服务发现来发现其他服务。@EnableZuulServer只在搭建自己的路由服务并不使用Zuul的预构建功能时使用。比如需要使用Zuul来配合其它不是Eureka的服务发现引擎,如Consul。

与Eureka结合使用

Zuul proxy server本来就是被设计用在Spring Cloud项目中。正因为如此,Zuul自动使用Eureka来作为服务发现的依赖,可以通过服务发现其它服务,然后在Zuul内部使用Ribbon实现客户端负载均衡。

添加application.yml文件,然后在该文件中加入如下配置:

server:
  port: 5555

eureka:
  instance:
    preferIpAddress: true
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

配置路由规则

Zuul的核心是反向代理。反向代理实际上是一个中间服务器,处于想要访问某个资源的客户端与资源中间。反向代理会捕捉客户端的请求并代表客户端想远程资源发起请求。

在微服务架构中,Zuul(反向代理)从客户端“得到”一个调用,然后转发给下游服务。所以,从客户端服务角度来看,与客户端交互的实际上是Zuul。而Zuul需要与下游的服务进行交互,所以必须知道客户端的请求想要路由到哪个服务。Zuul可以通过几种途径来达到这一目标,包括:

  • 通过服务发现自动映射路由
  • 使用服务发现手动映射路由
  • 使用静态URLs手动映射路由

通过服务发现自动映射路由

Zuul的所有路由映射都可以在zuul服务的application.yml文件中定义。然而,Zuul还可以在零配置的情况下,根据请求url携带的serviceId将请求正确路由到目标服务的某个实例。如果没有指定特定的路由(手动配置,下文会介绍),Zuul默认会使用被调用服务的Eureka service ID并将其映射到其中一个目标服务实例。举个简单的例子,如果你想访问organization-service服务的一个接口,该接口是根据orgId获取对应的organization详细信息,而且希望使用Zuul的自动路由,那么你可以让客户端直接访问Zuul服务的实例,然后使用如下URL:

http://localhost:5555/organizationservice/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a

其中http://localhost:5555,是Zuul服务实例的访问地址;而organizationservice是organization服务的service ID;剩下的部分则是希望调用的接口。看下面的图可能更容易理解,如下:

image.png

Eureka配合Zuul使用的优美之处在于,不仅可以通过单个端点来访问应用的所有服务,而且,在添加或移除服务实例的时候不用修改Zuul的路由配置。另外,也可以添加一个新的服务到Eureka,而Zuul会对访问新添加的服务自动路由,因为Zuul是通过与Eureka通信然后从Eureka获取微服务实例真正物理地址,只要服务托管在Eureka中。

如果想要查看Zuul服务器管理的路由,可以访问Zuul暴露的/routes端点。该端点会返回所有服务的映射列表。启动所有服务,然后访问http://localhost:5555/routes,若跟着本教程走,应该可以看到类似如下图的返回:

image.png

如果出现下面的情况:


image.png

则需要在application.yml或bootstrap.yml文件中加入如下配置:

management:
  security:
    enabled: false

观察正常访问http://localhost:5555/routes后的返回json对象,类似"/config-server/**"的key,是Zuul基于Eureka service ID自动为服务创建的服务路由,请求匹配到的服务路由就可以映射得到Eureka service ID,即json对象的value,最后就可以根据这个ID定位具体的服务实例。

使用服务发现手动映射路由

Zuul允许配置更细粒度的路由映射规则,可以明确定义路由映射而不是单纯依赖使用服务的Eureka service ID自动创建。假设想要缩短organizationservice来简化路由,而不是使用Zuul默认提供的/organizationservice/**,那么可以通过在Zuul服务的配置文件中手动定义路由映射关系,例如:

zuul:
  routes: 
    organizationservice: /organization/**

在Zuul服务的application.yml加上上面的配置后,就可以使用类似/organization/v1/organizations/{organization-id}的路由来访问organization服务了。此时,若重启Zuul然后再次访问http://localhost:5555/routes,可以出现如下返回结果:

image.png

观察上图,可以看到Zuul为organization服务提供了“入口”,第一个是刚刚手动配置上去的,而第二个则是Zuul默认提供的。

注意:如果使用Zuul基于Eureka service ID自动创建的路由映射,那么当某个服务没有任何一个实例处于运行状态,那么Zuul将不会为该服务创建路由映射。然而,如果手动将路由映射到Eureka service ID,那么,即使没有实例注册到Eureka,Zuul依旧会暴露出手动配置的。当然,若尝试使用不存在任何服务实例的路由,Zuul将直接返回500错误。

忽略某些服务

如果想要将Zuul自动创建的路由映射从路由列表中移除,只留下手动配置的,那么可以在application.yml文件中在加一个额外的Zuul参数——ignored-services即可。示例如下:

zuul:
  ignored-services: 'organizationservice'
  routes:
    organizationservice: /organization/**

这样,就能将Zuul根据Eureka Service ID自动创建的"/organizationservice/**": "organizationservice"从路由映射列表中移除。添加上面的配置然后重启,访问/routes端点,可以看到如下返回:


image.png

上图中,Zuul默认给organization服务创建的路由映射已经被忽略了,只留下手动配置的。如果希望Zuul忽略所有自动配置的路由映射,可以使用:

zuul:
  ignored-services: '*'
  routes:
    organizationservice: /organization/**

加上如上配置后重启,然后访问端点/routes,返回如下:


image.png
手动配置多个服务

云应用肯定会包含许多微服务,所以一般都有为多个服务手动配置路由映射的需求,这样的需求实现起来也比较简单,如下是对organization服务和license服务的路由映射做手动配置:

zuul:
  ignored-services: '*'
  routes:
    organizationservice: /organization/**
    licenseservice: /license/**

访问/routes结果是:


image.png
不同API路由共用一样的模型

在不同服务路由的开头附加一个的前缀是很常见的。比如希望在不同服务的路由的开头都加上一个/api的前缀,Zuul也是支持的。可以使用如下配置来实现这一功能:

zuul:
  ignored-services: '*'
  prefix: /api
  routes:
    organizationservice: /organization/**
    licenseservice: /license/**

可见,zuul.prefix属性可以用来定制所有服务路由的统一前缀。再次访问/routes端点,返回如下:


image.png

现在若需要访问organization服务的/v1/organizations/{organization-id}端口,则需要使用:

http://localhost:5555/api/organization/v1/organizations/{organization-id}

使用静态URLs手动映射路由

Zuul也可以用来转发没有注册到Eureka的服务的请求。在某些情况下,需要设置Zuul将部分请求直接路由到定义的静态URL。比如,假设organization服务是使用Python编写的,然后也想让Zuul来做反向代理,那么可以使用如下的配置实现:

zuul:
  prefix: /api
  routes:
    organizationstatic:
      path: /organizationstatic/**
      url: http://localhost:11000

关闭其他服务,只启动zuul服务和organization服务,启动过程中会报错,这是因为eureka没启动,服务没办法注册到eureka,可以不管它,只要成功启动就行。然后访问zuul的/routes端点,可以看到只有刚刚配置的静态URL,如图:


image.png

然后你会发现,访问
http://localhost:5555/api/organizationstatic/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/时,可以成功访问,结果如下:

image.png

下面来分析一下,之前的配置是什么含义,如下图:


image.png

但是,问题又来了,因为这样的配置会绕过eureka,这就导致请求都会指向单个路由,那么当organization服务有多个怎么办?怎么利用Ribbon来实现服务均衡?有两种做法:
第一种:

zuul:
  prefix: /api
  routes:
    organizationstatic:
      path: /organizationstatic/**
      serviceId: organizationstatic
ribbon:
  eureka:
    enabled: false
organizationstatic:
  ribbon:
    listOfServers: http://localhost:11000,http://localhost:11001

第二种:

zuul:
  prefix: /api
  routes:
    organizationstatic:
      path: /organizationstatic/**
      serviceId: organizationstatic
organizationstatic:
  ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    listOfServers: http://localhost:11000,http://localhost:11001

第一种实现方法需要Ribbon禁用eureka,感觉不太友好。官方文档是这样说的:


image.png

我的理解是:因为如果不禁用的话,zuul会以为organizationstatic这个service ID是来自Eureka的,所以就去eureka那边查找对应的服务实例,然而,organizationstatic根本就没注册到eureka,所以就直接报500了。

而第二种实现方法,是告诉zuul在使用Ribbon做负载均衡时,直接在提供的server列表中获取服务实例。观察第二种方法的配置,可以看到添加了.ribbon.NIWSServerListClassName属性。官方文档是这样说的:


image.png

官方文档地址:https://github.com/spring-cloud/spring-cloud-netflix/blob/master/docs/src/main/asciidoc/spring-cloud-netflix.adoc

使用上面的配置后,只启动gateway-zuul服务和两个organization服务实例,两个organization服务的端口分别是11000和11001。然后访问http://localhost:5555/api/organizationstatic/v1/organizations/e254f8c-c442-4ebe-a82a-e2fc1d1ff78a/,会发现负载均衡器起作用了。至于如何启动多个实例,请参考另一篇教程服务注册与发现——Netflix Eureka,这里面有提及。

非java服务的处理
之前说到静态映射路由负载均衡的第一种实现,需要zuul服务器的Ribbon禁用eureka,禁用后会有一个问题,就是其他注册到eureka的服务无法通过网关访问,需要手动自己配置.listOfServers属性。

所以对于其它语言编写的服务也想要通过zuul统一管理,有两个方案:第一,使用一个独立的zuul服务专门转发那些其它语言的服务;第二,创建Spring Cloud Sidecar实例(推荐使用这种),Spring Cloud sidecar允许注册其它语言实现的服务到eureka,然后就可以通过zuul代理了。Spring Cloud sidecar这里就不细讲了,实现也比较简单,可参考Polyglot support with Sidecar

zuul的超时时间

zuul使用Hystrix和Ribbon库来帮助避免一个耗时较长的调用影响到整个网关的性能。默认情况下,当一个调用经过1s后还为处理完成并返回,zuul会终止该调用并统一返回500错误(这个1s的超时时间,其实是Hystrix的默认超时时间)。但是,我们可以在zuul的配置文件中对这个超时时间进行修改。比如,将其修改成7s,可以这样设置:

...
hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 7000

假如需要修改特定的服务的超时时间,比如organization服务,则需要将上面的hystrix.command.defualt.换成hystrix.command.organizationservice,即将default换成对应的服务名。

最后,需要修改另一个超时时间属性。当我们覆盖hystrix默认超时时间且新超时时间大于5s,那么当一个调用超过5s时,Ribbon也会认为该调用超时。也就是说,当配置hystrix的超时时间大于5s,那么还需要配置Ribbon的超时时间,配置如下:

ribbon:
  ReadTimeout: 7000

如果需要指定某个服务的Ribbon超时时间,则要用(clientname为服务名):

clientname:
  ribbon:
    ReadTimeout: 7000

动态重新加载路由配置

zuul动态加载路由配置,需要具备Spring Cloud Config基础,若不了解Spring Cloud Config,可参考另一篇教程分布式配置——Spring Cloud Configuration

动态加载路由在实际应用中是极其有用的,因为可以在变更路由配置后,不用重新编译、重新部署zuul服务。在分布式配置——Spring Cloud Configuration中已经讲过如何将配置文件迁移到config server中,我们也可以将zuul的配置交由config server管理。(注意:本教程不使用git作为config server的配置文件存储库,而是直接使用config server的classpath,这样比较简单,只是每次改完配置文件需要重新启动config server)。

在config server的classpath:config目录下,创建目录gateway-zuul,然后创建gateway-zuul.yml文件,内容如下:

server:
  port: 5555

eureka:
  instance:
    preferIpAddress: true
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

接着修改config server服务的application.yml,添加config server扫描路径,如下:

...
spring:
  profiles:
    active: native
  cloud:
    config:
      server:
        native:
          search-locations: classpath:config/,classpath:config/licenseservice, classpath:config/gateway-zuul

然后修改zuul服务的pom.xml文件和bootstrap.yml文件,分别为:

...
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>
...
spring:
  application:
    name: gateway-zuul
  profiles:
    active: default
  cloud:
    config:
      uri: http://localhost:8888
management:
  security:
    enabled: false

然后重启config-server服务和gateway-zuul服务,当看到zuul服务的控制台有类似如下输出,才证明成功从config server加载配置文件(不然就要找找是哪里出错了):


image.png

访问zuul服务的/routes端点,返回结果如下:


image.png

修改config-server服务管理的zuul服务的配置文件,添加如下配置:

zuul:
  ignored-services: '*'
  prefix: /api
  routes:
    organizationservice: /organization/**
    licenseservice: /license/**

然后使用POST方式访问http://localhost:555/refresh(记得要先重启config-server服务),可以看到返回如下:

image.png

证明zuul服务的配置信息中的上面4个配置的值已变更。再次访问zuul服务的/routes,返回结果如下:


image.png

证明zuul已经动态加载配置成功了。

有关Spring Cloud Zuul的入门教程就介绍到这里,后续会在进阶教程中继续介绍zuul真正强大的功能——filter。

完!

推荐阅读更多精彩内容