zuul 1.x 源码阅读之 ZuulServlet 及 Filter 加载

本文详细介绍了Zuul作为API Gateway的角色及其工作原理。深入源码,分析了ZuulServlet如何处理HTTP请求,ZuulRunner如何串联过滤器,以及FilterLoader如何动态加载和管理过滤器。通过ZuulServlet的service方法,理解了request和response在filter之间的传递方式。

原创: http://www.imet.me/post/2018/07-03_zuul_1.x_source/

zuul 是什么,为什么需要 zuul,zuul 实现原理?

一、zuul overview

1.1 什么是 Zuul?

zuul 作为云服务的边界服务(Edge Service),可以用来做统一授权、压力测试、金丝雀测试(Canary Testing)、动态路由、减负载以及和 netflix 其他套件一起协作

1.2 zuul as edge service

直接用官方用图:

Zuul in Netflix’s Cloud Architecture

如上图,zuul 处于内部服务的入口,起到边界服务(Edge Service)的作用

1.3 zuul 的工作原理

先不解释看官方图:

Zuul Core Architecture

分析一下 zuul 的思路:

  1. ZuulServlet: zuul 的核心其实就是 ZuulServlet, 在 NIO 之前, java web 服务都是以 Servlet 作为入口提供服务的。
  2. ZuulFilter Runner: 通过 Runner 来串起来不同类型的 filter, 一个 http request 经过 “pre”, “route”, “post” 这三种类型的 filter。有点 servlet filter 的味道
  3. Filter: 可以在 Filter 里执行业务需要的逻辑。Filter 支持 java 类型的 filter 和 groovy 类型的 filter
  4. Filter Loader: 如果要做到动态添加或者移除 filter 的话,groovy 脚本动态编译是个不错的选择。通过监听指定目录,然后生成 filter 类,装载到 zuul runner
  5. Request Context: filter 如何获取 http request/response 呢。猜对了,ThreadLocal, 通过把 HttpRequest 和 HttpResposne Wrapper 起来,放到 threadlocal 变量里。

二、zuul-core + zuul-simple-webapp 源码解读之 ZuulServlet

先跑起工程再说,跑起 zuul-simple-webapp 工程,参考官方wiki

2.1 web.xml 声明

web.xml 声明完成了以下几件事情:

  • StartServer(ServletContextListener): 是 web 容器的监听器,在容器 context 初始化时做了以下几件事情:

    1. mock monitor
    2. 初始化 FilterFileManager: 去系统变量 zuul.filter.root 目录下定时检查是否有新的脚本
    3. 初始化 JavaFilter
  • ZuulServlet: 是 Servlet 时代处理 http 请求的主要主体。我们源码会分析

  • ContextLifecycleFilter: 主要是及时清理 threadLocal 相关的变量: ZuulServlet 为了使 ZuulFilter 能获取Request相关的 Context, 把 requestContext 放到 threadlocal 中。

<listener>
    <listener-class>com.netflix.zuul.StartServer</listener-class>
</listener>

