1. 程式人生 > >微服務核心元件 Zuul 閘道器原理剖析 | 併發程式設計網

微服務核心元件 Zuul 閘道器原理剖析 | 併發程式設計網

一、前言

Zuul 閘道器是具體核心業務服務的看門神,相比具體實現業務的系統服務來說它是一個邊緣服務,主要提供動態路由,監控,彈性,安全性等功能。在分散式的微服務系統中,系統被拆為了多套系統,通過zuul閘道器來對使用者的請求進行路由,轉發到具體的後臺服務系統中。

本 Chat 主要內容如下:

  • 服務閘道器演化歷程。
  • Zuul 1.0 服務架構與原始碼剖析。
  • Zuul 2.0 服務架構新特性。

二、服務閘道器演化歷程

閘道器是具體核心業務服務的看門神,相比具體實現業務的系統服務來說它是一個邊緣服務,主要提供動態路由,監控,彈性,安全性等功能,下面我們從單體應用到多體應用的演化過程來講解閘道器的演化歷程。

一般業務系統發展歷程都是基本相似的,從單體應用到多應用,從本地呼叫到遠端呼叫。對應單體應用架構模式(如下圖1),由於只需一個應用,所有業務模組的功能都打包為了一個 War 包進行部署,這樣可以減少機器資源和部署的繁瑣。

image.png

圖1 單體應用

在單體應用中,閘道器模組是和應用部署到同一個jvm程序裡面的,當外部移動裝置或者web站點訪問單體應用的功能時候,請求是先被應用的閘道器模組攔截的,閘道器模組對請求進行鑑權、限流等動作後在把具體的請求轉發到當前應用對應的模組進行處理。

隨著業務的發展,網站的流量會越來越大,在單體應用中簡單的通過加機器的方式可以帶來的承受流量衝擊的能力也越來越低,這時候就會考慮根據業務將單體應用拆成若干個功能獨立的應用,單體應用拆為多個應用後,由於不同的應用開發對應的功能,所以多應用開發之間可以獨立開發而不用去理解對方的業務,另外不同的應用模組只承受對應業務流量的壓力,不會對其他應用模組造成影響,這時候多體的分散式系統就出現了,如下圖2。

image.png

圖2 多體應用

如上圖在多體應用中業務模組A和B單獨起了個應用,每個應用裡面有自己的閘道器模組,如果業務模組多了,那麼每個應用都有自己的閘道器模組,這樣複用性不好,所以可以考慮把閘道器模組提起出來,單獨作為一個應用來做服務路由,如下圖3:

image.png

如上圖當移動裝置發起請求時候是具體傳送到閘道器應用的,經過鑑權後請求會被轉發到具體的後端服務應用上,對應前端移動裝置來說他們不在乎也不知道後端伺服器應用是一個還是多個,他們只能感知到閘道器應用的存在。

Zuul是Netflix開源的一個閘道器元件,在Netflix內部系統中Zuul被用來作為內部系統的門面,如下圖是Zuul在Netflix內部使用的一個架構圖:

image.png

如上圖最上層的移動裝置或者網站首先通過aws負載均衡器把請求路由到zuul閘道器上,zuul閘道器則負責把請求路由到具體的後端service上。

三、Zuul 1.0 服務架構與原始碼剖析

3.1 Zuul 1.0 服務架構概述

Zuul閘道器的核心是一系列的過濾器,這些過濾器可以對請求或者響應結果做一系列過濾,Zuul 提供了一個框架可以支援動態載入,編譯,執行這些過濾器,這些過濾器是使用責任鏈方式順序對請求或者響應結果進行處理的,這些過濾器直接不會直接進行通訊,但是通過責任鏈傳遞的RequestContext引數可以共享一些東西。

雖然Zuul 支援任何可以在jvm上跑的語言,但是目前zuul的過濾器只能使用Groovy指令碼來編寫。編寫好的過濾器指令碼一般放在zuul伺服器的固定目錄,zuul伺服器會開啟一個執行緒定時去輪詢被修改或者新增的過濾器,然後動態進行編譯,載入到記憶體,然後等後續有請求進來,新增或者修改後的過濾器就會生效了。

在zuul中過濾器分為四種:

  • PRE Filters(前置過濾器) :當請求會路由轉發到具體後端伺服器前執行的過濾器,比如鑑權過濾器,日誌過濾器,還有路由選擇過濾器
  • ROUTING Filters (路由過濾器):該過濾器作用是把請求具體轉發到後端伺服器上,一般是通過Apache HttpClient 或者 Netflix Ribbon把請求傳送到具體的後端伺服器上
  • POST Filters(後置過濾器):當把請求路由到具體後端伺服器後執行的過濾器;場景有新增標準http 響應頭,收集一些統計資料(比如請求耗時等),寫入請求結果到請求方等。
  • ERROR Filters(錯誤過濾器):當上面任何一個型別過濾器執行出錯時候執行該過濾器

如下圖為zuul1.0的工作原理:

image.png

如上圖,當zuul接受到請求後,首先會由前置過濾器進行處理,然後在由路由過濾器具體把請求轉發到後端應用,然後在執行後置過濾器把執行結果寫會到請求方,當上面任何一個型別過濾器執行出錯時候執行該過濾器。

3.2 Zuul 1.0 原始碼分析

本節作者使用zuul的版本:

<dependency>
            <groupId>com.netflix.zuul</groupId>
            <artifactId>zuul-core</artifactId>
            <version>1.0.28</version>
        </dependency> 

3.2.1 核心處理流程-ZuulServlet類

在Zuul1.0中最核心的是ZuulServlet類,該類是個servlet,用來對匹配條件的請求執行核心的 pre, routing, post過濾器;下面我們來看下該類核心流程:

image.png

如上類圖可知當請求過來後,先後執行了FilterProcessor管理的三種過濾器,這下面我們看下原始碼:

 public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
            ...
            try {
            //1 前置過濾器
                preRoute();
            } catch (ZuulException e) {
                //1.1 錯誤過濾器
                error(e);
                //1.2 後置過濾器
                postRoute();
                return;
            }
            try {
                //2 路由過濾器
                route();
            } catch (ZuulException e) {
                //2.1 錯誤過濾器
                error(e);
                //2.2 後置過濾器
                postRoute();
                return;
            }
            try {
                //3 後置過濾器
                postRoute();
            } catch (ZuulException e) {
                //3.1 錯誤過濾器
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
        }
    }

可知如果在三種過濾器執行過程中發生了錯誤,會執行error(e)方法,該方法其實是執行錯誤過濾器的,需要注意的是如果在pre,route過濾器執行過程中出現了錯誤,在執行錯誤過濾器後還是需要在執行後置過濾器的。

下面我們著重看下FilterProcessor類的runFilters方法如何執行具體過濾器的:

    public Object runFilters(String sType) throws Throwable {
        ...
        boolean bResult = false;
        //2 獲取sType型別的過濾器
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            for (int i = 0; i < list.size(); i++) {
                ZuulFilter zuulFilter = list.get(i);
                //2.1具體執行過濾器
                Object result = processZuulFilter(zuulFilter);
                if (result != null && result instanceof Boolean) {
                    bResult |= ((Boolean) result);
                }
            }
        }
        return bResult;
    }

如上程式碼可知程式碼2是具體獲取不同型別的過濾器的,程式碼2.1是具體執行過濾器的,首先我們看下程式碼2看FilterLoader類的getFiltersByType方法如何獲取不同型別的過濾器:

 public List<ZuulFilter> getFiltersByType(String filterType) {

        //3 分類快取是否存在
        List<ZuulFilter> list = hashFiltersByType.get(filterType);
        if (list != null) return list;

        list = new ArrayList<ZuulFilter>();
        //4 獲取所有過濾器
        Collection<ZuulFilter> filters = filterRegistry.getAllFilters();
        for (Iterator<ZuulFilter> iterator = filters.iterator(); iterator.hasNext(); ) {
            ZuulFilter filter = iterator.next();
            if (filter.filterType().equals(filterType)) {
                list.add(filter);
            }
        }
        Collections.sort(list); // sort by priority
        //5 儲存到分類快取
        hashFiltersByType.putIfAbsent(filterType, list);
        return list;
    }
    private final ConcurrentHashMap<String, List<ZuulFilter>> hashFiltersByType = new ConcurrentHashMap<String, List<ZuulFilter>>();

如上程式碼3首先看分類快取裡面是否有該型別的過濾器,如果有直接返回,否者執行程式碼4獲取所有註冊的過濾器,然後從中過濾出當前需要的類別,然後快取到分類過濾器。看到這裡想必大家知道FilterRegistry類是存放所有過濾器的類,FilterRegistry裡面 : private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();存放所有註冊的過濾器,那麼這些過濾器什麼時候放入的那?這個我們後面講解。

這裡我們剖析瞭如何獲取具體型別的過濾器的,下面回到程式碼2.1看如何執行過濾器的:

 public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

        RequestContext ctx = RequestContext.getCurrentContext();
        boolean bDebug = ctx.debugRouting();
        final String metricPrefix = "zuul.filter-";
        long execTime = 0;
        String filterName = "";
        try {
            ...
            //5 執行過濾器
            ZuulFilterResult result = filter.runFilter();
            ExecutionStatus s = result.getStatus();
            execTime = System.currentTimeMillis() - ltime;

            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);
                    
                    break;
                default:
                    break;
            }
            
            if (t != null) throw t;

            usageNotifier.notify(filter, s);
            return o;

        } catch (Throwable e) {
          ...
        }
    }

3.2.2 動態裝載過濾器-FilterFileManager類

….

總結:zuul1.0時候當zuul接受到一個請求後會同步執行前置過濾器、路由過濾器、後置過濾器,等執行完畢後在同步把結果返回為呼叫方,呼叫方在整個過程中是阻塞的。其實SpringBoot整合的zuul就是自己實現了個前置過濾器做選擇路由,然後自己實現了個路由過濾器根據前置過濾器選擇的路由具體做路由轉發。

四、Zuul 2.0 服務架構新特性

4.1 新特性

Netty作為高效能非同步網路通訊框架,在dubbo,rocketmq,sofa等知名開源框架中都有使用,如下圖zuul2.0使用netty server作為閘道器監聽伺服器監聽客戶端發來的請求,然後把請求轉發到前置過濾器(inbound filters)進行處理,處理完畢後在把請求使用netty client代理到具體的後端伺服器進行處理,處理完畢後在把結果交給後者過濾器(outbound filters)進行處理,然後把處理結果通過nettyServer寫回客戶端。

image.png

… 總: 在zuul1.0時候客戶端發起的請求後需要同步等待zuul閘道器返回,zuul閘道器這邊對每個請求會分派一個執行緒來進行處理,這會導致併發請求數量有限。而zuul2.0使用netty作為非同步通訊,可以大大加大併發請求量。

五、最後

加多

加多

高階 Java 攻城獅 at 阿里巴巴加多,目前就職於阿里巴巴,熱衷併發程式設計、ClassLoader,Spring等開源框架,分散式RPC框架dubbo,springcloud等;愛好音樂,運動。微信公眾號:技術原始積累。知識星球賬號:技術原始積累