SpringCloud 网关组件 Zuul-1.0 原理深度解析

为什么要使用网关?

在当下流行的微服务架构中,面对多端应用时我们往往会做前后端分离:如前端分成 APP 端、网页端、小程序端等,使用 Vue 等流行的前端框架交给前端团队负责实现;后端拆分成若干微服务,分别交给不同的后端团队负责实现。

不同的微服务一般会有不同的服务地址,客户端在访问这些地址的时候,需要记录几十甚至几百个地址,客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度。而且这样的机制会增加身份认证的难度,每个微服务需要独立认证,微服务网关就应运而生。

微服务网关 介于客户端与服务器之间的中间层,是系统对外的唯一入口:所有的外部请求都会先经过微服务网关,客户端只需要与网关交互,只知道一个网关地址即可。

SpringCloud 网关组件 Zuul-1.0 原理深度解析
u=893403937,81938005&fm=253&fmt=auto&app=138&f=JPEG.webp

网关是 SpringCloud 生态体系中的基础组件之一,它的主流实现方案有两个:

  1. Spring Cloud Netflix Zuul
  2. Spring Cloud Gateway

两者的主要作用都是一样的,都是代理和路由,本文主要聚焦于 Spring Cloud Netflix Zuul。

1. Zuul 网关简介

Zuul 是 Spring Cloud 中的微服务网关,是为微服务架构中的服务提供了统一的访问入口。 Zuul 本质上是一个Web servlet应用,为微服务架构中的服务提供了统一的访问入口,客户端通过 API 网关访问相关服务。

SpringCloud 网关组件 Zuul-1.0 原理深度解析
src=http___img-blog.csdnimg.cn_20210420224647274.png_x-oss-process=image_watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Rvcm55TGlu,size_16,color_FFFFFF,t_70&refer=http___img-blog.csd.webp

Zuul 网关的作用

网关在整个微服务的系统中角色是非常重要的,网关的作用非常多,比如路由、限流、降级、安全控制、服务聚合等。

  • 统一入口:唯一的入口,网关起到外部和内部隔离的作用,保障了后台服务的安全性;
  • 身份验证和安全性:对需要身份验证的资源进行过滤,拒绝处理不符合身份认证的请求;
  • 动态路由:动态的将请求路由到不同的后端集群中;
  • 负载均衡:设置每种请求的处理能力,删除那些超出限制的请求;
  • 静态响应处理:提供静态的过滤器,直接响应一些请求,而不将它们转发到集群内部;
  • 减少客户端与服务端的耦合:服务可以独立发展,通过网关层来做映射。

2. Zuul 架构总览

整体架构上可以分为两个部分,即 Zuul CoreSpring Cloud Netflix Zuul

其中 Zuul Core 部分即 Zuul 的核心,负责网关核心流程的实现;Spring Cloud Netflix Zuul 负责包装Zuul Core,其中包括 Zuul 服务的初始化、过滤器的加载、路由过滤器的实现等。

SpringCloud 网关组件 Zuul-1.0 原理深度解析
zuul_01.png

3. Zuul 工作原理

  1. 容器启动时,Spring Cloud 初始化 Zuul 核心组件,如 ZuulServlet、过滤器等。
  2. ZuulServlet 处理外部请求:
    • 初始化 RequestContext;
    • ZuulRunner 发起执行 Pre 过滤器,并最终通过 FilterProcessor 执行;
    • ZuulRunner 发起执行 Route 过滤器,并最终通过 FilterProcessor 执行;
    • ZuulRunner 发起执行 Post 过滤器,并最终通过 FilterProcessor 执行;
    • 返回 Http Response。

Zuul 初始化过程

Spring Cloud Netflix Zuul中初始化网关服务有两种方式:@EnableZuulServer@EnableZuulProxy

这两种方式都可以启动网关服务,不同的主要地方是:

  1. @EnableZuulProxy 是 @EnableZuulServer 的超集,即使用 @EnableZuulProxy 加载的组件除了包含使用 @EnableZuulServer 加载的组件外,还增加了其他组件和功能;
  2. @EnableZuulServer 是纯净版的网关服务,不具备代理功能,只实现了简单的请求转发、响应等基本功能,需要自行添加需要的组件;
  3. @EnableZuulProxy 在 @EnableZuulServer 的基础上实现了代理功能,并可以通过服务发现来路由服务。
