浅谈服务网关和联邦云
第一局部:网关和联邦云
第二局部:Zuul简介
源码剖析 - Servlet 集成 Zuul
用例展现 - 用 Servlet 模式集成 Zuul Filter
源码剖析 - 在Spring MVC中集成Zuul
用例展现 - 用 Spring Dispatcher 集成 Zuul Filter
两种用法的比照

实战 - 编写一个用户认证 Zuul Filter
写在最初
援用

笔者最近参加了星环数据云平台的联邦云性能(以下简称联邦云)的设计和开发。联邦云旨在为用户提供一站式的,跨集群、跨租户的计算资源管理。它在网络,认证,API多个维度买通了租户和集群之间的隔膜,并提供统一的用户体验。

因为联邦云这种对租户资源的整合很容易让人联想到网关,所以笔者对网关进行了一些调研。如下图所示,对一种技术的调研能够先进行联想和发散,分明每种调研对象的能力和大抵的用法,就像这张脑图里展现的。

分明了每种软件的能力当前,就能够对调研对象进行排除和收敛。思考到公司外部web服务生态以java为主,并且联邦云自身有比拟强的业务属性,像Nginx之类的网关应该无奈满足需要,所以调研的次要对象还是集中在Java的网关。在网上收集了一些对于Zuul, Zuul 2 以及 Spring cloud gateway的材料。首先它们都是优良的网管框架,Zuul已经是 Spring cloud 中的组件,是基于阻塞的多线程Web服务器实现的。而Zuul 2是基于netty进行实现。Spring cloud gateway 也是一个异步的计划,和 Spring Webflux高度集成,它是 Spring cloud 用于代替 Zuul 的计划。在抉择Java系服务网关时可能就须要思考到这些因素。

扩展性是否能满足业务需要
你的web框架是同步的还是异步的
是否须要思考到和Spring的集成水平
是否须要思考高并发,工作负载时CPU密集还是IO密集
联合以上因素和现有Web框架的个性,笔者抉择Zuul 作为试行的计划,并对其进行了浅显的学习。因为Zuul的文档不多,所以有些配置还是须要看一下源码能力晓得怎么配置,也就有了这篇文章。

第一局部:网关和联邦云
比起微服务网关,联邦云的场景更加简单,然而两者又有千头万绪的分割。例如在Zuul中,申请路由的外围规定是url的模式匹配。通过pattern match,为申请定位到上游服务,不论是基于Servlet,还是基于Spring Dispatcher都是如此。而在联邦云的场景中,咱们关怀的是集群,租户,租户中的资源,甚至是租户的版本,这一类贴近业务的实体,所以申请路由变得不再聚焦于url,而是具体的资源。

尽管无奈间接满足需要,然而 Zuul 提供了一个十分精简,扩展性极强的内核。这使它成为了在联邦云中进行认证注入,租户定位,申请转发等工作的实现框架。在一个联邦云中,最重要的是资源聚合机制和针对联邦租户专门设计的面向特定租户内资源的路由机制。而Zuul更像是作为一个可插拔的Http申请解决工具。

第二局部:Zuul简介
Zuul 是一个基于同步多线程模式(Servlet)的微服务网关,其核心思想是基于 Filter 模式来实现对HTTP申请的装璜和解决。 因为Zuul提供了通用的编程接口,它的灵活性极强。比起Nginx这样须要借助脚本来实现性能扩大的网关,Zuul能够反对作为一个SDK嵌入在Java Web服务中,所以能够很轻松地实现路由,负载平衡,熔断限流,认证鉴权等性能。除此之外,和企业外部的其余服务,中间件,甚至容器平台的对接都成为可能。

从这张图能够看出,基于申请的生命周期,Filter被分为5类,其中,咱们比拟罕用的可能就是 pre 类型的 Filter。一个常见的场景就是,基于申请的门路,以及服务发现能力,为申请设置对应的 host,这样一来,Zuul 内置的 SimpleHostRoutingFilter 就会把申请发送到正确的地位。

