Zuul1与Spring Cloud Gateway对比 返回首页

发表于 2019-09-25 | 本文共 2341 字

一、API网关

  微服务架下,服务之间容易形成网状的调用关系。这种网状的调用关系呢,不便于管理和维护,这种场景下API网关应运而生。

  目前,很多微服务都基于Spring Cloud生态构建。Spring Cloud生态为我们提供了两种API网关产品,分别是Netflix开源的Zuul1和Spring自己开发的Spring Cloud Gateway(下边简称为Gateway)。Spring Cloud以Finchley版本为分界线,Finchley版本发布之前使用Zuul1作为API网关,之后更推荐使用Gateway。

  Netflix在2018年5月开源了Zuul2,但是Spring Cloud已经推出了Gateway,并且在github上表示没有集成Zuul2的计划。所以从Spring Cloud发展的趋势来看,Gateway代替Zuul是必然的。

1.1 Zuul1简介

  Zuul1是Netflix在2013年开源的网关组件,大规模的应用在Netflix的生产环境中,经受了实践考验。它可以与Eureka、Ribbon、Hystrix等组件配合使用,实现路由转发、负载均衡、熔断等功能。Zuul1的核心是一系列过滤器,过滤器简单易于扩展,已经有一些三方库如spring-cloud-zuul-ratelimit等提供了过滤器支持。

  Zuul1基于Servlet2.5构建(zuul-core依赖的servlet-api版本是2.5),使用的是阻塞的IO,Servlet3.1才在ServletOutputStream和ServletInputStream加入了抽象的非阻塞方法。

1.1.1 Tomcat

  使用Zuul1作为微服务网关时,一般使用Spring Boot内置的Tomcat作为Web容器,Tomcat的版本与Spring Boot对应关系如下表:

spring-boot-starter-parent tomcat
1.5.0.RELEASE 8.5
2.0.0.RELEASE 8.5
2.1.0.RELEASE 9.0

  Tomcat在8及以上版本默认使用的是NIO模式,但是:

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel,SocketChannel> {

    // 在NioEndpoint#bind方法中被调用
    protected void initServerSocket() throws Exception {
        // AbstractSelectableChannel#configureBlocking方法的javadoc如下:
        // If true then this channel will be placed inblocking mode; if false then it will be placednon-blocking mode
        serverSock.configureBlocking(true); //mimic APR behavior
    }
}

  线程模式使用的是线程池来处理请求,每个请求都需要独立的线程来处理,从线程池中取出一个工作线程执行,下游微服务返回响应之前这个工作线程一直是阻塞的。 多线程系统架构

1.2 Spring Cloud Gateway

  Spring Cloud Gateway 是Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul1。Gateway可以与Spring Cloud Discovery Client(如Eureka)、Ribbon、Hystrix等组件配合使用,实现路由转发、负载均衡、熔断等功能,并且Gateway还内置了限流过滤器,实现了限流的功能。

  Gateway基于Spring 5、Spring boot 2和Reactor构建,使用Netty作为运行时环境,比较完美的支持异步非阻塞编程。Netty使用非阻塞的IO,线程处理模型建立在主从Reactors多线程模型上。其中Boss Group轮询到新连接后与Client建立连接,生成NioSocketChannel,将channel绑定到Worker;Worker Group轮询并处理Read、Write事件。 主从reactors模型

二、对比

2.0 产品对比

下边以表格形式对Zuul1和Gateway作简单对比:

对比项 Zuul1.x Gateway
实现 基于Servlet2.x构建,使用阻塞的API。 基于Spring 5、Project Reactor、Spring Boot 2,使用非阻塞式的API。
长连接 不支持 支持
不适用场景 后端服务响应慢或者高并发场景下,因为线程数量是固定(有限)的,线程容易被耗尽,导致新请求被拒绝。 中小流量的项目,使用Zuul1.x更合适。
限流 无 内置限流过滤器
上手难度 同步编程,上手简单 门槛较高,上手难度中等
Spring Cloud集成 是 是
Sentinel集成 是 是
技术栈沉淀 Zuul1开源近七年,经受考验,稳定成熟。 未见实际落地案例
Github used by 1007 repositories 102 repositories
Github issues 88 Open / 2736 Closed 135 Open / 850 Closed

