Bootstrap

微服务网关Zuul和Gateway的区别

版权声明:本文为博主原创文章,遵循 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

 
  1. public class ZuulServlet extends HttpServlet {

  2. private ZuulRunner zuulRunner;

  3. public void init(ServletConfig config) throws ServletException {

  4. ...

  5. }

  6. public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {

  7. ...

  8. }

  9. }

它继承了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

 
  1. try {

  2. init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

  3. // Marks this request as having passed through the "Zuul engine", as opposed to servlets

  4. // explicitly bound in web.xml, for which requests will not have the same data attached

  5. RequestContext context = RequestContext.getCurrentContext();

  6. context.setZuulEngineRan();

  7. try {

  8. preRoute();

  9. } catch (ZuulException e) {

  10. error(e);

  11. postRoute();

  12. return;

  13. }

  14. try {

  15. route();

  16. } catch (ZuulException e) {

  17. error(e);

  18. postRoute();

  19. return;

  20. }

  21. try {

  22. postRoute();

  23. } catch (ZuulException e) {

  24. error(e);

  25. return;

  26. }

  27. } catch (Throwable e) {

  28. error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));

  29. } finally {

  30. RequestContext.getCurrentContext().unset();

  31. }

可以看到,它做了以下几件事:

  1. 前置路由
  2. 路由
  3. 后置路由
  4. 异常处理

不管是preRoute()route()postRoute()error(),它们最终调用了com.netflix.zuul.FilterProcessor#runFilters

 
  1. public Object runFilters(String sType) throws Throwable {

  2. if (RequestContext.getCurrentContext().debugRouting()) {

  3. Debug.addRoutingDebug("Invoking {" + sType + "} type filters");

  4. }

  5. boolean bResult = false;

  6. List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);

  7. if (list != null) {

  8. for (int i = 0; i < list.size(); i++) {

  9. //ZuulFilter list

  10. //轮流执行ZuulFilter的逻辑,result=false或执行完所有ZuulFilter时调用链结束

  11. ZuulFilter zuulFilter = list.get(i);

  12. Object result = processZuulFilter(zuulFilter);

  13. if (result != null && result instanceof Boolean) {

  14. bResult |= ((Boolean) result);

  15. }

  16. }

  17. }

  18. return bResult;

  19. }

这里FilterLoader的源码就不深入分析了,它主要的功能是:

从FilterRegistry(相当于内存中的filter)加载Zuulfilter list
编译groovy文件(笔者的Zuul版本1.3.0,目前仅支持groovy文件)并加载Zuulfilter
对于FilterRegistry,则是用于内存中保存filter,可以动态变化的,注册新的filter以及移除filter等,可提供给jmx、endpoint做远程控制。

继续看看com.netflix.zuul.FilterProcessor#processZuulFilter

 
  1. public Object processZuulFilter(ZuulFilter filter) throws ZuulException {

  2. RequestContext ctx = RequestContext.getCurrentContext();

  3. boolean bDebug = ctx.debugRouting();

  4. final String metricPrefix = "zuul.filter-";

  5. long execTime = 0;

  6. String filterName = "";

  7. try {

  8. long ltime = System.currentTimeMillis();

  9. filterName = filter.getClass().getSimpleName();

  10. RequestContext copy = null;

  11. Object o = null;

  12. Throwable t = null;

  13. if (bDebug) {

  14. Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName);

  15. copy = ctx.copy();

  16. }

  17. //执行ZuulFilter的runFilter逻辑

  18. ZuulFilterResult result = filter.runFilter();

  19. ExecutionStatus s = result.getStatus();

  20. //执行耗时统计(可以发现Zuul还没有完善这个功能,只是形成了框架)

  21. execTime = System.currentTimeMillis() - ltime;

  22. //处理执行结果,无论成功与否,都记录了debug日志

  23. switch (s) {

  24. case FAILED:

  25. t = result.getException();

  26. ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);

  27. break;

  28. case SUCCESS:

  29. o = result.getResult();

  30. ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime);

  31. if (bDebug) {

  32. Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms");

  33. Debug.compareContextState(filterName, copy);

  34. }

  35. break;

  36. default:

  37. break;

  38. }

  39. if (t != null) throw t;

  40. //目前作为空壳存在,可见是为了方便扩展

  41. usageNotifier.notify(filter, s);

  42. return o;

  43. } catch (Throwable e) {

  44. if (bDebug) {

  45. Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage());

  46. }

  47. usageNotifier.notify(filter, ExecutionStatus.FAILED);

  48. if (e instanceof ZuulException) {

  49. throw (ZuulException) e;

  50. } else {

  51. ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName);

  52. ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime);

  53. throw ex;

  54. }

  55. }

  56. }

