API網關與zuul1.x與springcloud的三角關係

[TOC]java

1. API網關簡介

API 網關能夠看作系統與外界聯通的入口,咱們能夠在網關處理一些非業務邏輯的邏輯,好比權限驗證,監控,緩存,請求路由等等。所以API網關能夠承接兩個方向的入口。git

  1. 移動APP/WEB的統一入口網關。
  2. 業務方快速提開放能力。

2. 如何實現一個網關

2.1 網關核心

API網關通常按照職責鏈的模式實現,核心鏈路通常分爲三個部分: 預處理、請求轉發和處理結果。github

職責鏈能夠經過過濾器的方式去實現,過濾器中定義是否須要執行和執行的順序,經過上下文變量透傳給每一個過濾器。web

2.1.1 預處理

這一環節,能夠可插拔式的,擴展不少過濾器,例如:spring

2.1.1.1 初始化API

將API信息、服務提供方信息查出來,並驗證API的合法性。後端

2.1.1.2 API鑑權

對API進行鑑權認證,可自定義鑑權方式,例如OAuth二、簽名認證。設計模式

2.1.1.3 訪問控制

對API的訪問進行控制,調用者是否進入黑名單,調用方是否已受權調用該API。數組

2.1.1.4 限流控制

對API進行流量控制,能夠根據調用者、API兩個維度進行流量控制,流量控制相對比較靈活,能夠按照組合方式進行流控。緩存

2.1.1.5 參數轉換

根據API路由到後端地址的規則,進行參數轉換,構建出須要請求的參數。tomcat

2.1.2 請求轉發

這一環節,能夠根據協議的不通選擇不一樣的轉發方式,rpc、http協議轉發的方式不一樣,這一環節能夠藉助一些框架來實現,rpc可選擇dubbo、http可選擇ribbon,這樣方便解決負載均衡的調用。同時能夠爲調用作資源隔離、保證路由轉發時具有容錯機制,市面上較爲主流的爲hystrix。

2.1.3 處理結果

這一環節,對於調用須要處理的報文進行處理,記錄下來,用於對調用狀況作分析統計。同時也對一些異常狀況處理,加上默認的響應報文。

2.2 網關設計圖

3. netlifx-zuul 1.x

zuul是由netflix開源的一個網關,能夠提供動態的路由、監控和安全性保證。 zuul 1.x是基於servlet構建的一個框架,經過一系列filter,完成職責鏈的設計模式。而zuul1.x主要包含了四類過濾器:

  1. pre: 請求路由被調用前的前置過濾器。
  2. route:真正實現路由轉發的過濾器,這種過濾器講請求轉發至真實的後端微服務,返回相關請求結果。
  3. post: 收到請求後進行調用。用於收集統計信息,處理響應報文。
  4. error: 在任意階段發生錯誤後會執行,進行統一的異常處理。

3.1 Zuul Request Lifecycle

3.2 zuul實現

3.2.1 ZuulServlet

ZuulServlet是Zuul的轉發引擎,全部的請求都由該servlet統一處理,調用servlet的service函數對請求進行過濾。

public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            // 初始化當前的zuul request context,將request和response放入上下文中
            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();
            //////////////// zuul對請求的處理流程 start  ////////////////
            // zuul對一個請求的處理流程:pre -> route -> post
            // 1. post是必然執行的(能夠類比finally塊),但若是在post中拋出了異常,交由error處理完後就結束,避免無限循環
            // 2. 任何階段拋出了ZuulException,都會交由error處理
            // 3. 非ZuulException會被封裝後交給error處理
            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();
        }
    }
複製代碼

3.2.2 RequestContext

保存請求、響應、狀態信息和數據,以便zuulfilters訪問和共享,能夠經過設置ContextClass來替換RequestContext的擴展。

3.2.3 ZuulRunner

該類將servlet請求和響應初始化爲RequestContext幷包裝FilterProcessor(filter的處理器)調用,用於處理 reRoute(), route(), postRoute(), and error()。