注:Github used by和Github issues统计时间截止2019/8/26。

2.1 性能对比

2.1.1 低并发场景

  不同的tps,同样的请求时间(50s),对两种网关产品进行压力测试,结果如下:

tps 测试样本Zuul1/Gateway,单位个 平均响应时间Zuul1/Gateway, 单位毫秒 99%响应时间小于Zuul1/Gateway,单位毫秒 错误比例Zuul1/Gateway
20tps 20977 / 20580 11 / 14 16 / 40 0% / 0%
50tps 42685 / 50586 18 / 12 66 / 22 0% / 0%

  并发较低的场景下,两种网关的表现差不多

2.1.2 高并发场景

  配置同样的线程数(2000),同样的请求时间(5分钟),后端服务在不同的响应时间(休眠时间),对两种网关产品进行压力测试,结果如下:

| 休眠时间 | 测试样本Zuul1/Gateway,单位个 | 平均响应时间Zuul1/Gateway, 单位毫秒 | 99%响应时间小于Zuul1/Gateway,单位毫秒 | 错误次数Zuul1/Gateway,单位个 | 错误比例Zuul1/Gateway | |————|——————|—————|—————|———–|————-| | 休眠100ms | 294134 / 1059321 | 2026 / 546 | 6136 / 1774 | 104 / 0 | 0.04% / 0% | | 休眠300ms | 101194 / 399909 | 5595 / 1489 | 15056 / 1690 | 1114 / 0 | 1.10% / 0% | | 休眠600ms | 51732 / 201262 | 11768 / 2975 | 27217 / 3203 | 2476 / 0 | 4.79% / 0% | | 休眠1000ms | 31896 / 120956 | 19359 / 4914 | 46259 / 5115 | 3598 / 0 | 11.28% / 0% |   Zuul网关的tomcat最大线程数为400,hystrix超时时间为100000。

  Gateway在高并发和后端服务响应慢的场景下比Zuul1的表现要好。

2.1.3 官方性能对比

  Spring Cloud Gateway的开发者提供了benchmark项目用来对比Gateway和Zuul1的性能,官方提供的性能对比结果如下:

网关 Avg Req/sec/Thread Avg Latency
Spring Cloud Gateway 3.24k 6.61ms
Zuul1 2.09k 12.56ms
none 11.77k 2.09ms

  测试工具为wrk,测试时间30秒,线程数为10,连接数为200。   从官方的对比结果来看,Gateway的RPS是Zuul1的1.55倍,平均延迟是Zuul1的一半。

2.2 工作对比

2.2.1 Zuul1

zuul-works

  Zuul1过滤器通过RequestContext在过滤器之间共享数据,ZuulServlet#service按类型调用过滤器,ZuulFilter#runFilter按优先级调用过滤器,ZuulFilter最后判断是否执行过滤器。以下Zuul的代码是1.3.1版本,并省略了非关键部分代码。

RequestContext

过滤器之间通过RequestContext共享数据,RequestContext是一个ConcurrentHashMap,通过ThreadLocal保存,通过RequestContext#getCurrentContext获取。

public class RequestContext extends ConcurrentHashMap<String, Object> {

    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);
            }
        }
    };

    // 通过getCurrentContext()方法获取
    public static RequestContext getCurrentContext() {
        if (testContext != null) return testContext;

        RequestContext context = threadLocal.get();
        return context;
    }
}

FilterLoader

Zuul核心类之一,提供按类型获取ZuulFilters的方法FilterLoader#getFiltersByType

/**
 * This class is one of the core classes in Zuul. It compiles, loads from a File, and checks if source code changed.
 * It also holds ZuulFilters by filterType.
 */
public class FilterLoader {

    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
    private FilterRegistry filterRegistry = FilterRegistry.instance();