SpringCloud 网关组件 Zuul-1.0 原理深度解析
zuul_02.png

如图所示,@EnableZuulServer 和 @EnableZuulProxy 的初始化过程一致,最大的区别在于加载的过滤器不同。其中蓝色是 @EnableZuulServer 加载的过滤器;红色是 @EnableZuulProxy 额外添加的过滤器。

Zuul 初始化源码分析

在程序的启动类加上 @EnableZuulProxy:

@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy 
{
}

引用了 ZuulProxyConfiguration,跟踪 ZuulProxyConfiguration,该类注入了 DiscoveryClient、RibbonCommandFactoryConfiguration 用作负载均衡相关的。注入了一些列的 filters,比如 PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter,代码如如下:

ZuulProxyConfiguration.java

    @Bean
    public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
        return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), this.zuulProperties, proxyRequestHelper);
    }

    @Bean
    public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) {
        RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers);
        return filter;
    }

    @Bean
    public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties) {
        return new SimpleHostRoutingFilter(helper, zuulProperties);
    }

父类 ZuulConfiguration ,引用了一些相关的配置。在缺失 zuulServlet bean 的情况下注入了 ZuulServlet,该类是 zuul 的核心类。

ZuulConfiguration.java

    @Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
                this.zuulProperties.getServletPattern());
        // The whole point of exposing this servlet is to provide a route that doesn't
        // buffer requests.
        servlet.addInitParameter("buffer-requests""false");
        return servlet;
    }

同时也注入了其他的过滤器,比如 ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter,这些过滤器都是 pre 类型 的。

    @Bean
    public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
    }
 
    @Bean
    public FormBodyWrapperFilter formBodyWrapperFilter() {
        return new FormBodyWrapperFilter();
    }
 
    @Bean
    public DebugFilter debugFilter() {
        return new DebugFilter();
    }
 
    @Bean
    public Servlet30WrapperFilter servlet30WrapperFilter() {
        return new Servlet30WrapperFilter();
    }

同时还注入了 post 类型 的,比如 SendResponseFilter,error 类型,比如 SendErrorFilter,route 类型比如 SendForwardFilter,代码如下:

    @Bean
    public SendResponseFilter sendResponseFilter() {
        return new SendResponseFilter();
    }
 
    @Bean
    public SendErrorFilter sendErrorFilter() {
        return new SendErrorFilter();
    }
 
    @Bean
    public SendForwardFilter sendForwardFilter() {
        return new SendForwardFilter();
    }

初始化 ZuulFilterInitializer 类,将所有的 filter 向 FilterRegistry 注册:

    @Configuration
    protected static class ZuulFilterConfiguration {
 
        @Autowired
        private Map<String, ZuulFilter> filters;
 
        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(
                CounterFactory counterFactory, TracerFactory tracerFactory)
 
{
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }
 
    }

FilterRegistry 管理了一个 ConcurrentHashMap,用作存储过滤器的,并有一些基本的 CURD 过滤器的方法,代码如下:

FilterRegistry.java

 public class FilterRegistry {
 
    private static final FilterRegistry INSTANCE = new FilterRegistry();
 
    public static final FilterRegistry instance() {
        return INSTANCE;
    }
 
    private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
 
    private FilterRegistry() {
    }
 
    public ZuulFilter remove(String key) {
        return this.filters.remove(key);
    }
 
    public ZuulFilter get(String key) {
        return this.filters.get(key);
    }
 
    public void put(String key, ZuulFilter filter) {
        this.filters.putIfAbsent(key, filter);
    }
 
    public int size() {
        return this.filters.size();
    }
 
    public Collection<ZuulFilter> getAllFilters() {
        return this.filters.values();
    }
 
}

FilterLoader 类持有 FilterRegistry,FilterFileManager 类持有 FilterLoader,所以最终是由FilterFileManager 注入 filterFilterRegistry 的 ConcurrentHashMa p的。FilterFileManager 到开启了轮询机制,定时的去加载过滤器,代码如下:

