目录
注意:本文参考
docs/system-design/framework/springcloud/springcloud-intro.md · SnailClimb/JavaGuide - Gitee.com
20000 字的 Spring Cloud 总结,从此任何问题也难不住你
Zuul工作原理_吴声子夜歌的博客-CSDN博客_zuul原理
为什么要有Zuul
基于上面的学习,我们现在的架构很可能会设计成这样:
这样的架构会有两个比较麻烦的问题:
1 路由规则与服务实例的维护间题:外层的负载均衡(nginx)需要维护所有的服务实例清单(图上的OpenService)
2 签名校验、 登录校验冗余问题:为了保证对外服务的安全性, 我们在服务端实现的微服务接口,往往都会有一定的权限校验机制,但我们的服务是独立的,我们不得不在这些应用中都实现这样一套校验逻辑,这就会造成校验逻辑的冗余。
还是画个图来理解一下吧:
每个服务都有自己的IP地址,Nginx想要正确请求转发到服务上,就必须维护着每个服务实例的地址!
更是灾难的是:这些服务实例的IP地址还有可能会变,服务之间的划分也很可能会变。
http://123.123.123.123
http://123.123.123.124
http://123.123.123.125
http://123.123.123.126
http://123.123.123.127
购物车和订单模块都需要用户登录了才可以正常访问,基于现在的架构,只能在购物车和订单模块都编写校验逻辑,这无疑是冗余的代码。
为了解决上面这些常见的架构问题,API网关的概念应运而生。在SpringCloud中了提供了基于Netfl ix Zuul实现的API网关组件Spring Cloud Zuul。
Spring Cloud Zuul是这样解决上述两个问题的:
SpringCloud Zuul通过与SpringCloud Eureka进行整合,将自身注册为Eureka服务治理下的应用,同时从Eureka中获得了所有其他微服务的实例信息。外层调用都必须通过API网关,使得将维护服务实例的工作交给了服务治理框架自动完成。
在API网关服务上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。
Zuul天生就拥有线程隔离和断路器的自我保护功能,以及对服务调用的客户端负载均衡功能。也就是说:Zuul也是支持Hystrix和Ribbon。
Zuul简介
Zuul包含了对请求的路由和过滤两个最主要的功能:
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础.
Zuul和Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的消息,也即以后的访问微服务都是通过Zuul跳转后获得。
注意:Zuul服务最终还是会注册进Eureka
Zuul提供了代理+路由+过滤三大功能
Zuul是Netflix开源的微服务网关,核心是一系列过滤器。这些过滤器可以完成以下功能。
1. 是所有微服务入口,进行分发。
2. 身份认证与安全。识别合法的请求,拦截不合法的请求。
3. 监控。在入口处监控,更全面。
4. 动态路由。动态将请求分发到不同的后端集群。
5. 压力测试。可以逐渐增加对后端服务的流量,进行测试。
6. 负载均衡。也是用ribbon。
7. 限流。比如我每秒只要1000次,10001次就不让访问了。
8. 服务熔断
网关和服务的关系:演员和剧场检票人员的关系。
路由访问映射规则
简单配置
本来想给你们复制一些代码,但是想了想,因为各个代码配置比较零散,看起来也比较零散,我决定还是给你们画个图来解释吧。
比如这个时候我们已经向 Eureka Server
注册了两个 Consumer
、三个 Provicer
,这个时候我们再加个 Zuul
网关应该变成这样子了。
emmm,信息量有点大,我来解释一下。关于前面的知识我就不解释了
首先,Zuul
需要向 Eureka
进行注册,注册有啥好处呢?
你傻呀,Consumer
都向 Eureka Server
进行注册了,我网关是不是只要注册就能拿到所有 Consumer
的信息了?
拿到信息有什么好处呢?
我拿到信息我是不是可以获取所有的 Consumer
的元数据(名称,ip,端口)?
拿到这些元数据有什么好处呢?拿到了我们是不是直接可以做路由映射?比如原来用户调用 Consumer1
的接口 localhost:8001/studentInfo/update
这个请求,我们是不是可以这样进行调用了呢?localhost:9000/consumer1/studentInfo/update
呢?你这样是不是恍然大悟了?
这里的 url 为了让更多人看懂所以没有使用 restful 风格。
上面的你理解了,那么就能理解关于 Zuul
最基本的配置了,看下面。
server:
port: 9000
eureka:
client:
service-url:
# 这里只要注册 Eureka 就行了
defaultZone: http://localhost:9997/eureka
然后在启动类上加入 @EnableZuulProxy
注解就行了。没错,就是那么简单
统一前缀
这个很简单,就是我们可以在前面加一个统一的前缀,比如我们刚刚调用的是 localhost:9000/consumer1/studentInfo/update
,这个时候我们在 yaml
配置文件中添加如下。
zuul:
prefix: /zuul
这样我们就需要通过 localhost:9000/zuul/consumer1/studentInfo/update
来进行访问了。
相当于 路由ip:路由端口/具体微服务名字/该微服务请求路径
原来是 具体微服务ip:具体微服务端口/该微服务请求路径
路由策略配置
你会发现前面的访问方式(直接使用服务名),需要将微服务名称暴露给用户,会存在安全性问题。所以,可以自定义路径来替代微服务名称,即自定义路由策略。
将地址里的微服务 具体名称替换,防止别人发现名称
zuul:
routes:
consumer1: /FrancisQ1/**
consumer2: /FrancisQ2/**
这个时候你就可以使用 ``localhost:9000/zuul/FrancisQ1/studentInfo/update` 进行访问了。
服务名忽略
这个时候你别以为你好了,你可以试试,在你配置完路由策略之后使用微服务名称还是可以访问的,这个时候你需要将服务名屏蔽。
zuul:
ignore-services: "*"
路径屏蔽
Zuul
还可以指定屏蔽掉的路径 URI,即只要用户请求中包含指定的 URI 路径,那么该请求将无法访问到指定的服务。通过该方式可以限制用户的权限。
zuul:
ignore-patterns: **/auto/**
这样关于 auto 的请求我们就可以过滤掉了。
** 代表匹配多级任意路径
*代表匹配一级任意路径
敏感请求头屏蔽
默认情况下,像 Cookie
、Set-Cookie
等敏感请求头信息会被 zuul
屏蔽掉,我们可以将这些默认屏蔽去掉,当然,也可以添加要屏蔽的请求头。
设置统一的公共前缀
访问地址:http://myzuul.com:9527/atguigu/mydept/dept/get/1
zuul:
prefix: /atguigu
ignored-services: "*"
routes:
mydept.serviceId: microservicecloud-dept
mydept.path: /mydept/**
最后的yml
server:
port: 9527
spring:
application:
name: microservicecloud-zuul-gateway
zuul:
prefix: /atguigu
ignored-services: "*"
routes:
mydept.serviceId: microservicecloud-dept
mydept.path: /mydept/**
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka,http://eureka7003.com:7003/eureka
instance:
instance-id: gateway-9527.com
prefer-ip-address: true
info:
app.name: atguigu-microcloud
company.name: www.atguigu.com
build.artifactId: $project.artifactId$
build.version: $project.version$
Zuul 的过滤功能
如果说,路由功能是 Zuul
的基操的话,那么过滤器就是 Zuul
的利器了。毕竟所有请求都经过网关(Zuul),那么我们可以进行各种过滤,这样我们就能实现 限流,灰度发布,权限控制 等等。
简单实现一个请求时间日志打印
要实现自己定义的 Filter
我们只需要继承 ZuulFilter
然后将这个过滤器类以 @Component
注解加入 Spring 容器中就行了。
在给你们看代码之前我先给你们解释一下关于过滤器的一些注意点。
过滤器类型:Pre
、Routing
、Post
。前置Pre
就是在请求之前进行过滤,Routing
路由过滤器就是我们上面所讲的路由策略,而Post
后置过滤器就是在 Response
之前进行过滤的过滤器。你可以观察上图结合着理解,并且下面我会给出相应的注释。
// 加入Spring容器
@Component
public class PreRequestFilter extends ZuulFilter {
// 返回过滤器类型 这里是前置过滤器
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
// 指定过滤顺序 越小越先执行,这里第一个执行
// 当然不是只真正第一个 在Zuul内置中有其他过滤器会先执行
// 那是写死的 比如 SERVLET_DETECTION_FILTER_ORDER = -3
@Override
public int filterOrder() {
return 0;
}
// 什么时候该进行过滤
// 这里我们可以进行一些判断,这样我们就可以过滤掉一些不符合规定的请求等等
@Override
public boolean shouldFilter() {
return true;
}
// 如果过滤器允许通过则怎么进行处理
@Override
public Object run() throws ZuulException {
// 这里我设置了全局的RequestContext并记录了请求开始时间
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());
return null;
}
}
// lombok的日志
@Slf4j
// 加入 Spring 容器
@Component
public class AccessLogFilter extends ZuulFilter {
// 指定该过滤器的过滤类型
// 此时是后置过滤器
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
// SEND_RESPONSE_FILTER_ORDER 是最后一个过滤器
// 我们此过滤器在它之前执行
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
// 过滤时执行的策略
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
// 从RequestContext获取原先的开始时间 并通过它计算整个时间间隔
Long startTime = (Long) context.get("startTime");
// 这里我可以获取HttpServletRequest来获取URI并且打印出来
String uri = request.getRequestURI();
long duration = System.currentTimeMillis() - startTime;
log.info("uri: " + uri + ", duration: " + duration / 100 + "ms");
return null;
}
}
上面就简单实现了请求时间日志打印功能,你有没有感受到 Zuul
过滤功能的强大了呢?
没有?好的、那我们再来。
令牌桶限流
当然不仅仅是令牌桶限流方式,Zuul
只要是限流的活它都能干,这里我只是简单举个例子
我先来解释一下什么是 令牌桶限流 吧。
首先我们会有个桶,如果里面没有满那么就会以一定 固定的速率 会往里面放令牌,一个请求过来首先要从桶中获取令牌,如果没有获取到,那么这个请求就拒绝,如果获取到那么就放行。很简单吧,啊哈哈、
下面我们就通过 Zuul
的前置过滤器来实现一下令牌桶限流。
package com.lgq.zuul.filter;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("访问量超载");
// 指定当前请求未通过过滤
context.setSendZuulResponse(false);
// 向客户端返回响应码429,请求数量过多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
这样我们就能将请求数量控制在一秒两个,有没有觉得很酷?
Zuul过滤原理
zuul的核心逻辑都是由一系列filter过滤器链实现的
Zuul的核心是一系列过滤器,可以在Http请求的发起和响应返回期间执行一系列的过滤器。Zuul包括以下4中过滤器:
PRE过滤器:它是在请求路由道具体的服务之前执行的,这种类型的过滤器可以做安全验证,例如身份验证、参数验证、认证鉴权,限流等。
ROUTING过滤器:它用于将请求路由到具体的微服务实例。在默认情况下,它使用Http Client进行网络请求,现在也支持OKHTTP。
POST过滤器:它是请求已被路由到微服务后执行的。一般情况下,用作收集统计信息、指标,以及将响应传送到客户端。如果需要对返回的结果进行再次处理,可以在这种过滤中处理逻辑。
ERROR过滤器:它是在其他过滤器发生错误时执行的。如果发生异常,就执行该filter,可以做全局异常处理。
Zuul采取了动态读取、编译和运行这些过滤器。过滤器之间不能直接相互通信,而是通过RequestContext对象来共享数据,每个请求都会创建一个RequestContext对象。
但是filter的类型不同,执行的时机也不同,效果自然也不一样,主要特点如下:
1 filter的类型:filter的类型,决定了它在整个filter链中的执行顺序,可能在端点路由前执行,也可能在端点路由时执行,还有可能在端点路由后执行,甚至是端点路由发生异常时执行。
2 filter的执行顺序:同一种类型的filter,可以通过filterOrder()方法设置执行顺序,一般都是根据业务场景自定义filter执行顺序。
3 filter执行条件:filter运行所需的标准,或条件。
4 filter执行效果:符合某个filter执行条件,产生执行效果。
zuul内部有一套完整的机制,可以动态读取编译运行filter机制,filter与filter之间不直接通信,在请求线程中会通过RequestContext来共享状态,它内部是用ThreadLocal实现的,例如HttpServletRequest、HttpServletResponse、异常信息等。部分源码如下:
public class RequestContext extends ConcurrentHashMap<String, Object> {
private static final Logger LOG = LoggerFactory.getLogger(RequestContext.class);
protected static Class<? extends RequestContext> contextClass = RequestContext.class;
private static RequestContext testContext = null;
protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
@Override
protected RequestContext initialValue() {
try {
return contextClass.newInstance();
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}; //.......}
当一个客户端Request请求进入Zuul网关服务时,网关先进入“pre filter”, 进行一系列的验证、操作或者判断。然后交给“routing filter” 进行路由转发,转发到具体的服务实例进行逻辑处理、返回数据。当具体的服务处理完后,最后由“post filter”进行处理,该类型的处理器处理完之后,将Response信息返回给客户端。
ZuulServlet是Zuul的核心Servlet。 ZuulServlet 的作用是初始化ZuulFilter,并编排这些ZuulFilter的执行顺序。该类中有一个service()方法,执行了过滤器执行的逻辑。
从上面的代码可知,首先执行preRoute()方法,这个方法执行的是PRE类型的过滤器的逻辑。如果执行这个方法时出错了,那么会执行error(e)和postRoute()。然后执行route()方法,该方法是执行ROUTING类型过滤器的逻辑。最后执行postRoute(),该方法执行了POST类型过滤器的逻辑。
public class ZuulServlet extends HttpServlet {
private static final long serialVersionUID = -3374242278843351500L;
private ZuulRunner zuulRunner;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
String bufferReqsStr = config.getInitParameter("buffer-requests");
boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;
zuulRunner = new ZuulRunner(bufferReqs);
}
@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
// Marks this request as having passed through the "Zuul engine", as opposed to servlets
// explicitly bound in web.xml, for which requests will not have the same data attached
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
preRoute(); //如果preRoute方法在执行的时候出现异常,直接就抛出500异常,不会走catch中的error方法,见下图FilterProcessor类中的preRoute方法。
} catch (ZuulException e) {
error(e); //如果preRoute在执行过程中,抛出Zuul异常,这里被捕捉到以后,会执行error方法,打印堆栈信息,见下图FilterProcessor类中的error方法。
postRoute();
return;
}
try {
route();
} catch (ZuulException e) {
error(e);
postRoute();
return;
}
try {
postRoute();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
} //.......
}
关于 Zuul 的其他
Zuul
的过滤器的功能肯定不止上面我所实现的两种,它还可以实现 权限校验,包括我上面提到的 灰度发布 等等。
当然,Zuul
作为网关肯定也存在 单点问题 ,如果我们要保证 Zuul
的高可用,我们就需要进行 Zuul
的集群配置,这个时候可以借助额外的一些负载均衡器比如 Nginx
。
Zuul和Feign和Nginx
Zuul支持Ribbon和Hystrix,也能够实现客户端的负载均衡。我们的Feign不也是实现客户端的负载均衡和Hystrix的吗?既然Zuul已经能够实现了,那我们的Feign还有必要吗?
或者可以这样理解:
zuul是对外暴露的唯一接口相当于路由的是controller的请求,而Ribbon和Fegin路由了service的请求
zuul做最外层请求的负载均衡 ,而Ribbon和Fegin做的是系统内部各个微服务的service的调用的负载均衡
有了Zuul,还需要Nginx吗?他俩可以一起使用吗?
我的理解:Zuul和Nginx是可以一起使用的(毕竟我们的Zuul也是可以搭成集群来实现高可用的),要不要一起使用得看架构的复杂度了(业务)~~~
zuul是一个微服务的网关,所有的web请求都需要进过它进行路由。简单来说,A请求路由给A微服务,B请求交给B微服务处理。它仅支持http请求。不支持tcp协议。
nginx是一个网关,同样它可以路由web请求到对应的服务节点(有独立IP的服务器)。它一般做负载均衡使用。
说白点,两者设计的维度不同。
zuul路由的业务,对业务进行了归类,并交给了对应的微服务。
nginx路由请求的压力,对请求进行平均后,交给了服务器处理。