Spring Cloud(八):路由和过滤器之API网关 Zuul
API 网关是对外服务的一个入口,隐藏了内部架构的实现,是微服务架构不可或缺的一部分。
Zuul 是 Netflix 基于JVM的路由器和服务器端负载均衡器。Zuul 能够与 Eureka、Ribbon、Hystrix等组件配合使用。
相关文档可参考Spring Cloud文档:Router and Filter-Zuul Netflix Zuul GitHub,Zuul 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 | <dependency> |
启用Zuul代理
项目 Spring Boot 启动类上添加开启 Zuul 代理的注解 @EnableZuulProxy
1 |
|
开启 Zuul 代理后,Spring Cloud 会创建一个嵌入式 Zuul 代理,会将本地请求转发到代理服务,简化了前端对后端服务的代理调用
简单配置
application.properties
1 | 9090 = |
配置地址转发,把本地 /uer/**
路径转换到 http://localhost:8081/user/。
如上配围置示例,8081端口的服务器提供了 /user/getUser
API,向网关发送请求 http://localhost:9090/user/getUser,请求会被转换到内部服务 http://localhost:8081/user/getUser ,返回内部服务的接口数据。
集成 Eureka
在配置文件添加注册到 Eureka 服务器配置
1 | 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
2
3
4
5# 规则,以下两条相等
you-local-uri =
you-local-uri =
# 示例
/userApi/** =如上的示例,是将本地以
/userApi
开头的 URI 路由转发到 consumer-service 服务。例如,向网关发送本地请求 /userApi/user/getUser 被路由转发到 /user/getUser 。为每一个服务指定路由转换规则
如果 zuul.routes 后的第一个 key 不是服名,则需要使用 path 属性指定路由规则,使用 service-id 属性指定服务名或 url 属性指定服务的物理地址。
1
2
3
4/userApi/** =
# service-id 或者 url
consumer-service =
http://localhost:8081/ =URI /userApi/** 后面跟了两个星号,表示可以转到任意层级;如果配配置了一个星号,则只能转换一级。
注意:如果使用了 path 属性来定义路由规则,则上面第一种方式和通过包含服务器名的路径来访问是无效的。
使用 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
23zuul:
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另一种方式是指定服务路由为
serviceId
配置 Ribbon 客户端,但需要禁用 Ribbon 中的Eureka,如下:1
2
3
4
5
6
7
8
9
10
11
12
13zuul:
routes:
users:
path: /myusers/**
serviceId: users
ribbon:
eureka:
enabled: false
users:
ribbon:
listOfServers: example.com,google.com还有种方式是使用正则表达式将 serviceId 与路由进行匹配,
从 serviceId 中提取变量并将其注入到路由模型中,变量必须同时存在与 servicePattern 和 routePattern 中。如果 serviceId 与 servicePattern 不匹配,则使用默认行为。
1
2
3
4
5
6
7
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 | /rest = |
本地跳转
Zuul 的 API 路由还提供了路由重定向,通过 forward 实现。
例如,若需要迁移应用或 API 时,可以使用 Zuul 的 forward 将一些请求重定向到新的端点(uri)。示例:
1 | # 路由重定向,zuul.routes 后面的第一个属性可以是服务名,也可以自定义,不影响 |
上面示例,将 /api 路由重定向到网关本地服务的 /local 路径上,如请求 /api/zuul/123,被重定向到 /local/zuul/123
1 |
|
官方示例:application.yml.
1 | zuul: |
上面示例,忽略遗留(legacy)系统对所有请求的路由映射,这些请求也不与其它路由规则模式匹配。这里的忽略模式并不是完全忽略,只是不被代理处理。
- /first/** 中的路径已提取到具有外部URL的新服务中。
- /second/** 中的路径被转发,以便在本地处理它们(例如,使用普通的 Spring @RequestMapping)。
- /third/**中的路径也被转发,但前缀不同( /third/foo 被转发到 /3rd/foo)。
忽略服务
Zuul 集成了 Eureka 注册到 Eureka 后,默认会自动添加注册列表中的服务作为服务路由路径,但某些底层服务是不直接给前端通过 API网关访问的,而是给在 API 网关后面还有个聚合层的服务调用,那这类服务就不允许通过 API 网关路由访问。在配置文件中添加忽略这些服务,如下。
1 | user-service,consumer-service = |
这样就无法通过API网关来访问这些服务,也就是网关不对这些服务进行代理转发,这些服务的路由配置失效。
如果忽略的服务匹配了表达式,但又包含在显式配置的路由映射中,那么该服务的忽略是无效的,如下:
1 | '*' = |
上面示例,users 服务满足了忽略表达式,但包含在显式配置的路由中是,则忽略的服务不包含 users 。
Zuul Http客户端
Zuul 使用的默认 HTTP客户端现在由 Apache HTTP Client 支持,而不是 Ribbon RestClient。
要使用 RestClient 或 okhttp3.OkHttpClient,需要分别设置 ribbon.restclient.enabled = true 或 ribbon.okhttp.enabled = true。
如果要自定义 Apache HTTP 客户端或 OK HTTP 客户端,需要提供 ClosableHttpClient 或 OkHttpClient 类型的bean。
Cookies和Headres
敏感 Headers
同一个系统中的服务之间可以共享头信息,若不希望敏感的头信息泄漏到外部服务器,可以在路由配置中指定敏感的头列表,则代理会屏蔽这个敏感头信息不暴露给外部。
Cookie 实际也是属性头信息,头名是 Set-Cookie。
可以为每个路由配置敏感头信息,允许配置多个,用逗号分隔,如以下示例所示:
1 | /myusers/** = |
注意:上面 sensitiveHeaders 的配置也是默认值,这些信息不暴露给外部,如果不需要改变则无需设置。
sensitiveHeaders 相当于一个黑名单,默认是不为空,如果允许 Zuul 发送所有头信息,必须设置为空。如果要将 cookie 或指定的头信息传给后端,则必须将其从黑名单中去除。
1 | /myusers/** = |
还可通过 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 | # 允许上传的最大文件大小 |
如果有路由 zuul.routes.customers=/customers/**
,那么上传文件发布到/zuul/customers/*
。
若要绕过网关,让后端各个服务自己控制上传文件的大小,即绕过 /zuul/*
的Spring DispatcherServlet(以避免多部分处理),可通过设置 zuul.servletPath
让 Servlet 路径外部化。
1 | / = |
如果代理路由还使用了 Ribbon 负载均衡,对于上传大文件还需要增加超时时长。如下示例:
1 | 60000 = |
注意:要使用流来上传大型文件,可以在请求中使用分块编码(这不是某些浏览器的默认处理方式),如以下示例所示:
1 | curl -v -H "Transfer-Encoding: chunked" \ |
网关其它参考
-
Zuul 1.x 网关官方已不做大的更新,Zuul 2.x 已闭源。所以 Spring 自己开发了网关组件。
-
基于Nginx+Lua进行二次开发的方案。
-
一个基于OpenResty / Nginx的HTTP API Gateway
自研网关方案思路
- 基于Nginx+Lua+ OpenResty的方案,Kong 和 Orange。
- 基于Netty、非阻塞IO模型。通过网上搜索可以看到国内的宜人贷等一些公司是基于这种方案,是一种成熟的方案。
- 基于Node.js的方案。这种方案是应用了Node.js天生的非阻塞的特性。
- 基于java Servlet的方案。zuul基于的就是这种方案,这种方案的效率不高,这也是zuul总是被诟病的原因。
ServiceMesh,Istio 目前发展非常迅速。
Spring Cloud(八):路由和过滤器之API网关 Zuul
http://blog.gxitsky.com/2019/04/01/SpringCloud-08-route-filter-Zuul/