FilterFileManager.java

  void startPoller() {
        poller = new Thread("GroovyFilterFileManagerPoller") {
            public void run() {
                while (bRunning) {
                    try {
                        sleep(pollingIntervalSeconds * 1000);
                        manageFiles();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        poller.setDaemon(true);
        poller.start();
    }

Zuul 请求处理过程

  1. 初始化 RequestContext;
  2. ZuulRunner 发起执行 Pre 过滤器,并最终通过 FilterProcessor 执行;
  3. ZuulRunner 发起执行 Route 过滤器,并最终通过 FilterProcessor 执行;
  4. ZuulRunner 发起执行 Post 过滤器,并最终通过 FilterProcessor 执行;
  5. 返回 Http Response。
SpringCloud 网关组件 Zuul-1.0 原理深度解析
src=http___rumenz.com_static_cimg_20191022_1571750678.png&refer=http___rumenz.webp

Zuul 默认注入的过滤器,它们的执行顺序在 FilterConstants 类,我们可以先定位在这个类,然后再看这个类的过滤器的执行顺序以及相关的注释,可以很轻松定位到相关的过滤器。

过滤器 顺序 描述 类型
ServletDetectionFilter -3 检测请求是用 DispatcherServlet 还是 ZuulServlet pre
Servlet30WrapperFilter -2 在 Servlet 3.0 下,包装 requests pre
FormBodyWrapperFilter -1 解析表单数据 pre
SendErrorFilter 0 如果中途出现错误 error
DebugFilter 1 设置请求过程是否开启 debug pre
PreDecorationFilter 5 根据 uri 决定调用哪一个 route 过滤器 pre
RibbonRoutingFilter 10 如果写配置的时候用 ServiceId 则用这个 route 过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断 route
SimpleHostRoutingFilter 100 如果写配置的时候用 url 则用这个 route 过滤 route
SendForwardFilter 500 用 RequestDispatcher 请求转发 route
SendResponseFilter 1000 用 RequestDispatcher 请求转发 post

过滤器的 order 值越小,就越先执行。并且在执行过滤器的过程中,它们 共享了一个 RequestContext 对象,该对象的生命周期贯穿于请求。

可以看出优先执行了 pre 类型的过滤器,并将执行后的结果放在 RequestContext 中,供后续的 filter 使用,比如在执行 PreDecorationFilter 的时候,决定使用哪一个 route,它的结果的是放在 RequestContext 对象中,后续会执行所有的 route 的过滤器,如果不满足条件就不执行该过滤器的 run() 方法,最终达到了就执行一个 route 过滤器的 run() 方法。

  • error 类型的过滤器,是在程序发生异常的时候执行的。
  • post 类型的过滤,在默认的情况下,只注入了 SendResponseFilter,该类型的过滤器是将最终的请求结果以流的形式输出给客户端。

Zuul 请求处理源码分析

Zuulservlet 作为类似于 Spring MVC 中的 DispatchServlet,起到了前端控制器的作用,所有的请求都由它接管。它的核心代码如下:

Zuulservlet.java

 
   @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();
            } catch (ZuulException e) {
                error(e);
                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();
        }
    }
    

跟踪 init() 方法,可以发现这个方法 init() 为每个请求生成了 RequestContext(底层使用 ThreadLocal 保存数据),RequestContext 继承了 ConcurrentHashMap:


    public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
 
        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }
 
        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
 
    }
 
 
    public void preRoute() throws ZuulException {
        FilterProcessor.getInstance().preRoute();
    }
    

FilterProcessor 类为调用 filters 的类,比如调用 pre 类型所有的过滤器,route、post 类型的过滤器的执行过程和 pre 执行过程类似:

FilterProcessor.java

  public void preRoute() throws ZuulException {
        try {
            runFilters("pre");
        } catch (ZuulException e) {
            throw e;
        } catch (Throwable e) {
            throw new ZuulException(e, 500"UNCAUGHT_EXCEPTION_IN_PRE_FILTER_" + e.getClass().getName());
        }
    }

跟踪 runFilters() 方法,可以发现,它最终调用了 FilterLoader 的 getFiltersByType(sType) 方法来获取同一类的过滤器,然后用 for 循环遍历所有的 ZuulFilter,执行了 processZuulFilter() 方法,跟踪该方法可以发现最终是执行了 ZuulFilter 的方法,最终返回了该方法返回的 Object 对象:

    public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

SimpleHostRoutingFilter

现在来看一下 SimpleHostRoutingFilter 是如何工作的。进入到 SimpleHostRoutingFilter 类的 run() 方法,核心代码如下:

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        // 省略代码
 
        String uri = this.helper.buildZuulRequestURI(request);
        this.helper.addIgnoredHeaders();
 
        try {
            CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
                    headers, params, requestEntity);
            setResponse(response);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
        return null;
    }

