API 网关可以看做系统与外界联通的入口,我们可以在网关处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。因此API网关可以承接两个方向的入口。
API网关一般按照职责链的模式实现,核心链路一般分为三个部分: 预处理、请求转发和处理结果。
职责链可以通过过滤器的方式去实现,过滤器中定义是否需要执行和执行的顺序,通过上下文变量透传给每个过滤器。
这一环节,可以可插拔式的,扩展很多过滤器,例如:
将API信息、服务提供方信息查出来,并验证API的合法性。
对API进行鉴权认证,可自定义鉴权方式,例如OAuth2、签名认证。
对API的访问进行控制,调用者是否进入黑名单,调用方是否已授权调用该API。
对API进行流量控制,可以根据调用者、API两个维度进行流量控制,流量控制相对比较灵活,可以按照组合方式进行流控。
根据API路由到后端地址的规则,进行参数转换,构建出需要请求的参数。
这一环节,可以根据协议的不通选择不同的转发方式,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; } } 复制代码
参考: github.com/spring-clou…
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相对比较简单和灵活,但是开发成本较高。