Zuul
诞生背景
微服务架构将后端拆解成许多个单独的应用:看似清晰的服务拆分,实则杂乱无章,完成一个业务逻辑,需要到不同主机和不同端口上面调取接口。于是一个面向服务治理、服务编排的㢟出现了——微服务网关。
Zuul 是从设备和网站到后端应用程序所有请求的前门,为内部服务提供可配置的对外 URL 到服务的映射关系,基于 JVM 的后端路由器,其具备以下功能:
- 统一接入:智能路由、AB 测试、灰度测试、负载均衡、容灾处理、日志埋点
- 流量监控:限流处理、服务降级
- 安全防护:鉴权处理、监控、机器网络隔离
虽然 Zuul2.x 采用 Netty 有较大的性能提升,单改动较大,考虑到稳定性 Spring Cloud Finchley 继续沿用 Netflix Zuul 1.x 版本,另外由于 Spring Cloud Gateway 已经孵化成功,相较于 Zuul 在功能以及性能上都有明显提升,Pivotal 公司正在走一条“去 Netflix 化”的路线。
主流开源网关概览
入门案例
Maven 依赖
1<dependency>
2 <groupId>org.springframework.cloud</groupId>
3 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
4 </dependency>
5 <dependency>
6 <groupId>org.springframework.cloud</groupId>
7 <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
8 </dependency>
添加配置
1#Zuul对外端口号
2server:
3 port: 9000
4
5#服务的名称
6spring:
7 application:
8 name: api-gateway
9 cloud:
10 client:
11 ipAddress: 192.168.31.230
12
13#指定注册中心地址
14eureka:
15 client:
16 serviceUrl:
17 defaultZone: http://localhost:8761/eureka/
18 instance:
19 prefer-ip-address: true #调试Eureka管控台实例的显示格式
20 instance-id: ${spring.cloud.client.ipAddress}:${server.port}:${spring.application.name}
启动类添加注解 @EnableZuulProxy
1@SpringBootApplication
2@EnableZuulProxy
3public class ApiGatewayApplication {
4
5 public static void main(String[] args) {
6 SpringApplication.run(ApiGatewayApplication.class, args);
7 }
8}
访问 Eureka 管控台 http://localhost:8761/
#Zuul 内部实现细节
Zuul 原理
- 前置过滤器,在这个过程中可以自定义一些过滤器扩扩展功能(日志埋点、限流、鉴权等…)。
- 智能路由,将请求路由到对应的服务。
- 如果出错进入错误过滤器。
- 最终都会经过后置过滤器进行最后的处理,然后响应给前端请求。
过滤器执行顺序
过滤器的 order 值越小,越优先执行。
通过 RequestContext 对象共享上下文内容。
网关默认访问规则
改写请求,通过网关统一接入
1http://192.168.31.230:8781/api/v1/order/save?product_id=4&user_id=2
2http://192.168.31.230:9000/order-service/api/v1/order/save?product_id=4&user_id=2 #次请求经过网关
3
4http://192.168.31.230:8771/api/v1/product/list
5http://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
1#自定义路由映射
2zuul:
3 routes:
4 #解决路由映射重复覆盖问题
5 order-service: /apigateway/order/**
6 product-service: /apigateway/product/**
7 #统一入口为上面的配置,其他入口忽略
8 ignored-patterns: /*-service/**
9 #忽略整个服务,对外提供接口
10 #gnored-services: product-service
处理 http 请求头传递问题
默认 "Cookie"、"Set-Cookie"、"Authorization" 这三个请求头会被 Zuul 过滤掉,并不会传递到后端,可以通过相关配置解决此问题:
1#自定义路由映射
2zuul:
3 routes:
4 #解决http请求头经过Zuul时不传递的问题
5 sensitive-headers:
自定义 Zuul 过滤器实现登录鉴权
新建类并实现 ZuulFilter,然后实现方法,最后在加上 @Component 注解
1import com.netflix.zuul.ZuulFilter;
2import com.netflix.zuul.context.RequestContext;
3import com.netflix.zuul.exception.ZuulException;
4import org.apache.commons.lang.StringUtils;
5import org.apache.http.HttpStatus;
6import org.springframework.stereotype.Component;
7import javax.servlet.http.HttpServletRequest;
8import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
9
10@Component
11public class LoginFilter extends ZuulFilter {
12 /**
13 * 设置顾虑器类型:前置过滤器
14 * @return
15 */
16 @Override
17 public String filterType() {
18 return PRE_TYPE;
19 }
20
21 /**
22 * 过滤器执行优先级:数字越小,越优先执行。
23 * @return
24 */
25 @Override
26 public int filterOrder() {
27 return 4;
28 }
29 /**
30 * 过滤器是否生效
31 * @return
32 */
33 @Override
34 public boolean shouldFilter() {
35 RequestContext requestContext = RequestContext.getCurrentContext();
36 HttpServletRequest request = requestContext.getRequest();
37 System.out.println(request.getRequestURI()); // /api-gateway/order/api/v1/order/save
38 System.out.println(request.getRequestURL()); // http://192.168.31.230:9000/api-gateway/order/api/v1/order/save
39 //ACL 访问控制列表,一般都是定期从redis中拉取过来动态填写。
40 //针对指定的服务URI进行拦截,而不是对所有URI进行拦截,这里可以用if进行判断。
41 if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
42 return true;
43 }
44 //if(){ } ......
45 return false;
46 }
47
48 /**
49 * 业务逻辑
50 * @return
51 * @throws ZuulException
52 */
53 @Override
54 public Object run() throws ZuulException {
55 //JWT 查看是否有token
56 RequestContext requestContext = RequestContext.getCurrentContext();
57 HttpServletRequest request = requestContext.getRequest();
58
59 //获取token
60 String token = request.getHeader("token");
61 if(StringUtils.isBlank(token)){//如果从head中获取为空,就从参数中获取
62 token = request.getParameter("token");
63 }
64
65 //登录校验逻辑,根据公司情况自定义
66 if(StringUtils.isBlank(token)) { //如果为token为空则返回错误响应
67 requestContext.setSendZuulResponse(false);
68 requestContext.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
69 }
70 return null;
71 }
72}
不带 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 进行限流,令牌可以存放在公共缓存中供集群中所有节点使用。
1import com.google.common.util.concurrent.RateLimiter;
2
3import com.netflix.zuul.ZuulFilter;
4import com.netflix.zuul.context.RequestContext;
5import com.netflix.zuul.exception.ZuulException;
6import org.springframework.http.HttpStatus;
7import org.springframework.stereotype.Component;
8
9import javax.servlet.http.HttpServletRequest;
10
11import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
12
13/**
14 * 订单限流
15 */
16@Component
17public class OrderRateLimiterFilter extends ZuulFilter {
18
19
20 //每秒产生1000个令牌
21 ; 令牌的具体数量需要根据业务进行测试而定。
22 private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);
23
24 @Override
25 public String filterType() {
26 return PRE_TYPE;
27 }
28
29 @Override
30 public int filterOrder() {
31 return -4;
32 }
33
34 @Override
35 public boolean shouldFilter() {
36
37 RequestContext requestContext = RequestContext.getCurrentContext();
38 HttpServletRequest request = requestContext.getRequest();
39
40 //只对订单接口限流
41 if ("/apigateway/order/api/v1/order/save".equalsIgnoreCase(request.getRequestURI())){
42 return true;
43 }
44
45 return false;
46 }
47
48 @Override
49 public Object run() throws ZuulException {
50 RequestContext requestContext = RequestContext.getCurrentContext();
51 if(!RATE_LIMITER.tryAcquire()){ //如果没有获得令牌,则返回错误状态码
52 requestContext.setSendZuulResponse(false);
53 requestContext.setResponseStatusCode(HttpStatus.TOO_MANY_REQUESTS.value());
54 }
55 return null;
56 }
57}
网关的限流与高可用
- 通常情况下限流需要反响代理 Nginx 结合使用
- Zuul 的高可用比较简单,启动多个节点即可。
- Nginx 的高可用可以通过 nginx+lvs+keepalive 实现。