<servlet>
    <servlet-name>Zuul</servlet-name>
    <servlet-class>com.netflix.zuul.http.ZuulServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Zuul</servlet-name>
    <url-pattern>/*</url-pattern>
</servlet-mapping>

<filter>
    <filter-name>ContextLifecycleFilter</filter-name>
    <filter-class>com.netflix.zuul.context.ContextLifecycleFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>ContextLifecycleFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

2.2 ZuulServlet

a.) ZuulServlet 是继承 HttpServlet

通过实现 Servlet 的 initservic 来分别完成初始化和处理 http 逻辑。

b.) init 方法中创建 ZuulRunner 对象。
  1. 查看 Config param 里是否包含 buffer-requests
  2. ZuulServlet 的实际执行都是在 ZuulRunner 去处理的
c.) service 方法干了以下几件事情
  • 通过 ZuulRunner 初始化 request

.ZuulRunner.java

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
  // 主要就是把 request 和 resposne 放到 requestContext 的 ThreadLocal 变量里
  RequestContext ctx = RequestContext.getCurrentContext();
  if (bufferRequests) {
      ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
  } else {
      ctx.setRequest(servletRequest);
  }

  ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}
  • RequestContext 是以 ConcurrentHashMap 来存储 http 请求中 header,body 等各种参数的
  • 同时通过 RequestContext.getCurrentContext() 来获取 threadlocal 中的 context

.RequestContext.java

public class RequestContext extends ConcurrentHashMap<String, Object> {

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

    // threadlocal 相关的设置
    public static RequestContext getCurrentContext() {
      if (testContext != null) return testContext;
      RequestContext context = threadLocal.get();
      return context;
    }
}
  • 然后 ZuulServlet 在 service() 方法中完成 preRoute(), route(), postRoute() 还有 error() 的处理,当然在 finnaly,要 clean threadlocal requestContext

2.3 ZuulRunner

我们再来看 ZuulServlet 的劳工 Runner,所有的具体执行逻辑都交给 Runner 来执行

.ZuulServlet.java service() 方法调用相关

// 将 request, response 放到 requestContext hreadlocal
void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
    zuulRunner.init(servletRequest, servletResponse);
}

// preRoute, route, postRoute, error, init 都是有 zuulRunner 来执行的。但是在 service 中进行了编排和异常处理
void preRoute() throws ZuulException {
    zuulRunner.preRoute();
}

... route(), postRoute(), error()

再来看 ZuulRunner 中的 preRoute, route, postRoute 这三个方法都是有 FilterProcessor 来做

.ZuulRunner.java

// zuulRunner 通过 FilterProcessor 来执行相应类型的 filter
public void preRoute() throws ZuulException {
    FilterProcessor.getInstance().preRoute();
}

2.4 FilterProcessor

  • preRoute(), postRoute, route() 都是由 runFilters 来执行的
  • 在 runFilters 是通过 FilterLoader 来获取 filter 列表的

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

public Object runFilters(String sType) throws Throwable {
  if (RequestContext.getCurrentContext().debugRouting()) {
      Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
  }
  boolean bResult = false;
  // 通过 FilterLoader 来获取 filters
  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;
}
  • 然后挨个执行 processZuulFilter 方法,来获取 result

.FilterProcessor.java processZuulFilter()

 // 主要内容就是 runFilter
 ZuulFilterResult result = filter.runFilter();
 ExecutionStatus s = result.getStatus();
 execTime = System.currentTimeMillis() - ltime;

2.5 ZuulFilter runFilter()

主要执行 某个 Filter 的 run() 方法,然后将结果放到 ZuulFilterResult 中

.ZuulFilter.java

public ZuulFilterResult runFilter() {
  ZuulFilterResult zr = new ZuulFilterResult();
  if (!isFilterDisabled()) {
      if (shouldFilter()) {
          Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName());
          try {
              Object res = run();
              zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS);
          } catch (Throwable e) {
              t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed");
              zr = new ZuulFilterResult(ExecutionStatus.FAILED);
              zr.setException(e);
          } finally {
              t.stopAndLog();
          }
      } else {
          zr = new ZuulFilterResult(ExecutionStatus.SKIPPED);
      }
  }
  return zr;
}

2.6 filter 总结

至此,我们看到一个 filter 是如何被执行的了。

ZuulServlet.service()
  -> ZuulRunner.xxRoute()
    -> FilterProcessor.runFilters(byType)
      -> xxZuulFilter.runFilter
        ->  xxZuulFilter.run()**

而其中的 request, resposne 变量传递是通过 RequestContext threadLocal

三、zuul-core 源码分析之 Filter 加载

3.1 FilterLoader 加载 filter

再看 FilterProcessor 是从 FilterLoader 获取 fitler list 的。

.FilterLoader.java 的 hashFiltersByType & filterRegistry

public class FilterLoader {
    final static FilterLoader INSTANCE = new FilterLoader();

    // 通过 4 个 ConcurrentHashMap 来维护 filter 信息
    // 维护 xxGrovvyZuulFilter.groovy filter 文件的最近修改时间; key 为 filter 文件名(绝对路径), value 为文件最后修改时间
    private final ConcurrentHashMap<String, Long> filterClassLastModified = new ConcurrentHashMap<String, Long>();
    // 目测实际上没有进行相关处理操作 (目测代码很少的路径会走到这); key 为 filter 文件名, value 为 sourceCode
    private final ConcurrentHashMap<String, String> filterClassCode = new ConcurrentHashMap<String, String>();
    // 主要是用来检测 xxGrovvyZuulFilter 这样的 filter name 是否已经被注册 (目测代码很少的路径会走到这); key 为 filter 文件名, value 也为文件名
    private final ConcurrentHashMap<String, String> filterCheck = new ConcurrentHashMap<String, String>();
    // *** 这个是最主要的,根据 filter 类型: "pre", "post", "route" 等来返回对于的 filter list ***
    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();
    // 而真正的 filterList 的引用都是存在 filterRegistry 里面。
    // 可以看到 filterRegistry 存储 private final ConcurrentHashMap<String, ZuulFilter> filters
    private FilterRegistry filterRegistry = FilterRegistry.instance();

    static DynamicCodeCompiler COMPILER;

    static FilterFactory FILTER_FACTORY = new DefaultFilterFactory();
}

3.2 FilterFileManager 管理加载

FilterFileManager 获取指定目录文件,FilterLoader.DynamicCodeCompiler 编译文件

  • 我们找到了 filterList 的存储,再看什么时候放进来的

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

// 先获取文件,然后在处理 groovy 文件
void manageFiles() throws Exception, IllegalAccessException, InstantiationException {
    List<File> aFiles = getFiles();
    processGroovyFiles(aFiles);
}

// 交给 FilterLoader 来去编译并注册 动态 Filter 文件
void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {

    for (File file : aFiles) {
        FilterLoader.getInstance().putFilter(file);
    }
}
  • FilterLoader putFilter 的过程

.FilterLoader.java putFilter()

public boolean putFilter(File file) throws Exception {
    // 1. 获取绝对路径
    String sName = file.getAbsolutePath() + file.getName();
    // 2. 如果文件修改了,就从 filterRegistry map 里面删除掉
    if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
        LOG.debug("reloading filter " + sName);
        filterRegistry.remove(sName);
    }

    // 3. 当再从 FilterRegistry 获取的时候,为空的话,就进行重新编译
    ZuulFilter filter = filterRegistry.get(sName);
    if (filter == null) {
        Class clazz = COMPILER.compile(file);
        if (!Modifier.isAbstract(clazz.getModifiers())) {
            filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
            List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
            if (list != null) {
                hashFiltersByType.remove(filter.filterType()); //rebuild this list
            }
            filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
            filterClassLastModified.put(sName, file.lastModified());
            return true;
        }
    }

    return false;
}
  • StartServer 初始化的时候,初始化 groovyFilterManager

那什么时候初始化 FilterFileManager 的呢。我们可以看到在 web.xml 里面指定了 StartServer 作为 listner,然后在 listner 初始化中初始化 FilterFileManager

.StartServer.java

private void initGroovyFilterManager() {
    FilterLoader.getInstance().setCompiler(new GroovyCompiler());

    String scriptRoot = System.getProperty("zuul.filter.root", "");
    if (scriptRoot.length() > 0) scriptRoot = scriptRoot + File.separator;
    try {
        FilterFileManager.setFilenameFilter(new GroovyFileFilter());
        FilterFileManager.init(5, scriptRoot + "pre", scriptRoot + "route", scriptRoot + "post");
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

3.3 总结 FilterFile 动态加载的流程

StartServer.initGroovyFilterManager()  // context 初始化
  -> FilterFileManager.init()          // 定时获取 filter 初始化
    -> FilterLoader.putFilter()        // 动态编译并加载 filter 文件到 FilterRegistry 的 ConcurrentHashMap 里
      -> FilterProcessor.runFilters(byType)  // 从 FilterLoader 获取 filter list

至此将 filter 工作流程串起来了。

参考

  1. zuul 官宣: Announcing Zuul: Edge Service in the Cloud
  2. zuul 2 官宣: Zuul 2 : The Netflix Journey to Asynchronous, Non-Blocking Systems
  3. zuul github wiki: How Zuul Works
  4. zuul simple webapp
<think>嗯,用户遇到了Zuul过滤器中偶尔出现ZuulException的问题,特别是在本地调试时请求偶尔报错,涉及SendErrorFilter。我需要结合之前的引用Spring Cloud Zuul的知识来分析可能的原因解决方案。 首先,ZuulException可能有多种原因,比如超时、路由配置错误、过滤器链问题等。根据用户提供的引用,之前处理过超时问题,所以需要先考虑超时设置是否正确。引用1、2、3都提到了超时相关的配置,包括connect-timeout-millis、socket-timeout-millis,还有HystrixRibbon的超时设置。需要确保这些超时时间配置合理,避免因服务响应慢导致超时异常。 然后,用户提到SendErrorFilter,这是Zuul内置的一个错误处理过滤器,当发生异常时会转发到/error路径。如果SendErrorFilter自身出现问题,比如处理错误时又发生异常,可能会导致问题。比如在过滤器中未正确捕获异常,或者在SendErrorFilter处理错误时再次触发异常,形成死循环。 接下来,可能的原因包括:1. 超时配置不当,各组件(Zuul、Hystrix、Ribbon)的超时时间没有正确协调。例如,Hystrix的超时时间应大于Ribbon的超时,而Zuul的socket-timeout应大于Hystrix的。2. 过滤器逻辑问题,自定义过滤器可能在处理请求时抛出异常,尤其是在某些特定情况下,比如网络波动或资源不足时。3. 服务间调用异常,比如下游服务响应慢或不可用,导致Zuul无法及时获取响应,触发超时。4. 线程池或资源不足,在高并发下,Zuul的线程池可能被占满,无法处理新请求,导致异常。 解决方案方面,首先检查超时配置,确保各个组件的超时时间设置合理。比如Zuul的socket-timeout应足够大,Hystrix的timeoutInMilliseconds要大于Ribbon的ReadTimeoutConnectTimeout的总。其次,检查自定义过滤器的逻辑,特别是prepost过滤器,确保异常被正确处理,避免未捕获的异常传递到SendErrorFilter。可能需要添加异常处理逻辑,记录日志或返回友好错误信息。 另外,开启Zuul的debug日志可以帮助定位问题,查看具体是哪个阶段出的错。检查下游服务的健康状况,确保服务稳定。如果存在资源竞争或线程池问题,可能需要调整线程池配置或增加资源。针对SendErrorFilter的问题,可以考虑自定义错误处理过滤器,覆盖默认的SendErrorFilter,更灵活地处理异常。 用户可能需要具体的配置示例,比如如何设置超时参数,如何在过滤器中处理异常,以及如何自定义错误过滤器。此外,生成的相关问题可以涉及如何优化Zuul配置、过滤器开发注意事项、性能调优等,帮助用户全面了解Zuul的使用故障排除。</think>### Zuul过滤器中ZuulException错误的解决方案 #### 一、问题定位与分析 Zuul过滤器中的$com.netflix.zuul.exception.ZuulException$通常由以下原因导致: 1. **超时配置冲突** - Zuul的`socket-timeout-millis`需大于Hystrix的`timeoutInMilliseconds`,而Hystrix的超时需覆盖Ribbon的超时总(`ReadTimeout + ConnectTimeout`)。若配置顺序错误会导致超时异常[^1][^3]。 2. **过滤器逻辑缺陷** - 自定义过滤器中未正确处理异常(如未捕获$IOException$或$RuntimeException$),导致异常传递到$SendErrorFilter$后二次触发错误[^2]。 3. **下游服务不稳定** - 目标服务偶发响应延迟或宕机,Zuul未正确处理重试逻辑。 4. **线程资源竞争** - 高并发场景下,Hystrix线程池资源耗尽,触发熔断机制。 --- #### 二、具体解决方案 ##### 1. 超时配置优化 在`application.yml`中按以下优先级配置: ```yaml zuul: host: connect-timeout-millis: 15000 # HTTP连接建立超时 socket-timeout-millis: 60000 # 响应读取超时(需 > Hystrix超时) hystrix: command: default: execution: isolation: thread: timeoutInMilliseconds: 60000 # 控制熔断超时 ribbon: ReadTimeout: 5000 # 单次请求读取超时 ConnectTimeout: 5000 # 单次请求连接超时 MaxAutoRetries: 1 # 同一实例重试次数 ``` *关键点*:`socket-timeout-millis > hystrix.timeout > (ribbon.ReadTimeout + ribbon.ConnectTimeout) * (MaxAutoRetries + 1)`[^3]。 ##### 2. 过滤器异常处理 在自定义过滤器中添加异常捕获逻辑(以`pre`类型为例): ```java public class CustomFilter extends ZuulFilter { @Override public Object run() { try { // 业务逻辑 } catch (Exception ex) { // 记录异常日志 RequestContext.getCurrentContext().setResponseBody("{\"code\":500, \"msg\":\"服务异常\"}"); RequestContext.getCurrentContext().setSendZuulResponse(false); // 终止路由 } return null; } } ``` *作用*:防止异常穿透到$SendErrorFilter$导致循环错误[^2]。 ##### 3. 增强SendErrorFilter容错性 通过禁用默认$SendErrorFilter$并自定义错误处理: ```java @Bean public SendErrorFilter sendErrorFilter() { return new SendErrorFilter() { @Override public boolean shouldFilter() { return false; // 禁用默认实现 } }; } @Bean public ErrorFilter customErrorFilter() { return new ErrorFilter() { @Override public String filterType() { return "error"; } @Override public Object run() { // 自定义错误响应逻辑 } }; } ``` ##### 4. 日志监控增强 在`logback-spring.xml`中开启DEBUG日志: ```xml <logger name="com.netflix.zuul" level="DEBUG"/> ``` 通过日志关键词`Forwarding error`或`Hystrix Readed time out`可快速定位问题阶段[^1][^3]。 --- #### 三、验证步骤 1. 使用`curl`模拟高延迟请求: ```bash curl -H "X-Forwarded-Delay: 5000" http://localhost:8020/producer/api ``` 2. 通过`/actuator/hystrix.stream`监控熔断状态: ```json { "type": "HystrixThreadPool", "name": "sc-producer", "currentQueueSize": 0, "currentActiveCount": 2 } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值