Zuul API网关
Zuul API网关
解决问题: 服务器集群的集成调用, 集成了ribbon, hystrix, actuator等
zuul API 网关,为微服务应用提供统一的对外访问接口。
zuul 还提供过滤器,对所有微服务提供统一的请求校验。
快速开始
前提
例如我们有三中业务服务器, 服务器id名为: item-service
, order-service
, user-service
并且配置了eureka注册中心
1. 创建项目导入依赖
创建springboot项目, 并导入依赖Zuul
, Eureka Discovery Client
<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>
2. yml配置文件
spring:
application:
name: zuul # 服务器id/名称
server:
port: 3001 # 服务器端口
# 配置eureka
eureka:
client:
service-url:
defaultZone: http://eureka1:2001/eureka,http://eureka2:2002/eureka
# 配置zuul
# 默认规则:
# 如果不配置,zuul会根据注册表的注册信息,完成自动配置
# 最好自己手动配置,避免注册表信息不全,自动配置不完整
zuul:
routes:
item-service: /item-service/**
user-service: /user-service/**
order-service: /order-service/**
3. 主程序注解
添加 @EnableZuulProxy
和 @EnableDiscoveryClient
(高版本springboot可省略此注解) 注解
@EnableZuulProxy
// @EnableDiscoveryClient // springboot高版本可省略
@SpringBootApplication
public class Sp11ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(Sp11ZuulApplication.class, args);
}
}
4. 访问测试
例如我们item-service
有个/getItem
请求, 我们可以访问我们的zuul服务器来发送请求, 如下
http://localhost:3001/item-service/getItem
// 3001 为zuul的端口, 即访问的zuul服务器
Zuul 请求过滤
请求过滤的本质就是对接口的访问权限进行限制,实现的思路大致有以下几种:
- 为每个服务写一套校验签名和鉴别权限的过滤器和拦截器。(代码是真的冗余)。
- 将校验权限剥离,构成一个独立的鉴权服务。(换汤不换药,还是代码冗余)。
- 在API网关中实现对客户端请求的校验。(请求过滤)。
过滤方式
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
- ERROR:在其他阶段发生错误时执行该过滤器。
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
1. 定义过滤器
过滤器需要继承ZuulFilter
抽象类, 并重写其4个方法, 使用@Component
注解注入spring容器
package cn.tedu.sp11.filter;
import cn.tedu.web.util.JsonResult;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class AccessFilter extends ZuulFilter {
/**
* 指定过滤器的类型: pre(前置), post(发布), routing(路由), error(错误) <br>
* 多数情况下用pre
* @return 可以直接返回字符串, 例如"pre", 或者使用FilterConstants中的常量
*/
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
/**
* 顺序号 <br>
* 在默认过滤器中, 第5个过滤器在上下文对象中添加了 service-id(服务id) <br>
* 所以在第5个过滤器之后, 才能从上下文对象访问 service-id(服务id) <br>
* 如果需要用到service-id(服务id), 建议使用5之后的数字
* @return 6
*/
@Override
public int filterOrder() {
return 6;
}
/**
* 对当前请求, 是否要进行过滤 <br>
* @return
* 如果返回true, 要进行过滤, 会执行下面的过滤方法run() <br>
* 如果返回false, 不需要过滤, 跳过run()方法, 继续执行后面的流程
*/
@Override
public boolean shouldFilter() {
// 判断用户调用的是否是商品服务
// 如果是商品服务进行过滤, 其他服务不过滤
// 获取上下文请求对象
RequestContext ctx = RequestContext.getCurrentContext();
// 从上下文对象获取客户端调用的service-id
String serviceId = (String) ctx.get(FilterConstants.SERVICE_ID_KEY);
// 如果是商品服务, 返回true, 如果不是则返回false
return "item-service".equals(serviceId);
}
/**
* 过滤代码, 如果有token参数, 则继续执行, 如果为空, 则返回对应内容
* @return 返回值再当前zuul版本中没有启动, 返回任何数据都无效, 作者考虑了返回值后续可能会有用
* @throws ZuulException 异常
*/
@Override
public Object run() throws ZuulException {
// http://localhost:3001/item-service/123?token=ds54f6af
// 获取上下文请求对象
RequestContext ctx = RequestContext.getCurrentContext();
// 从上下文对象获request对象
HttpServletRequest request = ctx.getRequest();
// 获取请求参数
String token = request.getParameter("token");
if (StringUtils.isBlank(token)) { // 判断是否为空串
// 如果为空, 没有token参数, 阻止这次调用继续执行
ctx.setSendZuulResponse(false);
// 在这里, 直接向客户端发送响应
// JsonResult: {code: 400, msg: not log in, data: null}
ctx.setResponseStatusCode(JsonResult.NOT_LOGIN);
ctx.setResponseBody(
JsonResult.err()
.code(JsonResult.NOT_LOGIN)
.msg("not log in")
.toString());
}
return null;
}
}
2. 访问测试即可
测试访问: http://localhost:3001/item-service/35
和 http://localhost:3001/item-service/35?token=1234
一个有token参数, 一个没有
zuul集成ribbon
负载均衡和重试
zuul已经集成了ribbon, 默认已经实现了负载均衡, 但是没有默认开启重试功能(zuul框架不推荐开启重试)
1. zuul + ribbon 重试
需要 spring-retry 依赖 springboot依赖里没有, 从这里复制即可
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
2. 配置重试 yml
需要配置zuul.retryable=true
来开启重试功能, 可配置重试参数
zuul:
routes:
item-service: /item-service/**
user-service: /user-service/**
order-service: /order-service/**
retryable: true # 开启重试功能
# zuul:
# retryable: true
# 配置重试参数 (可选)
# ribbon:
# ConnectTimeout: 1000
# ReadTimeout: 1000
# MaxAutoRetriesNextServer: 1
# MaxAutoRetries: 1
zuul集成hystrix
1. 创建降级类
创建降级类, 需要实现FallbackProvider
接口, 重写其两个方法, 记得加@Component
注解, 注入ioc容器
package cn.tedu.sp11.fallback;
import cn.tedu.web.util.JsonResult;
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;
@Component
public class ItemFallback implements FallbackProvider {
/**
* 返回服务id, 即service-id <br>
* 针对哪个服务应用当前降级类
* @return 如果返回"*"或null, 表示对所有服务都执行当前降级类 <br>
* 如果返回"item-service" 只对item-service服务降级
*/
@Override
public String getRoute() {
return "item-service";
}
/**
* 降级执行的代码
* ClientHttpResponse 是封装降级响应的对象
*/
@Override
public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
return new ClientHttpResponse() {
// 状态文本和状态码
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
// 状态码
@Override
public int getRawStatusCode() throws IOException {
return HttpStatus.OK.value();
}
// 状态文本
@Override
public String getStatusText() throws IOException {
return HttpStatus.OK.getReasonPhrase();
}
@Override
public void close() {
}
// 返回响应体
@Override
public InputStream getBody() throws IOException {
// JsonResult: {code: 200, msg: "调用后台服务失败", data: null}
String json = JsonResult.err().msg("调用商品服务失败").toString();
ByteArrayInputStream in = new ByteArrayInputStream(json.getBytes());
return in;
}
// 返回响应头
@Override
public HttpHeaders getHeaders() {
// Content-Type: application/json
HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON_UTF8);
return h;
}
};
}
}
2. hystrix 超时时间
为了方便测试, 这里设置为500, 一般设置比ribbon重试时间长即可
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 500
3. 测试
自行想办法测试, 相信大家都会
zuul + hystrix 数据监控
暴露 hystrix.stream 监控端点
- zuul 已经包含 actuator 依赖
management:
endpoints:
web:
exposure:
include: hystrix.stream
启动服务,查看暴露端点
所有暴露的监控点: http://localhost:3001/actuator
hystrix监控点: http://localhost:3001/actuator/hystrix.stream
启动仪表盘项目
关于dashboard 断路器仪表盘项目的创建: dashboard 断路器仪表盘
启动仪表盘项目后, 填入上面的hystrix的监控点链接即可
注意: 必须通过zuul网关访问后台服务才会长生监控数据
zuul + turbine 聚合监控
我们可以在turbin的项目中把zuul添加进去即可 (使用逗号隔开)
turbine中配置
turbine:
app-config: order-service, zuul
cluster-name-expression: new String("default")
熔断测试
并发工具下载地址: http://httpd.apache.org/docs/current/platform/windows.html#down
熔断测试,ab -n 20000 -c 50 http://localhost:3001/order-service/123abc
zuul Cookie过滤
zuul 会过滤敏感 http 协议头,默认过滤以下协议头:
- Cookie
- Set-Cookie
- Authorization
可以设置 zuul 不过滤这些协议头
zuul:
sensitive-headers: