Zuul服务网关
1.理解Zuul
Zuul 是从设备和网站到应用程序后端的所有请求的前门。作为边缘服务应用程序,Zuul 旨在实现动
态路由,监视,弹性和安全性。Zuul 包含了对请求的路由和过滤两个最主要的功能。
Zuul 是 Netflix 开源的微服务网关,它可以和 Eureka、Ribbon、Hystrix 等组件配合使用。Zuul 的核心是一系列的过滤器,这些过滤器可以完成以下功能:
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产试图
- 压力测试:逐渐增加只想集群的流量,以了解性能
- 静态响应处理:在边缘位置直接建立部份响应,从而避免其转发到内部集群\
- 化,以及让系统的边缘更贴近系统的使用者
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求
- 动态路由:动态地将请求路由到不同的后端集群
- 负载分配:为每一种负载类型分配对应容量,并弃用超出限定值的请求
- 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(Elastic Load Balancing)使用的多样
2.什么是服务网关
API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为 企业级应用防火墙 ,主要起到 隔离外部访问与内部系统的作用 。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。
API 网关是一个服务器,是系统对外的唯一入口。API 网关封装了系统内部架构,为每个客户端提供
定制的 API。所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有非业务功能。API
网关并不是微服务场景中必须的组件,如下图,不管有没有 API 网关,后端微服务都可以通过 API 很好
地支持客户端的访问但对于服务数量众多、复杂度比较高、规模比较大的业务来说,引入 API 网关也有一系列的好处:
- 聚合接口使得服务对调用者透明,客户端与后端的耦合度降低
- 聚合后台服务,节省流量,提高性能,提升用户体验
- 提供安全、流控、过滤、缓存、计费、监控等 API 管理功能
3.为什么要使用网关
**单体应用:**浏览器发起请求到单体应用所在的机器,应用从数据库查询数据原路返回给浏览器,对
于单体应用来说是不需要网关的。
**微服务:**微服务的应用可能部署在不同机房,不同地区,不同域名下。此时客户端(浏览器/手机/
软件工具)想要请求对应的服务,都需要知道机器的具体 IP 或者域名 URL,当微服务实例众多
时,这是非常难以记忆的,对于客户端来说也太复杂难以维护。此时就有了网关,客户端相关的请
求直接发送到网关,由网关根据请求标识解析判断出具体的微服务地址,再把请求转发到微服务实
例。这其中的记忆功能就全部交由网关来操作了。
总结
如果让客户端直接与各个微服务交互:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 身份认证问题,每个微服务需要独立身份认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务
- 某些微服务可能使用了防火墙/浏览器不友好的协议,直接访问会有一定的困难
因此,我们需要网关介于客户端与服务器之间的中间层,所有外部请求率先经过微服务网关,客户端只
需要与网关交互,只需要知道网关地址即可。这样便简化了开发且有以下优点:
- 易于监控,可在微服务网关收集监控数据并将其推送到外部系统进行分析
- 易于认证,可在微服务网关上进行认证,然后再将请求转发到后端的微服务,从而无需在每个微服务中进行认证
- 减少了客户端与各个微服务之间的交互次数
4.网关功能解决问题
网关具有身份认证与安全、审查与监控、动态路由、负载均衡、缓存、请求分片与管理、静态响应
处理等功能。当然最主要的职责还是与“外界联系”。
5.环境准备
eureka-server :注册中心
eureka-server02 :注册中心
provider-service :商品服务,提供了根据主键查询商品接口
http://localhost:7070/product/{id}
order-server :订单服务,提供了根据主键查询订单接口
http://localhost:9090/order/{id}
6.Nginx实现API网关
下载
官网:http://nginx.org/en/download.html 下载稳定版。为了方便学习,请下载 Windows 版本。
安装
解压文件后直接运行根路径下的 nginx.exe 文件即可。
Nginx 默认端口为 80,访问:http://localhost:80/
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nS0EcQUD-1590394355468)(F:\A20200102\高级资源\微服务SpringCloud\img\nginx.png)]
配置路由规则
进入 Nginx 的 conf 目录,打开 nginx.conf 文件,配置路由规则
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html;
index index.html index.htm;
}
# 路由到商品服务
location /api-product {
proxy_pass http://localhost:7070/;
}
#路由到订单服务
location /api-order {
proxy_pass http://localhost:9090/;
}
.....
.....
}
访问
http://localhost/api-product/product/1
http://localhost/api-order/order/1
7.Zuul实现API网关
搭建网关服务
创建zuul-server 项目
添加依赖
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>zuul-server</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承父依赖 -->
<parent>
<groupId>com.example</groupId>
<artifactId>zuul-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<!-- 项目打包和编译使用的编码字符集以及 jdk 版本 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<!-- 项目依赖 -->
<dependencies>
<!-- spring cloud netflix zuul 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring cloud netflix hystrix dashboard 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!-- spring cloud zuul ratelimit 依赖 -->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.3.0.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>
<!-- spring retry 依赖 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
</dependencies>
</project>
配置文件
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
server:
port: 9000 # 端口
# 路由规则
zuul:
# 服务限流
ratelimit:
# 开启限流保护
enabled: true
# 限流数据存储方式
repository: REDIS
# default-policy-list 默认配置,全局生效
# default-policy-list:
# - limit: 3
# refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
# quota: 30 # 请求时间总和不得超过 30 秒
# type:
# - origin
# - url
# - user
# policy-list 自定义配置,局部生效
policy-list:
# 指定需要被限流的服务名称
order-service:
- limit: 5
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
quota: 30 # 请求时间总和不得超过 30 秒
type:
- origin
- url
- user
# 禁用 Zuul 默认的异常处理 filter
SendErrorFilter:
error:
disable: true
#prefix: /api
#ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径
#ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
#routes:
#product-service: # 路由 id 自定义
#path: /product-service/** # 配置请求 url 的映射路径
#url: http://localhost:7070/ # 映射路径对应的微服务地址
#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/
# 度量指标监控与健康检查
management:
endpoints:
web:
exposure:
include: hystrix.stream
# Hystrix 超时时间设置
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 10000 # 线程池隔离,默认超时时间 1000ms
# Ribbon 超时时间设置:建议设置小于 Hystrix
ribbon:
ConnectTimeout: 5000 # 请求连接的超时时间: 默认 5000ms
ReadTimeout: 5000 # 请求处理的超时时间: 默认 5000ms
# 重试次数
MaxAutoRetries: 1 # MaxAutoRetries 表示访问服务集群下原节点(同路径访问)
MaxAutoRetriesNextServer: 1 # MaxAutoRetriesNextServer表示访问服务集群下其余节点(换台服务器)
# Ribbon 开启重试
OkToRetryOnAllOperations: true
启动类
@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/ # 映射路径对应的微服务地址
通配符含义:
通配 符 | 含义 | 举例 | 解释 |
---|---|---|---|
? | 匹配任意单个字符 | /product service/? | /product-service/a,/product service/b,… |
* | 匹配任意数量字符不包括子 路径 | /product service/* | /product-service/aa,/product service/bbb,… |
** | 匹配任意数量字符包括所有 下级路径 | /product service/** | /product-service/aa,/product service/aaa/b/ccc |
访问:http://localhost:9000/product-service/product/1
服务名称路由
微服务一般是由几十、上百个服务组成,对于 URL 地址路由的方式,如果对每个服务实例手动指定
一个唯一访问地址,这样做显然是不合理的。
Zuul 支持与 Eureka 整合开发,根据 serviceId 自动从注册中心获取服务地址并转发请求,这样做好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改 Zuul 的路
由配置。
添加依赖Eureka Client
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置文件
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
serviceId: product-service # 自动从注册中心获取服务地址并转发请求
# 配置 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/
启动类
@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
访问:http://localhost:9000/product-service/product/1
简化路由配置
Zuul 为了方便大家使用提供了默认路由配置:如果路由 id 和 微服务名称 一致的话,path 默认对
应 微服务名称/** ,比如以下配置就没必要再写了
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
访问:http://localhost:9000/order-service/order/1
路由排除
我们可以通过路由排除设置不允许被访问的服务。允许被访问的服务可以通过路由规则进行设置。
URL地址排除
# 路由规则
zuul:
ignored-patterns: /**/order/** # URL 地址排除,排除所有包含 /order/ 的路径
# 不受路由排除影响
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路
服务名称排除
# 路由规则
zuul:
ignored-services: order-service # 服务名称排除,多个服务逗号分隔,'*' 排除所有
# 不受路由排除影响
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路
路由前缀
zuul:
prefix: /api
访问:http://localhost:9000/api/product-service/product/1
8.网关过滤器
Zuul 包含了对请求的路由和过滤两个核心功能,其中路由功能负责将外部请求转发到具体的微服务
实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请
求校验,服务聚合等功能的基础。然而实际上,路由功能在真正运行时,它的路由映射和请求转发都是
由几个不同的过滤器完成的。
路由映射主要通过 pre 类型的过滤器完成,它将请求路径与配置的路由规则进行匹配,以找到需
要转发的目标地址;而请求转发的部分则是由 routing 类型的过滤器来完成,对 pre 类型过滤器
获得的路由地址进行转发。所以说,过滤器可以说是 Zuul 实现 API 网关功能最核心的部件,每一个进入
Zuul 的 http 请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端。
关键名词
- 类型:定义路由流程中应用过滤器的阶段。共 pre、routing、post、error 4 个类型。
- 执行顺序:在同类型中,定义过滤器执行的顺序。比如多个 pre 类型的执行顺序。
- 条件:执行过滤器所需的条件。true 开启,false 关闭。
- 动作:如果符合条件,将执行的动作。具体操作。
过滤器类型
**pre:**请求被路由到源服务器之前执行的过滤器
身份认证
选路由
请求日志
routing:处理将请求发送到源服务器的过滤器
**post:**响应从源服务器返回时执行的过滤器
对响应增加 HTTP 头
收集统计和度量指标
将响应以流的方式发送回客户端
**error:**上述阶段中出现错误时执行的过滤器
入门案例
创建过滤器
Spring Cloud Netflix Zuul 中实现过滤器必须包含 4 个基本特征:过滤器类型,执行顺序,执行条
件,动作(具体操作)。这些步骤都是 ZuulFilter
接口中定义的 4 个抽象方法:
/**
* 网关过滤器
*/
@Component
public class CustomFilter extends ZuulFilter {
private 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 {
System.out.println("pre过滤器被执行...");
// 获取请求上下文
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
权限验证案例
接下来我们在网关过滤器中通过 token 判断用户是否登录,完成一个权限验证案例
创建过滤器
/**
* 权限验证过滤器
*/
@Component
public class AccessFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(AccessFilter.class);
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 业务逻辑
*
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
// Integer.parseInt("zuul");
// 获取请求上下文
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
9.Zuul请求的生命周期
- HTTP 发送请求到 Zuul 网关
- Zuul 网关首先经过 pre filter
- 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出
错,则执行 error filter- 继续往下执行 post filter
- 最后返回响应给 HTTP 客户端
10.网关过滤器异常统一处理
创建过滤器
/**
* 异常过滤器
*/
@Component
public class ErrorFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 业务逻辑
* @return
* @throws ZuulException
*/
@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);
}
}
模拟异常
在 pre 过滤器中添加模拟异常代码。
// 模拟异常
Integer.parseInt("zuul");
配置文件
禁用 Zuul 默认的异常处理 filter: SendErrorFilter
zuul:
# 禁用 Zuul 默认的异常处理 filter
SendErrorFilter:
error:
disable: true
访问:http://localhost:9000/product-service/product/1
11.Zuul和Hystrix无缝结合
在 Spring Cloud 中,Zuul 启动器中包含了 Hystrix 相关依赖,在 Zuul 网关工程中,默认是提供了
Hystrix Dashboard 服务监控数据的(hystrix.stream),但是不会提供监控面板的界面展示。在 Spring Cloud中,Zuul 和 Hystrix 是无缝结合的,我们可以非常方便的实现网关容错处理。
网关服务监控
Zuul 的依赖中包含了 Hystrix 的相关 jar 包,所以我们不需要在项目中额外添加 Hystrix 的依赖。
但是需要开启数据监控的项目中要添加 dashboard 依赖。
<!-- spring cloud netflix hystrix dashboard 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
配置文件
在配置文件中开启 hystrix.stream 端点
# 度量指标监控与健康检查
management:
endpoints:
web:
exposure:
include: hystrix.stream
启动类
在需要开启数据监控的项目启动类中添加 @EnableHystrixDashboard 注解
@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/actuator/hystrix.stream
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S96DPMrM-1590394355475)(F:\A20200102\高级资源\微服务SpringCloud\img\hystrix.png)]
网关服务降级
在 Edgware 版本之前,Zuul 提供了接口 ZuulFallbackProvider 用于实现 fallback 处理。从
Edgware 版本开始,Zuul 提供了接口 FallbackProvider 来提供 fallback 处理。
Zuul 的 fallback 容错处理逻辑,只针对 timeout 异常处理,当请求被 Zuul 路由后,只要服务有返回(包括异常),都不会触发 Zuul 的 fallback 容错逻辑。
代码示例
ProductProviderFallback.java
/**
* 对商品服务做服务容错处理
*/
@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
网关服务限流
Zuul 网关组件也提供了限流保护。当请求并发达到阀值,自动触发限流保护,返回错误结果。只要
提供 error 错误处理机制即可。
添加依赖
Zuul 的限流保护需要额外依赖 spring-cloud-zuul-ratelimit 组件,限流数据采用 Redis 存储所以还要
添加 Redis 组件。
RateLimit 官网文档:https://github.com/marcosbarbero/spring-cloud-zuul-ratelimit
<!-- spring cloud zuul ratelimit 依赖 -->
<dependency>
<groupId>com.marcosbarbero.cloud</groupId>
<artifactId>spring-cloud-zuul-ratelimit</artifactId>
<version>2.3.0.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>
全局限流配置
使用全局限流配置,Zuul 会对代理的所有服务提供限流保护。
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
server:
port: 9000 # 端口
# 路由规则
zuul:
# 服务限流
ratelimit:
# 开启限流保护
enabled: true
# 限流数据存储方式
repository: REDIS
# default-policy-list 默认配置,全局生效
default-policy-list:
- limit: 3
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
quota: 30 # 请求时间总和不得超过 30 秒
type:
- origin
- url
- user
# 配置 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-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 |
访问:http://localhost:9000/product-service/product/1?token=abc123
局部限流配置
使用局部限流配置,Zuul 仅针对配置的服务提供限流保护。全局配置和局部配置可同时存在,局部
优先级高于全局。
# 路由规则
zuul:
# 服务限流
ratelimit:
# 开启限流保护
enabled: true
# 限流数据存储方式
repository: REDIS
# default-policy-list 默认配置,全局生效
# default-policy-list:
# - limit: 3
# refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
# quota: 30 # 请求时间总和不得超过 30 秒
# type:
# - origin
# - url
# - user
# policy-list 自定义配置,局部生效
policy-list:
# 指定需要被限流的服务名称
order-service:
- limit: 5
refresh-interval: 60 # 60s 内请求超过 3 次,服务端就抛出异常,60s 后可以恢复正常请求
quota: 30 # 请求时间总和不得超过 30 秒
type:
- origin
- url
- user
自定义限流策略
/**
* 自定义限流策略
*/
@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) {
// 对请求参数中相同的 token 值进行限流
return super.key(request, route, policy) + ":" + request.getParameter("token");
}
}
多次访问:http://localhost:9000/product-service/product/1?token=abc123
错误处理
配置 error 类型的网关过滤器进行处理即可。修改之前的 ErrorFilter 让其变的通用
/**
* 异常过滤器
*/
@Component
public class ErrorFilter extends ZuulFilter {
private Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
@Override
public String filterType() {
return "error";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
/**
* 业务逻辑
* @return
* @throws ZuulException
*/
@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 内部使用线程池隔离机制提供请求路由实现,其默认的超时时长为 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 # 请求连接的超时时间: 默认 5000ms
ReadTimeout: 5000 # 请求处理的超时时间: 默认 5000ms
# 重试次数
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 重试注解
@SpringBootApplication
// 开启 EurekaClient 注解,目前版本如果配置了 Eureka 注册中心,默认会开启该注解
//@EnableEurekaClient
// 开启 Zuul 注解
@EnableZuulProxy
// 开启数据监控注解
@EnableHystrixDashboard
// 开启重试注解
@EnableRetry
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
模拟超时
商品服务模拟超时
@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
12.Zuul和Sentinel整合
网关服务限流和降级
创建zuul-server-sentinel 项目
添加依赖
单独使用添加 sentinel-zuul-adapter 依赖即可。
若想跟 Sentinel Starter 配合使用,需要加上 spring-cloud-alibaba-sentinel-gateway 依赖,
同时需要添加 spring-cloud-starter-netflix-zuul 依赖来让 spring-cloud-alibaba-sentinelgateway 模块里的 Zuul 自动化配置类生效。
同时请将 spring.cloud.sentinel.filter.enabled 配置项置为 false(若在网关流控控制台
上看到了 URL 资源,就是此配置项没有置为 false)
<?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">
<modelVersion>4.0.0</modelVersion>
<artifactId>zuul-server-sentinel</artifactId>
<!-- 继承父依赖 -->
<parent>
<groupId>com.example</groupId>
<artifactId>zuul-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<properties>
<!-- 项目打包和编译使用的编码字符集以及 jdk 版本 -->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<!-- 项目依赖 -->
<dependencies>
<!-- spring cloud netflix zuul 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- netflix eureka client 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- 单独使用 -->
<!-- sentinel zuul adapter 依赖 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-zuul-adapter</artifactId>
</dependency>
<!-- 和 Sentinel Starter 配合使用 -->
<!--
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
-->
</dependencies>
</project>
配置文件
spring:
application:
name: zuul-server-sentinel # 应用名称
cloud:
sentinel:
filter:
enabled: false
server:
port: 9001 # 端口
# 配置 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/
网关服务配置类
配置网关服务过滤器和网关限流规则
/**
* 网关服务配置类
*/
@Configuration
public class ZuulConfig {
// 底层继承了 ZuulFilter
@Bean
public ZuulFilter sentinelZuulPreFilter() {
// We can also provider the filter order in the constructor.
return new SentinelZuulPreFilter();
}
// 底层继承了 ZuulFilter
@Bean
public ZuulFilter sentinelZuulPostFilter() {
return new SentinelZuulPostFilter();
}
// 底层继承了 ZuulFilter
@Bean
public ZuulFilter sentinelZuulErrorFilter() {
return new SentinelZuulErrorFilter();
}
/**
* Spring 容器初始化的时候执行该方法
*/
@PostConstruct
public void doInit() {
// 注册 FallbackProvider
ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
// 加载网关限流规则
initGatewayRules();
}
/**
* 网关限流规则
*/
private void initGatewayRules() {
Set<GatewayFlowRule> rules = new HashSet<>();
/*
resource:资源名称,可以是网关中的 route 名称或者用户自定义的 API 分组名称
count:限流阈值
intervalSec:统计时间窗口,单位是秒,默认是 1 秒
*/
rules.add(new GatewayFlowRule("order-service")
.setCount(3) // 限流阈值
.setIntervalSec(60)); // 统计时间窗口,单位是秒,默认是 1 秒
// 加载网关限流规则
GatewayRuleManager.loadRules(rules);
}
}
多次访问:http://localhost:9001/order-service/order/1
自定义限流处理(网关服务降级)
发生限流之后的处理流程 :
发生限流之后可自定义返回参数,通过实现 ZuulBlockFallbackProvider 接口,默认的实现
是 DefaultBlockFallbackProvider 。
默认的 fallback route 的规则是 route ID 或自定义的 API 分组名称。
编写限流处理类
/**
* 对订单服务做服务容错处理
*/
public class OrderBlockFallbackProvider implements ZuulBlockFallbackProvider {
private Logger logger = LoggerFactory.getLogger(OrderBlockFallbackProvider.class);
@Override
public String getRoute() {
return "order-service"; // 服务名称
}
@Override
public BlockResponse fallbackResponse(String route, Throwable cause) {
logger.error("{} 服务触发限流", route);
if (cause instanceof BlockException) {
return new BlockResponse(429, "服务访问压力过大,请稍后再试。", route);
} else {
return new BlockResponse(500, "系统错误,请联系管理员。", route);
}
}
}
将限流处理类注册至 Zuul 容器
/**
* Spring 容器初始化的时候执行该方法
*/
@PostConstruct
public void doInit() {
// 注册 FallbackProvider
ZuulBlockFallbackManager.registerProvider(new OrderBlockFallbackProvider());
// 加载网关限流规则
initGatewayRules();
}
http://localhost:9001/order-service/order/1
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/121454.html