    /**
     * Returns a list of filters by the filterType specified
     */
    public List<ZuulFilter> getFiltersByType(String filterType) {

        // 第一次获取返回null
        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        // list is null.
        list = new ArrayList<ZuulFilter>();

        // 从FilterRegistry#getAllFilters取所有的ZuulFilters。
        // 在ZuulFilterInitializer的PostConstruct方法里,会把ZuulFilters放进FilterRegistry
        // 的一个ConcurrentHashMap里。
        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                // 所需类型的ZuulFilter
                list.add(filter);
            }
        }
        // 排序
        Collections.sort(list); // sort by priority

        // 把对应类型的ZuulFilters放进hashFiltersByType(一个ConcurrentHashMap),
        // 这样第二次调用这个方法可以直接返回所需的ZuulFilters
        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }
}

ZuulServlet

请求都会被ZuulServlet接收并处理。在ZuulServlet#service把request和response放进RequestContext,并按类型顺序调用了过滤器。

public class ZuulServlet extends HttpServlet {
    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        // 调用了ZuulRunner#init初始化RequestContext,并把ServletRequest、ServletResponse放进RequestContext。
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        // 这里可以看出ZuulFilter的处理顺序:
        // pre->route->post;
        // 如果pre和route阶段出现异常,调用error->post;
        // 如果post阶段出现异常,调用error。
        try {
            preRoute(); // 调用了ZuulRunner#preRoute
        } catch (ZuulException e) {
            error(e); // 把异常放进RequestContext后调用了ZuulRunner#error
            postRoute(); // 调用了ZuulRunner#postRoute
            return;
        }
        try {
            route(); // 调用了ZuulRunner#route
        } catch (ZuulException e) {
            error(e);
            postRoute();
            return;
        }
        try {
            postRoute();
        } catch (ZuulException e) {
            error(e);
            return;
        }
    }

ZuulRunner

ZuulRunner比较简单,提供了init方法将request和response放进RequestContext,对ZuulFilters的处理直接调用FilterProcessor的同名方法。

public class ZuulRunner {

    public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        // 被ZuulServlet#service调用
        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else { // ZuulServlet#init创建了一个bufferRequests为false的ZuulRunner,所以没有warp request。
            ctx.setRequest(servletRequest);
        }
        // 把request和response放进RequestContext
        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
    }

    public void postRoute() throws ZuulException {
        // 调用FilterProcessor#postRoute,其他route、preRoute和error也是调用的FilterProcessor里的同名方法。
        FilterProcessor.getInstance().postRoute();
    }
    ...
}

FilterProcessor

FilterProcessor的javadoc注释直接注明This the the core class to execute filters.,按类型和优先级ZuulFilter#runFilter执行过滤器。

public class FilterProcessor {
    public void postRoute() throws ZuulException {
        // route方法和preRoute方法和postRoute方法一样,区别是调用runFilters时参数分别是route和pre
        // 出现异常往上抛出,try catch 代码略去
        runFilters("post");
    }
    public void error() {
        try {
            runFilters("error");
        } catch (Throwable e) {
            // 异常被捕获而不抛出,但是在ZuulServlet中异常已经被放进RequestContext。
        }
    }
    /**
     * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
     */
    public Object runFilters(String sType) throws Throwable {
        // 从FilterLoader中获取对应类型的ZuulFilter,是排过序的
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        for (int i = 0; i < list.size(); i++) {
            ZuulFilter zuulFilter = list.get(i);
            // 获取ZuulFilter,并调用processZuulFilter方法
            Object result = processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                // |= 操作,与 += 一样但比较少见,此外还有 &= 、^=
                bResult |= ((Boolean) result);
            }
        }
        return bResult;
    }

    /**
     * Processes an individual ZuulFilter. This method adds Debug information.
     * Any uncaught Thowables are caught by this method and converted to a ZuulException with a 500 status code.
     */
    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
        // get RequestContext
        RequestContext ctx = RequestContext.getCurrentContext();
        try {
            // 调用ZuulFilter#runFilter。根据过滤器里的shouldFilter判断是否执行过滤器的run()方法。
            // 如果执行过滤器的run()方法,返回执行成功(SUCCESS)或失败(FAILED),不执行则返回跳过(SKIPPED)
            ZuulFilterResult result = filter.runFilter();
            // ZuulFilter#runFilter执行状态
            ExecutionStatus s = result.getStatus();
            switch (s) {
                case FAILED:
                    // 过滤器的run()执行异常
                    t = result.getException();
                    break;
                case SUCCESS:
                    // 过滤器的的run()执行结束
                    o = result.getResult();
                    break;
                default:
                    // 过滤器的shouldFilter是false
                    break;
            }
            // 如果过滤器的run()异常,抛异常
            if (t != null) throw t;
            return o;
        } catch (Throwable e) {
            // 异常往上抛出,在ZuulServlet#service方法里被捕获,然后通过error类型的过滤器处理异常。
        }
    }
}