依据官网文档所说,Zuul反对在 Servlet 和 Spring Dispatcher 两种模式下工作。两种模式各有特点,配置的办法也略有不同。

Zuul is implemented as a Servlet. For the general cases, Zuul is embedded into the
Spring Dispatch mechanism. This lets Spring MVC be in control of the routing.
In this case, Zuul buffers requests.
If there is a need to go through Zuul without buffering requests (for example, for large file uploads),
the Servlet is also installed outside of the Spring Dispatcher.
By default, the servlet has an address of /zuul. This path can be changed
with the zuul.servlet-path property.

来自官网文档
本文会对 Servlet 和 Spring MVC Dispatcher 两种模式进行剖析,并简略介绍它在联邦云中表演的角色。

源码剖析 - Servlet 集成 Zuul
通过Servlet 继承的zuul就像这张图里展现的:

Zuul 和 Spring MVC 分属两个不同的 Servlet

Tomcat提供了ServletRequestWrapper类供第三方开发者继承,以实现为申请提供Servlet封装的成果。其中 ServletRequestWrapper 提供了对 ServletContext 和 Request 的双重感知。而 HttpServeletRequest 则是提供了额定的HTTP相干的封装。

其中 , HttpServletRequest 接口中提供的 getServletPath 定义了URL中用于调用servlet的局部, getHttpServletMapping()办法则会定义如何解决这个申请。 ServletRequestWrapper 因而也提供了上面两个办法。

/**

  • Servlet Path是URI路中的一部分。它以 / 结尾,并指向某个Servlet的名字
  • <p>
  • 如果指标Servlet应用了通配符 /*, 这个办法该当返回空字符串
    */

public String getServletPath();

