Netflix Zuul 1.x 的理念与原理学习

author : 丁家文

Netflix Zuul 1.x 的理念与原理学习

Zuul 的概念

Zuul,SpringCloud 系列的API网关,Netflix全家桶的核心组件之一。

在一个微服务集群中, Zuul 担任的角色既是 关口,也是 代理门户 。说它是关口,是因为它是整个集群提供的唯一的外部访问的通道,任何请求,如果想要访问集群内部的服务,都必须通过该关口进入。在通过Zuul时,Zuul可以对其进行鉴权,流量转发等操作,请求处理完毕时,又可通过zuul完成一些收尾工作。除此之外,Zuul的应用对于集群本身的监控与维护,也可提供流量控制、监控、页面级缓存等服务。

Zuul 的理念

Zuul1 的理念,就是一系列的过滤器,所有的功能,都可以通过过滤器实现,一个请求,在进入zuul后,会进行如下过程:

  1. 通过所有的前置(pre)过滤器;
  2. 通过所有的路由(route)过滤器;
  3. 通过所有的后置(post)过滤器;
  4. 如果在过程中发生了异常,则跳转到所有的错误(error)过滤器中。(如下图所示)
    在这里插入图片描述

Zuul 就像是一个层次分明的大过滤器,一个请求从进入Zuul之后,它该有怎样的命运,除去它本身的信息外,就由这星罗棋布、森罗万象但又鳞次栉比的过滤器决定。

Zuul 的原理

Http请求与响应格式

Zuul简单源码分析

一如Zuul基于过滤器的理念,Zuul的源码中很重要的一个类:ZuulFilter——所有自定义Filter的父类

利用IDEA生成的UML类图查看ZuulFilter的结构,可以看出ZuulFilter实现了IZuulFilter。

在这里插入图片描述

再看ZuulFilter的相关方法:

  • filterType()

在类 FilterConstants 中可以找到如下定义:

// Zuul Filter TYPE constants -----------------------------------

	/**
	 * {@link ZuulFilter#filterType()} error type.
	 */
	String ERROR_TYPE = "error";

	/**
	 * {@link ZuulFilter#filterType()} post type.
	 */
	String POST_TYPE = "post";

	/**
	 * {@link ZuulFilter#filterType()} pre type.
	 */
	String PRE_TYPE = "pre";

	/**
	 * {@link ZuulFilter#filterType()} route type.
	 */
	String ROUTE_TYPE = "route";

这是ZuulFilter的四种标准类型,代表了Request的生命周期:

Zuulfilter type使用时机作用
PRE将请求路由转发之前实现Authentication、选择源服务地址等
ROUTING路由转发之时使用HttpClient请求web-service
POST路由返回response后对Response结果进行修改
ERROR上述过程出错时错误处理
  • filterOrder()
public int compareTo(ZuulFilter filter) {
        return Integer.compare(this.filterOrder(), filter.filterOrder());
    }

ZuulFilter还实现了Comparable接口,在compareTo()方法中,对filterOrder()方法返回的值进行比较,由此可以看出,对于相同filterType的过滤器,filterOrder()方法中返回值的大小决定了filter的执行顺序

  • IZuulFilter 中的 shouldFilter()run() 方法
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;
    }

run()定义了filter处理逻辑,而shouldFilter()则定义了filter执行需要满足的条件。

ZuulFilter加载过程
public class FilterLoader {
    //省略部分代码

    /**
     * Given source and name will compile and store the filter if it detects that the filter code has changed or
     * the filter doesn't exist. Otherwise it will return an instance of the requested ZuulFilter
     */
    public ZuulFilter getFilter(String sCode, String sName) throws Exception {    }

    /**
     * @return the total number of Zuul filters
     */
    public int filterInstanceMapSize() {    }

    /**
     * From a file this will read the ZuulFilter source code, compile it, and add it to the list of current filters
     */
    public boolean putFilter(File file) throws Exception {    }

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

zuul框架主要的功能就是动态的读取,编译,运行这些filter。filter之间不直接通信,他们之间通过RequestContext来共享状态信息,既然filter都是对特定Request的处理,那么RequestContext就是Request的Context,RequestContext用来管理 Request的Context,不受其它Request的影响。
Filter源码文件放在zuul 服务特定的目录, zuul server会定期扫描目录下的文件的变化。如果有Filter文件更新,源文件会被动态的读取,编译加载进入服务,接下来的Request处理就由这些新加入的filter处理。

Zuul的过滤器是通过groovy语言编写的,所以,zuul加载过程中,GroovyFileFilter 起了很大作用:

public class GroovyFileFilter implements FilenameFilter {
    @Override
    public boolean accept(File dir, String name) {
        return name.endsWith(".groovy");
    }
}

之后对 groovy 文件进行编译:

public class GroovyCompiler implements DynamicCodeCompiler {
    //省略部分代码

    /**
     * Compiles Groovy code and returns the Class of the compiles code.
     */
    @Override
    public Class compile(String sCode, String sName) {
        GroovyClassLoader loader = getGroovyClassLoader();
        LOG.warn("Compiling filter: " + sName);
        Class groovyClass = loader.parseClass(sCode, sName);
        return groovyClass;
    }

