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,只需做些配置即可启用。
<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
@SpringBootApplication
@EnableZuulProxy
public class GatewayZuulApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayZuulApplication.class, args);
}
}
开启 Zuul 代理后,Spring Cloud 会创建一个嵌入式 Zuul 代理,会将本地请求转发到代理服务,简化了前端对后端服务的代理调用
简单配置
application.properties
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 服务器配置
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。
服务路由配置
服务路由配置支持多种方式,非常灵活。
显式指定服务的路由映射,如下:
# 规则,以下两条相等 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 。为每一个服务指定路由转换规则
如果 zuul.routes 后的第一个 key 不是服名,则需要使用 path 属性指定路由规则,使用 service-id 属性指定服务名或 url 属性指定服务的物理地址。
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 属性来定义路由规则,则上面第一种方式和通过包含服务器名的路径来访问是无效的。
使用 url 并指定服务列表
使用 url 指定服务物理地址,不能使用 Ribbon 对 URL 进行负载均衡,也不能作为 HystrixCommand 执行,要实现这些功能,可以使用静态服务器列表指定ServiceID,如下所示:
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
另一种方式是指定服务路由为
serviceId
配置 Ribbon 客户端,但需要禁用 Ribbon 中的Eureka,如下:zuul: routes: users: path: /myusers/** serviceId: users ribbon: eureka: enabled: false users: ribbon: listOfServers: example.com,google.com
还有种方式是使用正则表达式将 serviceId 与路由进行匹配,
从 serviceId 中提取变量并将其注入到路由模型中,变量必须同时存在与 servicePattern 和 routePattern 中。如果 serviceId 与 servicePattern 不匹配,则使用默认行为。
@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
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)。示例:
# 路由重定向,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
@RestController
@RequestMapping("/local")
public class LocalController {
@GetMapping("/zuul/{id}")
public String localZuul(@PathVariable String id){
return "Local Zuul + " + id;
}
}
官方示例:application.yml.
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 网关路由访问。在配置文件中添加忽略这些服务,如下。
zuul.ignored-services=user-service,consumer-service
这样就无法通过API网关来访问这些服务,也就是网关不对这些服务进行代理转发,这些服务的路由配置失效。
如果忽略的服务匹配了表达式,但又包含在显式配置的路由映射中,那么该服务的忽略是无效的,如下:
zuul.ignoredServices='*'
zuul.routes.users=/myusers/**
上面示例,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。
可以为每个路由配置敏感头信息,允许配置多个,用逗号分隔,如以下示例所示:
zuul.routes.users.path=/myusers/**
zuul.routes.users.sensitiveHeaders=Cookie,Set-Cookie,Authorization
zuul.routes.users.url=https://downstream
注意:上面 sensitiveHeaders 的配置也是默认值,这些信息不暴露给外部,如果不需要改变则无需设置。
sensitiveHeaders 相当于一个黑名单,默认是不为空,如果允许 Zuul 发送所有头信息,必须设置为空。如果要将 cookie 或指定的头信息传给后端,则必须将其从黑名单中去除。
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)。
# 允许上传的最大文件大小
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 路径外部化。
zuul.servlet-path=/
如果代理路由还使用了 Ribbon 负载均衡,对于上传大文件还需要增加超时时长。如下示例:
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000
注意:要使用流来上传大型文件,可以在请求中使用分块编码(这不是某些浏览器的默认处理方式),如以下示例所示:
$ curl -v -H "Transfer-Encoding: chunked" \
-F "file=@mylarge.iso" localhost:9999/zuul/simple/file
网关其它参考
-
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 目前发展非常迅速。
注意:本文归作者所有,未经作者允许,不得转载