Spring Cloud(八):路由和过滤器之API网关 Zuul

  API 网关是对外服务的一个入口,隐藏了内部架构的实现,是微服务架构不可或缺的一部分。

  Zuul 是 Netflix 基于JVM的路由器和服务器端负载均衡器。Zuul 能够与 Eureka、Ribbon、Hystrix等组件配合使用。

  相关文档可参考Spring Cloud文档:Router and Filter-Zuul Netflix Zuul GitHubZuul Wiki 文档

API 网关

在微服务架构中,随着业务扩展,服务越来越多,对外提供的 API 也快速增加,就有必要对 API 进行统一的管理,包括对 API 访问认证、转发路由、限流、防爬虫等等。

API 网关相当于一道门,在服务调用者和服务提供者中间加了一道隔离层,在隔离层可以做一些逻辑操作。

API 可以管理大量的 API 接口,聚合内部服务,提供统一对外的 API 接口给前端系统调用,屏蔽内部实现细节。

Zuul 介绍

Spring Cloud 集成的 Zuul 是 1.x 版本的,是基于 Http Servlet 和 过滤器开发扩展的。已有 2.x 版本,是基本 Netty 服务器的高性能代理服务。

Zuul 也是 Netflix 公司开发的 OSS 套件中的一员,核心是一系列不同类型的滤器,可以在 HTTP 请求和响应的路由过程中执行一系列的操作,可以对功能进行快速灵活的扩展。

Zuul 扩展了很多功能,比如:

  • 认证签权:对API请求做认证,拒绝非法请求,保护后端服务。无需为所有后端独立管理CORS 和身份验证问题。
  • 动态路由:动态将请求路由到后端不同的服务,将请求 uri 映射到后端路径。
  • 服务迁移:需要服务迁移时,隔离了对前端的影响,在网关层修改 API 映射即可。
  • 请求限流:为各种类型的请求分配容量,并丢弃超出限制的请求。
  • 请求监控:对请求进行监控,收集指标数据并进行统计,以便提供准确的视图。
  • 压力测试:逐渐增加集群流量,以评估性能。
  • 静态响应处理:直接在网关层构建响应,而不将请求转发到内部服务。

Zuul 规则引擎允许任何 JVM 语言编写规则和过滤器,内置了对 Java 和 Groovy 的支持。

备注:zuul.max.host.connections 属性已被 2 个新的属性取代,分别是 zuul.host.maxTotalConnections 和 zuul.host.maxPerRouteConnections,默认值分别是 200 和 20。

备注: 所有路由的默认 Hystrix 隔离模式(ExecutionIsolationStrategy) 是信号量(SEMAPHORE);若要修改隔离模式,可将 zuul.ribbonIsolationStrategy 改为线程(THREAD)。

Zuul 搭建

添加依赖

创建一个网关项目,添加 Zuul 和 Eureka。spring-cloud-starter-netflix-zuul 包还集成了熔断器 Hystrix 、客户端负载均衡 Ribbon,只需做些配置即可启用。

1
2
3
4
5
6
7
8
<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>

启用Zuul代理

项目 Spring Boot 启动类上添加开启 Zuul 代理的注解 @EnableZuulProxy