/**
HttpServletMapping 也是提供了非常灵活的Servlet匹配策略

<servlet>   <servlet-name>MyServlet</servlet-name>   <servlet-class>MyServlet</servlet-class>

</servlet>
<servlet-mapping>

   <servlet-name>MyServlet</servlet-name>   <url-pattern>/MyServlet</url-pattern>   <url-pattern>""</url-pattern>   <url-pattern>*.extension</url-pattern>   <url-pattern>/path/*</url-pattern>

</servlet-mapping>
例如有这样的 Servlet 申明,那么当有如下申请进来时,匹配状况各不相同
如下图

Zuul提供的 zuul.servlet-path,那么这个配置项是如何在一个 Spring 利用中失效的呢?首先,这个配置项会对应到 ZuulProperty 这个属性类中的 servletPath 字段。在 Spring 的配置类中,会有创立一个 ServletRegistrationBean, 在实例化这个 Bean 时会调用 getServletPattern() 这个办法。如下

@Bean
@ConditionalOnMissingBean(name = "zuulServlet")
@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
public ServletRegistrationBean zuulServlet() {

// 这里初始化了ZuulServlet,并且将它注册到配置好的pattern上
// 后续匹配这个pattern的申请将会间接由ZuulServlet解决
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;
}
其中,getServletPattern() 办法的实现如下

public String getServletPattern() {    // 在这里调用了servletPath的属性    String path = this.servletPath;if (!path.startsWith("/")) {    path = "/" + path;}if (!path.contains("*")) {    path = path.endsWith("/") ? (path + "*") : (path + "/*");}return path;}

这个 ServletRegistrationBean 提供了如下的性能

向ServletContext中注册Servlet
为Servlet增加Uri的映射 在这个Bean的帮忙下,咱们就不再须要拜访上层的Servlet框架,而只须要加上 @EnableZuulProxy 的注解,而后让 Spring 主动帮咱们进行配置。
注册Servlet的外围流程如下

private ServletRegistration.Dynamic addServlet(String servletName, String servletClass,

    Servlet servlet, Map<String,String> initParams) throws IllegalStateException {...Wrapper wrapper = (Wrapper) context.findChild(servletName);// Context中的Child个别都是Wrapper,wrapper是对Servlet对象的一层包装。if (wrapper == null) {    wrapper = context.createWrapper();    wrapper.setName(servletName);    context.addChild(wrapper);} else {    ...}ServletSecurity annotation = null;if (servlet == null) {    wrapper.setServletClass(servletClass);    Class<?> clazz = Introspection.loadClass(context, servletClass);    if (clazz != null) {        annotation = clazz.getAnnotation(ServletSecurity.class);    }} else {    // 把 Servlet 实例设置到 wrapper 中,以供后续调用    wrapper.setServletClass(servlet.getClass().getName());    wrapper.setServlet(servlet);    if (context.wasCreatedDynamicServlet(servlet)) {        annotation = servlet.getClass().getAnnotation(ServletSecurity.class);    }}...return registration;

}

这个 Wrapper 会在 StandardContextValve 类中被应用,也就是。 Valve 是相似与 Filter 的层层嵌套的调用链。区别就是, Valve 是 container级别,也就是在所有servlet里面,而 FilterChain 则是对应具体的servlet。

具体的流程大略就是tomcat解决一个申请的时候会获取申请的门路,而后去先前注册的 Servlet 中去进行匹配。每次匹配到,就将对应的 Servlet 塞到 Request 的上下文中。在 Request 实现后,会调用 recycle() 对其进行清理。

@Override
public final void invoke(Request request, Response response)

throws IOException, ServletException {...Wrapper wrapper = request.getWrapper();if (wrapper == null || wrapper.isUnavailable()) {    ...}// Acknowledge the request...// 在这里会把申请发送到Request中对应的wrapper, 也就是代理给匹配的Servlet// 来进行解决wrapper.getPipeline().getFirst().invoke(request, response);

}
用例展现 - 用 Servlet 模式集成 Zuul Filter
有了Filter当前,咱们心愿将它集成到咱们的Servlet服务器中。通过下面大节的源码剖析,咱们晓得只须要做如下的配置,Spring框架就能够帮忙咱们将ZuulServlet注册到服务器中。

zuul.servletPath: /zuul
从下面的源码逻辑能够看出,这个配置最终会被翻译成

/zuul/* -> ZuulServlet
这样的映射关系。所以这样一来,咱们间接拜访对应的资源地址就能够了,比方/zuul/xxx/xxx

因为servlet会被Spring框架主动注册,所以无需任何额定的路由定义工作,十分简洁。然而有一个毛病,就是servlet path只能配置一次,不足灵活性。

源码剖析 - 在Spring MVC中集成Zuul
如果抉择应用DispatcherServlet 集成 zuul, 那么咱们的软件架构就变成了上面的样子。

在这种状况下,么咱们能够跳过进入 Servlet 前的所有步骤。对于这些步骤如何工作,能够参考Spring如何集成Servlet容器,以及Servlet的工作流程。Spring MVC的外围之一是 DispatcherServlet ,它反对将申请映射到被注册的 HandlerMapping 中。咱们平时应用的@RequestMapping 注解实际上就是帮忙咱们申明式地实现这些注册。

Spring cloud zuul 也实现了这个 HandlerMapping

public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {

// 这里设置了Zuul本人的路由表
// 用户能够定义这个RouteLocator的实现,并生成Bean
// Auto config会主动加载这些Bean
this.routeLocator = routeLocator;
// 这里设置Zuul Controll, 它实际上只是给
// Zuul Servlet包了一层皮,从而让Spring把申请Dispatch到Zuul的Servlet中
this.zuul = zuul;
setOrder(-200);
}
AutoConfig的类里是这么写的

@Beanpublic ZuulController zuulController() {// 这个Controller简直没有任何逻辑,只是handle申请// Zuul servlet会调用咱们定义的ZuulFilterreturn new ZuulController();}@Beanpublic ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {// 这边Autowire了route locator,也就是一个composite route locator// 意思就是它能够把多个Route Locator的Bean合成一个ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());mapping.setErrorController(this.errorController);mapping.setCorsConfigurations(getCorsConfigurations());return mapping;}

用例展现 - 用 Spring Dispatcher 集成 Zuul Filter
首先,咱们本人曾经定义了一些 ZuulFilter,因为 Zuul 反对spring cloud 全家桶,咱们只须要写一些 Bean 就能够了。

@Bean
public ZuulFilter actorAuthenticationFilter() {

return new ActorAuthenticationFilterFactory(ActorLocator.class).apply(actorLocator(), 1);

}
因为在Spring Dispatcher模式下,咱们没有间接配置pattern,所以咱们对那些须要利用 zuul filter 的申请门路进行路由规定的定义。同样的,只须要写一个 RouteLocator 类型的Bean.

@Bean
RouteLocator zuulDispatchingRouteLocator() {

return new RouteLocator() {    // 所有以fed结尾的申请会被路由到Zuul的Handler    // 这里无需写死指标地址,因为咱们会通过服务发现机制,在Filter中动静为Context中注入这些地址    private final Route fedRoute = new Route(            "fed", ProxyConsts.FEDERATION_EP_PATTERN, "no://op", "", false, new HashSet<>()    );    @Override    public Collection<String> getIgnoredPaths() {        return Collections.EMPTY_LIST;    }    @Override    public List<Route> getRoutes() {        // 框架会调用这个办法获取路由,并注册Handler        return Collections.singletonList(fedRoute);    }    @Override    public Route getMatchingRoute(String path) {        if (path.startsWith(ProxyConsts.FEDERATION_EP_PREFIX)) {            return fedRoute;        }        return null;    }};

}
这样一来,咱们就实现了 Spring Web MVC 和 Zuul 的集成。只须要拜访/fed上面的资源,即可将申请代理给咱们定义的Zuul Filter,例如

/fed/api/v1/tenants
两种用法的比照
最初,咱们能够比照一下 spring cloud zuul 两种用法的异同。次要看一下解决web申请时候的调用栈.

// Should filter 就是咱们实现的办法了,走到这一步
// 阐明曾经胜利进入Zuul Filter Chain
shouldFilter:16, TCCFederationPreFilter (io.transwarp.tcc.federation.filters)
runFilter:114, ZuulFilter (com.netflix.zuul)
processZuulFilter:193, FilterProcessor (com.netflix.zuul)
runFilters:157, FilterProcessor (com.netflix.zuul)
preRoute:133, FilterProcessor (com.netflix.zuul)
preRoute:105, ZuulRunner (com.netflix.zuul)
preRoute:125, ZuulServlet (com.netflix.zuul.http)
service:74, ZuulServlet (com.netflix.zuul.http)
internalDoFilter:231, ApplicationFilterChain x 8
...
Valve
...

lookupHandler:86, ZuulHandlerMapping (org.springframework.cloud.netflix.zuul.web)
getHandlerInternal:124, AbstractUrlHandlerMapping (org.springframework.web.servlet.handler)
getHandler:405, AbstractHandlerMapping (org.springframework.web.servlet.handler)
getHandler:1233, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1016, DispatcherServlet (org.springframework.web.servlet)
doService:943, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:626, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:733, HttpServlet (javax.servlet.http)
internalDoFilter:231, ApplicationFilterChain x 8
...
Valve
...
能够看到,后者的确是多了一层Spring 框架中的DispatcherServlet.

两种模式的另一个不同点官网文档中也阐明了,在复用 Spring Dispatcher 时,Zuul 会存在对申请的缓冲行为,这个时候不适用于体积十分大的申请,比方大文件的上传。所以在申请大小比拟小的状况下,能够不用动用 zuul 的 Servlet 模式。

实战 - 编写一个用户认证 Zuul Filter
以下是一个模仿在理论开发中对申请进行过滤,认证,转发的逻辑。

public class MyFilter extends ZuulFilter {

private final RouteService rs;public MyFilter(RouteService rs) {    // 初始化    // 这里的RouteService继承了服务发现,路由转发和认证性能    this.rs = rs;}@Overridepublic String filterType() {    // 这个Filter会在申请被路由之前调用    return "pre";}@Overridepublic int filterOrder() {    // 这边定义申请的秩序    // 在实践中,我举荐将所有的Filter order在同一个类中集中管理    return 5;}@Overridepublic boolean shouldFilter() {    // 因为是多线程同步模式,一旦这个线程开始解决申请,    // 这个申请都能通过Context间接获取,不必通过参数进行传递    // 这里的Context应用Thread Local实现    HttpServletRequest request = RequestContext            .getCurrentContext().getRequest();    // 能够通过uri进行判断是否过滤该申请    boolean shouldFilter = request.getRequestURI().startsWith("/tdc-fed");    // 当然也能够通过Attribute等灵便的形式进行判断    shouldFilter = request.getAttribute("X-TDC-Fed-Remote").equals(true);    return shouldFilter;}@Overridepublic Object run() throws ZuulException {    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();    // 为这个申请获取token    String token = rs.getToken(request);    if (token == null) {        throw new ZuulException("Unauthorized", 401, "Failed to get token");    }    // 咱们不必去间接批改申请,只须要往Context中设置申请头等参数    // Zuul 框架会在路由前将Context中的变量笼罩到申请中,十分不便    RequestContext.getCurrentContext().addZuulRequestHeader(            "Authorization", "Bearer " + token    );    // 这里间接将指标服务的URL设置到Context中    // 这里的locateService能够集成各种不同的服务发现机制    RequestContext.getCurrentContext().setRouteHost(rs.locateService(request));    // 更改申请的门路    // 这边间接通过继承Request并设置到Context中就能实现    RequestContext.getCurrentContext().setRequest(new HttpServletRequestWrapper(request) {        @Override        public String getRequestURI() {            return rs.targetUri(request);        }    });    return null;}

}
通过如上的 ZuulFilter 实现,咱们能够实现一个申请的身份的认证。然而,在网关的实际中,也可能暗藏一些坑,导致服务呈现奇怪的行为。以联邦云为例,在联邦云中,每一个成员租户都是一套残缺的,包含用户权限认证的服务,在引入网关认证的状况下,很容易引起认证的抵触。如下图所示,服务1和服务2地session会通过响应中地 set-cookie 头,把网关本人的sessionId笼罩掉,导致通过网关认证的用户呈现拜访异样。

如果上游服务同时具备认证的性能,那么网关无奈实现在服务之间流畅地切换,因为cookie会被频繁重置。Zuul 作为成熟地服务网关,当然也思考到了这类状况。咱们通过配置,能够让Zuul疏忽一些敏感性地HTTP头,如下所示

zuul.ignoredHeaders:

  • set-cookie

这样,图中所示地这套简略的架构就能依照咱们的想法进行工作了。

写在最初
随着异步Web框架的风行,可能很少人再去关注Zuul这类软件了。就连基于ThreadLocal实现的RequestContext 这种设计,也被人诟病为 “为了补救之前蹩脚的设计而做出的斗争”,这里所说的 “蹩脚的设计” 当然就是同步多线程的Web编程模式。然而其实Zuul仍然是一个足够简略,足够牢靠,并且容易保护的微服务网关。基于Filter的编程模式也使得代码能够写得比拟通用,有利于升高移植的老本。

而联邦云作为新生的软件,应该思考到 Web 生态一直迭代的事实,既要正当地应用现成的软件框架来满足需要,也要适当地和它们划清界限,从而在将来技术栈和业务需要的迭代中能够更加敏捷地进行降级。

前段时间看了Complexity is killing software developers,这篇文章所援用的咱们作为“技术的消费者”的角色,以及“有机械共鸣的车手”的比喻的阐述,的确是很容易引起开发者的共鸣。在这个时代,从事微服务开发的开发者们就像是“糖果屋中地孩子”,不论是CNCF社区丰盛的云原生我的项目,还是Spring Cloud全家桶集成的各种威力弱小的微服务SDK,都为咱们疾速构建微服务提供了微小的帮忙,同时也引入了微小的复杂度。愿咱们这些“在糖果屋中地孩子”都能感性地生产技术,让技术给咱们带来价值和乐趣,成为“有机械共鸣”地车手。

援用
https://www.infoworld.com/art...