3.2.4 FilterProcessor

過濾器的處理器,核心函數是runFilters():

/**
     * runs all filters of the filterType sType/ Use this method within filters to run custom filters by type
     *
     * @param sType the filterType.
     * @return
     * @throws Throwable throws up an arbitrary exception
     */
    public Object runFilters(String sType) throws Throwable {
        if (RequestContext.getCurrentContext().debugRouting()) {
            Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
        }
        boolean bResult = false;
        // 經過FilterLoader獲取指定類型的全部filter
        List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
        if (list != null) {
            // 這裏沒有進行try...catch... 意味着只要任何一個filter執行失敗了整個過程就會中斷掉
            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;
    }
複製代碼

3.2.5 ZuulFilter

/**
     * runFilter checks !isFilterDisabled() and shouldFilter(). The run() method is invoked if both are true.
     *
     * @return the return from ZuulFilterResult
     */
    public ZuulFilterResult runFilter() {
        ZuulFilterResult zr = new ZuulFilterResult();
        // 當前filter是否被禁用
        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;
    }
複製代碼

3.2.6 FilterRegistry

Filter 註冊類,包含一個ConcurrentHashMap, 按照類型保存filter。

3.2.7 FilterLoader

用來經過加載groovy的過濾器文件,註冊到FilterRegistry。

/**
     * From a file this will read the ZuulFilter source code, compile it, and add it to the list of current filters
     * a true response means that it was successful.
     * 從一個文件中,read出filter的源代碼,編譯它,並將其添加到當前過濾器列表中。
     *
     * @param file
     * @return true if the filter in file successfully read, compiled, verified and added to Zuul
     * @throws IllegalAccessException
     * @throws InstantiationException
     * @throws IOException
     */
    public boolean putFilter(File file) throws Exception {
        String sName = file.getAbsolutePath() + file.getName();
        if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
            LOG.debug("reloading filter " + sName);
            filterRegistry.remove(sName);
        }
        ZuulFilter filter = filterRegistry.get(sName);
        if (filter == null) {
            Class clazz = COMPILER.compile(file);
            if (!Modifier.isAbstract(clazz.getModifiers())) {
                filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
                List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
                if (list != null) {
                    hashFiltersByType.remove(filter.filterType()); //rebuild this list
                }
                filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
                filterClassLastModified.put(sName, file.lastModified());
                return true;
            }
        }

        return false;
    }
複製代碼

8.FileManager

/**
     * Initialized the GroovyFileManager.
     *
     * @param pollingIntervalSeconds the polling interval in Seconds 多少秒進行輪訓
     * @param directories            Any number of paths to directories to be polled may be specified
     * @throws IOException
     * @throws IllegalAccessException
     * @throws InstantiationException
     */
    public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException {
        if (INSTANCE == null) INSTANCE = new FilterFileManager();
        //文件夾路徑 ["src/main/groovy/filters/pre", "src/main/groovy/filters/route", "src/main/groovy/filters/post"]
        INSTANCE.aDirectories = directories;
        //輪訓時間
        INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds;
        //按照文件夾路徑掃出以.groovy文件結尾的文件數組,而後經過FilterLoader讀取filter,並放入filter到內存中。
        INSTANCE.manageFiles();
        //一直輪訓的線程
        INSTANCE.startPoller();

    }
複製代碼

3.2.8 StartServer

StartServer是一個ServletContextListener,負責在web應用啓動後執行一些初始化操做

4 spring-cloud-netflix-zuul

4.1 spring-cloud作了什麼?

4.1.1.ZuulHandlerMapping

ZuulHandlerMapping在註冊發生在第一次請求發生的時候,在ZuulHandlerMapping.lookupHandler方法中執行。在ZuulHandlerMapping.registerHandlers方法中首先獲取全部的路由,而後調用AbstractUrlHandlerMapping.registerHandler將路由中的路徑和ZuulHandlerMapping相關聯。

