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出去?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章