    /**
     * @return a new GroovyClassLoader
     */
    GroovyClassLoader getGroovyClassLoader() {
        return new GroovyClassLoader();
    }

    /**
     * Compiles groovy class from a file
     */
    @Override
    public Class compile(File file) throws IOException {
        GroovyClassLoader loader = getGroovyClassLoader();
        Class groovyClass = loader.parseClass(file);
        return groovyClass;
    }
}

netflix 内置了两种特殊的 filter ,StaticResponseFilter和SurgicalDebugFilter

  • StaticResponseFilter:允许从Zuul本身生成响应,而不是将请求转发到源
  • SurgicalDebugFilter:允许将特定请求路由到分隔的调试集群或主机
ZuulFilter执行过程
ZuulServlet

Zuul是基于Servlet的框架,ZuulServlet 用于处理所有的Request。其可以认为是Http Request的入口。

在ZuulServlet中,有方法叫做service(),其定义如下:

@Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
                preRoute();
                route();
                postRoute();
    }

在这里插入图片描述

RequestContext 提供了执行 filter Pipeline 所需要的 Context ,因为 Servlet 是单例多线程,这就要求 RequestContex t即要线程安全又要Request安全。context 使用 ThreadLocal 保存,这样每个 worker 线程都有一个与其绑定的 RequestContext ,因为 worker 仅能同时处理一个 Request ,这就保证了 Request Context 即是线程安全的由是 Request 安全的。所谓 Request 安全,即该Request的 Context 不会与其他同时处理Request冲突。

三个核心的方法 preRoute() , route() , postRoute()zuulrequest 处理逻辑都在这三个方法里,ZuulServlet 交给 ZuulRunner 去执行。由于 ZuulServlet 是单例,因此 ZuulRunner 也仅有一个实例。

ZuulRunner
public class ZuulRunner {
    //省略部分代码
    public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        //Init
    }
    public void postRoute() throws ZuulException {
        FilterProcessor.getInstance().postRoute();
    }
    public void route() throws ZuulException {
        FilterProcessor.getInstance().route();
    }
    public void preRoute() throws ZuulException {
        FilterProcessor.getInstance().preRoute();
    }
    public void error() {
        FilterProcessor.getInstance().error();
    }
}

可见,ZuulRunner 仅仅是直接将执行逻辑交由 FilterProcessor 处理。

FilterProcessor

核心方法:

public Object runFilters(String sType) throws Throwable {
        //省略部分代码
        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;
    }
  • 根据Type获取所有filterType是该类型的filter,组成 List list
  • 遍历该list,执行每个filter的处理逻辑 processZuulFilter(zuulFilter)

processZuulFilter()方法如下:

    public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        //省略部分代码
        try {
            ZuulFilterResult result = filter.runFilter();
            //省略部分代码
            switch (s) {
                case FAILED:
                    t = result.getException();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
                    break;
                case SUCCESS:
                    o = result.getResult();
                    ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);
                    if (bDebug) {
                        Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
                        Debug.compareContextState(filterName, copy);
                    }
                    break;
                default:
                    break;
            }
            //省略部分代码
            return o;
        } 
    }

Netflix Zuul 使用

Netflix Zuul 1.x 基本上就是一个 servlet 应用。

  • 首先在 web.xml 定义如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"             					xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee    	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  
    <listener>
        <listener-class>com.netflix.zuul.StartServer</listener-class>
    </listener>

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

    <servlet-mapping>
        <servlet-name>ZuulServlet</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>

</web-app>
  • 编写过滤器规则(groovy语言),示例:
class PreDecorationFilter extends ZuulFilter {
    @Override
    int filterOrder() {
        return 5
    }
    @Override
    String filterType() {
        return "pre"
    }
    @Override
    boolean shouldFilter() {
        return true;
    }
    @Override
    Object run() {
        RequestContext ctx = RequestContext.getCurrentContext()
        // sets origin
        ctx.setRouteHost(new URL("http://httpbin.org"));
        // sets custom header to send to the origin
        ctx.addOriginResponseHeader("cache-control", "max-age=3600");
    }
  • 之后编写启动类加载groovy文件,初始化过滤器,启动监听:
public class StartServer implements ServletContextListener {

    private static final Logger logger = LoggerFactory.getLogger(StartServer.class);

    @Override
    public void contextInitialized(ServletContextEvent sce) {

        // mocks monitoring infrastructure as we don't need it for this simple app
        MonitoringHelper.initMocks();

        // initializes groovy filesystem poller
        initGroovyFilterManager();

        // initializes a few java filter examples
        initJavaFilters();
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        logger.info("stopping server");
    }

    private void initGroovyFilterManager() {
        //省略部分代码
    }

    private void initJavaFilters() {
        final FilterRegistry r = FilterRegistry.instance();

        r.put("javaPreFilter", new ZuulFilter() {
            //省略部分代码
        });

        r.put("javaPostFilter", new ZuulFilter() {
            //省略部分代码
        });
    }
}

疑问

  • 集群内部的服务,如果需要调用外部其他服务,它所发出的请求,是不是需要经过zuul出去?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值