共计 11899 个字符,预计需要花费 30 分钟才能阅读完成。
浅谈服务网关和联邦云
第一局部:网关和联邦云
第二局部: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 的类里是这么写的
@Bean
public ZuulController zuulController() {
// 这个 Controller 简直没有任何逻辑,只是 handle 申请
// Zuul servlet 会调用咱们定义的 ZuulFilter
return new ZuulController();}
@Bean
public 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;
}
@Override
public String filterType() {
// 这个 Filter 会在申请被路由之前调用
return "pre";
}
@Override
public int filterOrder() {
// 这边定义申请的秩序
// 在实践中,我举荐将所有的 Filter order 在同一个类中集中管理
return 5;
}
@Override
public 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;
}
@Override
public 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…