[TOC]java
API 網關能夠看作系統與外界聯通的入口,咱們能夠在網關處理一些非業務邏輯的邏輯,好比權限驗證,監控,緩存,請求路由等等。所以API網關能夠承接兩個方向的入口。git
API網關通常按照職責鏈的模式實現,核心鏈路通常分爲三個部分: 預處理、請求轉發和處理結果。github
職責鏈能夠經過過濾器的方式去實現,過濾器中定義是否須要執行和執行的順序,經過上下文變量透傳給每一個過濾器。web
這一環節,能夠可插拔式的,擴展不少過濾器,例如:spring
將API信息、服務提供方信息查出來,並驗證API的合法性。後端
對API進行鑑權認證,可自定義鑑權方式,例如OAuth二、簽名認證。設計模式
對API的訪問進行控制,調用者是否進入黑名單,調用方是否已受權調用該API。數組
對API進行流量控制,能夠根據調用者、API兩個維度進行流量控制,流量控制相對比較靈活,能夠按照組合方式進行流控。緩存
根據API路由到後端地址的規則,進行參數轉換,構建出須要請求的參數。tomcat
這一環節,能夠根據協議的不通選擇不一樣的轉發方式,rpc、http協議轉發的方式不一樣,這一環節能夠藉助一些框架來實現,rpc可選擇dubbo、http可選擇ribbon,這樣方便解決負載均衡的調用。同時能夠爲調用作資源隔離、保證路由轉發時具有容錯機制,市面上較爲主流的爲hystrix。
這一環節,對於調用須要處理的報文進行處理,記錄下來,用於對調用狀況作分析統計。同時也對一些異常狀況處理,加上默認的響應報文。
zuul是由netflix開源的一個網關,能夠提供動態的路由、監控和安全性保證。 zuul 1.x是基於servlet構建的一個框架,經過一系列filter,完成職責鏈的設計模式。而zuul1.x主要包含了四類過濾器:
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();
}
}
複製代碼
保存請求、響應、狀態信息和數據,以便zuulfilters訪問和共享,能夠經過設置ContextClass來替換RequestContext的擴展。
該類將servlet請求和響應初始化爲RequestContext幷包裝FilterProcessor(filter的處理器)調用,用於處理 reRoute(), route(), postRoute(), and error()。
過濾器的處理器,核心函數是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;
}
複製代碼
/**
* 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;
}
複製代碼
Filter 註冊類,包含一個ConcurrentHashMap, 按照類型保存filter。
用來經過加載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;
}
複製代碼
/**
* 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();
}
複製代碼
StartServer是一個ServletContextListener,負責在web應用啓動後執行一些初始化操做
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);
}
}
}
複製代碼
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(); } } } 複製代碼
RouteLocator有三個實現類:SimpleRouteLocator、DiscoveryClientRouteLocator、CompositeRouteLocator。CompositeRouteLocator是一個綜合的路由定位器,會包含當前定義的全部路由定位器。
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兩個參數 | 從上下文解析出地址,而後取出路由信息,將路由信息放入上下文中。 |
判斷該請求是否過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; } 複製代碼
關於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;
}
}
複製代碼
route filter | 位置 | 是否執行 | 做用 |
---|---|---|---|
RibbonRoutingFilter | 10 | 一直執行 | 判斷該請求是否過dispatcherServlet,是否從spring mvc轉發過來 |
SimpleHostRoutingFilter | 100 | 上下文包含routeHost | 包裝HttpServletRequest |
SendForwardFilter | 500 | 上下文中包含forward.to | 獲取轉發的地址,作跳轉。 |
// 根據上下文建立command,command是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);
}
}
複製代碼
以默認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);
}
複製代碼
當調用者但願將請求分派給負載均衡器選擇的服務器時,應該使用此方法,而不是在請求的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…
默認選擇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 |
spring cloud對於zuul的封裝比較完善,同時也表現出較難擴展,尤爲對ribbon、hystrix等組件不夠熟悉的前提下,使用它無非是給本身將來製造難題,相比之下原生的zuul-core相對比較簡單和靈活,可是開發成本較高。