查阅这个类的全部代码可知,该类创建了一个 HttpClient 作为请求类,并重构了 url,请求到了具体的服务,得到的一个 CloseableHttpResponse 对象,并将 CloseableHttpResponse 对象的保存到 RequestContext 对象中。并调用了 ProxyRequestHelper 的 setResponse 方法,将请求状态码,流等信息保存在 RequestContext 对象中。

    private void setResponse(HttpResponse response) throws IOException {
        RequestContext.getCurrentContext().set("zuulResponse", response);
        this.helper.setResponse(response.getStatusLine().getStatusCode(),
                response.getEntity() == null ? null : response.getEntity().getContent(),
                revertHeaders(response.getAllHeaders()));
    }

SendResponseFilter

这个过滤器的 order 为 1000,在默认且正常的情况下,是最后一个执行的过滤器,该过滤器是最终将得到的数据返回给客户端的请求。在它的 run() 方法里,有两个方法:addResponseHeaders() 和 writeResponse(),即添加响应头和写入响应数据流。

    public Object run() {
        try {
            addResponseHeaders();
            writeResponse();
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }

其中 writeResponse() 方法是通过从 RequestContext 中获取 ResponseBody 获或者 ResponseDataStream 来写入到 HttpServletResponse 中的,但是在默认的情况下 ResponseBody 为 null,而 ResponseDataStream 在 route 类型过滤器中已经设置进去了。具体代码如下:


    private void writeResponse() throws Exception {
        RequestContext context = RequestContext.getCurrentContext();
 
        HttpServletResponse servletResponse = context.getResponse();
            // 省略代码
        OutputStream outStream = servletResponse.getOutputStream();
        InputStream is = null;
        try {
            if (RequestContext.getCurrentContext().getResponseBody() != null) {
                String body = RequestContext.getCurrentContext().getResponseBody();
                writeResponse(
                        new ByteArrayInputStream(
                                body.getBytes(servletResponse.getCharacterEncoding())),
                        outStream);
                return;
            }
 
            // 省略代码
            is = context.getResponseDataStream();
            InputStream inputStream = is;
                // 省略代码
 
            writeResponse(inputStream, outStream);
                // 省略代码
            }
        }
        // 省略代码
    }

4. Zuul-2.0 和 Zuul-1.0 对比

Zuul1.0 设计比较简单,代码很少也比较容易读懂,它本质上就是一个同步 Servlet,采用多线程阻塞模型。 Zuul2.0 的设计相对比较复杂,代码也不太容易读懂,它采用了 Netty 实现异步非阻塞编程模型。比较明确的是,Zuul2.0 在链接数方面表现要好于 Zuul1.0,也就是说 Zuul2.0 能接受更多的链接数。

Netflix 给出了一个比较模糊的数据,大体 Zuul2.0 的性能比 Zuul1.0 好 20% 左右,这里的性能主要指每节点每秒处理的请求数。为何说模糊呢?由于这个数据受实际测试环境,流量场景模式等众多因素影响,你很难复现这个测试数据。即使这个 20% 的性能提高是确实的,其实这个性能提高也并不大,和异步引入的复杂性相比,这 20 %的提高是否值得是个问题。

两者架构上的差异

Zuul2.0 的架构,和 Zuul1.0 没有本质区别,两点变化:

  • 前端用 Netty Server 代替 Servlet,目的是支持前端异步。后端用 Netty Client 代替 Http Client,目的是支持后端异步。
  • 过滤器换了一下名字,用 Inbound Filters 代替 Pre-routing Filters,用 Endpoint Filter 代替Routing Filter,用 Outbound Filters 代替 Post-routing Filters

线上环境使用建议

  1. 同步异步各有利弊,同步多线程编程模型简单,但会有线程开销和阻塞问题,异步非阻塞模式线程少并发高,可是编程模型变得复杂。
  2. 架构师作技术选型须要严谨务实,具有批判性思惟 (Critical Thinking),即便是对于一线大公司推出的开源产品,也要批判性看待,不可盲目追新。
  3. 我的 建议生产环境继续使用 Zuul1.0,同步阻塞模式的一些不足,可使用熔断组件 Hystrix 和AsyncServlet 等技术进行优化。


原文始发于微信公众号(白菜说技术):SpringCloud 网关组件 Zuul-1.0 原理深度解析

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/172421.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!