这样一来,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

 
  1. @Configuration

  2. @ConditionalOnProperty(name = "spring.cloud.gateway.enabled", matchIfMissing = true)

  3. @EnableConfigurationProperties

  4. @AutoConfigureBefore(HttpHandlerAutoConfiguration.class)

  5. @AutoConfigureAfter({GatewayLoadBalancerClientAutoConfiguration.class, GatewayClassPathWarningAutoConfiguration.class})

  6. @ConditionalOnClass(DispatcherHandler.class)

  7. public class GatewayAutoConfiguration {

  8. ...

  9. //由于内容比较多,就不全部贴出来了

  10. @Bean

  11. @ConditionalOnBean(DispatcherHandler.class)

  12. public ForwardRoutingFilter forwardRoutingFilter(DispatcherHandler dispatcherHandler) {

  13. return new ForwardRoutingFilter(dispatcherHandler);

  14. }

  15. ...

  16. }

继续点进去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

 
  1. protected Mono<?> getHandlerInternal(ServerWebExcha nge exchange) {

  2. exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getClass().getSimpleName());

  3. return lookupRoute(exchange)//根据exchange找匹配的route

  4. // .log("route-predicate-handler-mapping", Level.FINER) //name this

  5. .flatMap((Function<Route, Mono<?>>) r -> {//替换请求attributes值

  6. exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);

  7. if (logger.isDebugEnabled()) {

  8. logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);

  9. }

  10. exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);

  11. return Mono.just(webHandler);//执行filter的handle方法

  12. }).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {

  13. exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);

  14. if (logger.isTraceEnabled()) {

  15. logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");

  16. }

  17. })));//异常处理

  18. }

org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute

 
  1. protected Mono<Route> lookupRoute(ServerWebExchange exchange) {

  2. return this.routeLocator.getRoutes()

  3. .filterWhen(route -> {//找到匹配的route,一个route包含一个filter链

  4. // add the current route we are testing

  5. exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, route.getId());

  6. return route.getPredicate().apply(exchange);

  7. })

  8. // .defaultIfEmpty() put a static Route not found

  9. // or .switchIfEmpty()

  10. // .switchIfEmpty(Mono.<Route>empty().log("noroute"))

  11. .next()

  12. //TODO: error handling

  13. .map(route -> {

  14. if (logger.isDebugEnabled()) {

  15. logger.debug("Route matched: " + route.getId());

  16. }

  17. validateRoute(route, exchange);

  18. return route;

  19. });

  20. /* TODO: trace logging

  21. if (logger.isTraceEnabled()) {

  22. logger.trace("RouteDefinition did not match: " + routeDefinition.getId());

  23. }*/

  24. }

org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes

 
  1. @Override

  2. public Flux<Route> getRoutes() {

  3. return this.routeDefinitionLocator.getRouteDefinitions()

  4. .map(this::convertToRoute)

  5. //TODO: error handling

  6. .map(route -> {

  7. if (logger.isDebugEnabled()) {

  8. logger.debug("RouteDefinition matched: " + route.getId());

  9. }

  10. return route;

  11. });

  12. /* TODO: trace logging

  13. if (logger.isTraceEnabled()) {

  14. logger.trace("RouteDefinition did not match: " + routeDefinition.getId());

  15. }*/

  16. }

这里又发现了一个类org.springframework.cloud.gateway.route.RouteDefinitionLocator

 
  1. public interface RouteDefinitionLocator {

  2. Flux<RouteDefinition> getRouteDefinitions();

  3. }

从名称和方法可以看出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 建设情况,期待它能构建一个高活跃社区的、稳定的、适合中国特色(大流量、高并发)的微服务基础架构。

;