版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xc1158840657/article/details/90712084
微服务系列(一)聊聊服务网关
前几年随着分布式架构的演变,微服务开始兴起,自然也产生了一系列支持微服务的框架,例如本文要聊到的Spring Cloud。
Spring 相信做Java的小伙伴们已经耳熟能详了,也正是应该这个Spring生态获得广大的关注,在Spring之上开发的新兴框架如Spring Boot、Spring Cloud也很快让大家熟知。
下面主要针对Spring Cloud聊聊它所实现的服务网关,并针对市面上常用的Nginx做一些分析和比较。
由于笔者比较熟悉和擅长的语言是Java,所以仅针对Java框架来做一些源码层面的分析。
服务网关的角色
常见微服务架构
可以看到,网关层是最外层,浏览器与服务器交互时经过的第一个服务节点,它主要起屏蔽下游业务服务的作用,对于浏览器而言,只需要跟网关交互就相当于在与下游多个业务服务节点交互,让浏览器觉得他在和一台服务器交互。
这样的好处显而易见,不管是下游业务服务、支撑服务、基础服务,都对于浏览器屏蔽,与服务器的交互变的非常简单,浏览器无需关心各个节点的依赖关系、如何协同工作,浏览器只会了解到本次请求是否成功;开发者可以灵活的增加业务服务模块;可以在网关层做一些最上层的公用的操作,如过滤恶意请求、设置ip黑白名单、做身份认证、限流、负载均衡等。
换个角度考虑一下,如果去掉网关层,浏览器交互的最外层服务是业务服务层,由于需要解决单点登陆问题,必须在每个业务服务节点上多扮演一个auth client的角色,从开发的角度上看,明显增加了复杂度,试问本可以只需要在一个网关服务上构建auth client,为何要选择在多个(并且可能还会增加)的业务服务上构建auth client呢?
另外,从开发上讲可能还需要解决跨域请求的问题,前后端分离架构中后端api的展示也会是一个问题,也不便于管理;对于同一服务多节点的负载均衡也不好实现,难道需要浏览器每次访问前都去访问一次注册中心?
通过分析发现,微服务架构中,对于再小的业务量的项目,服务网关都是必不可少的。
Spring Cloud Netflix Zuul和Spring Cloud Gateway
Zuul在早期微服务架构中用的非常广泛,如今Spring Cloud推出了Spring Cloud Gateway,那么作为开发者,应该考虑以下问题:该如何选择?他们之间的差异有哪些?各有什么优势呢?
下面从源码入手,探索Zuul的工作原理,尝试理解他的设计理念。
为了图方便,就不去github上download源码了,直接在pom引入依赖,开干…进入com.netflix.zuul.http.ZuulServlet
-
public class ZuulServlet extends HttpServlet {
-
private ZuulRunner zuulRunner;
-
public void init(ServletConfig config) throws ServletException {
-
...
-
}
-
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
-
...
-
}
-
}
它继承了HttpServlet,熟悉吗?不熟悉的话,可以打开你熟悉的DispatchServlet,看看它继承了谁?
也就是说,它本质上用了java.servlet API,实现了一个有网关功能的servlet。
那么继续观察一下它的com.netflix.zuul.http.ZuulServlet#service方法:
---------------------
版权声明:本文为CSDN博主「XCXCXCXCX__」的原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/xc1158840657/article/details/90712084
-
try {
-
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();
-
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();
-
}
可以看到,它做了以下几件事:
- 前置路由
- 路由
- 后置路由
- 异常处理
不管是preRoute()
、route()
、postRoute()
、error()
,它们最终调用了com.netflix.zuul.FilterProcessor#runFilters
-
public Object runFilters(String sType) throws Throwable {
-
if (RequestContext.getCurrentContext().debugRouting()) {
-
Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
-
}
-
boolean bResult = false;
-
List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
-
if (list != null) {
-
for (int i = 0; i < list.size(); i++) {
-
//ZuulFilter list
-
//轮流执行ZuulFilter的逻辑,result=false或执行完所有ZuulFilter时调用链结束
-
ZuulFilter zuulFilter = list.get(i);
-
Object result = processZuulFilter(zuulFilter);
-
if (result != null && result instanceof Boolean) {
-
bResult |= ((Boolean) result);
-
}
-
}
-
}
-
return bResult;
-
}
这里FilterLoader的源码就不深入分析了,它主要的功能是:
从FilterRegistry(相当于内存中的filter)加载Zuulfilter list
编译groovy文件(笔者的Zuul版本1.3.0,目前仅支持groovy文件)并加载Zuulfilter
对于FilterRegistry,则是用于内存中保存filter,可以动态变化的,注册新的filter以及移除filter等,可提供给jmx、endpoint做远程控制。
继续看看com.netflix.zuul.FilterProcessor#processZuulFilter
-
public Object processZuulFilter(ZuulFilter filter) throws ZuulException {
-
RequestContext ctx = RequestContext.getCurrentContext();
-
boolean bDebug = ctx.debugRouting();
-
final String metricPrefix = "zuul.filter-";
-
long execTime = 0;
-
String filterName = "";
-
try {
-
long ltime = System.currentTimeMillis();
-
filterName = filter.getClass().getSimpleName();
-
RequestContext copy = null;
-
Object o = null;
-
Throwable t = null;
-
if (bDebug) {
-
Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);
-
copy = ctx.copy();
-
}
-
//执行ZuulFilter的runFilter逻辑
-
ZuulFilterResult result = filter.runFilter();
-
ExecutionStatus s = result.getStatus();
-
//执行耗时统计(可以发现Zuul还没有完善这个功能,只是形成了框架)
-
execTime = System.currentTimeMillis() - ltime;
-
//处理执行结果,无论成功与否,都记录了debug日志
-
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);
-
if (bDebug) {
-
Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");
-
Debug.compareContextState(filterName, copy);
-
}
-
break;
-
default:
-
break;
-
}
-
if (t != null) throw t;
-
//目前作为空壳存在,可见是为了方便扩展
-
usageNotifier.notify(filter, s);
-
return o;
-
} catch (Throwable e) {
-
if (bDebug) {
-
Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());
-
}
-
usageNotifier.notify(filter, ExecutionStatus.FAILED);
-
if (e instanceof ZuulException) {
-
throw (ZuulException) e;
-
} else {
-
ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);
-
ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);
-
throw ex;
-
}
-
}
-
}
这样一来,Zuul基本原理走完了,可以看出来非常的简单,实际上整个调用链是由ZuulFilter来组成,对于用户而言,只需要关心如果构建自定义的ZuulFilter以及它们之间的顺序。
Zuul提供了com.netflix.zuul.filters.StaticResponseFilter和com.netflix.zuul.filters.SurgicalDebugFilter两种抽象类,StaticResponseFilter会将请求直接处理并返回,即不会经过路由链路;SurgicalDebugFilter则会将请求路由到zuul.debug.vip 或 zuul.debug.host所指定的debug Eureka “VIP” or host。
另外,还有一个类也需要关注,com.netflix.zuul.filters.ZuulServletFilter,通过源码方向追踪后发现usages均是Test类,可见它应该是一个待开发的功能,去允许用户在路由前做过滤处理。
看完了源码,了解了工作原理后,整理一下Zuul的特点:
很明显,由于底层是servlet,Zuul处理的是http请求
Zuul的抽象写的非常简单易懂,易于扩展,易于debug
提供了两种特殊的抽象类,用户使用起来,比较灵活
zuul-core包不依赖Spring,依赖的包很少
没有提供异步支持
流控等均由hystrix支持
那么继续开始分析Spring Cloud Gateway源码…
Spring Cloud Gateway的代码相比zuul会比较难懂,特别是对于不熟悉流式编程的小伙伴来说。
我可以给个建议,如果实在是看不懂、从头到尾很懵的话,请结合spring mvc的源码对照来理解!
先来看看这个类org.springframework.cloud.gateway.config.GatewayAutoConfiguration
-
@Configuration
-
@ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)
-
@EnableConfigurationProperties
-
@AutoConfigureBefore(HttpHandlerAutoConfiguration.class)
-
@AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})
-
@ConditionalOnClass(DispatcherHandler.class)
-
public class GatewayAutoConfiguration {
-
...
-
//由于内容比较多,就不全部贴出来了
-
@Bean
-
@ConditionalOnBean(DispatcherHandler.class)
-
public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) {
-
return new ForwardRoutingFilter(dispatcherHandler);
-
}
-
...
-
}
继续点进去org.springframework.web.reactive.DispatcherHandler,先不用看它的源码,reactive???它是webflux的核心组件!!!
由于webflux的基本原理和webmvc大同小异,就不仔细分析其源码了,但需要了解的一点是,webflux大量运用流式编程,代码非常简短,也很契合的支持请求异步处理。
那么继续追踪org.springframework.cloud.gateway.filter.ForwardRoutingFilter,找到一个重要的接口org.springframework.cloud.gateway.filter.GlobalFilter。
注释:
Contract for interception-style, chained processing of Web requests that may be used to implement cross-cutting, application-agnostic requirements such as security, timeouts, and others.
译:
用于拦截式Web连接处理的合同,可用于实现跨领域,与应用程序无关的要求,如安全性,超时等。
这里先放着,后边会用到它。
发现了底层由org.springframework.web.reactive.DispatcherHandler来支持后,那么猜想可能会基于DispatcherHandler怎么做扩展呢?
继承DispatcherHandler重新实现分发逻辑(类似Zuul)
扩展DispatcherHandler的组件HandlerMapping
找到一个重要的类org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping,猜想(2)验证
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#getHandlerInternal
-
protected Mono<?> getHandlerInternal(ServerWebExcha nge exchange) {
-
exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName());
-
return lookupRoute(exchange)//根据exchange找匹配的route
-
// .log("route-predicate-handler-mapping", Level.FINER) //name this
-
.flatMap((Function<Route, Mono<?>>) r -> {//替换请求attributes值
-
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
-
if (logger.isDebugEnabled()) {
-
logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);
-
}
-
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);
-
return Mono.just(webHandler);//执行filter的handle方法
-
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
-
exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);
-
if (logger.isTraceEnabled()) {
-
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
-
}
-
})));//异常处理
-
}
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute
-
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
-
return this.routeLocator.getRoutes()
-
.filterWhen(route -> {//找到匹配的route,一个route包含一个filter链
-
// add the current route we are testing
-
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());
-
return route.getPredicate().apply(exchange);
-
})
-
// .defaultIfEmpty() put a static Route not found
-
// or .switchIfEmpty()
-
// .switchIfEmpty(Mono.<Route>empty().log("noroute"))
-
.next()
-
//TODO: error handling
-
.map(route -> {
-
if (logger.isDebugEnabled()) {
-
logger.debug("Route matched: " + route.getId());
-
}
-
validateRoute(route, exchange);
-
return route;
-
});
-
/* TODO: trace logging
-
if (logger.isTraceEnabled()) {
-
logger.trace("RouteDefinition did not match: " + routeDefinition.getId());
-
}*/
-
}
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes
-
@Override
-
public Flux<Route> getRoutes() {
-
return this.routeDefinitionLocator.getRouteDefinitions()
-
.map(this::convertToRoute)
-
//TODO: error handling
-
.map(route -> {
-
if (logger.isDebugEnabled()) {
-
logger.debug("RouteDefinition matched: " + route.getId());
-
}
-
return route;
-
});
-
/* TODO: trace logging
-
if (logger.isTraceEnabled()) {
-
logger.trace("RouteDefinition did not match: " + routeDefinition.getId());
-
}*/
-
}
这里又发现了一个类org.springframework.cloud.gateway.route.RouteDefinitionLocator
-
public interface RouteDefinitionLocator {
-
Flux<RouteDefinition> getRouteDefinitions();
-
}
从名称和方法可以看出RouteDefinitionLocator用于存放route信息,而真正可以执行route逻辑的则是Route,Route中包含filter链。
其扩展了以下几种:
DiscoveryClientRouteDefinitionLocator
CachingRouteDefinitionLocator
CompositeRouteDefinitionLocator
InMemoryRouteDefinitionRepository
PropertiesRouteDefinitionLocator
可以发现,这样做的好处是方便扩展功能,例如DiscoveryClientRouteDefinitionLocator可以实现从不同的注册中心上获取服务信息、CachingRouteDefinitionLocator则可以在本地JVM中用map缓存服务信息、或是用InMemoryRouteDefinitionRepository直接在内存中管理服务信息。
那么对于filter,它们均实现了前文所说的org.springframework.cloud.gateway.filter.GlobalFilter接口,Spring Cloud Gateway也提供了丰富的实现,如:
AdaptCachedBodyGlobalFilter
ForwardPathFilter
ForwardRoutingFilter
LoadBalancerClientFilter
NettyRoutingFilter
NettyWriteResponseFilter
RouteToRequestUrlFilter
WebClientHttpRoutingFilter
WebClientWriteResponseFilter
WebsocketRoutingFilter
可以看到,Spring Cloud Gateway对filter的支持更加丰富,包括NettyRoutingFilter&NettyWriteResponseFilter提供HttpClient代理请求的功能、WebClientHttpRoutingFilter&WebClientWriteResponseFilter提供WebClient代理请求的功能、LoadBalancerClientFilter提供负载均衡的支持(其内又有LoadBalancerClient的抽象,支持ribbon,也方便扩展)、ForwardPathFilter重建转发路径、ForwardRoutingFilter进行路由转发等。
另外,还有一些值得关注的功能有:
RateLimiter
GatewayControllerEndpoint
RouteRefreshListener
RateLimiter是一个接口,用户可以自行实现想要的限流策略及实现方式,Spring Cloud Gateway(2.0.0.RELESE)提供了RedisRateLimiter的实现,具体使用方式参考官网
GatewayControllerEndpoint提供了http控制RouteDefinition的endpoint,实现远程管理的效果
RouteRefreshListener是用于监听心跳事件、应用刷新事件、bean刷新、服务实例变化而发布route刷新事件,并及时刷新RouteBeanDefinition信息,与之类似的还有监听org.springframework.cloud.gateway.event.WeightDefinedEvent的org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter,可及时更新权重
到这里就算过了一遍Spring Cloud Gateway的工作原理和设计模型,更加细节上的功能还有待使用者“开发”,整理一下它的特点:
底层依然是servlet,但使用了webflux,多嵌套了一层框架
理解filter、handler、locator就能灵活使用它,但其大量使用的流式编程容易让人懵逼
提供了非常丰富的filter实现和灵活的RoutePredicateFactory(route匹配规则)
依赖spring-boot-starter-webflux和spring-cloud-starter
提供了异步支持
提供函数式编程api,使用起来方便快捷
提供了抽象流控,并默认实现了RedisRateLimiter
提供了抽象负载均衡
支持HttpClient、WebClient代理请求
ps.槽点就是作为Spring家族,注释竟然这么少!
对比Spring Cloud Netflix Zuul和Spring Cloud Gateway
前面整理了两者的特点,现在对比来分析,得出以下结论:
两者均是web网关,处理的是http请求
gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,而zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等
gateway很好的支持异步,而zuul仅支持同步,那么理论上gateway则更适合于提高系统吞吐量(但不一定能有更好的性能),最终性能还需要通过严密的压测来决定
从框架设计的角度看,gateway具有更好的扩展性,并且其已经发布了2.0.0的RELESE版本,稳定性也是非常好的
编码上看,zuul更加简洁易懂,注释规范清晰,而gateway作为Spring家族的一份子,竟然几乎不注释…
总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,单从流式编程+支持异步上就足以让开发者选择它了。
而对于小型微服务架构或是复杂架构(不仅包括微服务应用还有其他非Spring Cloud服务节点),zuul也是一个不错的选择,当然,这种场景下一般会选择nginx,因为nginx从各个方面都会表现的更好…
Nginx在微服务中的地位
最后简单聊一下nginx,在过去几年微服务架构还没有流行的日子里,nginx已经得到了广大开发者的认可,其性能高、扩展性强、可以灵活利用lua脚本构建插件的特点让人没有抵抗力。
有一个能满足我所有需求还很方便我扩展的东西,还免费,凭啥不用??
但是,如今很多微服务架构的项目中不会选择nginx,我认为原因有以下几点:
微服务框架一般来说是配套的,集成起来更容易
如今微服务架构中,仅有很少的公司会面对无法解决的性能瓶颈,而他们也不会因此使用nginx,而是选择开发一套适合自己的微服务框架
spring boot对于一些模板引擎如FreeMarker、themleaf的支持是非常好的,很多应用还没有达到动、静态文件分离的地步,对nginx的需求程度并不大。
无论如何,nginx作为一个好用的组件,最终使不使用它都是由业务来驱动的,只要它能为我们方便的解决问题,那用它又有何不可呢?
小结
通过总结发现,在微服务架构中网关上的选择,最好的方式是使用现在比较成熟的Spring Cloud套件,其提供了Spring Cloud Gateway网关,或是结合公司情况来开发一套适合自己的微服务套件,至少从网关上可以看出来其内部实现并不难,同时也比较期待开源项目Nacos、Spring Cloud Alibaba 建设情况,期待它能构建一个高活跃社区的、稳定的、适合中国特色(大流量、高并发)的微服务基础架构。