Zuul
诞生背景
微服务架构将后端拆解成许多个单独的应用:看似清晰的服务拆分,实则杂乱无章,完成一个业务逻辑,需要到不同主机和不同端口上面调取接口。于是一个面向服务治理、服务编排的㢟出现了——微服务网关。
Zuul 是从设备和网站到后端应用程序所有请求的前门,为内部服务提供可配置的对外 URL 到服务的映射关系,基于 JVM 的后端路由器,其具备以下功能:
- 统一接入:智能路由、AB 测试、灰度测试、负载均衡、容灾处理、日志埋点
- 流量监控:限流处理、服务降级
- 安全防护:鉴权处理、监控、机器网络隔离
虽然 Zuul2.x 采用 Netty 有较大的性能提升,单改动较大,考虑到稳定性 Spring Cloud Finchley 继续沿用 Netflix Zuul 1.x 版本,另外由于 Spring Cloud Gateway 已经孵化成功,相较于 Zuul 在功能以及性能上都有明显提升,Pivotal 公司正在走一条“去 Netflix 化”的路线。
主流开源网关概览
入门案例
Maven 依赖
<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>
添加配置
#Zuul对外端口号
server:
port: 9000
#服务的名称
spring:
application:
name: api-gateway
cloud:
client:
ipAddress: 192.168.31.230
#指定注册中心地址
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true #调试Eureka管控台实例的显示格式
instance-id: ${spring.cloud.client.ipAddress}:${server.port}:${spring.application.name}
启动类添加注解@EnableZuulProxy
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
访问 Eureka 管控台 http://localhost:8761/
#Zuul 内部实现细节
Zuul 原理
- 前置过滤器,在这个过程中可以自定义一些过滤器扩扩展功能(日志埋点、限流、鉴权等…)。
- 智能路由,将请求路由到对应的服务。
- 如果出错进入错误过滤器。
- 最终都会经过后置过滤器进行最后的处理,然后响应给前端请求。
过滤器执行顺序
过滤器的 order 值越小,越优先执行。
通过 RequestContext 对象共享上下文内容。
网关默认访问规则
改写请求,通过网关统一接入
http://192.168.31.230:8781/api/v1/order/save?product_id=4&user_id=2
http://192.168.31.230:9000/order-service/api/v1/order/save?product_id=4&user_id=2 #次请求经过网关
http://192.168.31.230:8771/api/v1/product/list
http://192.168.31.230:9000/product-service/api/v1/product/list #次请求经过网关
自定义路由规则
访问路径
http://192.168.31.230:9000/apigateway/product/api/v1/product/list
http://192.168.31.230:9000/apigateway/orderapi/v1/order/save?product_id=4&user_id=2
#自定义路由映射
zuul:
routes:
#解决路由映射重复覆盖问题
order-service: /apigateway/order/**
product-service: /apigateway/product/**
#统一入口为上面的配置,其他入口忽略
ignored-patterns: /*-service/**
#忽略整个服务,对外提供接口
#gnored-services: product-service
处理 http 请求头传递问题
默认 "Cookie"、"Set-Cookie"、"Authorization" 这三个请求头会被 Zuul 过滤掉,并不会传递到后端,可以通过相关配置解决此问题:
#自定义路由映射
zuul:
routes:
#解决http请求头经过Zuul时不传递的问题
sensitive-headers:
自定义 Zuul 过滤器实现登录鉴权
新建类并实现 ZuulFilter,然后实现方法,最后在加上@Component 注解
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.apache.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
@Component
public class LoginFilter extends ZuulFilter {
/**
* 设置顾虑器类型:前置过滤器
* @return
*/
@Override
public String filterType() {
return PRE_TYPE;
}
/**
* 过滤器执行优先级:数字越小,越优先执行。
* @return
*/
@Override
public int filterOrder() {
return 4;
}
/**
* 过滤器是否生效
* @return
*/
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
System.out.println(request.getRequestURI()); // /api-gateway/order/api/v1/order/save
System.out.println(request.getRequestURL()); // http://192.168.31.230:9000/api-gateway/order/api/v1/order/save
//ACL 访问控制列表,一般都是定期从redis中拉取过来动态填写。
//针对指定的服务URI进行拦截,而不是对所有URI进行拦截,这里可以用if进行判断。
if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
return true;
}
//if(){ } ......
return false;
}
/**
* 业务逻辑
* @return
* @throws ZuulException
*/
@Override
public Object run() throws ZuulException {
//JWT 查看是否有token
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//获取token
String token = request.getHeader("token");
if(StringUtils.isBlank(token)){//如果从head中获取为空,就从参数中获取
token = request.getParameter("token");
}
//登录校验逻辑,根据公司情况自定义
if(StringUtils.isBlank(token)) { //如果为token为空则返回错误响应
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
}
return null;
}
}
不带 token 参数的请求 http://192.168.31.230:9000/apigateway/order/api/v1/order/save?user_id=2&product_id=2
带 token 参数的请求
http://192.168.31.230:9000/apigateway/order/api/v1/order/save?user_id=2&product_id=2&token=asdasd
限流
构建一个自我修复型系统一直是各大企业进行架构设计的难点所在,在 Hystrix 中,我们可以通过熔断器来实现,通过触发某个阈值对异常流量进行降级处理。除此之外,也可以对异常流量进行提前的预防措施,比如:异步队列的削峰、流量排队、限流、分流等…
限流算法
- 漏桶(Leaky Bucket)
请求数据涌来,在漏桶的作用下流量被整形,不能满足要求的部分请求被削减掉(收集到一个队列中,做流量排队),漏铜算法能够强制限定流量速率。
- 令牌桶(Token Bucket)
桶里面存放令牌,而令牌又是以一个恒定的速率被加入桶内,可以积压,可以溢出。当数据涌来,量化请求用于获取令牌,如果获取到令牌则放行,同时桶内丢掉这个令牌,如果不能去得到令牌,请求则被丢弃。由于令牌桶内可以存在一定数量的令牌,那么就可能存在一定程度的流量突发,这也是决定漏桶算法与令牌桶算法使用与不同应用场景的主要原因。
谷歌 guava 框架
基于令牌桶算法
在网关中编写过滤器:集群中需要结合 Redis 进行限流,令牌可以存放在公共缓存中供集群中所有节点使用。
import com.google.common.util.concurrent.RateLimiter;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
/**
* 订单限流
*/
@Component
public class OrderRateLimiterFilter extends ZuulFilter {
//每秒产生1000个令牌
; 令牌的具体数量需要根据业务进行测试而定。
private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);
@Override
public String filterType() {
return PRE_TYPE;
}
@Override
public int filterOrder() {
return -4;
}
@Override
public boolean shouldFilter() {
RequestContext requestContext = RequestContext.getCurrentContext();
HttpServletRequest request = requestContext.getRequest();
//只对订单接口限流
if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
return true;
}
return false;
}
@Override
public Object run() throws ZuulException {
RequestContext requestContext = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()){ //如果没有获得令牌,则返回错误状态码
requestContext.setSendZuulResponse(false);
requestContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
}
return null;
}
}
网关的限流与高可用
- 通常情况下限流需要反响代理 Nginx 结合使用
- Zuul 的高可用比较简单,启动多个节点即可。
- Nginx 的高可用可以通过 nginx+lvs+keepalive 实现。