Zuul服务网关☣
目录
前提知识点总结(不属于正文)
一、什么是网关?
API Gateway(APIGW / API 网关),顾名思义,是出现在系统边界上的一个面向 API 的、串行集中式的强管控服务,这里的边界是企业 IT 系统的边界,可以理解为
企业级应用防火墙
,主要起到隔离外部访问与内部系统的作用
。在微服务概念的流行之前,API 网关就已经诞生了,例如银行、证券等领域常见的前置机系统,它也是解决访问认证、报文转换、访问统计等问题的。(网关是用户访问系统的第一关,网关里记录者注册中心中的所有的服务地址,通过网关进行认证,请求分发)
- Nginx 适合做门户网关,是作为整个全局的网关,对外的处于最外层的那种;而 Zuul 属于业务网关,主要用来对应不同的客户端提供服务,用于聚合业务。各个微服务独立部署,职责单一,对外提供服务的时候需要有一个东西把业务聚合起来。
- Zuul 可以实现熔断、重试等功能,这是 Nginx 不具备的。
二、Zuul实现API网关
官网文档:
准备一个聚合项目,一个父项目内两个注册中心,一个消费者,一个服务提供者,这里不惜介绍前面文章有提,启动项目测试看一下服务是否注册到注册中心:
① 添加子模块zuul-server
创建一个java项目,引入依赖和配置即可
<?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.yjxxt</groupId>
<artifactId>zuul-server</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 继承父依赖 -->
<parent>
<groupId>com.yjxxt</groupId>
<artifactId>zuul-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<!-- 项目依赖 -->
<dependencies>
<!-- spring cloud netflix zuul 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 9000 # 端口
spring:
application:
name: zuul-server # 应用名称
启动类
@SpringBootApplication
// 开启 Zuul 注解
@EnableZuulProxy
public class ZuulServerApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulServerApplication.class, args);
}
}
这里zuul模块建立完毕
② 配置路由
配置路由后平时的服务请求假设:http://localhost:7070/order/10,配置路由后:http://localhost:7070/product-service/order/10,这样指定某服务下的接口
application.yml配置
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
url: http://localhost:7070/ # 映射路径对应的微服务地址
启动测试:
弊端就是每次更换服务都需要手动修改
③ 服务名称路由
- 添加 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 # 根据 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/
- 启动类
package com.yjxxt;
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);
}
}
④ 简化路由
Zuul 为了方便大家使用,提供了默认路由配置:路由 id 和
微服务名称
一致,path 默认对应/微服务名称/**
,所以以下配置就没必要再写了。
# 路由规则
zuul:
routes:
product-service: # 路由 id 自定义
path: /product-service/** # 配置请求 url 的映射路径
serviceId: product-service # 根据 serviceId 自动从注册中心获取服务地址并转发请求
还可以路由地址排除和路由名称排除以及路由前缀等,此处不做解析
三、网关过滤器
四个过滤器,pre过滤器主要进行请求的映射与转发服务,还有一部分有routing过滤器进行转发,post过滤器则在routing 和 error 过滤器之后被调用是对结果的返回,error过滤器只有在出现错误时才会触发
① 入门Demo
创建类Filter/CustomFilter .java过滤器
Spring Cloud Netflix Zuul 中实现过滤器必须包含 4 个基本特征:过滤器类型,执行顺序,执行条件,动作(具体操作)。这些步骤都是ZuulFilter
接口中定义的 4 个抽象方法:
/**
* 网关过滤器
*/
@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.warn("CustomFilter...method={}, url={}",
request.getMethod(),
request.getRequestURL().toString());
return null;
}
}
测试http://localhost:9000/product-service/product/521
请求日志的打印
② 统一鉴权
类似于认证,即token,否则无权限访问服务
修改过滤器:
/*通过过滤器进行认证*/
@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/521
③ 网关过滤器异常统一处理
修改过滤器:
/*通过过滤器统一管理异常*/
@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;
}
制造一个异常,添加一个类AccessFilter.java(把认证过滤器提出一个类出来)
@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 {
// 模拟异常
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;
}
}
添加异常处理过滤器
/**
* 异常过滤器
*/
@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:
# 禁用 Zuul 默认的异常处理 filter
SendErrorFilter:
error:
disable: true
结果测试:
④ Zuul请求的声明周期
- HTTP 发送请求到 Zuul 网关
- Zuul 网关首先经过 pre filter
- 验证通过后进入 routing filter,接着将请求转发给远程服务,远程服务执行完返回结果,如果出错,则执行 error filter
- 继续往下执行 post filter
- 最后返回响应给 HTTP 客户端
四、Zuul 和 Hystrix 无缝结合
想要实现网络监控,就要配置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>
# 配置
# 度量指标监控与健康检查
management:
endpoints:
web:
exposure:
include: hystrix.stream
启动类// 开启 Zuul 注解@EnableZuulProxy
启动项目测试访问:http://localhost:9000/hystrix
监听服务地址http://localhost:9000/actuator/hystrix.stream
② 网关熔断
在 Edgware 版本之前,Zuul 提供了接口
ZuulFallbackProvider
用于实现 fallback 处理。从 Edgware 版本开始,Zuul 提供了接口FallbackProvider
来提供 fallback 处理。编写实现类:ProductProviderFallback(熔断只有在服务宕机的时候没有任何反馈才会熔断,所有测试时挂掉服务即可)
/**
* 对商品服务做服务容错处理
*/
@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() {
}
};
}
}
一定是服务宕机后刷新才会出现:
③ 网关限流
就是在指定的时间内规定多少请求,一旦超标,则不接受请求,多于请求直接返回,限流算法:三种
- 计数器算法
- 漏桶(Leaky Bucket)算法
- 令牌桶(Token Bucket)算法
计数器
设置一个计数器,一分钟比如允许100个请求,所以计数器一分钟之内没满一百则重新计数。缺点就是恶意在59s100个请求,1分钟又100个导致压垮服务,这是一个漏洞,而且容易闲置
漏桶算法
就是用一个水桶下面一个洞,水桶去存放大量请求,再通过小洞一个一个处理请求,一旦请求大于水桶,则直接fallback,类似消息队列
令牌桶算法
一个桶里存放令牌,请求会去拿令牌,令牌会一直生成,多余令牌直接舍弃,请求拿到令牌才能去访问资源,拿到令牌的请求会被执行,否则fallback或者缓存
代码实现:
// 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 会对代理的所有服务提供限流保护(记得开启redis缓存数据库)。
server:
port: 9000 # 端口
spring:
application:
name: zuul-server # 应用名称
# redis 缓存
redis:
host: 192.168.10.106 # Redis服务器地址
# 配置 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 和 Sentinel 整合等,内容很多这里就先到这。