likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 13

为什么要使用网关?

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

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

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

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

网关是 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 原理深度解析

Zuul 网关的作用

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

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

2. Zuul 架构总览

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

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

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

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 原理深度解析

如图所示,@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 原理深度解析

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

过滤器顺序描述类型
ServletDetectionFilter-3检测请求是用 DispatcherServlet 还是 ZuulServletpre
Servlet30WrapperFilter-2在 Servlet 3.0 下,包装 requestspre
FormBodyWrapperFilter-1解析表单数据pre
SendErrorFilter0如果中途出现错误error
DebugFilter1设置请求过程是否开启 debugpre
PreDecorationFilter5根据 uri 决定调用哪一个 route 过滤器pre
RibbonRoutingFilter10如果写配置的时候用 ServiceId 则用这个 route 过滤器,该过滤器可以用Ribbon 做负载均衡,用hystrix做熔断route
SimpleHostRoutingFilter100如果写配置的时候用 url 则用这个 route 过滤route
SendForwardFilter500用 RequestDispatcher 请求转发route
SendResponseFilter1000用 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 等技术进行优化。