@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;
		}
		//默認dirty爲true,第一次請求進入。
		if (this.dirty) {
			synchronized (this) {
				if (this.dirty) {
				    //註冊handler,將自定義的路由映射到springmvc的map中。
					registerHandlers();
					this.dirty = false;
				}
			}
		}
		//調用抽象類的lookupHandler,匹配不到的話,直接拋出404。ZuulHandlerMapping藉助springmvc特性,作路由匹配。
		return super.lookupHandler(urlPath, request);
	}

	private boolean isIgnoredPath(String urlPath, Collection<String> ignored) {
		if (ignored != null) {
			for (String ignoredPath : ignored) {
				if (this.pathMatcher.match(ignoredPath, urlPath)) {
					return true;
				}
			}
		}
		return false;
	}

	private void registerHandlers() {
	    //經過路由定位器掃出路由信息,遍歷路由,調用springmvc的路由。轉發的handler是自定義的ZuulController,用於包裝ZuulServlet。
		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);
			}
		}
	}
複製代碼

4.1.2 ZuulController

ZuulController是ZuulServlet的一個包裝類,ServletWrappingController是將當前應用中的某個Servlet直接包裝爲一個Controller,全部到ServletWrappingController的請求其實是由它內部所包裝的這個Servlet來處理。

public class ZuulController extends ServletWrappingController {

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

	@Override
	public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
		try {
			// We don't care about the other features of the base class, just want to // handle the request return super.handleRequestInternal(request, response); } finally { // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter RequestContext.getCurrentContext().unset(); } } } 複製代碼

4.1.3 RouteLocator

RouteLocator有三個實現類:SimpleRouteLocator、DiscoveryClientRouteLocator、CompositeRouteLocator。CompositeRouteLocator是一個綜合的路由定位器,會包含當前定義的全部路由定位器。

4.1.4 springcloud提供的filter

pre filter 位置 是否執行 做用
ServletDetectionFilter -3 一直執行 判斷該請求是否過dispatcherServlet,是否從spring mvc轉發過來
Servlet30WrapperFilter -2 一直執行 包裝HttpServletRequest
FormBodyWrapperFilter -1 Content-Type爲application/x-www-form-urlencoded或multipart/form-data request包裝成FormBodyRequestWrapper
DebugFilter 1 配置了zuul.debug.parameter或者請求中包含zuul.debug.parameter 設置debugRouting和debugRequest參數設置爲true,能夠經過開啓此參數,激活debug信息。
PreDecorationFilter 5 上下文不存在forward.to和serviceId兩個參數 從上下文解析出地址,而後取出路由信息,將路由信息放入上下文中。
4.1.4.1 ServletDetectionFilter

判斷該請求是否過dispatcherServlet,是否從spring mvc轉發過來。

@Override
	public Object run() {
		RequestContext ctx = RequestContext.getCurrentContext();
		HttpServletRequest request = ctx.getRequest();
		if (!(request instanceof HttpServletRequestWrapper) 
				&& isDispatcherServletRequest(request)) {
			ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, true);
		} else {
			ctx.set(IS_DISPATCHER_SERVLET_REQUEST_KEY, false);
		}

		return null;
	}
複製代碼
@Bean
	@ConditionalOnMissingBean(name = "zuulServlet")
	public ServletRegistrationBean zuulServlet() {
	//servlet
		ServletRegistrationBean<ZuulServlet> 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; } 複製代碼
4.1.4.2 Servlet30WrapperFilter

關於Servlet30WrapperFilter的存在,存在乎義不是很大,主要是爲了給zuul1.2.2版本容錯,最新版的zuul1.x已經修改,bug緣由是,從zuul獲取的request包裝類,拿到的是HttpServletRequestWrapper,老版本的zuul,是這麼作的:

public class HttpServletRequestWrapper implements HttpServletRequest 
複製代碼

而在tomcat容器中的ApplicationDispatcher類中對request包裝類判斷,會致使直接break。

while (!same) {
            if (originalRequest.equals(dispatchedRequest)) {
                same = true;
            }
            if (!same && dispatchedRequest instanceof ServletRequestWrapper) {
                dispatchedRequest =
                    ((ServletRequestWrapper) dispatchedRequest).getRequest();
            } else {
                break;
            }
        }
複製代碼

參考:github.com/spring-clou…

route filter 位置 是否執行 做用
RibbonRoutingFilter 10 一直執行 判斷該請求是否過dispatcherServlet,是否從spring mvc轉發過來
SimpleHostRoutingFilter 100 上下文包含routeHost 包裝HttpServletRequest
SendForwardFilter 500 上下文中包含forward.to 獲取轉發的地址,作跳轉。
4.1.4.3 RibbonRoutingFilter
// 根據上下文建立commandcommand是hystrix包裹後的實例。
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
    RibbonCommand command = this.ribbonCommandFactory.create(context);
    try {
        ClientHttpResponse response = command.execute();
        return response;
    }catch (HystrixRuntimeException ex) {
        return handleException(info, ex);
    }
}