1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulApplication {

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

开启 Zuul 代理后,Spring Cloud 会创建一个嵌入式 Zuul 代理,会将本地请求转发到代理服务,简化了前端对后端服务的代理调用

简单配置

application.properties

1
2
3
4
server.port=9090
spring.application.name=gateway-zuul
zuul.routes.user.path=/user/**
zuul.routes.user.url=http://localhost:8081/user/

配置地址转发,把本地 /uer/** 路径转换到 http://localhost:8081/user/。
如上配围置示例,8081端口的服务器提供了 /user/getUser API,向网关发送请求 http://localhost:9090/user/getUser,请求会被转换到内部服务 http://localhost:8081/user/getUser ,返回内部服务的接口数据。

集成 Eureka

在配置文件添加注册到 Eureka 服务器配置

1
eureka.client.service-url.defaultZone=http://admin:123456@eureka.master.com:8761/eureka,http://admin:123456@eureka.slave.com:8762/eureka

@EnableZuulProxy 注解集成了由 Spring Cloud 提供熔断器功能的 @EnableCircuitBreaker 注解,开启了熔断器功能,Zuul 自动配置了还注入了 DiscoveryClient Bean,所以加入注册地址即可使用。

重启服务,在网关端就可以基于服务名来访问后端服务了。
通过默认的转发规则来访问 Eureka 中的服务,访问规则是 http://api 网关地址 / 访问服务名 / API接口路径

例如访问用户服务获取用户接口:http://localhost:9090/consumer-service/user/getUser ,与访问 http://localhost:9090/user/getUser ,地址得到同样的结果。

Zuul 路由配置

Zuul 路由实际是对请求进行代理转发,也是反向代理,屏蔽后台服务。使用 @EnableZuulProxy 注解开启 Zuul 代理,所有请求都是在 hystrix command 中执行,所以请求失败会出现在 Hystrix 度量指标中,一旦触发了熔断,代理就不再联系服务器。

Zuul 的属性类是 org.springframework.cloud.netflix.zuul.filters.ZuulProperties,读取的属性前缀是 zuul,Zuul 代理自动配置类 ZuulProxyAutoConfiguration,继承了 ZuulServerAutoConfiguration。

服务路由配置

服务路由配置支持多种方式,非常灵活。

  1. 显式指定服务的路由映射,如下:

    1
    2
    3
    4
    5
    # 规则,以下两条相等
    zuul.routes.your-service-name=you-local-uri
    zuul.routes.your-service-name.path=you-local-uri
    # 示例
    zuul.routes.consumer-service=/userApi/**

    如上的示例,是将本地以 /userApi开头的 URI 路由转发到 consumer-service 服务。例如,向网关发送本地请求 /userApi/user/getUser 被路由转发到 /user/getUser 。

  2. 为每一个服务指定路由转换规则

    如果 zuul.routes 后的第一个 key 不是服名,则需要使用 path 属性指定路由规则,使用 service-id 属性指定服务名或 url 属性指定服务的物理地址。

    1
    2
    3
    4
    zuul.routes.user.path=/userApi/**
    # service-id 或者 url
    zuul.routes.user.service-id=consumer-service
    zuul.routes.user.url=http://localhost:8081/

    URI /userApi/** 后面跟了两个星号,表示可以转到任意层级;如果配配置了一个星号,则只能转换一级。

    注意:如果使用了 path 属性来定义路由规则,则上面第一种方式和通过包含服务器名的路径来访问是无效的。

  3. 使用 url 并指定服务列表

    使用 url 指定服务物理地址,不能使用 Ribbon 对 URL 进行负载均衡,也不能作为 HystrixCommand 执行,要实现这些功能,可以使用静态服务器列表指定ServiceID,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    zuul:
    routes:
    echo:
    path: /myusers/**
    serviceId: myusers-service
    stripPrefix: true

    hystrix:
    command:
    myusers-service:
    execution:
    isolation:
    thread:
    timeoutInMilliseconds: ...

    myusers-service:
    ribbon:
    NIWSServerListClassName: com.netflix.loadbalancer.ConfigurationBasedServerList
    listOfServers: http://example1.com,http://example2.com
    ConnectTimeout: 1000
    ReadTimeout: 3000
    MaxTotalHttpConnections: 500
    MaxConnectionsPerHost: 100
  4. 另一种方式是指定服务路由为 serviceId 配置 Ribbon 客户端,但需要禁用 Ribbon 中的Eureka,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    zuul:
    routes:
    users:
    path: /myusers/**
    serviceId: users

    ribbon:
    eureka:
    enabled: false

    users:
    ribbon:
    listOfServers: example.com,google.com
  5. 还有种方式是使用正则表达式将 serviceId 与路由进行匹配,

    从 serviceId 中提取变量并将其注入到路由模型中,变量必须同时存在与 servicePattern 和 routePattern 中。如果 serviceId 与 servicePattern 不匹配,则使用默认行为。

    1
    2
    3
    4
    5
    6
    7
    @Bean
    public PatternServiceRouteMapper serviceRouteMapper() {
    //PatternServiceRouteMapper(String servicePattern, String routePattern)
    return new PatternServiceRouteMapper(
    "(?<name>^.+)-(?<version>v.+$)",
    "${version}/${name}");
    }

    上面示例解读:将 serviceId 是 myusers-v1 映射到 /v1/myusers/** ,serviceId 从读取自 Eureka 服务注册列表(仅适用于发现的服务)

    此方式不推荐使用,不易理解和阅读,更多可参考 Spring Cloud 官方文档。

路由前缀

提外提供的 API 可能需要配置一个统一的前缀,可通过 zuul.prefix 进行配置,默认跳过前缀,即添加了前缀对路由不会有任何影响。
例如给访问前缀添加 /rest。则访问链接是:http://localhost:9090/rest/userApi/user/getUser/28

1
2
3
4
5
zuul.prefix=/rest
//是否跳过前缀,默认是 true
#zuul.routes.user.strip-prefix=true
zuul.routes.user.path=/userApi/**
zuul.routes.user.service-id=consumer-service

本地跳转

Zuul 的 API 路由还提供了路由重定向,通过 forward 实现。

例如,若需要迁移应用或 API 时,可以使用 Zuul 的 forward 将一些请求重定向到新的端点(uri)。示例:

1
2
3
4
5
6
# 路由重定向,zuul.routes 后面的第一个属性可以是服务名,也可以自定义,不影响
#zuul.routes.gateway-zuul.path=/api/**
#zuul.routes.gateway-zuul.url=forward:/local

zuul.routes.gateway.path=/api/**
zuul.routes.gateway.url=forward:/local

上面示例,将 /api 路由重定向到网关本地服务的 /local 路径上,如请求 /api/zuul/123,被重定向到 /local/zuul/123

1
2
3
4
5
6
7
8
9
@RestController
@RequestMapping("/local")
public class LocalController {

@GetMapping("/zuul/{id}")
public String localZuul(@PathVariable String id){
return "Local Zuul + " + id;
}
}

官方示例:application.yml.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
zuul:
routes:
first:
path: /first/**
url: http://first.example.com
second:
path: /second/**
url: forward:/second
third:
path: /third/**
url: forward:/3rd
legacy:
path: /**
url: http://legacy.example.com

上面示例,忽略遗留(legacy)系统对所有请求的路由映射,这些请求也不与其它路由规则模式匹配。这里的忽略模式并不是完全忽略,只是不被代理处理。

  • /first/** 中的路径已提取到具有外部URL的新服务中。
  • /second/** 中的路径被转发,以便在本地处理它们(例如,使用普通的 Spring @RequestMapping)。
  • /third/**中的路径也被转发,但前缀不同( /third/foo 被转发到 /3rd/foo)。

忽略服务

Zuul 集成了 Eureka 注册到 Eureka 后,默认会自动添加注册列表中的服务作为服务路由路径,但某些底层服务是不直接给前端通过 API网关访问的,而是给在 API 网关后面还有个聚合层的服务调用,那这类服务就不允许通过 API 网关路由访问。在配置文件中添加忽略这些服务,如下。

1
zuul.ignored-services=user-service,consumer-service

这样就无法通过API网关来访问这些服务,也就是网关不对这些服务进行代理转发,这些服务的路由配置失效。

如果忽略的服务匹配了表达式,但又包含在显式配置的路由映射中,那么该服务的忽略是无效的,如下:

1
2
zuul.ignoredServices='*'
zuul.routes.users=/myusers/**

上面示例,users 服务满足了忽略表达式,但包含在显式配置的路由中是,则忽略的服务不包含 users

Zuul Http客户端

Zuul 使用的默认 HTTP客户端现在由 Apache HTTP Client 支持,而不是 Ribbon RestClient。

要使用 RestClientokhttp3.OkHttpClient,需要分别设置 ribbon.restclient.enabled = trueribbon.okhttp.enabled = true

如果要自定义 Apache HTTP 客户端或 OK HTTP 客户端,需要提供 ClosableHttpClientOkHttpClient 类型的bean。

Cookies和Headres

敏感 Headers

同一个系统中的服务之间可以共享头信息,若不希望敏感的头信息泄漏到外部服务器,可以在路由配置中指定敏感的头列表,则代理会屏蔽这个敏感头信息不暴露给外部。

Cookie 实际也是属性头信息,头名是 Set-Cookie

可以为每个路由配置敏感头信息,允许配置多个,用逗号分隔,如以下示例所示:

1
2
3
zuul.routes.users.path=/myusers/**
zuul.routes.users.sensitiveHeaders=Cookie,Set-Cookie,Authorization
zuul.routes.users.url=https://downstream

注意:上面 sensitiveHeaders 的配置也是默认值,这些信息不暴露给外部,如果不需要改变则无需设置。

sensitiveHeaders 相当于一个黑名单,默认是不为空,如果允许 Zuul 发送所有头信息,必须设置为空。如果要将 cookie 或指定的头信息传给后端,则必须将其从黑名单中去除。

1
2
3
zuul.routes.users.path=/myusers/**
zuul.routes.users.sensitiveHeaders=
zuul.routes.users.url=https://downstream

还可通过 zuul.sensitiveHeaders 来设置敏感头信息。如果在路由上设置了 sensitiveHeaders ,它将覆盖全局 sensitiveHeaders 设置。

忽略 Headres

除了设置路由敏感的头信息,还可以通过 zuul.ignoredheaders 设置全局忽略的请求和响应头,这些忽略头值在与下游服务交互期间会被丢弃。

默认情况下,并不包含 Spring Security 包,ignoredheaders 是空的。否则,它们被初始化由 Spring Security 所指定的一组的“安全”头(例如,涉及缓存),此情况下,若下游服务也可以添加头信息,但需要使用代理的值非常有用。

若添加了 Spring Security 包,但不想丢弃指定的这些安全头,可以将 zuul.ignoreSecurityHeaders 设置为 false。如果在 Spring Security 中禁用了 HTTP 安全响应头并希望下游服务提供的值,这样做可能很有用。

Zuul 文件上传

可通过 Zuul 可使用代理路径上传文件,默认允许上传文件的大小是 1MB(spring.servlet.multipart.max-file-size=1MB)。

1
2
3
4
# 允许上传的最大文件大小
spring.servlet.multipart.max-file-size=1MB
# 允许请求的大小
spring.servlet.multipart.max-request-size=10MB

如果有路由 zuul.routes.customers=/customers/**,那么上传文件发布到/zuul/customers/*

若要绕过网关,让后端各个服务自己控制上传文件的大小,即绕过 /zuul/* 的Spring DispatcherServlet(以避免多部分处理),可通过设置 zuul.servletPath 让 Servlet 路径外部化。

1
zuul.servlet-path=/

如果代理路由还使用了 Ribbon 负载均衡,对于上传大文件还需要增加超时时长。如下示例:

1
2
3
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000

注意:要使用流来上传大型文件,可以在请求中使用分块编码(这不是某些浏览器的默认处理方式),如以下示例所示:

1
2
$ curl -v -H "Transfer-Encoding: chunked" \
-F "file=@mylarge.iso" localhost:9999/zuul/simple/file

网关其它参考

  1. Spring Cloud Gateway

    Zuul 1.x 网关官方已不做大的更新,Zuul 2.x 已闭源。所以 Spring 自己开发了网关组件。

  2. Kong kong

    基于Nginx+Lua进行二次开发的方案。

  3. Orange

    一个基于OpenResty / Nginx的HTTP API Gateway

  4. 自研网关方案思路

    • 基于Nginx+Lua+ OpenResty的方案,Kong 和 Orange。
    • 基于Netty、非阻塞IO模型。通过网上搜索可以看到国内的宜人贷等一些公司是基于这种方案,是一种成熟的方案。
    • 基于Node.js的方案。这种方案是应用了Node.js天生的非阻塞的特性。
    • 基于java Servlet的方案。zuul基于的就是这种方案,这种方案的效率不高,这也是zuul总是被诟病的原因。
  5. ServiceMesh,Istio 目前发展非常迅速。

Spring Cloud(八):路由和过滤器之API网关 Zuul

http://blog.gxitsky.com/2019/04/01/SpringCloud-08-route-filter-Zuul/

作者

光星

发布于

2019-04-01

更新于

2022-06-17

许可协议

评论