本文较大篇幅引用https://www.mrhelloworld.com/zuul/,相关内容版权归该文章作者所有
什么是 Zuul
Zuul 是从设备和网站到应用程序后端的所有请求的前门。作为边缘服务应用程序,Zuul 旨在实现动态路由,监视,弹性和安全性。Zuul 包含了对请求的路由和过滤两个最主要的功能。
Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产试图
- 动态路由:动态地将请求路由到不同的后端集群
- 压力测试:逐渐增加只想集群的流量,以了解性能
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 静态响应处理:在边缘位置直接建立部份响应,从而避免其转发到内部集群\
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样化,以及让系统的边缘更贴近系统的使用者
什么是服务网关
API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为企业级应用防火墙
,主要起到隔离外部访问与内部系统的作用
。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API 网关的流行,源于近几年来移动应用与企业间互联需求的兴起。移动应用、企业互联,使得后台服务支持的对象,从以前单一的 Web 应用,扩展到多种使用场景,且每种使用场景对后台服务的要求都不尽相同。这不仅增加了后台服务的响应量,还增加了后台服务的复杂性。随着微服务架构概念的提出,API 网关成为了微服务架构的一个标配组件
。
API 网关是一个服务器,是系统对外的唯一入口。API 网关封装了系统内部架构,为每个客户端提供定制的 API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API 网关并不是微服务场景中必须的组件,如下图,不管有没有 API 网关,后端微服务都可以通过 API 很好地支持客户端的访问。
但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入 API 网关也有一系列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
- 聚合后台服务,节省流量,提高性能,提升用户体验
- 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能
为什么要使用网关
- 单体应用:浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对于单体应用来说是不需要网关的。
- 微服务:微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/软件工具)想要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把请求转发到微服务实例。这其中的记忆功能就全部交由网关来操作了。
总结
如果让客户端直接与各个微服务交互:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 身份认证问题,每个微服务需要独立身份认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务
- 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难
因此,我们需要网关介于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只需要与网关交互,只需要知道网关地址即可。这样便简化了开发且有以下优点:
- 易于监控,可在微服务网关收集监控数据并将其推送到外部系统进行分析
- 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,从而无需在每个微服务中进行认证
- 减少了客户端与各个微服务之间的交互次数
网关解决了什么问题
网关具有身份认证与安全、审查与监控、动态路由、负载均衡、缓存、请求分片与管理、静态响应处理等功能。当然最主要的职责还是与“外界联系”。
总结一下,网关应当具备以下功能:
- 性能:API 高可用,负载均衡,容错机制。
- 安全:权限身份认证、脱敏,流量清洗,后端签名(保证全链路可信调用),黑名单(非法调用的限制)。
- 日志:日志记录,一旦涉及分布式,全链路跟踪必不可少。
- 缓存:数据缓存。
- 监控:记录请求响应数据,API 耗时分析,性能监控。
- 限流:流量控制,错峰流控,可以定义多种限流规则。
- 灰度:线上灰度部署,可以减小风险。
- 路由:动态路由规则。
Zuul 是 Netflix 公司开源的一个 API 网关组件,Spring Cloud 对其进行二次基于 Spring Boot 的注解式封装做到开箱即用。
目前来说,结合 Sring Cloud 提供的服务治理体系,可以做到请求转发,根据配置或者默认的路由规则进行路由和 Load Balance,无缝集成 Hystrix。
虽然可以通过自定义 Filter 实现我们想要的功能,但是由于 Zuul 本身的设计是基于单线程的接收请求和转发处理
,是阻塞 IO,不支持长连接。
目前来看 Zuul 就显得很鸡肋,随着 Zuul 2.x 一直跳票(2019 年 5 月发布了 Zuul 2.0 版本),Spring Cloud 推出自己的 Spring Cloud Gateway。
大意就是:Zuul 已死,Spring Cloud Gateway 永生(手动狗头)。但是我们这里还是先学一下
环境准备
zuul-demo
聚合工程。SpringBoot 2.2.4.RELEASE
、Spring Cloud Hoxton.SR1
。
eureka-server
:注册中心eureka-server02
:注册中心product-service
:商品服务,提供了根据主键查询商品接口http://localhost:7070/product/{id}
order-service
:订单服务,提供了根据主键查询订单接口http://localhost:9090/order/{id}
且订单服务调用商品服务。
Zuul 实现 API 网关
搭建网关服务
1.创建项目
创建 zuul-server
项目。
2.添加依赖
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 com.example zuul-server 1.0-SNAPSHOT com.example zuul-demo 1.0-SNAPSHOT org.springframework.cloud spring-cloud-starter-netflix-zuul
3.配置文件
server: port: 9000 # 端口 spring: application: name: zuul-server # 应用名称
4.启动类
启动类需要开启 @EnableZuulProxy
注解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication // 开启 Zuul 注解 @EnableZuulProxy public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
配置路由规则
URL 地址路由
# 路由规则 zuul: routes: product-service: # 路由 id 自定义 path: /product-service/** # 配置请求 url 的映射路径 url: http://localhost:7070/ # 映射路径对应的微服务地址
通配符含义:
访问:http://localhost:9000/product-service/product/1 结果如下:
相当于访问http://localhost:7070/product/1
服务名称路由
微服务一般是由几十、上百个服务组成,对于 URL 地址路由的方式,如果对每个服务实例手动指定一个唯一访问地址,这样做显然是不合理的。
Zuul 支持与 Eureka 整合开发,根据 serviceId 自动从注册中心获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改 Zuul 的路由配置。
1.添加 Eureka Client 依赖
org.springframework.cloud spring-cloud-starter-netflix-eureka-client
2.配置注册中心和路由规则
# 路由规则 zuul: routes: product-service: # 路由 id 自定义 path: /product-service/** # 配置请求 url 的映射路径 serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求 # 配置 Eureka Server 注册中心 eureka: instance: prefer-ip-address: true # 是否使用 ip 地址注册 instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port client: service-url: # 设置服务注册中心地址 defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
3.启动类
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication // 开启 Zuul 注解 @EnableZuulProxy // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
4.访问
访问:http://localhost:9000/product-service/product/1 结果如下:
简化路由配置
Zuul 为了方便大家使用,提供了默认路由配置:路由 id 和 微服务名称
一致,path 默认对应 /微服务名称/**
,所以以下配置就没必要再写了。
# 路由规则 zuul: routes: product-service: # 路由 id 自定义 path: /product-service/** # 配置请求 url 的映射路径 serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求
访问
此时我们并没有配置任何订单服务的路由规则,访问:http://localhost:9000/order-service/order/1 结果如下:
路由排除
我们可以通过路由排除设置不允许被访问的资源。允许被访问的资源可以通过路由规则进行设置。
1.URL 地址排除
# 路由规则 zuul: ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径 # 不受路由排除影响 routes: product-service: # 路由 id 自定义 path: /product-service/** # 配置请求 url 的映射路径 serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求
2.服务名称排除
# 路由规则 zuul: ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有 # 不受路由排除影响 routes: product-service: # 路由 id 自定义 path: /product-service/** # 配置请求 url 的映射路径 serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求
路由前缀
zuul:
prefix: /api
访问
访问:http://localhost:9000/api/product-service/product/1 结果如下:
网关过滤器
Zuul 包含了对请求的路由和过滤两个核心功能,其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;
而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。然而实际上,路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的。
路由映射主要通过 pre
类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;
而请求转发的部分则是由 routing
类型的过滤器来完成,对 pre
类型过滤器获得的路由地址进行转发。
所以说,过滤器可以说是 Zuul 实现 API 网关功能最核心的部件,每一个进入 Zuul 的 http 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
关键名词
- 类型:定义路由流程中应用过滤器的阶段。共 pre、routing、post、error 4 个类型。
- 执行顺序:在同类型中,定义过滤器执行的顺序。比如多个 pre 类型的执行顺序。
- 条件:执行过滤器所需的条件。true 开启,false 关闭。
- 动作:如果符合条件,将执行的动作。具体操作
过滤器类型
- pre:请求被路由到源服务器之前执行的过滤器
- 身份认证
- 选路由
- 请求日志
- routing:处理将请求发送到源服务器的过滤器
- post:响应从源服务器返回时执行的过滤器
- 对响应增加 HTTP 头
- 收集统计和度量指标
- 将响应以流的方式发送回客户端
- error:上述阶段中出现错误时执行的过滤器
入门案例
1.创建过滤器
Spring Cloud Netflix Zuul 中实现过滤器必须包含 4 个基本特征:过滤器类型,执行顺序,执行条件,动作(具体操作)。这些步骤都是 ZuulFilter
接口中定义的 4 个抽象方法:
package com.example.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * 网关过滤器 */ @Component public class CustomFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(CustomFilter.class); /** * 过滤器类型 * pre * routing * post * error * * @return */ @Override public String filterType() { return "pre"; } /** * 执行顺序 * 数值越小,优先级越高 * * @return */ @Override public int filterOrder() { return 0; } /** * 执行条件 * true 开启 * false 关闭 * * @return */ @Override public boolean shouldFilter() { return true; } /** * 动作(具体操作) * 具体逻辑 * * @return * @throws ZuulException */ @Override public Object run() throws ZuulException { // 获取请求上下文 RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); logger.info("CustomFilter...method={}, url={}", request.getMethod(), request.getRequestURL().toString()); return null; } }
filterType
:该函数需要返回一个字符串代表过滤器的类型,而这个类型就是在 http 请求过程中定义的各个阶段。在 Zuul 中默认定义了 4 个不同的生命周期过程类型,具体如下:- pre:请求被路由之前调用
- routing: 路由请求时被调用
- post:routing 和 error 过滤器之后被调用
- error:处理请求时发生错误时被调用
filterOrder
:通过 int 值来定义过滤器的执行顺序,数值越小优先级越高。shouldFilter
:返回一个 boolean 值来判断该过滤器是否要执行。run
:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续路由,或是在请求路由返回结果之后,对处理结果做一些加工等。
访问
访问:http://localhost:9000/product-service/product/1 控制台输出如下:
CustomFilter...method=GET, url=http://localhost:9000/product-service/product/1
统一鉴权
接下来我们在网关过滤器中通过 token 判断用户是否登录,完成一个统一鉴权案例。
package com.example.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.PrintWriter; /** * 权限验证过滤器 */ @Component public class AccessFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(AccessFilter.class); @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 1; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { // 获取请求上下文 RequestContext rc = RequestContext.getCurrentContext(); HttpServletRequest request = rc.getRequest(); // 获取表单中的 token String token = request.getParameter("token"); // 业务逻辑处理 if (null == token) { logger.warn("token is null..."); // 请求结束,不在继续向下请求。 rc.setSendZuulResponse(false); // 响应状态码,HTTP 401 错误代表用户没有访问权限 rc.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{\"message\":\"" + HttpStatus.UNAUTHORIZED.getReasonPhrase() + "\"}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } } else { // 使用 token 进行身份验证 logger.info("token is OK!"); } return null; } }
访问
访问:http://localhost:9000/product-service/product/1 结果如下:
访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
Zuul 请求的生命周期
- HTTP 发送请求到 Zuul 网关
- Zuul 网关首先经过 pre filter
- 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出错,则执行 error filter
- 继续往下执行 post filter
- 最后返回响应给 HTTP 客户端
网关过滤器异常统一处理
创建过滤器
package com.example.filter; import com.netflix.zuul.ZuulFilter; import com.netflix.zuul.context.RequestContext; import com.netflix.zuul.exception.ZuulException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import java.io.IOException; import java.io.PrintWriter; /** * 异常过滤器 */ @Component public class ErrorFilter extends ZuulFilter { private static final Logger logger = LoggerFactory.getLogger(ErrorFilter.class); @Override public String filterType() { return "error"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext rc = RequestContext.getCurrentContext(); Throwable throwable = rc.getThrowable(); logger.error("ErrorFilter..." + throwable.getCause().getMessage(), throwable); // 响应状态码,HTTP 500 服务器错误 rc.setResponseStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); // 响应类型 rc.getResponse().setContentType("application/json; charset=utf-8"); PrintWriter writer = null; try { writer = rc.getResponse().getWriter(); // 响应内容 writer.print("{\"message\":\"" + HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase() + "\"}"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != writer) writer.close(); } return null; } }
模拟异常
在 pre 过滤器中添加模拟异常代码。
// 模拟异常 Integer.parseInt("zuul");
配置文件
禁用 Zuul 默认的异常处理 filter:SendErrorFilter
zuul: # 禁用 Zuul 默认的异常处理 filter SendErrorFilter: error: disable: true
访问
访问:http://localhost:9000/product-service/product/1 结果如下:
Zuul 和 Hystrix 无缝结合
在 Spring Cloud 中,Zuul 启动器中包含了 Hystrix 相关依赖,
在 Zuul 网关工程中,默认是提供了 Hystrix Dashboard 服务监控数据的(hystrix.stream),但是不会提供监控面板的界面展示。在 Spring Cloud 中,Zuul 和 Hystrix 是无缝结合的,我们可以非常方便的实现网关容错处理。
网关监控
Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。
但是需要开启数据监控的项目中要添加 dashboard
依赖。
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId> dependency>
配置文件
在配置文件中开启 hystrix.stream
端点。
# 度量指标监控与健康检查
management:
endpoints:
web:
exposure:
include: hystrix.stream
启动类
在需要开启数据监控的项目启动类中添加 @EnableHystrixDashboard
注解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; @SpringBootApplication // 开启 Zuul 注解 @EnableZuulProxy // 开启数据监控注解 @EnableHystrixDashboard public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
访问并查看数据
访问:http://localhost:9000/hystrix 监控中心界面如下:
请求多次:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
网关熔断
在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider
用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口 FallbackProvider
来提供 fallback 处理。
Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。
因为对于Zuul网关来说,做请求路由分发的时候,结果由远程服务运算。远程服务反馈了异常信息,Zuul 网关不会处理异常,因为无法确定这个错误是否是应用程序真实想要反馈给客户端的。
代码示例
ProductProviderFallback.java
package com.example.fallback; import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpResponse; import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; /** * 对商品服务做服务容错处理 */ @Component public class ProductProviderFallback implements FallbackProvider { /** * return - 返回 fallback 处理哪一个服务。返回的是服务的名称。 * 推荐 - 为指定的服务定义特性化的 fallback 逻辑。 * 推荐 - 提供一个处理所有服务的 fallback 逻辑。 * 好处 - 某个服务发生超时,那么指定的 fallback 逻辑执行。如果有新服务上线,未提供 fallback 逻辑,有一个通用的。 */ @Override public String getRoute() { return "product-service"; } /** * 对商品服务做服务容错处理 * * @param route 容错服务名称 * @param cause 服务异常信息 * @return */ @Override public ClientHttpResponse fallbackResponse(String route, Throwable cause) { return new ClientHttpResponse() { /** * 设置响应的头信息 * @return */ @Override public HttpHeaders getHeaders() { HttpHeaders header = new HttpHeaders(); header.setContentType(new MediaType("application", "json", Charset.forName("utf-8"))); return header; } /** * 设置响应体 * Zuul 会将本方法返回的输入流数据读取,并通过 HttpServletResponse 的输出流输出到客户端。 * @return */ @Override public InputStream getBody() throws IOException { return new ByteArrayInputStream("{\"message\":\"商品服务不可用,请稍后再试。\"}".getBytes()); } /** * ClientHttpResponse 的 fallback 的状态码 返回 HttpStatus * @return */ @Override public HttpStatus getStatusCode() throws IOException { return HttpStatus.INTERNAL_SERVER_ERROR; } /** * ClientHttpResponse 的 fallback 的状态码 返回 int * @return */ @Override public int getRawStatusCode() throws IOException { return this.getStatusCode().value(); } /** * ClientHttpResponse 的 fallback 的状态码 返回 String * @return */ @Override public String getStatusText() throws IOException { return this.getStatusCode().getReasonPhrase(); } /** * 回收资源方法 * 用于回收当前 fallback 逻辑开启的资源对象。 */ @Override public void close() { } }; } }
访问
关闭商品服务,访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
网关限流
顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。
通过限流,我们可以很好地控制系统的 QPS,从而达到保护系统的目的。Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要提供 error 错误处理机制即可。
1.,为什么需要限流
比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(好事)
- 因为某个热点事件(微博热搜)
- 竞争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知道什么时候会有 10 倍甚至 20 倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的
从上图可以看出,对内而言:上游的 A、B 服务直接依赖了下游的基础服务 C,对于 A,B 服务都依赖的基础服务 C 这种场景,
服务 A 和 B 其实处于某种竞争关系,如果服务 A 的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务 C 并影响服务 B,即雪崩效应。
添加依赖
Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要添加 Redis 组件。
RateLimit 官网文档:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
<dependency> <groupId>com.marcosbarbero.cloudgroupId> <artifactId>spring-cloud-zuul-ratelimitartifactId> <version>2.3.0.RELEASEversion> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-data-redisartifactId> dependency> <dependency> <groupId>org.apache.commonsgroupId> <artifactId>commons-pool2artifactId> dependency>
全局限流配置
使用全局限流配置,Zuul 会对代理的所有服务提供限流保护。
server:
port: 9000 # 端口
spring:
application:
name: zuul-server # 应用名称
# redis 缓存
redis:
timeout: 10000 # 连接超时时间
host: 192.168.10.101 # Redis服务器地址
port: 6379 # Redis服务器端口
password: root # Redis服务器密码
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 1024 # 最大连接数,默认 8
max-wait: 10000 # 最大连接阻塞等待时间,单位毫秒,默认 -1
max-idle: 200 # 最大空闲连接,默认 8
min-idle: 5 # 最小空闲连接,默认 0
# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true # 是否使用 ip 地址注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # ip:port
client:
service-url: # 设置服务注册中心地址
defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
zuul:
# 服务限流
ratelimit:
# 开启限流保护
enabled: true
# 限流数据存储方式
repository: REDIS
# default-policy-list 默认配置,全局生效
default-policy-list:
- limit: 3
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
Zuul-RateLimiter 基本配置项:
Bucket4j 实现需要相关的 bean @Qualifier(“RateLimit”):
- JCache - javax.cache.Cache
- Hazelcast - com.hazelcast.core.IMap
- Ignite - org.apache.ignite.IgniteCache
- Infinispan - org.infinispan.functional.ReadWriteMap
Policy 限流策略配置项说明:
访问
访问:http://localhost:9000/product-service/product/1?token=abc123 控制台结果如下:
ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS
局部限流配置
使用局部限流配置,Zuul 仅针对配置的服务提供限流保护。
zuul:
# 服务限流
ratelimit:
# 开启限流保护
enabled: true
# 限流数据存储方式
repository: REDIS
# policy-list 自定义配置,局部生效
policy-list:
# 指定需要被限流的服务名称
order-service:
- limit: 5
refresh-interval: 60 # 60s 内请求超过 5 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
访问:http://localhost:9000/order-service/order/1?token=abc123 控制台结果如下:
ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS
自定义限流策略
如果希望自己控制限流策略,可以通过自定义 RateLimitKeyGenerator
的实现来增加自己的策略逻辑。
修改商品服务控制层代码如下,添加 /product/single
:
package com.example.controller; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) { return productService.selectProductById(id); } /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/single") public Product selectProductSingle(Integer id) { return productService.selectProductById(id); } }
自定义限流策略类。
package com.example.ratelimit; import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.RateLimitUtils; import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.config.properties.RateLimitProperties; import com.marcosbarbero.cloud.autoconfigure.zuul.ratelimit.support.DefaultRateLimitKeyGenerator; import org.springframework.cloud.netflix.zuul.filters.Route; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; /** * 自定义限流策略 */ @Component public class RateLimitKeyGenerator extends DefaultRateLimitKeyGenerator { public RateLimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) { super(properties, rateLimitUtils); } /** * 限流逻辑 * * @param request * @param route * @param policy * @return */ @Override public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) { // 对请求参数中相同的 id 值进行限流 return super.key(request, route, policy) + ":" + request.getParameter("id"); } }
多次访问:http://localhost:9000/api/product-service/product/single?token=abc123&id=1 被限流后,马上更换 id=2
重新访问发现服务任然可用,再继续多次访问,发现更换过的 id=2
也被限流了。Redis 信息如下:
127.0.0.1:6379> keys * 1) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:1" 2) "zuul-server:product-service:0:0:0:0:0:0:0:1:/product/single:anonymous:2"
错误处理
配置 error
类型的网关过滤器进行处理即可。修改之前的 ErrorFilter
让其变的通用。
网关调优
使用 Zuul 的 Spring Cloud 微服务结构图:
从上图中可以看出。整体请求逻辑还是比较复杂的,在没有 Zuul 网关的情况下,client 请求 service 的时候,也有请求超时的可能。那么当增加了 Zuul 网关的时候,请求超时的可能就更明显了。
当请求通过 Zuul 网关路由到服务,并等待服务返回响应,这个过程中 Zuul 也有超时控制。Zuul 的底层使用的是 Hystrix + Ribbon 来实现请求路由。
Zuul 中的 Hystrix 内部使用线程池隔离机制提供请求路由实现,其默认的超时时长为 1000 毫秒。Ribbon 底层默认超时时长为 5000 毫秒。
如果 Hystrix 超时,直接返回超时异常。
如果 Ribbon 超时,同时 Hystrix 未超时,Ribbon 会自动进行服务集群轮询重试,直到 Hystrix 超时为止。如果 Hystrix 超时时长小于 Ribbon 超时时长,Ribbon 不会进行服务集群轮询重试。
配置文件
Zuul 中可配置的超时时长有两个位置:Hystrix 和 Ribbon。具体配置如下:
zuul: # 开启 Zuul 网关重试 retryable: true # Hystrix 超时时间设置 hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 10000 # 线程池隔离,默认超时时间 1000ms # Ribbon 超时时间设置:建议设置小于 Hystrix ribbon: ConnectTimeout: 5000 # 请求连接的超时时间: 默认超时时间 1000ms ReadTimeout: 5000 # 请求处理的超时时间: 默认超时时间 1000ms # 重试次数 MaxAutoRetries: 1 # MaxAutoRetries 表示访问服务集群下原节点(同路径访问) MaxAutoRetriesNextServer: 1 # MaxAutoRetriesNextServer表示访问服务集群下其余节点(换台服务器) # Ribbon 开启重试 OkToRetryOnAllOperations: true
添加依赖
Spring Cloud Netflix Zuul 网关重试机制需要使用 spring-retry 组件。
org.springframework.retry spring-retry
启动类
启动类需要开启 @EnableRetry
重试注解。
package com.example; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; import org.springframework.cloud.netflix.zuul.EnableZuulProxy; import org.springframework.retry.annotation.EnableRetry; @SpringBootApplication // 开启 Zuul 注解 @EnableZuulProxy // 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解 //@EnableEurekaClient // 开启数据监控注解 @EnableHystrixDashboard // 开启重试注解 @EnableRetry public class ZuulServerApplication { public static void main(String[] args) { SpringApplication.run(ZuulServerApplication.class, args); } }
模拟超时
商品服务模拟超时。
package com.example.controller; import com.example.pojo.Product; import com.example.service.ProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") public class ProductController { @Autowired private ProductService productService; /** * 根据主键查询商品 * * @param id * @return */ @GetMapping("/{id}") public Product selectProductById(@PathVariable("id") Integer id) { // 模拟超时 try { Thread.sleep(2000L); } catch (InterruptedException e) { e.printStackTrace(); } return productService.selectProductById(id); } }
访问
配置前访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下(触发了网关服务降级):
配置后访问:http://localhost:9000/product-service/product/1?token=abc123 结果如下:
至此 Zuul 服务网关所有的知识点就讲解结束了。