[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相对比较简单和灵活,可是开发成本较高。