转载

API网关与zuul1.x与springcloud的三角关系

API 网关可以看做系统与外界联通的入口,我们可以在网关处理一些非业务逻辑的逻辑,比如权限验证,监控,缓存,请求路由等等。因此API网关可以承接两个方向的入口。

  1. 移动APP/WEB的统一入口网关。
  2. 业务方快速提开放能力。

2. 如何实现一个网关

2.1 网关核心

API网关一般按照职责链的模式实现,核心链路一般分为三个部分: 预处理、请求转发和处理结果。

职责链可以通过过滤器的方式去实现,过滤器中定义是否需要执行和执行的顺序,通过上下文变量透传给每个过滤器。

2.1.1 预处理

这一环节,可以可插拔式的,扩展很多过滤器,例如:

2.1.1.1 初始化API

将API信息、服务提供方信息查出来,并验证API的合法性。

2.1.1.2 API鉴权

对API进行鉴权认证,可自定义鉴权方式,例如OAuth2、签名认证。

2.1.1.3 访问控制

对API的访问进行控制,调用者是否进入黑名单,调用方是否已授权调用该API。

2.1.1.4 限流控制

对API进行流量控制,可以根据调用者、API两个维度进行流量控制,流量控制相对比较灵活,可以按照组合方式进行流控。

2.1.1.5 参数转换

根据API路由到后端地址的规则,进行参数转换,构建出需要请求的参数。

2.1.2 请求转发

这一环节,可以根据协议的不通选择不同的转发方式,rpc、http协议转发的方式不同,这一环节可以借助一些框架来实现,rpc可选择dubbo、http可选择ribbon,这样方便解决负载均衡的调用。同时可以为调用做资源隔离、保证路由转发时具备容错机制,市面上较为主流的为hystrix。

2.1.3 处理结果

这一环节,对于调用需要处理的报文进行处理,记录下来,用于对调用情况做分析统计。同时也对一些异常情况处理,加上默认的响应报文。

API网关与zuul1.x与springcloud的三角关系

2.2 网关设计图

API网关与zuul1.x与springcloud的三角关系

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

API网关与zuul1.x与springcloud的三角关系

3.2 zuul实现

API网关与zuul1.x与springcloud的三角关系

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

API网关与zuul1.x与springcloud的三角关系

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

// 根据上下文创建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);
		}
	}

复制代码

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);
    });
}
复制代码
API网关与zuul1.x与springcloud的三角关系

关于默认情形下为什么不会重试?参考: 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相对比较简单和灵活,但是开发成本较高。

原文  https://juejin.im/post/5cf60771f265da1bcc192dd0
正文到此结束
Loading...