隨行付微服務之基於Zuul自研服務閘道器

馬力_隨行付發表於2019-01-08

隨行付微服務之服務閘道器

微服務是時下最流行的架構之一,作為微服務不可或缺的一部分,API閘道器的作用至關重要。本文將對隨行付微服務的API閘道器實踐進行介紹。

API閘道器的作用

我們知道,在一個微服務系統中,整個系統被劃分為許多小模組,客戶端想要呼叫服務,可能需要維護很多ip+port資訊,管理十分複雜。API閘道器作為整個系統的統一入口,所有請求由閘道器接收並路由轉發給內部的微服務。對於客戶端而言,系統相當於一個黑箱,客戶端不需要關心其內部結構。

隨著業務的發展,服務端可能需要對微服務進行重新劃分等操作,由於閘道器將客戶端和具體服務隔離,因此可以在儘量不改動客戶端的情況下進行。閘道器可以完成許可權驗證、限流、安全、監控、快取、服務路由、協議轉換、服務編排、灰度釋出等功能剝離出來,講這些非業務功能統一解決、統一機制處理。

Zuul原理簡介

隨行付微服務API閘道器基於Netflix的Zuul實現。Netflix是實踐微服務最成功的公司之一,他們建立並開源了一系列微服務相關的框架,Zuul便是用來實現閘道器功能的框架。Zuul的整體架構圖如下:

隨行付微服務之基於Zuul自研服務閘道器

Zuul基於Servlet開發,ZuulServlet是整個框架的入口。Zuul的核心元件是Filter,Filter分為四類,分別是pre、route、post、error。pre-filter用來實現前置邏輯,route-filter用來實現對目標服務的呼叫邏輯,post-filter用來實現收尾邏輯,error-filter則在任意位置發生異常時做異常處理(此處應該注意,如果pre或route發生異常,執行error後,仍然會執行post),其示意圖如下:

隨行付微服務之基於Zuul自研服務閘道器

在Filter中可以定義某些條件下是否執行過濾器邏輯,以及同種類Filter的優先順序。Filter的各個方法中並不存在入參,其引數傳遞是通過一個基於ThreadLocal實現的RequestContext,雖然RequestContext中定義了很多引數的讀寫方法,但初始的可用引數僅有req和res,對應HttpSerlvetRequest和HttpServletResponse。Filter程式碼範例如下:

public class TestFilter extends ZuulFilter {
    
    @Override /** 是否攔截 */
    public boolean shouldFilter() {
        return false;
    }
    
    @Override /** filter邏輯 */
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();// 獲取當前執行緒的
        HttpServletRequest req = context.getRequest();// 獲取請求資訊
        return null; // 從原始碼來看,這個返回值沒什麼用
    }
    
    @Override /** filter型別 */
    public String filterType() {
        return "pre";// pre/route/post/error
    }
    
    @Override /** filter優先順序,僅在同型別filter中生效 */
    public int filterOrder() {
        return 0;
    }
}
複製程式碼

Filter通常使用groovy編寫,以便於動態載入。當我們編寫好一個Filter類後,將其放在指定的磁碟路徑下,FilterFileManager會啟動一個守護執行緒去定期讀取並載入。通過動態載入,我們可以在不停機的情況下新增、修改功能模組。FilterFileManager原始碼摘要如下:

public class FilterFileManager {
    ...
    /**
     * Initialized the GroovyFileManager.
     *
     * @throws Exception
     */
    @PostConstruct
    public void init() throws Exception
    {
        long startTime = System.currentTimeMillis();
        
        filterLoader.putFiltersForClasses(config.getClassNames());
        manageFiles();
        startPoller();
        
        LOG.warn("Finished loading all zuul filters. Duration = " + (System.currentTimeMillis() - startTime) + " ms.");
    }
    
    ...
    /** 啟動執行緒定時讀取檔案 */
    void startPoller() {
        poller = new Thread("GroovyFilterFileManagerPoller") {
            public void run() {
                while (bRunning) {
                    try {
                        sleep(config.getPollingIntervalSeconds() * 1000);
                        manageFiles();
                    }
                    catch (Exception e) {
                        LOG.error("Error checking and/or loading filter files from Poller thread.", e);
                    }
                }
            }
        };
        poller.start();
    }
    ...
    /** 讀取檔案並載入 */
    void manageFiles()
    {
        try {
            List<File> aFiles = getFiles();
            processGroovyFiles(aFiles);
        }
        catch (Exception e) {
            String msg = "Error updating groovy filters from disk!";
            LOG.error(msg, e);
            throw new RuntimeException(msg, e);
        }
    }
}
複製程式碼

