Zuul 入门教程 - 基础篇

大纲

Zuul 介绍

Zuul 是什么

Zuul 是由 Netflix 孵化的一个致力于 “网关” 解决方案的开源组件,在动态路由、监控、弹性、服务治理以及安全方面起着举足轻重的作用。从 2012 年 3 月以来,陆续发布了 Zuul 1.0 与 Zuul 2.0 版本,后经 Pivotal 公司将 Zuul 1.0 整合到 Spring Cloud 的生态系统中,即现在的 Spring Cloud Zuul。在 Netflix 官方的解释中,Zuul 是从设备和网站到后端应用程序所有请求的前门,为内部服务提供可配置的对外 URL 到服务的映射关系,基于 JVM 的后端路由器。其底层基于 Servlet 实现,本质组件是一系列 Filter 所构成的责任链,并且 Zuul 的逻辑引擎与 Filter 可用其他基于 JVM 的编程语言编写(比如 Groovy)。Zuul 默认集成了 Ribbon、Hystrix,其中 Zuul 2.x 版本改动相较 1.x 比较大,底层使用了 Netty。虽然 Netflix 已经在 2018 年 5 月开源了 Zuul 2.x,但由于 Zuul 2.x 在 Spring Cloud Gateway 孵化之前一直跳票发布,而且 Spring Cloud Gateway 目前已经孵化成功,相较于 Zuul 1.x 在功能以及性能上都有明显的提升。因此在 Spring Boot 2.0 以上版本中,并没有对 Zuul 2.0 以上最新高性能版本进行集成,仍然使用 Zuul 1.x 非 Reactor 模式(基于 Servlet 2.5 阻塞架构)的旧版本。更多介绍可参考:Zuul 项目Zuul 官方英文教程Spring Cloud Zuul 官方中文文档

Zuul 的特性

主要特性包括:认证和鉴权、压力控制、动态路由、负载削减、静态响应处理、主动流量管理、金丝雀测试

Zuul 1.x 与 Zuul 2.x 对比

Zuul 1.x 是一个基于 Servlet 2.5 的同步阻塞 I/O 网关,不支持任何长连接(如 WebSocket)。Zuul 1.x 的设计和 Nginx 比较像,每次 I/O 操作都是从工作线程池中选择一个来执行,请求线程被阻塞到工作线程完成为止;但是差别是 Nginx 是基于 C/C++ 实现,而 Zuul 1.x 是使用 Java 实现,而 JVM 本身会有第一次加载较慢的情况,使得 Zuul 1.x 的性能相对较差。根据官方提供的基准测试,Spring Cloud Gateway 的 RPS(每秒请求数)是 Zuul 1.x 的 1.6 倍,平均延迟是 Zuul 1.x 的一半。

zuul-1-framework

Zuul 2.x 的理念更先进,基于 Netty 的异步非阻塞 I/O 模型,支持长连接。Zuul 2.x 最大的改进就是基于 Netty Server 实现了异步非阻塞 I/O 来接入请求,同时基于 Netty Client 实现了到后端业务服务 API 的请求,这样就可以实现更高的性能、更低的延迟。此外也调整了 Filter 类型,将原来的三个核心 Filter 显式命名为:Inbound Filter、Endpoint Filter 和 Outbound Filter。值得一提的是,Zuul 2.x 与 Spring Cloud Gateway 的性能差不多。Zuul 2.x 的核心功能如下:

  • GZip
  • HTTP/2
  • Retries
  • Mutual TLS
  • WebSocket/SSE
  • Proxy Protocol
  • Load Balancing
  • Request Passport
  • Request Attempts
  • Status Categories
  • Service Discovery
  • Connection Pooling
  • Origin Concurrency Protection

zuul-2-framework

Zuul 入门案例

这里的案例将使用到的 Spring Cloud 组件是 Eureka 与 Zuul,另外再使用一个普通服务作为 Zuul 路由的下级服务,来模拟真实开发中的一次路由过程。

1. 版本说明

