版权声明:本文为博主原创文章,遵循 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
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 建设情况,期待它能构建一个高活跃社区的、稳定的、适合中国特色(大流量、高并发)的微服务基础架构。
评论记录:
回复评论: