天天看點

SpringCloud 網關元件 Zuul-1.0 原理深度解析

SpringCloud 網關元件 Zuul-1.0 原理深度解析

為什麼要使用網關?

在當下流行的微服務架構中,面對多端應用時我們往往會做前後端分離:如前端分成 APP 端、網頁端、小程式端等,使用 Vue 等流行的前端架構交給前端團隊負責實作;後端拆分成若幹微服務,分别交給不同的後端團隊負責實作。

不同的微服務一般會有不同的服務位址,用戶端在通路這些位址的時候,需要記錄幾十甚至幾百個位址,用戶端會請求多個不同的服務,需要維護不同的請求位址,增加開發難度。而且這樣的機制會增加身份認證的難度,每個微服務需要獨立認證,微服務網關就應運而生。

微服務網關 介于用戶端與伺服器之間的中間層,是系統對外的唯一入口:所有的外部請求都會先經過微服務網關,用戶端隻需要與網關互動,隻知道一個網關位址即可。
SpringCloud 網關元件 Zuul-1.0 原理深度解析

網關是 SpringCloud 生态體系中的基礎元件之一,它的主流實作方案有兩個:

  1. Spring Cloud Netflix Zuul
  2. Spring Cloud Gateway

兩者的主要作用都是一樣的,都是代理和路由,本文主要聚焦于 Spring Cloud Netflix Zuul。

1. Zuul 網關簡介

Zuul 是 Spring Cloud 中的微服務網關,是為微服務架構中的服務提供了統一的通路入口。 Zuul 本質上是一個Web servlet應用,為微服務架構中的服務提供了統一的通路入口,用戶端通過 API 網關通路相關服務。

SpringCloud 網關元件 Zuul-1.0 原理深度解析

Zuul 網關的作用

網關在整個微服務的系統中角色是非常重要的,網關的作用非常多,比如路由、限流、降級、安全控制、服務聚合等。

  • 統一入口:唯一的入口,網關起到外部和内部隔離的作用,保障了背景服務的安全性;
  • 身份驗證和安全性:對需要身份驗證的資源進行過濾,拒絕處理不符合身份認證的請求;
  • 動态路由:動态的将請求路由到不同的後端叢集中;
  • 負載均衡:設定每種請求的處理能力,删除那些超出限制的請求;
  • 靜态響應處理:提供靜态的過濾器,直接響應一些請求,而不将它們轉發到叢集内部;
  • 減少用戶端與服務端的耦合:服務可以獨立發展,通過網關層來做映射。

2. Zuul 架構總覽

整體架構上可以分為兩個部分,即 Zuul Core 和 Spring Cloud Netflix Zuul。

其中 Zuul Core 部分即 Zuul 的核心,負責網關核心流程的實作;Spring Cloud Netflix Zuul 負責包裝Zuul Core,其中包括 Zuul 服務的初始化、過濾器的加載、路由過濾器的實作等。

SpringCloud 網關元件 Zuul-1.0 原理深度解析

3. Zuul 工作原理

容器啟動時,Spring Cloud 初始化 Zuul 核心元件,如 ZuulServlet、過濾器等。ZuulServlet 處理外部請求:初始化 RequestContext;ZuulRunner 發起執行 Pre 過濾器,并最終通過 FilterProcessor 執行;ZuulRunner 發起執行 Route 過濾器,并最終通過 FilterProcessor 執行;ZuulRunner 發起執行 Post 過濾器,并最終通過 FilterProcessor 執行;傳回 Http Response。

Zuul 初始化過程

Spring Cloud Netflix Zuul中初始化網關服務有兩種方式: @EnableZuulServer 和 @EnableZuulProxy。

這兩種方式都可以啟動網關服務,不同的主要地方是:

  1. @EnableZuulProxy 是 @EnableZuulServer 的超集,即使用 @EnableZuulProxy 加載的元件除了包含使用 @EnableZuulServer 加載的元件外,還增加了其他元件和功能;
  2. @EnableZuulServer 是純淨版的網關服務,不具備代理功能,隻實作了簡單的請求轉發、響應等基本功能,需要自行添加需要的元件;
  3. @EnableZuulProxy 在 @EnableZuulServer 的基礎上實作了代理功能,并可以通過服務發現來路由服務。
SpringCloud 網關元件 Zuul-1.0 原理深度解析

如圖所示,@EnableZuulServer 和 @EnableZuulProxy 的初始化過程一緻,最大的差別在于加載的過濾器不同。其中藍色是 @EnableZuulServer 加載的過濾器;紅色是 @EnableZuulProxy 額外添加的過濾器。

Zuul 初始化源碼分析

在程式的啟動類加上 @EnableZuulProxy:

@EnableCircuitBreaker
@EnableDiscoveryClient
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyConfiguration.class)
public @interface EnableZuulProxy {
}
複制代碼           

引用了 ZuulProxyConfiguration,跟蹤 ZuulProxyConfiguration,該類注入了 DiscoveryClient、RibbonCommandFactoryConfiguration 用作負載均衡相關的。注入了一些列的 filters,比如 PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter,代碼如如下:

ZuulProxyConfiguration.java

@Bean
    public PreDecorationFilter preDecorationFilter(RouteLocator routeLocator, ProxyRequestHelper proxyRequestHelper) {
        return new PreDecorationFilter(routeLocator, this.server.getServletPrefix(), this.zuulProperties, proxyRequestHelper);
    }

    @Bean
    public RibbonRoutingFilter ribbonRoutingFilter(ProxyRequestHelper helper, RibbonCommandFactory<?> ribbonCommandFactory) {
        RibbonRoutingFilter filter = new RibbonRoutingFilter(helper, ribbonCommandFactory, this.requestCustomizers);
        return filter;
    }

    @Bean
    public SimpleHostRoutingFilter simpleHostRoutingFilter(ProxyRequestHelper helper, ZuulProperties zuulProperties) {
        return new SimpleHostRoutingFilter(helper, zuulProperties);
    }
複制代碼           

父類 ZuulConfiguration ,引用了一些相關的配置。在缺失 zuulServlet bean 的情況下注入了 ZuulServlet,該類是 zuul 的核心類。

ZuulConfiguration.java

@Bean
    @ConditionalOnMissingBean(name = "zuulServlet")
    public ServletRegistrationBean zuulServlet() {
        ServletRegistrationBean servlet = new ServletRegistrationBean(new ZuulServlet(),
                this.zuulProperties.getServletPattern());
        // The whole point of exposing this servlet is to provide a route that doesn't
        // buffer requests.
        servlet.addInitParameter("buffer-requests", "false");
        return servlet;
    }
複制代碼           

同時也注入了其他的過濾器,比如 ServletDetectionFilter、DebugFilter、Servlet30WrapperFilter,這些過濾器都是 pre 類型 的。

@Bean
    public ServletDetectionFilter servletDetectionFilter() {
        return new ServletDetectionFilter();
    }
 
    @Bean
    public FormBodyWrapperFilter formBodyWrapperFilter() {
        return new FormBodyWrapperFilter();
    }
 
    @Bean
    public DebugFilter debugFilter() {
        return new DebugFilter();
    }
 
    @Bean
    public Servlet30WrapperFilter servlet30WrapperFilter() {
        return new Servlet30WrapperFilter();
    }
複制代碼           

同時還注入了 post 類型 的,比如 SendResponseFilter,error 類型,比如 SendErrorFilter,route 類型比如 SendForwardFilter,代碼如下:

@Bean
    public SendResponseFilter sendResponseFilter() {
        return new SendResponseFilter();
    }
 
    @Bean
    public SendErrorFilter sendErrorFilter() {
        return new SendErrorFilter();
    }
 
    @Bean
    public SendForwardFilter sendForwardFilter() {
        return new SendForwardFilter();
    }
複制代碼           

初始化 ZuulFilterInitializer 類,将所有的 filter 向 FilterRegistry 注冊:

@Configuration
    protected static class ZuulFilterConfiguration {
 
        @Autowired
        private Map<String, ZuulFilter> filters;
 
        @Bean
        public ZuulFilterInitializer zuulFilterInitializer(
                CounterFactory counterFactory, TracerFactory tracerFactory) {
            FilterLoader filterLoader = FilterLoader.getInstance();
            FilterRegistry filterRegistry = FilterRegistry.instance();
            return new ZuulFilterInitializer(this.filters, counterFactory, tracerFactory, filterLoader, filterRegistry);
        }
 
    }
複制代碼           

FilterRegistry 管理了一個 ConcurrentHashMap,用作存儲過濾器的,并有一些基本的 CURD 過濾器的方法,代碼如下:

FilterRegistry.java

public class FilterRegistry {
 
    private static final FilterRegistry INSTANCE = new FilterRegistry();
 
    public static final FilterRegistry instance() {
        return INSTANCE;
    }
 
    private final ConcurrentHashMap<String, ZuulFilter> filters = new ConcurrentHashMap<String, ZuulFilter>();
 
    private FilterRegistry() {
    }
 
    public ZuulFilter remove(String key) {
        return this.filters.remove(key);
    }
 
    public ZuulFilter get(String key) {
        return this.filters.get(key);
    }
 
    public void put(String key, ZuulFilter filter) {
        this.filters.putIfAbsent(key, filter);
    }
 
    public int size() {
        return this.filters.size();
    }
 
    public Collection<ZuulFilter> getAllFilters() {
        return this.filters.values();
    }
 
}
複制代碼           

FilterLoader 類持有 FilterRegistry,FilterFileManager 類持有 FilterLoader,是以最終是由FilterFileManager 注入 filterFilterRegistry 的 ConcurrentHashMa p的。FilterFileManager 到開啟了輪詢機制,定時的去加載過濾器,代碼如下:

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();
    }
複制代碼           

Zuul 請求處理過程

  1. 初始化 RequestContext;
  2. ZuulRunner 發起執行 Pre 過濾器,并最終通過 FilterProcessor 執行;
  3. ZuulRunner 發起執行 Route 過濾器,并最終通過 FilterProcessor 執行;
  4. ZuulRunner 發起執行 Post 過濾器,并最終通過 FilterProcessor 執行;
  5. 傳回 Http Response。
SpringCloud 網關元件 Zuul-1.0 原理深度解析

Zuul 預設注入的過濾器,它們的執行順序在 FilterConstants 類,我們可以先定位在這個類,然後再看這個類的過濾器的執行順序以及相關的注釋,可以很輕松定位到相關的過濾器。

過濾器 順序 描述 類型
ServletDetectionFilter -3 檢測請求是用 DispatcherServlet 還是 ZuulServlet pre
Servlet30WrapperFilter -2 在 Servlet 3.0 下,包裝 requests pre
FormBodyWrapperFilter -1 解析表單資料 pre
SendErrorFilter 如果中途出現錯誤 error
DebugFilter 1 設定請求過程是否開啟 debug pre
PreDecorationFilter 5 根據 uri 決定調用哪一個 route 過濾器 pre
RibbonRoutingFilter 10 如果寫配置的時候用 ServiceId 則用這個 route 過濾器,該過濾器可以用Ribbon 做負載均衡,用hystrix做熔斷 route
SimpleHostRoutingFilter 100 如果寫配置的時候用 url 則用這個 route 過濾 route
SendForwardFilter 500 用 RequestDispatcher 請求轉發 route
SendResponseFilter 1000 用 RequestDispatcher 請求轉發 post
過濾器的 order 值越小,就越先執行。并且在執行過濾器的過程中,它們 共享了一個 RequestContext 對象,該對象的生命周期貫穿于請求。

可以看出優先執行了 pre 類型的過濾器,并将執行後的結果放在 RequestContext 中,供後續的 filter 使用,比如在執行 PreDecorationFilter 的時候,決定使用哪一個 route,它的結果的是放在 RequestContext 對象中,後續會執行所有的 route 的過濾器,如果不滿足條件就不執行該過濾器的 run() 方法,最終達到了就執行一個 route 過濾器的 run() 方法。

  • error 類型的過濾器,是在程式發生異常的時候執行的。
  • post 類型的過濾,在預設的情況下,隻注入了 SendResponseFilter,該類型的過濾器是将最終的請求結果以流的形式輸出給用戶端。

Zuul 請求處理源碼分析

Zuulservlet 作為類似于 Spring MVC 中的 DispatchServlet,起到了前端控制器的作用,所有的請求都由它接管。它的核心代碼如下:

Zuulservlet.java

@Override
   public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
 
            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();
 
            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }
 
        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }
    
複制代碼           

跟蹤 init() 方法,可以發現這個方法 init() 為每個請求生成了 RequestContext(底層使用 ThreadLocal 儲存資料),RequestContext 繼承了 ConcurrentHashMap:

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
 
        RequestContext ctx = RequestContext.getCurrentContext();
        if (bufferRequests) {
            ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
        } else {
            ctx.setRequest(servletRequest);
        }
 
        ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
 
    }
 
 
    public void preRoute() throws ZuulException {
        FilterProcessor.getInstance().preRoute();
    }
    
複制代碼           

而 FilterProcessor 類為調用 filters 的類,比如調用 pre 類型所有的過濾器,route、post 類型的過濾器的執行過程和 pre 執行過程類似:

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());
        }
    }
複制代碼           

跟蹤 runFilters() 方法,可以發現,它最終調用了 FilterLoader 的 getFiltersByType(sType) 方法來擷取同一類的過濾器,然後用 for 循環周遊所有的 ZuulFilter,執行了 processZuulFilter() 方法,跟蹤該方法可以發現最終是執行了 ZuulFilter 的方法,最終傳回了該方法傳回的 Object 對象:

public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        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;
    }
複制代碼           

SimpleHostRoutingFilter

現在來看一下 SimpleHostRoutingFilter 是如何工作的。進入到 SimpleHostRoutingFilter 類的 run() 方法,核心代碼如下:

@Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        // 省略代碼
 
        String uri = this.helper.buildZuulRequestURI(request);
        this.helper.addIgnoredHeaders();
 
        try {
            CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,
                    headers, params, requestEntity);
            setResponse(response);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
        return null;
    }
複制代碼           

查閱這個類的全部代碼可知,該類建立了一個 HttpClient 作為請求類,并重構了 url,請求到了具體的服務,得到的一個 CloseableHttpResponse 對象,并将 CloseableHttpResponse 對象的儲存到 RequestContext 對象中。并調用了 ProxyRequestHelper 的 setResponse 方法,将請求狀态碼,流等資訊儲存在 RequestContext 對象中。

private void setResponse(HttpResponse response) throws IOException {
        RequestContext.getCurrentContext().set("zuulResponse", response);
        this.helper.setResponse(response.getStatusLine().getStatusCode(),
                response.getEntity() == null ? null : response.getEntity().getContent(),
                revertHeaders(response.getAllHeaders()));
    }
複制代碼           

SendResponseFilter

這個過濾器的 order 為 1000,在預設且正常的情況下,是最後一個執行的過濾器,該過濾器是最終将得到的資料傳回給用戶端的請求。在它的 run() 方法裡,有兩個方法:addResponseHeaders() 和 writeResponse(),即添加響應頭和寫入響應資料流。

public Object run() {
        try {
            addResponseHeaders();
            writeResponse();
        }
        catch (Exception ex) {
            ReflectionUtils.rethrowRuntimeException(ex);
        }
        return null;
    }
複制代碼           

其中 writeResponse() 方法是通過從 RequestContext 中擷取 ResponseBody 獲或者 ResponseDataStream 來寫入到 HttpServletResponse 中的,但是在預設的情況下 ResponseBody 為 null,而 ResponseDataStream 在 route 類型過濾器中已經設定進去了。具體代碼如下:

private void writeResponse() throws Exception {
        RequestContext context = RequestContext.getCurrentContext();
 
        HttpServletResponse servletResponse = context.getResponse();
            // 省略代碼
        OutputStream outStream = servletResponse.getOutputStream();
        InputStream is = null;
        try {
            if (RequestContext.getCurrentContext().getResponseBody() != null) {
                String body = RequestContext.getCurrentContext().getResponseBody();
                writeResponse(
                        new ByteArrayInputStream(
                                body.getBytes(servletResponse.getCharacterEncoding())),
                        outStream);
                return;
            }
 
            // 省略代碼
            is = context.getResponseDataStream();
            InputStream inputStream = is;
                // 省略代碼
 
            writeResponse(inputStream, outStream);
                // 省略代碼
            }
        }
        // 省略代碼
    }
複制代碼           

4. Zuul-2.0 和 Zuul-1.0 對比

Zuul1.0 設計比較簡單,代碼很少也比較容易讀懂,它本質上就是一個同步 Servlet,采用多線程阻塞模型。 Zuul2.0 的設計相對比較複雜,代碼也不太容易讀懂,它采用了 Netty 實作異步非阻塞程式設計模型。比較明确的是,Zuul2.0 在連結數方面表現要好于 Zuul1.0,也就是說 Zuul2.0 能接受更多的連結數。

Netflix 給出了一個比較模糊的資料,大體 Zuul2.0 的性能比 Zuul1.0 好 20% 左右,這裡的性能主要指每節點每秒處理的請求數。為何說模糊呢?由于這個資料受實際測試環境,流量場景模式等衆多因素影響,你很難複現這個測試資料。即使這個 20% 的性能提高是确實的,其實這個性能提高也并不大,和異步引入的複雜性相比,這 20 %的提高是否值得是個問題。

兩者架構上的差異

Zuul2.0 的架構,和 Zuul1.0 沒有本質差別,兩點變化:

  • 前端用 Netty Server 代替 Servlet,目的是支援前端異步。後端用 Netty Client 代替 Http Client,目的是支援後端異步。
  • 過濾器換了一下名字,用 Inbound Filters 代替 Pre-routing Filters,用 Endpoint Filter 代替Routing Filter,用 Outbound Filters 代替 Post-routing Filters。

線上環境使用建議

  1. 同步異步各有利弊,同步多線程程式設計模型簡單,但會有線程開銷和阻塞問題,異步非阻塞模式線程少并發高,可是程式設計模型變得複雜。
  2. 架構師作技術選型須要嚴謹務實,具有批判性思惟 (Critical Thinking),即便是對于一線大公司推出的開源産品,也要批判性看待,不可盲目追新。
  3. 我的 建議生産環境繼續使用 Zuul1.0,同步阻塞模式的一些不足,可使用熔斷元件 Hystrix 和AsyncServlet 等技術進行優化。