複製代碼

RibbonCommand根據RibbonCommandFactory來建立,工廠類一共有三個實現類,分別對應三種http調用框架:httpClient、okHttp、restClient。默認選擇HttpClient:

@Configuration
	@ConditionalOnRibbonHttpClient
	protected static class HttpClientRibbonConfiguration {

		@Autowired(required = false)
		private Set<FallbackProvider> zuulFallbackProviders = Collections.emptySet();

		@Bean
		@ConditionalOnMissingBean
		public RibbonCommandFactory<?> ribbonCommandFactory(
				SpringClientFactory clientFactory, ZuulProperties zuulProperties) {
			return new HttpClientRibbonCommandFactory(clientFactory, zuulProperties, zuulFallbackProviders);
		}
	}

複製代碼
4.1.4.4 RibbonCommand

以默認HttpClientRibbonCommand爲例:

public HttpClientRibbonCommand create(final RibbonCommandContext context) {
    //獲取全部ZuulFallbackProvider,即當Zuul調用失敗後的降級方法
    FallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
    //建立轉發的client類,是RibbonLoadBalancingHttpClient類型的。
    final String serviceId = context.getServiceId();
    final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(serviceId, RibbonLoadBalancingHttpClient.class);
    //設置LoadBalancer
    client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));
    // 建立Command,設置hystrix配置的衆多參數。
    return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider, clientFactory.getClientConfig(serviceId));
}
複製代碼

RibbonCommand根據模板的設計模式,抽象類中有默認的實現方式:

@Override
	protected ClientHttpResponse run() throws Exception {
		final RequestContext context = RequestContext.getCurrentContext();

		RQ request = createRequest();
		RS response;
		
		boolean retryableClient = this.client instanceof AbstractLoadBalancingClient
				&& ((AbstractLoadBalancingClient)this.client).isClientRetryable((ContextAwareRequest)request);
		
		if (retryableClient) {
			response = this.client.execute(request, config);
		} else {
			response = this.client.executeWithLoadBalancer(request, config);
		}
		context.set("ribbonResponse", response);

		// Explicitly close the HttpResponse if the Hystrix command timed out to
		// release the underlying HTTP connection held by the response.
		//
		if (this.isResponseTimedOut()) {
			if (response != null) {
				response.close();
			}
		}

		return new RibbonHttpResponse(response);
	}
複製代碼
4.1.4.5 executeWithLoadBalancer

當調用者但願將請求分派給負載均衡器選擇的服務器時,應該使用此方法,而不是在請求的URI中指定服務器。