SpringCloud-Zuul

Spring Cloud通過整合Zuul來實現API閘道器模組,我們來簡單介紹一下它的整合原理。

SpringCloud-Zuul的核心配置類是ZuulServerAutoConfiguration以及ZuulProxyAutoConfiguration。Spring首先使用ZuulController來封裝ZuulServlet,然後定義一個ZuulHandlerMapping,使得除一些特殊請求以外(如/error)的大部分請求被轉發到ZuulController進行處理。原始碼摘要如下:

    @Configuration
    @EnableConfigurationProperties({ ZuulProperties.class })
    @ConditionalOnClass(ZuulServlet.class)
    @ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
    // Make sure to get the ServerProperties from the same place as a normal web app would
    @Import(ServerPropertiesAutoConfiguration.class)
    public class ZuulServerAutoConfiguration {
        ...
        @Bean
        public ZuulController zuulController() {
            return new ZuulController();
        }
        
        @Bean
        public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
            ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
            mapping.setErrorController(this.errorController);
            return mapping;
        }
    }

    public class ZuulController extends ServletWrappingController {
        
        public ZuulController() {
            setServletClass(ZuulServlet.class);
            setServletName("zuul");
            setSupportedMethods((String[]) null); // Allow all
        }
        ...
    }

    public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
        ...
        private final ZuulController zuul;
        ...
        @Override
        protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
            if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
                return null;
            }
            if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
            RequestContext ctx = RequestContext.getCurrentContext();
            if (ctx.containsKey("forward.to")) {
                return null;
            }
            if (this.dirty) {
                synchronized (this) {
                    if (this.dirty) {
                        registerHandlers();
                        this.dirty = false;
                    }
                }
            }
            return super.lookupHandler(urlPath, request);
        }
        ...
        private void registerHandlers() {
            Collection<Route> routes = this.routeLocator.getRoutes();
            if (routes.isEmpty()) {
                this.logger.warn("No routes found from RouteLocator");
            }
            else {
                for (Route route : routes) {
                    registerHandler(route.getFullPath(), this.zuul);
                }
            }
        }
    }
複製程式碼

SpringCloud預設定義了一些Filter來實現閘道器邏輯,其中最核心的Filter——RibbonRoutingFilter是負責實際轉發操作的,在它的過濾邏輯裡又整合了hystrix、ribbon等其他重要框架。原始碼摘要如下:

public class RibbonRoutingFilter extends ZuulFilter {
    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);//構建請求資料
            ClientHttpResponse response = forward(commandContext);//執行請求
            setResponse(response);//設定應答資訊
            return response;
        }
        catch (ZuulException ex) {
            throw new ZuulRuntimeException(ex);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
    }
}
複製程式碼

載入Filter的方式通過ZuulFilterInitializer擴充套件為可以從ApplicationContext中獲取。原始碼摘要:

/** 程式碼出自ZuulServerAutoConfiguration */
@Configuration
protected static class ZuulFilterConfiguration {

    @Autowired
    private Map<String, ZuulFilter> filters;//從spring上下文中獲取Filter bean

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

public class ZuulFilterInitializer {
    private final Map<String, ZuulFilter> filters;
    ...
    @PostConstruct
    public void contextInitialized() {
        ...
        // 設定filter
        for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
            filterRegistry.put(entry.getKey(), entry.getValue());
        }
    }
}
複製程式碼

Zuul2

隨著業務的不斷髮展,Zuul對於Netflix來說效能已經不太夠用,於是Netflix又開發了Zuul2。Zuul2最大的變革是基於Netty實現了框架的非同步化,從而提升其效能。根據官方的資料,Zuul2的效能比Zuul1約有20%的提升。Zuul2架構圖如下:

隨行付微服務之基於Zuul自研服務閘道器

由於框架改為了非同步的模式,Zuul2在提升效能的同時,也帶來了除錯、運維的困難。在實際的使用當中,對於絕大多數公司來說,併發量遠遠沒有Netflix那樣龐大,選擇開發除錯更簡單、且效能夠用的Zuul1是更合適的選擇。

作者簡介

任金昊,隨行付架構部高階開發工程師。擅長分散式、微服務架構,負責隨行付微服務生態平臺開發。

隨行付微服務之基於Zuul自研服務閘道器

相關文章