目录
资料来源: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 包含了对请求的路由和过滤两个核心功能,其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验,服务聚合等功能的基础。然而实际上,路由功能在真正运行时,它的路由映射和请求转发都是由几个不同的过滤器完成的。
路由映射主要通过 pre
类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需要转发的目标地址;而请求转发的部分则是由 routing
类型的过滤器来完成,对 pre
类型过滤器获得的路由地址进行转发。所以说,过滤器可以说是 Zuul 实现 API 网关功能最核心的部件,每一个进入 Zuul 的 http 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
关键名词
- 类型:定义路由流程中应用过滤器的阶段。共 pre、routing、post、error 4 个类型。
- 执行顺序:在同类型中,定义过滤器执行的顺序。比如多个 pre 类型的执行顺序。
- 条件:执行过滤器所需的条件。true 开启,false 关闭。
- 动作:如果符合条件,将执行的动作。具体操作。
过滤器类型
- pre:请求被路由到源服务器之前执行的过滤器
- 身份认证
- 选路由
- 请求日志
- routing:处理将请求发送到源服务器的过滤器
- post:响应从源服务器返回时执行的过滤器
- 对响应增加 HTTP 头
- 收集统计和度量指标
- 将响应以流的方式发送回客户端
- error:上述阶段中出现错误时执行的过滤器
Zuul 请求的生命周期
- HTTP 发送请求到 Zuul 网关.
- Zuul 网关首先经过 pre filter.
- 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出错,则执行 error filter.
- 继续往下执行 post filter.
- 最后返回响应给 HTTP 客户端.
Zuul 和 Hystrix 无缝结合
在 Spring Cloud 中,Zuul 启动器中包含了 Hystrix 相关依赖,在 Zuul 网关工程中,默认是提供了 Hystrix Dashboard 服务监控数据的(hystrix.stream),但是不会提供监控面板的界面展示。在 Spring Cloud 中,Zuul 和 Hystrix 是无缝结合的,我们可以非常方便的实现网关容错处理。Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。
网关熔断
在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider
用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口 FallbackProvider
来提供 fallback 处理。
Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。
因为对于Zuul网关来说,做请求路由分发的时候,结果由远程服务运算。远程服务反馈了异常信息,Zuul 网关不会处理异常,因为无法确定这个错误是否是应用程序真实想要反馈给客户端的。
网关限流
顾名思义,限流就是限制流量,就像你宽带包有 1 个 G 的流量,用完了就没了。通过限流,我们可以很好地控制系统的 QPS(峰值时间每秒请求数),从而达到保护系统的目的。Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要提供 error 错误处理机制即可。
为什么需要限流
比如 Web 服务、对外 API,这种类型的服务有以下几种可能导致机器被拖垮:
- 用户增长过快(好事)
- 因为某个热点事件(微博热搜)
- 竞争对象爬虫
- 恶意的请求
这些情况都是无法预知的,不知道什么时候会有 10 倍甚至 20 倍的流量打进来,如果真碰上这种情况,扩容是根本来不及的。
从上图可以看出,对内而言:上游的 A、B 服务直接依赖了下游的基础服务 C,对于 A,B 服务都依赖的基础服务 C 这种场景,服务 A 和 B 其实处于某种竞争关系,如果服务 A 的并发阈值设置过大,当流量高峰期来临,有可能直接拖垮基础服务 C 并影响服务 B,即雪崩效应。
限流算法
常见的限流算法有:
- 计数器算法
- 漏桶(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
计数器算法
计数器算法是限流算法里最简单也是最容易实现的一种算法。比如我们规定,对于 A 接口来说,我们 1 分钟的访问次数不能超过 100 个。那么我们可以这么做:在一开始的时候,我们可以设置一个计数器 counter,每当一个请求过来的时候,counter 就加 1,如果 counter 的值大于 100 并且该请求与第一个请求的间隔时间还在 1 分钟之内,触发限流;如果该请求与第一个请求的间隔时间大于 1 分钟,重置 counter 重新计数,具体算法的示意图如下:
这个算法虽然简单,但是有一个十分致命的问题,那就是临界问题,我们看下图:
从上图中我们可以看到,假设有一个恶意用户,他在 0:59 时,瞬间发送了 100 个请求,并且 1:00 又瞬间发送了 100 个请求,那么其实这个用户在 1 秒里面,瞬间发送了 200 个请求。我们刚才规定的是 1 分钟最多 100 个请求,也就是每秒钟最多 1.7 个请求,用户通过在时间窗口的重置节点处突发请求, 可以瞬间超过我们的速率限制。用户有可能通过算法的这个漏洞,瞬间压垮我们的应用。
还有资料浪费的问题存在,我们的预期想法是希望 100 个请求可以均匀分散在这一分钟内,假设 30s 以内我们就请求上限了,那么剩余的半分钟服务器就会处于闲置状态,比如下图:
漏桶算法
漏桶算法其实也很简单,可以粗略的认为就是注水漏水的过程,往桶中以任意速率流入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。
漏桶算法是使用队列机制实现的。
漏桶算法主要用途在于保护它人(服务),假设入水量很大,而出水量较慢,则会造成网关的资源堆积可能导致网关瘫痪。而目标服务可能是可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务那边的资源浪费。
漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终保持不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就会丢弃。
令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌。
场景大概是这样的:桶中一直有大量的可用令牌,这时进来的请求可以直接拿到令牌执行,比如设置 QPS 为 100/s,那么限流器初始化完成一秒后,桶中就已经有 100 个令牌了,等服务启动完成对外提供服务时,该限流器可以抵挡瞬时的 100 个请求。当桶中没有令牌时,请求会进行等待,最后相当于以一定的速率执行。
Zuul 内部使用 Ratelimit 组件实现限流,使用的就是该算法,大概描述如下:
- 所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
- 根据限流大小,设置按照一定的速率往桶里添加令牌;
- 桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃或者拒绝;
- 请求到达后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
- 令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流。
漏桶算法主要用途在于保护它人,而令牌桶算法主要目的在于保护自己,将请求压力交由目标服务处理。假设突然进来很多请求,只要拿到令牌这些请求会瞬时被处理调用目标服务。
网关调优
使用 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模块
配制pom.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>com.haogenmin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--zuul路由网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
配置application.yml文件
server:
port: 7001 # 服务端口
spring:
application:
name: microservice-zuul
eureka:
client:
registerWithEureka: true # 服务注册开关
fetchRegistry: true # 服务发现开关
serviceUrl:
defaultZone: http://eureka6001.com:6001/eureka,http://eureka6002.com:6002/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定实例ID,就不会显示主机名了
preferIpAddress: true #访问路径可以显示IP地址
配置启动类
package com.haogenmin.zuul;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
/**
* @author :HaoGenmin
* @Title :ZuulApplication
* @date :Created in 2020/8/20 11:04
* @description:
*/
@SpringBootApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
测试是否注册到注册中心
配置路由
配置文件添加
zuul:
routes:
provider-product: # 路由名称,名称任意,路由名称唯一
path: /provider/** # 访问路径
serviceId: microservice-product # 指定服务ID,会自动从Eureka中找到此服务的ip和端口
stripPrefix: false # 代理转发时去掉前缀,false:代理转发时不去掉前缀 例如:为true时请求 /product/get/1,代理转发到/get/1
路由测试
发现路由是带有负载均衡的,默认轮训算法
过滤器实践
自定义过虑器需要继承 ZuulFilter,ZuulFilter是一个抽象类,需要覆盖它的4个方法,如下:
- filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
- pre:可以在请求被路由之前调用。
- routing:在路由请求时候被调用。
- post:在routing和error过滤器之后被调用。
- error:处理请求时发生错误时被调用。
- filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。
- shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
- run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。
创建过滤器
package com.haogenmin.zuul.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;
import java.io.IOException;
/**
* @author :HaoGenmin
* @Title :LoginFilter
* @date :Created in 2020/8/20 15:48
* @description:
*/
@Component
public class LoginFilter extends ZuulFilter {
Logger logger = LoggerFactory.getLogger(getClass());
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
//1.获取请求上下文
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String token = request.getParameter("token");
//如果说请求带了token值,则表示已经登录过
if(token == null) {
logger.warn("此操作需要先登录系统");
//没有登录 过,则不进行路由转发
context.setSendZuulResponse(false);//拒绝访问
context.setResponseStatusCode(200); //响应状态码
try {
context.getResponse().getWriter().write("token is empty...");
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
//通过,会进行路由转发
logger.info("通过,会进行路由转发");
return null;
}
}
过滤测试
熔断监视
添加监控依赖
<!--监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
添加监控端点
management:
endpoints:
web:
exposure:
include: hystrix.stream
测试
网关过滤器异常统一处理
创建过滤器
package com.haogenmin.zuul.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 java.io.IOException;
import java.io.PrintWriter;
/**
* @author :HaoGenmin
* @Title :ErrorFilter
* @date :Created in 2020/8/24 10:48
* @description:
*/
@Component
public class ErrorFilter extends ZuulFilter {
Logger logger = LoggerFactory.getLogger(getClass());
@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
网关熔断
创建Fallback处理类
package com.haogenmin.zuul.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;
/**
* @author :HaoGenmin
* @Title :ProductProviderFallback
* @date :Created in 2020/8/24 11:09
* @description:
*/
@Component
public class ProductProviderFallback implements FallbackProvider {
/**
* return - 返回 fallback 处理哪一个服务。返回的是服务的名称。
* 推荐 - 为指定的服务定义特性化的 fallback 逻辑。
* 推荐 - 提供一个处理所有服务的 fallback 逻辑。
* 好处 - 某个服务发生超时,那么指定的 fallback 逻辑执行。如果有新服务上线,未提供 fallback 逻辑,有一个通用的。
*/
@Override
public String getRoute() {
return "microservice-product";
}
/**
* 对商品服务做服务容错处理
*
* @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() {
}
};
}
}
测试
关闭商品提供者。
访问:
网关限流
Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要添加 Redis 组件。
RateLimit 官网文档:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
添加依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>parent</artifactId>
<groupId>com.haogenmin.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--监控依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--zuul路由网关依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- spring cloud zuul ratelimit 依赖 -->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.4.1.RELEASE</version>
</dependency>
<!-- spring boot data redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- commons-pool2 对象池依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
</project>
全局限流配置
使用全局限流配置,Zuul 会对代理的所有服务提供限流保护。
server:
port: 7001
spring:
application:
name: microservice-zuul
redis:
timeout: 5000 # 连接超时时间
host: 127.0.0.1 # Redis服务器地址
port: 6379 # Redis服务器端口
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 8 # 最大连接数,默认 8
max-wait: 5000 # 最大连接阻塞等待时间,单位毫秒,默认 -1
max-idle: 8 # 最大空闲连接,默认 8
min-idle: 5 # 最小空闲连接,默认 0
eureka:
client:
registerWithEureka: true # 服务注册开关
fetchRegistry: true # 服务发现开关
serviceUrl: # 客户端(服务提供者)注册到哪一个Eureka Server服务注册中心,多个用逗号分隔
defaultZone: http://eureka6001.com:6001/eureka,http://eureka6002.com:6002/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定实例ID,就不会显示主机名了
preferIpAddress: true #访问路径可以显示IP地址
zuul:
ratelimit: # 服务限流
enabled: true # 开启限流保护
repository: redis # 限流数据存储方式
default-policy-list: # default-policy-list 默认配置,全局生效
- limit: 3
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
SendErrorFilter: #禁止默认错误处理过滤器
error:
disable: true
#ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
#ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径
routes:
provider-product: # 路由名称,名称任意,路由名称唯一
path: /provider/** # 访问路径
serviceId: microservice-product # 指定服务ID,会自动从Eureka中找到此服务的ip和端口
stripPrefix: false # 代理转发时去掉前缀,false:代理转发时不去掉前缀 例如:为true时请求 /product/get/1,代理转发到/get/1
management:
endpoints:
web:
exposure:
include: hystrix.stream
Zuul-RateLimiter 基本配置项:
配置项 | 可选值 | 说明 |
---|---|---|
enabled | true/false | 是否启用限流 |
repository | REDIS:基于 Redis,使用时必须引入 Redis 相关依赖 CONSUL:基于 Consul JPA:基于 SpringDataJPA,需要用到数据库 使用 Java 编写的基于令牌桶算法的限流库: BUCKET4J_JCACHE BUCKET4J_HAZELCAST BUCKET4J_IGNITE BUCKET4J_INFINISPAN | 限流数据的存储方式,无默认值必填项 |
key-prefix | String | 限流 key 前缀 |
default-policy-list | List of Policy | 默认策略 |
policy-list | Map of Lists of Policy | 自定义策略 |
post-filter-order | - | postFilter 过滤顺序 |
pre-filter-order | - | preFilter 过滤顺序 |
Bucket4j 实现需要相关的 bean @Qualifier(“RateLimit”):
- JCache - javax.cache.Cache
- Hazelcast - com.hazelcast.core.IMap
- Ignite - org.apache.ignite.IgniteCache
- Infinispan - org.infinispan.functional.ReadWriteMap
Policy 限流策略配置项说明:
项 | 说明 |
---|---|
limit | 单位时间内请求次数限制 |
quota | 单位时间内累计请求时间限制(秒),非必要参数 |
refresh-interval | 单位时间(秒),默认 60 秒 |
type | 限流方式: ORIGIN:访问 IP 限流 URL:访问 URL 限流 USER:特定用户或用户组限流(比如:非会员用户限制每分钟只允许下载一个文件) URL_PATTERN ROLE HTTP_METHOD |
访问
前三次正常,第四次出现如下情况:
控制台报异常:
13:32:49.546 ERROR 14532 --- [nio-7001-exec-7] com.haogenmin.zuul.filter.ErrorFilter : ErrorFilter...com.netflix.zuul.exception.ZuulException: 429 TOO_MANY_REQUESTS
局部限流配置
policy-list: # policy-list 自定义配置,局部生效
microservice-product: # 指定需要被限流的服务名称
- limit: 2
refresh-interval: 60 # 60s 内请求超过 5 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
访问
前两次可以,第三次报异常,但是这里有一个问题,要直接把服务id加在访问路径里面才生效,具体原因之后找到会补上。
自定义限流策略类
@Configuration
public class ZuulConfig {
@Bean
public RateLimitKeyGenerator ratelimitKeyGenerator(RateLimitProperties properties, RateLimitUtils rateLimitUtils) {
return new DefaultRateLimitKeyGenerator(properties, rateLimitUtils) {
@Override
public String key(HttpServletRequest request, Route route, RateLimitProperties.Policy policy) {
return super.key(request, route, policy) + ":" + request.getMethod();
}
};
}
}
错误处理
对之前的错误处理过滤器进行修改,或者实现 org.springframework.boot.web.servlet.error.ErrorController
重写 getErrorPath()。
package com.haogenmin.zuul.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.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
/**
* @author :HaoGenmin
* @Title :ErrorFilter
* @date :Created in 2020/8/24 10:48
* @description:
*/
@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();
ZuulException exception = this.findZuulException(rc.getThrowable());
logger.error("ErrorFilter..." + exception.errorCause, exception);
HttpStatus httpStatus = null;
if (429 == exception.nStatusCode)
httpStatus = HttpStatus.TOO_MANY_REQUESTS;
if (500 == exception.nStatusCode)
httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
// 响应状态码
rc.setResponseStatusCode(httpStatus.value());
// 响应类型
rc.getResponse().setContentType("application/json; charset=utf-8");
PrintWriter writer = null;
try {
writer = rc.getResponse().getWriter();
// 响应内容
writer.print("{\"message\":\"" + httpStatus.getReasonPhrase() + "\"}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != writer)
writer.close();
}
return null;
}
private ZuulException findZuulException(Throwable throwable) {
if (throwable.getCause() instanceof ZuulRuntimeException)
return (ZuulException) throwable.getCause().getCause();
if (throwable.getCause() instanceof ZuulException)
return (ZuulException) throwable.getCause();
if (throwable instanceof ZuulException)
return (ZuulException) throwable;
return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null);
}
}
网关调优
配置文件
Zuul 中可配置的超时时长有两个位置:Hystrix 和 Ribbon。具体配置如下:
修改三个部分,开启zuul网关重试,以及hystrix和ribbon的配置。
server:
port: 7001
#-------------------------------------------------------------------------------------------------
spring:
application:
name: microservice-zuul
#-------------------------------------------------------------------------------------------------
redis:
timeout: 5000 # 连接超时时间
host: 127.0.0.1 # Redis服务器地址
port: 6379 # Redis服务器端口
database: 0 # 选择哪个库,默认0库
lettuce:
pool:
max-active: 8 # 最大连接数,默认 8
max-wait: 5000 # 最大连接阻塞等待时间,单位毫秒,默认 -1
max-idle: 8 # 最大空闲连接,默认 8
min-idle: 5 # 最小空闲连接,默认 0
#-------------------------------------------------------------------------------------------------
eureka:
client:
registerWithEureka: true # 服务注册开关
fetchRegistry: true # 服务发现开关
serviceUrl: # 客户端(服务提供者)注册到哪一个Eureka Server服务注册中心,多个用逗号分隔
defaultZone: http://eureka6001.com:6001/eureka,http://eureka6002.com:6002/eureka
instance:
instanceId: ${spring.application.name}:${server.port} # 指定实例ID,就不会显示主机名了
preferIpAddress: true #访问路径可以显示IP地址
zuul:
retryable: true # 开启 Zuul 网关重试
#-------------------------------------------------------------------------------------------------
ratelimit: # 服务限流
enabled: true # 开启限流保护
repository: redis # 限流数据存储方式
behind-proxy: true
response-headers: standard
policy-list: # policy-list 自定义配置,局部生效
microservice-product: # 指定需要被限流的服务名称
- limit: 2
refresh-interval: 60 # 60s 内请求超过 5 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
default-policy-list: # default-policy-list 默认配置,全局生效
- limit: 3
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
type:
- origin
- url
- user
#-------------------------------------------------------------------------------------------------
SendErrorFilter: #禁止默认错误处理过滤器
error:
disable: true
#-------------------------------------------------------------------------------------------------
#ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
#ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径
routes:
provider-product: # 路由名称,名称任意,路由名称唯一
path: /provider/** # 访问路径
serviceId: microservice-product # 指定服务ID,会自动从Eureka中找到此服务的ip和端口
stripPrefix: false # 代理转发时去掉前缀,false:代理转发时不去掉前缀 例如:为true时请求 /product/get/1,代理转发到/get/1
#-------------------------------------------------------------------------------------------------
management:
endpoints:
web:
exposure:
include: hystrix.stream
#-------------------------------------------------------------------------------------------------
# Hystrix 超时时间设置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000 # 线程池隔离,默认超时时间 1000ms
#-------------------------------------------------------------------------------------------------
# Ribbon 超时时间设置:建议设置小于 Hystrix
ribbon:
ConnectTimeout: 1000 # 请求连接的超时时间: 默认超时时间 1000ms
ReadTimeout: 1000 # 请求处理的超时时间: 默认超时时间 1000ms
# 重试次数
MaxAutoRetries: 1 # MaxAutoRetries 表示访问服务集群下原节点(同路径访问)
MaxAutoRetriesNextServer: 1 # MaxAutoRetriesNextServer表示访问服务集群下其余节点(换台服务器)
# Ribbon 开启重试
OkToRetryOnAllOperations: true
添加依赖
Spring Cloud Netflix Zuul 网关重试机制需要使用 spring-retry 组件
<!-- spring retry 依赖 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
启动类
需要开启 @EnableRetry
重试注解。
模拟超时
我们再provider1中注掉熔断回调函数,添加超时代码段。
// @HystrixCommand(fallbackMethod = "getFallback")
@RequestMapping(value = "/products/{id}", method = RequestMethod.GET)
public Product get(@PathVariable("id") Long id) {
try {
Thread.sleep(2000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
Product product = productService.get(id);
//模拟异常
if(product == null) {
throw new RuntimeException("ID=" + id + "无效");
}
return product;
}
访问
配置之前:以下内容是因为负载均衡器指向provider1的时候,触发zuul网关的熔断,指向provider2的时候,正常显示。
配置之后:
始终出现这个数据,其实是因为负载均衡器指向provider1的时候,请求因为超时被重新转发,结果指向了provider2,指向provider2的时候,正常显示。
Zuul 和 Sentinel 整合
这里就不再叙述了,以下是官网,需要的小伙伴看一下官网即可,都是中文。
- https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
- https://github.com/alibaba/Sentinel/wiki/网关限流#zuul-1x