ZuulFilter

ZuulFilters的父类,注释好清晰的,简单易懂。提供了:

public abstract class ZuulFilter implements IZuulFilter, Comparable<ZuulFilter> {
    // 过滤器类型
    abstract public String filterType();
    // 过滤器优先级
    abstract public int filterOrder();
    // 过滤器是否被禁用。
    // 在runFilter()方法里,通过判断isFilterDisabled与过滤器的shouldFilter方法,来确定是否执行过滤器的run方法
    public boolean isFilterDisabled() {
        ...
    }
    /**
     * runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true.
     */
    public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        // 是否被禁用
        if (!isFilterDisabled()) {
            // 是否应该执行
            if (shouldFilter()) {
                try {
                    Object res = run(); // 执行过滤器的run()
                    zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
                } catch (Throwable e) {
                    zr = new ZuulFilterResult(ExecutionStatus.FAILED);
                    zr.setException(e);
                } finally {
                }
            } else {
                zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
            }
        }
        return zr;
    }
}

2.2.2 Gateway

Gateway的工作机制可参考下图(图片来自官方)

gateway-works

ReactorHttpHandlerAdapter

https://xujin.org/sc/gw/gw02/ http://www.iocoder.cn/Spring-Cloud-Gateway/ouwenxue/intro/?vip Gateway的请求入口是ReactorHttpHandlerAdapter

public class ReactorHttpHandlerAdapter implements BiFunction<HttpServerRequest, HttpServerResponse, Mono<Void>> {
  @Override
  public Mono<Void> apply(HttpServerRequest reactorRequest, HttpServerResponse reactorResponse) {
    NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(reactorResponse.alloc());
    try {
      ReactorServerHttpRequest request = new ReactorServerHttpRequest(reactorRequest, bufferFactory);
      ServerHttpResponse response = new ReactorServerHttpResponse(reactorResponse, bufferFactory);

      if (request.getMethod() == HttpMethod.HEAD) {
        response = new HttpHeadResponseDecorator(response);
      }

      return this.httpHandler.handle(request, response)
          .doOnError(ex -> logger.trace(request.getLogPrefix() + "Failed to complete: " + ex.getMessage()))
          .doOnSuccess(aVoid -> logger.trace(request.getLogPrefix() + "Handling completed"));
    }
    catch (URISyntaxException ex) {
      if (logger.isDebugEnabled()) {
        logger.debug("Failed to get request URI: " + ex.getMessage());
      }
      reactorResponse.status(HttpResponseStatus.BAD_REQUEST);
      return Mono.empty();
    }
  }
}

总结

Zuul1的开源时间很早,Netflix、Riot、携程、拍拍贷等公司都已经在生产环境中使用,自身经受了实践考验,是生产级的API网关产品。
Gateway在2019年离开Spring Cloud孵化器,应用于生产的案例少,稳定性有待考证。
从性能方面比较,两种产品在流量小的场景下性能表现差不多;并发高的场景下Gateway性能要好很多。从开发方面比较,Zuul1编程模型简单,易于扩展;Gateway编程模型稍难,代码阅读难度要比Zuul高不少,扩展也稍复杂一些。

附:

深度解读Tomcat中的NIO模型
一文理解Netty模型架构