Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。Zuul 可以适当的对多个 Amazon Auto Scaling Groups 进行路由请求。
其架构如下图所示:
Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行。过滤器之间没有直接的相互通信。他们是通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据。
过滤器是由Groovy写成。这些过滤器文件被放在Zuul Server上的特定目录下面。Zuul会定期轮询这些目录。修改过的过滤器会动态的加载到Zuul Server中以便于request使用。
客户定制:比如我们可以定制一种STATIC类型的过滤器,用来模拟生成返回给客户的response。
过滤器的生命周期如下所示:
就像上图中所描述的一样,Zuul 提供了四种过滤器的 API,分别为前置(Pre)、后置(Post)、路由(Route)和错误(Error)四种处理方式。
一个请求会先按顺序通过所有的前置过滤器,之后在路由过滤器中转发给后端应用,得到响应后又会通过所有的后置过滤器,最后响应给客户端。在整个流程中如果发生了异常则会跳转到错误过滤器中。
一般来说,如果需要在请求到达后端应用前就进行处理的话,会选择前置过滤器,例如鉴权、请求转发、增加请求参数等行为。在请求完成后需要处理的操作放在后置过滤器中完成,例如统计返回值和调用时间、记录日志、增加跨域头等行为。路由过滤器一般只需要选择 Zuul 中内置的即可,错误过滤器一般只需要一个,这样可以在 Gateway 遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果。
Zuul可以通过加载动态过滤机制,从而实现以下各项功能:
验证与安全保障: 识别面向各类资源的验证要求并拒绝那些与要求不符的请求。
审查与监控: 在边缘位置追踪有意义数据及统计结果,从而为我们带来准确的生产状态结论。
动态路由: 以动态方式根据需要将请求路由至不同后端集群处。
压力测试: 逐渐增加指向集群的负载流量,从而计算性能水平。
负载分配: 为每一种负载类型分配对应容量,并弃用超出限定值的请求。
静态响应处理: 在边缘位置直接建立部分响应,从而避免其流入内部集群。
多区域弹性: 跨越AWS区域进行请求路由,旨在实现ELB使用多样化并保证边缘位置与使用者尽可能接近。
除此之外,Netflix公司还利用Zuul的功能通过金丝雀版本实现精确路由与压力测试。
Ribbon客户端默认选择静态类HttpClientRibbonConfiguration创建的HttpClientRibbonCommandFactory工厂类创建,HTTP Client客户端选择HttpClientConfiguration中静态类ApacheHttpClientConfiguration创建Apache相关客户端。
候选类ZuulProxyAutoConfiguration初始化RestClientRibbonConfiguration、OkHttpRibbonConfiguration、HttpClientRibbonConfiguration以及RibbonRoutingFilter、SimpleHostRoutingFilter。
RibbonCommandFactoryConfiguration:设置Ribbon相关属性。
ZuulServerAutoConfiguration:初始化ZuulController、ZuulHandlerMapping、CompositeRouteLocator、SimpleRouteLocator。
Zuul提供了两种形式的网关服务:微服务【服务治理】方式 & http协议方式。
1.http协议方式提供网关功能
public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration {@Autowiredprivate DiscoveryClient discovery;@Bean@ConditionalOnMissingBean(DiscoveryClientRouteLocator.class)public DiscoveryClientRouteLocator discoveryRouteLocator() {String prefix = this.server.getServlet().getServletPrefix()return new DiscoveryClientRouteLocator(prefix, this.discovery, this.zuulProperties,this.serviceRouteMapper, this.registration);}}public class ZuulServerAutoConfiguration {@Autowiredprotected ZuulProperties zuulProperties;@Bean@Primarypublic CompositeRouteLocator primaryRouteLocator(Collection<RouteLocator> routeLocators) {//routeLocators:DiscoveryClientRouteLocatorreturn new CompositeRouteLocator(routeLocators);}
}
1.1.ZuulProperties
ZuulProperties:负责加载zuul相关的路由属性。
zuul.routes.blog.path=/blog/**
zuul.routes.blog.serviceId=http://mp-admin-blog.csdn.net
@ConfigurationProperties("zuul")
public class ZuulProperties {private String prefix = "";// 集合routes中key表示 服务名 或者 域名private Map<String, ZuulRoute> routes = new LinkedHashMap<>();private Set<String> ignoredServices = new LinkedHashSet<>();private Set<String> ignoredPatterns = new LinkedHashSet<>();private Host host = new Host();//设置http连接相关属性,如超时相关属性public static class ZuulRoute {//zuul.routes.blog.path=/blog/** ,其中id 即为blogprivate String id;private String path;private String serviceId;private String url;private boolean stripPrefix = true;private Boolean retryable;private Set<String> sensitiveHeaders = new LinkedHashSet<>();private boolean customSensitiveHeaders = false;}
}
1.2.ZuulHandlerMapping 的初始化
ZuulHandlerMapping通过Order控制其所有接口HandlerMapping实现类中优先级最高。
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {private final RouteLocator routeLocator;private final ZuulController zuul;public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {this.routeLocator = routeLocator;//CompositeRouteLocatorthis.zuul = zuul;setOrder(-200);}
}
截此为止服务启动过程中完成 ZuulHandlerMapping 持有路由相关属性。
1.3.ZuulHandlerMapping执行路由匹配请求
当SpringMVC处理请求时,由于在众多HandlerMapping实现类中ZuulHandlerMapping优先级是最高的,所以任何请求ZuulHandlerMapping优先处理。如果ZuulHandlerMapping通过request uri可以得到目标handler,即ZuulController,则后续请求流程由当前ZuulHandlerMapping触发完成,否则RequestMappingHandlerMapping完成。那ZuulHandlerMapping是如何匹配到ZuulController呢?
public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {private final RouteLocator routeLocator;private final ZuulController zuul;private volatile boolean dirty = true;@Overrideprotected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {return null;}// 集合 IgnoredPaths 是否包含 当前路径 urlPathif (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;RequestContext ctx = RequestContext.getCurrentContext();if (ctx.containsKey("forward.to")) {return null;}if (this.dirty) {synchronized (this) {if (this.dirty) {// 将 ZuulProperties 配置的所有路由path信息 与 ZuulController建立绑定关系, 添加至 HandlerMapping抽象类registerHandlers();this.dirty = false;}}}// SpringMVC 正常相关逻辑return super.lookupHandler(urlPath, request);}private void registerHandlers() {Collection<Route> routes = this.routeLocator.getRoutes();if (routes.isEmpty()) {this.logger.warn("No routes found from RouteLocator");}else {for (Route route : routes) {// 最后将fullPath & ZuulController对应关系添加到抽象类AbstractUrlHandlerMapping属性handlerMap中registerHandler(route.getFullPath(), this.zuul);}}}
}
urlPath【当前请求对应的uri】如果命中集合 IgnoredPaths 中元素,表明当前请求是被ZuulFilter所忽略,返回null即意味着当前请求继续被RequestMappingHandlerMapping处理。
lookupHandler:利用urlPath正则匹配【Ant模式】抽象类AbstractUrlHandlerMapping属性handlerMap中元素,如果匹配通过则返回handler之ZuulController。
属性dirty的重要性:提前建立ZuulRoute 与 ZuulController之间的对应关系。
1.3.1.dirty属性
抽象类AbstractUrlHandlerMapping存在Map类型的属性之handlerMap。属性元素key为fullPath,value为ZuulController。当务之急就是将类ZuulRoute中属性转化为类Route相关属性。
public Route(String id, String path, String location, String prefix,Boolean retryable, Set<String> ignoredHeaders, boolean prefixStripped) {this(id, path, location, prefix, retryable, ignoredHeaders);this.prefixStripped = prefixStripped;
}
- path:请求下游服务真实URI路径。
- location: 优先获取ZuulRoute中URL属性,否则选择path属性。
- prefix:ZuulProperties中prefix属性。
- prefixStripped:ZuulRoute中属性stripPrefix,默认为true。
- fullPath:prefix + path。
- ZuulRoute & ZuulProperties均存在属性stripPrefix,表示是否对path进行截取,最终目的是得到path。
public class CompositeRouteLocator{private final Collection<? extends RouteLocator> routeLocators;@Overridepublic List<Route> getRoutes() {List<Route> route = new ArrayList<>();// 通常情况下 集合routeLocators 中元素只有 DiscoveryClientRouteLocatorfor (RouteLocator locator : routeLocators) {route.addAll(locator.getRoutes());}return route;}
}public class SimpleRouteLocator{@Overridepublic List<Route> getRoutes() {List<Route> values = new ArrayList<>();// ZuulRoute元素其实即为ZuulProperties映射的配置属性for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {ZuulRoute route = entry.getValue();String path = route.getPath();values.add(getRoute(route, path));}return values;}protected Route getRoute(ZuulRoute route, String path) {if (route == null) {return null;}String targetPath = path;//首先判断ZuulProperties中属性 zuul.prefix 是否存在值String prefix = this.properties.getPrefix();if(prefix.endsWith("/")) {//去掉前缀值中存在的后斜杠prefix = prefix.substring(0, prefix.length() - 1);}if (path.startsWith(prefix + "/") && this.properties.isStripPrefix()) {targetPath = path.substring(prefix.length());//将path中prefix截掉}if (route.isStripPrefix()) {// 获取首个字符 * 的索引indexint index = route.getPath().indexOf("*") - 1;if (index > 0) {// 获取path中首个字符 * 之前的全部字符String routePrefix = route.getPath().substring(0, index);//将 routePrefix 字符串 全部替换为 空字符串targetPath = targetPath.replaceFirst(routePrefix, "");// 重新在 routePrefix 之前拼接 prefix。// 如果 prefix = admin, path = admin/like/**, 则此时 prefix 为 admin/admin/likeprefix = prefix + routePrefix;}}Boolean retryable = this.properties.getRetryable();if (route.getRetryable() != null) {retryable = route.getRetryable();}return new Route(route.getId(), targetPath, route.getLocation(), prefix,retryable,route.isCustomSensitiveHeaders() ? route.getSensitiveHeaders() : null, route.isStripPrefix());}
}
通过dirty属性提前在handlerMap建立ZuulRoute 与 ZuulController之间的对应关系,下一步就需要通过请求URL从handlerMap获取对应的ZuulController。
以上完成请求地址的路由匹配。
1.4.ZuulController执行过滤器ZuulFilter
- 通过ZuulServlet触发过滤器流程的开始。毕竟过滤器属于Servlet的范畴。
- 由ZuulRunner引申出FilterProcessor,FilterProcessor控制pre、route、post三类过滤器先后执行顺序。
- FilterProcessor加载出不同类型的全部过滤器,并依次执行全部的过滤器。
- 过滤器真正执行逻辑是由抽象类ZuulFilter定义的。
public class SimpleControllerHandlerAdapter implements HandlerAdapter {@Overridepublic boolean supports(Object handler) {return (handler instanceof Controller);}@Override@Nullablepublic ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler){return ((Controller) handler).handleRequest(request, response);}
ZuulController匹配的adapter为SimpleControllerHandlerAdapter。
public class ZuulController extends ServletWrappingController {@Overridepublic ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {return super.handleRequestInternal(request, response);}
}
public class ServletWrappingController extends AbstractController{private Servlet servletInstance;//ZuulServletprotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response){this.servletInstance.service(request, response);return null;}
}
由ZuulServlet真正触发过滤器的执行流程。
public class ZuulServlet extends HttpServlet {public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);RequestContext context = RequestContext.getCurrentContext();context.setZuulEngineRan();try {preRoute();//前置路由route();// 路由postRoute();// 后置路由} catch (ZuulException e) {postRoute();return;}}
}
public class ZuulServlet extends HttpServlet {private ZuulRunner zuulRunner;public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);RequestContext context = RequestContext.getCurrentContext();context.setZuulEngineRan();try {preRoute();//前置路由route();// 路由postRoute();// 后置路由} catch (ZuulException e) {postRoute();return;}}void postRoute() throws ZuulException {zuulRunner.postRoute();}void route() throws ZuulException {zuulRunner.route();}void preRoute() throws ZuulException {zuulRunner.preRoute();}
}
public class ZuulRunner {public void postRoute() throws ZuulException {FilterProcessor.getInstance().postRoute();}public void route() throws ZuulException {//FilterProcessor#FilterProcessor.getInstance().route();}public void preRoute() throws ZuulException {FilterProcessor.getInstance().preRoute();}
}
public class FilterProcessor {public void postRoute() throws ZuulException {runFilters("post");}public void route() throws ZuulException {runFilters("route");}public void preRoute() throws ZuulException {runFilters("pre");}public Object runFilters(String sType) {// 所有网关涉及的全部过滤器必经路径boolean bResult = false;//获取当前类型【pre 、post、route】的全部过滤器List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);if (list != null) {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);}}}//pre 、post、route返回值没有任何意义return bResult;}public Object processZuulFilter(ZuulFilter filter) throws ZuulException {RequestContext ctx = RequestContext.getCurrentContext();String filterName = "";RequestContext copy = null;Object o = null;Throwable t = null;// 执行全部过滤器的父类过滤器ZuulFilterResult result = filter.runFilter();ExecutionStatus s = result.getStatus();switch (s) {case FAILED:// 处理核心逻辑异常信息t = result.getException();ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);break;case SUCCESS:// 核心方法任何返回值都按成功处理o = result.getResult();ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);break;default:break;}if (t != null) throw t;usageNotifier.notify(filter, s);return o;}
}
前置过滤器包含:ServletDetectionFilter、Servlet30WrapperFilter、FormBodyWrapperFilter、DebugFilter、PreDecorationFilter。
PreDecorationFilter主要是设置一些代理东西,不常用
对于route类型的过滤器存在三种默认的过滤器: RibbonRoutingFilter、SimpleHostRoutingFilter、SendForwardFilter【重定向相关拦截处理】。
后置过滤器:SendResponseFilter。
1.4.1.ZuulFilter
以下是每个类型的过滤器都会执行的必经逻辑:
- 首先执行shoulderFilter。
- 其次执行真正的核心流程run。
核心方法run任何返回值类型都按success处理;只有出现异常则按失败处理,即过滤器直接抛出异常。
public abstract class ZuulFilter{public ZuulFilterResult runFilter() {ZuulFilterResult zr = new ZuulFilterResult();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) {// 只有核心方法run 出现异常,才会终止请求流程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;}
}
1.5.调用下游downstream服务
对于route类型的过滤器,自定义的过滤器的优先级远高于自带三种过滤器。如果需要触发下游服务继续调用,目前只有RibbonRoutingFilter、SimpleHostRoutingFilter两种过滤器支持。其中,RibbonRoutingFilter是通过微服务(服务治理)方式实现,SimpleHostRoutingFilter则是通过http方式调用下游服务。
RibbonRoutingFilter、SimpleHostRoutingFilter、SendResponseFilter过滤器生效的共同条件是:RequestContext之sendZuulResponse属性必须为true。意味着在自定义过滤器内部如果允许下游服务继续访问,必须显式设置sendZuulResponse的属性值。
1.5.1.RibbonRoutingFilter
public class RibbonRoutingFilter extends ZuulFilter {@Overridepublic boolean shouldFilter() {RequestContext ctx = RequestContext.getCurrentContext();return (ctx.getRouteHost() == null && ctx.get("serviceId") != null&& ctx.sendZuulResponse());}
}
1.5.2.SimpleHostRoutingFilter
public class SimpleHostRoutingFilter extends ZuulFilter {@Overridepublic boolean shouldFilter() {return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse();}public Object run() {RequestContext context = RequestContext.getCurrentContext();...String uri = this.helper.buildZuulRequestURI(request);this.helper.addIgnoredHeaders();// 通过http方式调用下游服务CloseableHttpResponse response = forward(this.httpClient, verb, uri, request,headers, params, requestEntity);setResponse(response);return null;}private void setResponse(HttpResponse response) throws IOException {// 将下游服务的响应封装在 上下文 属性zuulResponse中RequestContext.getCurrentContext().set("zuulResponse", response);// 将下游服务的响应头、响应实体等信息 单独添加至 上下文中this.helper.setResponse(response.getStatusLine().getStatusCode(),response.getEntity() == null ? null : response.getEntity().getContent(),revertHeaders(response.getAllHeaders()));}
}
触发下游服务 & 将下游服务响应的相关属性跟上下文RequestContext绑定。
1.5.3.SendResponseFilter
通过以下得知,该过滤器整合下游服务响应内容的前提是RibbonRoutingFilter or SimpleHostRoutingFilter 至少有一个生效。
public class SendResponseFilter extends ZuulFilter {@Overridepublic boolean shouldFilter() {RequestContext context = RequestContext.getCurrentContext();return context.getThrowable() == null&& (!context.getZuulResponseHeaders().isEmpty()// 下游服务的响应头必须被添加至上下文ZuulResponseHeaders属性中// 此处是指下游服务的响应不能为空|| context.getResponseDataStream() != null|| context.getResponseBody() != null);}@Overridepublic Object run() {addResponseHeaders();writeResponse();return null;}private void writeResponse() throws Exception {RequestContext context = RequestContext.getCurrentContext();if (context.getResponseBody() == null && context.getResponseDataStream() == null) {return;}HttpServletResponse servletResponse = context.getResponse();if (servletResponse.getCharacterEncoding() == null) { // only set if not setservletResponse.setCharacterEncoding("UTF-8");}//此处是指在自定义过滤器中添加的响应信息OutputStream outStream = servletResponse.getOutputStream();InputStream is = null;if (context.getResponseBody() != null) {// 如果在自定义过滤器中添加了ResponseBody,则下游服务的响应不会被添加到最终响应值中String body = context.getResponseBody();is = new ByteArrayInputStream(body.getBytes(servletResponse.getCharacterEncoding()));}else {is = context.getResponseDataStream();// 获取下游服务的响应值if (is!=null && context.getResponseGZipped()) {// 下游服务是否需要压缩if (isGzipRequested(context)) {servletResponse.setHeader(ZuulHeaders.CONTENT_ENCODING, "gzip");}else {is = handleGzipStream(is);}}}if (is!=null) {// 将下游服务的响应is写到 outStream中,即为最终响应内容writeResponse(is, outStream);}...}
}
自定义过滤器内部如果存在通过字符流Writer写入数据,则在SendResponseFilter内部抛出异常:getWriter() has already been called for this response,但是不会打印出堆栈信息,该异常非常隐蔽。自定义过滤器内部可以选择字节流方式写入数据。
记 SpringBoot 拦截器报错 getWriter() has already been called for this response