在本文中,默认使用的 Spring Cloud 版本是 Finchley.RELEASE,对应的 Spring Boot 版本是 2.0.3,Zuul 版本是 1.x,点击下载完整的案例代码

2. 创建 Maven 父级 Pom 工程

在父工程里面配置好工程需要的父级依赖,目的是为了更方便管理与简化配置,具体 Maven 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
</parent>

<!-- 利用传递依赖,公共部分 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<!-- 管理依赖 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<!--注意:这里需要添加以下配置,否则可能会有各种依赖问题 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

3. 创建 Eureka Server 工程

创建 Eureka Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-server

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>

创建 Eureka Server 的启动主类,这里添加相应注解,作为程序的入口:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

添加 Eureka Server 需要的 application.yml 配置文件到工程中

1
2
3
4
5
6
7
8
9
10
11
server:
port: 8090

eureka:
instance:
hostname: 127.0.0.1
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

4. 创建 Provider 下游服务工程

创建 Provider 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-client

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>

创建 Provider 的启动主类,添加注解 @EnableDiscoveryClient,将服务注册到 Eureka Server:

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class ProviderApplication {

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

application.yml 文件中指定服务名称(provider-service)、注册中心地址与端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9090

spring:
application:
name: provider-service

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

创建用于测试的 Controller 类:

1
2
3
4
5
6
7
8
@RestController
public class ProviderController {

@GetMapping("/provider/add")
public String add(Integer a, Integer b, HttpServletRequest request) {
return "From Port: " + request.getServerPort() + ", Result: " + (a + b);
}
}

5. 创建 Zuul Server 工程

创建 Zuul Server 的 Maven 工程,配置工程里的 pom.xml 文件,需要引入 spring-cloud-starter-netflix-eureka-clientspring-cloud-starter-netflix-zuul

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>

创建 Zuul Server 的启动主类,添加注解 @EnableZuulProxy@EnableDiscoveryClient

1
2
3
4
5
6
7
8
9
@EnableZuulProxy
@EnableDiscoveryClient
@SpringBootApplication
public class ZuulServerApplication {

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

application.yml 文件中指定服务名称(zuul-server)、注册中心地址与端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server:
port: 8092

spring:
application:
name: zuul-server

eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8090/eureka
instance:
prefer-ip-address: true

#路由映射规则
zuul:
routes:
provider-service:
path: /provider/service/**
serviceId: provider-service

6. 测试效果

  1. 分别启动 eureka-server、provider-service、zuul-server 应用
  2. 访问 provider-service 应用:http://127.0.0.1:9090/provider/add?a=3&b=5
  3. 方式一:通过 zuul-server 访问 provider-service 应用:http://127.0.0.1:8092/provider-service/provider/add?a=3&b=5provider-service 为服务实例的名称
  4. 方式二:通过 zuul-server 访问 provider-service 应用:http://127.0.0.1:8092/provider/service/provider/add?a=3&b=6,这里使用了路由映射规则 /provider/service/**
  5. 若上面通过 zuul-server 访问 provider-service 应用后,都可以正常返回结果,则说明 Zuul 成功发挥了网关的作用
    提示:若在 Zuul 的配置文件中指定了路由映射规则,当向 Zuul Server 发起请求的时候,Zuul 会去 Eureka 注册中心拉取服务列表,如果发现有指定的路由映射规则,就会按照映射规则路由到相应的服务接口

Zuul 路由配置

路由配置简化

1
2
3
4
5
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a

上述的配置中,是一个从 /client/** 路由到 client-a 服务的一个映射规则,它可以简化成如下的简单配置,在这种情况下,Zull 会为 client-a 服务添加一个默认的映射规则 /client/**

1
2
3
zuul:
routes:
client-a: /client/**

单实例 URL 映射

除了路由到服务外,还支持路由到物理笛子,将 serviceId 替换为 url 即可:

1
2
3
4
5
zuul:
routes:
client-a:
path: /client/**
serviceId: http://127.0.0.1:8080

多实例路由映射

在默认情况下,Zuul 会使用 Eureka 中集成的负载均衡功能,如果想要使用 Ribbon 的客户端负载均衡功能,就需要指定一个 serviceId,此操作需要禁止 Ribbon 使用 Eureka。提示:Spring Cloud 在 E 版本之后,新增了负载均衡策略的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
zuul:
routes:
client-a:
path: /ribbon/**
serviceId: client-a

ribbon:
eureka:
enabled: false

client-a:
ribbon:
NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
listOfServers: 127.0.0.1:8001,127.0.0.1:8002

Forward 本地跳转

在 Zuul 中有时候会做一些逻辑处理,先在网关(Zuul Server)中写好一个接口,如下:

1
2
3
4
5
6
7
8
@RestController
public class TestController {

@GetMapping("/add")
public String add(Integer a, Integer b){
return "本地跳转: " + (a + b);
}
}

如果希望在访问 /provider/service 接口的时候,跳转到上面的 add 方法上来处理,就需要用到 Zuul 的本地跳转,配置如下:

1
2
3
4
5
zuul:
routes:
provider-service:
path: /provider/service/**
url: forward:/add

当访问 http://127.0.0.1:8092/provider/service?a=2&b=3,会跳转到 TestController 类的 add 本地方法

相同路径的加载规则

有一种特殊的情况,为一个映射路径指定多个 serviceId 时,那么 Zuul 总是会路由到 YML 配置文件中最后面的那个服务。即在 YML 解释器工作的时候,如果同一个映射路径对应多个服务,按照加载顺序,最后加载的映射规则会把之前的映射规则覆盖掉。

1
2
3
4
5
6
7
8
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a
client-b:
path: /client/**
serviceId: client-b

路由通配符

此外,映射路径 /client/** 之后的 /** 也大有讲究,其还可以配置为 /* 或者 /?,具体规则如下:

zuul-route-rule

Zuul 功能配置

路由前缀

在配置路由规则的时候,可以配置一个统一的代理前缀,下次通过 Zuul 访问后端接口的时候就需要加上这个后缀了。提示,请求路径会变成 /pre/client/add,但实际起作用的是 /client/add,可以使用 stripPrefix=false 来关闭此功能;关闭之后,请求路径是 /pre/client/add,实际起作用的还是 /pre/client/add,一般不推荐使用这个配置。

1
2
3
4
5
6
7
zuul:
prefix: /pre
routes:
client-a:
path: /client/**
serviceId: client-a
stripPrefix: false

敏感头信息

在构建系统的时候,使用 HTTP 的 header 传值是十分方便的,协议的一些认证信息默认也在 header 里,比如 Cookie,或者习惯把基本认证信息通过 BASE64 加密后放在 Authorization 里面,但是如果系统要和外部系统通信,就可能会出现这些信息的泄漏。Zuul 支持在配置文件里面指定敏感头,切断它和下层服务之间的交互,配置如下:

1
2
3
4
5
6
zuul:
routes:
client-a:
path: /client/**
serviceId: client-a
sensitiveHeaders: Cookie,Set-Cookie,Authorization

重定向问题

假设客户端通过 Zuul 请求认证服务,认证成功之后重定向到一个欢迎页面,但是发现重定向的这个欢迎页面的 host 变成了这个认证服务的 host,而不是 Zuul 的 host,直接导致了认证服务地址的暴露(如下图),此时可以使用下述配置来解决:

zuul-redirect

1
2
3
4
5
6
zuul:
add-host-header: true #解决重定向的header问题
routes:
client-a:
path: /client/**
serviceId: client-a

服务屏蔽与路径屏蔽

有时候为了避免某些服务或者路径的侵入,加入 ignored-servicesignored-patterns 之后,可以将它们屏蔽掉:

1
2
3
4
5
6
7
zuul:
ignored-services: client-b #忽略的服务,防服务侵入
ignored-patterns: /**/div/** #忽略的接口,屏蔽接口
routes:
client-a:
path: /client/**
serviceId: client-a