/**
     * This method should be used when the caller wants to dispatch the request to a server chosen by
     * the load balancer, instead of specifying the server in the request's URI. * It calculates the final URI by calling {@link #reconstructURIWithServer(com.netflix.loadbalancer.Server, java.net.URI)} * and then calls {@link #executeWithLoadBalancer(ClientRequest, com.netflix.client.config.IClientConfig)}. * * @param request request to be dispatched to a server chosen by the load balancer. The URI can be a partial * URI which does not contain the host name or the protocol. */ public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { // 專門用於失敗切換其餘服務端進行重試的 Command LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); try { return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }) .toBlocking() .single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } } 複製代碼
public Observable<T> submit(final ServerOperation<T> operation) {
    // ...
    
    // 外層的 observable 爲了避免同目標的重試
    // selectServer() 是進行負載均衡,返回的是一個 observable,能夠重試,重試時再從新挑選一個目標server
    Observable<T> o = selectServer().concatMap(server -> {
    	// 這裏又開啓一個 observable 主要是爲了同機重試
    	Observable<T> o = Observable
	      .just(server)
	      .concatMap(server -> {
	          return operation.call(server).doOnEach(new Observer<T>() {
	          	 @Override
                 public void onCompleted() {
                 	// server 狀態的統計,譬如消除聯繫異常,抵消activeRequest等
                 }
                 
                 @Override
                 public void onError() {
                 	// server 狀態的統計,錯誤統計等
                 }
                 
                 @Override
                 public void onNext() {
                    // 獲取 entity, 返回內容
                 }
	          });
	    })
	    // 若是設置了同機重試,進行重試
	    if (maxRetrysSame > 0) 
	        // retryPolicy 判斷是否重試,具體分析看下面
	        o = o.retry(retryPolicy(maxRetrysSame, true));
	    return o;
    })
    
    // 設置了異機重試,進行重試
    if (maxRetrysNext > 0) 
        o = o.retry(retryPolicy(maxRetrysNext, false));
    
    return o.onErrorResumeNext(exp -> {
    	return Observable.error(e);
    });
}
複製代碼

關於默認情形下爲何不會重試?參考: blog.didispace.com/spring-clou…

4.1.4.6 ribbon的IRule負載均衡策略

默認選擇ZoneAvoidanceRule策略,該策略剔除不可用區域,判斷出最差的區域,在剩下的區域中,將按照服務器實例數的機率抽樣法選擇,從而判斷斷定一個zone的運行性能是否可用,剔除不可用的zone(的全部server),AvailabilityPredicate用於過濾掉鏈接數過多的Server。

具體的策略參考該博文:ju.outofmemory.cn/entry/25384…

post filter 位置 是否執行 做用
LocationRewriteFilter 900 http響應碼是3xx 對 狀態是 301 ,相應頭中有 Location 的相應進行處理
SendResponseFilter 1000 沒有拋出異常,RequestContext中的throwable屬性爲null(若是不爲null說明已經被error過濾器處理過了,這裏的post過濾器就不須要處理了),而且RequestContext中zuulResponseHeaders、responseDataStream、responseBody三者有同樣不爲null(說明實際請求的響應不爲空)。 將服務的響應數據寫入當前響應
error filter 位置 是否執行 做用
SendErrorFilter 0 上下文throable不爲null 處理上下文有錯誤的filter

4.2 藉助spring-cloud如何擴展?

  1. 若是服務發現不是用eureka,要本身重寫服務發現邏輯,也就是ribbon獲取ServerList,前提是用ribbon。
  2. 若是用ribbon,而且用RibbonCommand,那麼會捆綁Hystrix組件,容錯不可自選。
  3. 若是走rpc協議,須要本身重寫route的全部邏輯。
  4. springcloud的autoconfig,默認開啓了不少配置,須要禁用filter、以及重寫一些bean的建立。

4.3 總結

spring cloud對於zuul的封裝比較完善,同時也表現出較難擴展,尤爲對ribbon、hystrix等組件不夠熟悉的前提下,使用它無非是給本身將來製造難題,相比之下原生的zuul-core相對比較簡單和靈活,可是開發成本較高。

相關文章
相關標籤/搜索