springcloud+nacos+gateway整合
一、背景
1、为什么需要网关
网关层是浏览器与服务器交互时经过的第一个服务节点,它主要起屏蔽下游业务服务的作用,对于浏览器而言,只需要跟网关交互就相当于在与下游多个业务服务节点交互,让浏览器觉得他在和一台服务器交互。
2、好处
不管是下游业务服务、支撑服务、基础服务,都对于浏览器屏蔽,与服务器的交互变的非常简单,浏览器无需关心各个节点的依赖关系、如何协同工作,浏览器只会了解到本次请求是否成功;开发者可以灵活的增加业务服务模块;可以在网关层做一些最上层的公用的操作,如过滤恶意请求、设置ip黑白名单、做身份认证、限流、负载均衡、单点登录以及跨域等。
二、选型说明
1、gateway和zuul的简单源码分析
1、zuul源码
1⃣️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 {
...
}
它本质上用了java.servlet API,实现了一个有网关功能的servlet
2⃣️com.netflix.zuul.http.ZuulServlet#service
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
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
3⃣️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;
}
从FilterRegistry(相当于内存中的filter)加载Zuulfilter list
编译groovy文件并加载Zuulfilter
对于FilterRegistry,则是用于内存中保存filter,可以动态变化的,注册新的filter以及移除filter等,可提供给jmx、endpoint做远程控制。
4⃣️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;
}
}
实际上整个调用链是由ZuulFilter来组成,对于用户而言,只需要关心如果构建自定义的ZuulFilter以及它们之间的顺序。
由于底层是servlet,Zuul处理的是http请求,Zuul的抽象写的非常简单易懂,易于扩展,但是zuul-core包不依赖Spring,依赖的包很少,没有提供异步支持,流控等均由hystrix支持
2、gateway
在Spring mvc是通过HandlerMapping解析请求链接,然后根据请求链接找到执行这个请求Controller类 。而在Spring GateWay中也是使用HandlerMapping对请求的链接进行解析匹配对应的Route进行代理转发到对应的服务。
用户请求先通过DispatcherHandler找到对应GateWwayHandlerMapping,再通过GateWwayHandlerMapping解析匹配到对应的Handler。Handler处理完后,再经过Filter,最终到Proxied Service
1⃣️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
public RoutePredicateHandlerMapping routePredicateHandlerMapping(FilteringWebHandler webHandler, RouteLocator routeLocator, GlobalCorsProperties globalCorsProperties, Environment environment) {
return new RoutePredicateHandlerMapping(webHandler, routeLocator, globalCorsProperties, environment);//webHandler是一个封装了Global Filter的对象,routeLocator保存了所有Route的对象
}
...
}
2⃣️org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping处理获取路由
private final RouteLocator routeLocator;存储了我们启动的时候加载的路由对象信息。
先看下org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#getHandlerInternal 从RouteLocator获取路由存放在ServerWebExchange中,返回webFilter
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {
if (this.managementPortType == RoutePredicateHandlerMapping.ManagementPortType.DIFFERENT && this.managementPort != null && exchange.getRequest().getURI().getPort() == this.managementPort) {
return Mono.empty();
} else {
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_HANDLER_MAPPER_ATTR, this.getSimpleName());
return this.lookupRoute(exchange).flatMap((r) -> {
exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Mapping [" + this.getExchangeDesc(exchange) + "] to " + r);
}
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR, r);
return Mono.just(this.webHandler);
}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {
exchange.getAttributes().remove(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
if (this.logger.isTraceEnabled()) {
this.logger.trace("No RouteDefinition found for [" + this.getExchangeDesc(exchange) + "]");
}
})));
}
}
继续看
org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes().concatMap((route) -> {
return Mono.just(route).filterWhen((r) -> {
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return (Publisher)r.getPredicate().apply(exchange);
}).doOnError((e) -> {
this.logger.error("Error applying predicate for route: " + route.getId(), e);
}).onErrorResume((e) -> {
return Mono.empty();
});
}).next().map((route) -> {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Route matched: " + route.getId());
}
this.validateRoute(route, exchange);
return route;
});
}
继续看org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getRoutes
public Flux<Route> getRoutes() {
Flux<Route> routes = this.routeDefinitionLocator.getRouteDefinitions().map(this::convertToRoute);
if (!this.gatewayProperties.isFailOnRouteDefinitionError()) {
routes = routes.onErrorContinue((error, obj) -> {
if (this.logger.isWarnEnabled()) {
this.logger.warn("RouteDefinition id " + ((RouteDefinition)obj).getId() + " will be ignored. Definition has invalid configs, " + error.getMessage());
}
});
}
return routes.map((route) -> {
if (this.logger.isDebugEnabled()) {
this.logger.debug("RouteDefinition matched: " + route.getId());
}
return route;
});
}
继续看org.springframework.cloud.gateway.route.RouteDefinitionLocator
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}
RouteDefinitionLocator用于存放route信息,而真正可以执行route逻辑的则是Route,Route中包含路由filter链。
route包含:
URI:路由地址uri。
Predicate:包含接受请求的方法、path等信息
Filter:Route自定义的过滤器
2、gateway和zuul对比
1、zuul是基于servlet 2.5,兼容servlet3.0,使用的是阻塞API,不支持长连接如websocket,
虽然zuul2支持非阻塞API,但并没有和spring cloud集成,而且不再维护,所以不考虑使用
2、Gateway基于spring5,Reactor和Spring boot2,使用了非阻塞API,支持websocket,和spring完美集成,对开发者友好。
Reactor:提供了一个非阻塞的,高并发的基于健壮的Netty框架的网络运行API,包括本地tcp/http/udp 客户端和服务端。
Flux:表示的是包含 0 到 N 个元素的异步序列。在该序列中可以包含三种不同类型的消息通知:正常的包含元素的消息、序列结束的消息和序列出错的消息。当消息通知产生时,订阅者中对应的方法 onNext(), onComplete()和 onError()会被调用
Mono:Mono 表示的是包含 0 或者 1 个元素的异步序列。该序列中同样可以包含与 Flux 相同的三种类型的消息通知。
Flux 和 Mono 之间可以进行转换。对一个 Flux 序列进行计数操作,得到的结果是一个 Mono对象。把两个 Mono 序列合并在一起,得到的是一个 Flux 对象。
简单说Mono返回单个元素,Flux返回多个元素
3、因为zuul使用了阻塞API,所以在spring项目使用中需要依赖spring-web,而gateway使用非阻塞,所以需要依赖spring-webflux,两者是冲突的,需要考虑包冲突问题。
Spring WebFlux:是Spring Framework 5.0中引入的新的反应式Web框架,它不需要Servlet API,完全异步和非阻塞, 并通过Reactor项目实现Reactive Streams规范。 并且可以在诸如Netty,Undertow和Servlet 3.1+容器的服务器上运行,核心控制器DispatcherHandler,等同于阻塞方式的DispatcherServlet。
webflux流程:
1.通过 HandlerMapping获取到HandlerAdapter放到ServerWebExchange的属性中
2.获取到HandlerAdapter后触发handle方法,得到HandlerResult
3.通过HandlerResult,触发handleResult,针对不同的返回类找到不同的HandlerResultHandler如视图渲染ViewResolutionResultHandler,ServerResponseResultHandler,ResponseBodyResultHandler,ResponseEntityResultHandler
4、gateway在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,而zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等
5、gateway很好的支持异步,那么理论上gateway则更适合于提高系统吞吐量,从框架设计的角度看,gateway具有更好的扩展性,稳定性也是非常好的
总的来说,在微服务架构,如果使用了Spring Cloud生态的基础组件,则Spring Cloud Gateway相比而言更加具备优势,流式编程+支持异步太吸引人。
而对于小型微服务架构,zuul是一个不错的选择
二、整合过程
1、导入maven依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2、配置说明
spring:
cloud:
gateway:
discovery:
locator:
# 是否与服务发现组件进行结合,通过serviceId转发到具体的服务实例。默认为false,为true代表开启基于服务发现的路由规则。
enabled: false
# 配置之后访问时无需大写
lower-case-service-id: true
routes:
- id: homework-user
# lb: 使用loadBalanceClient实现负载均衡,后面homework-user是微服务的名称,用于集群
uri: lb://homework-user
predicates:
- Path=/homework-user/** #类型路径路由至homework-user微服务
filters:
- StripPrefix=1 #请求微服务,自动去掉第一个前缀
- name: RequestRateLimiter #限流过滤器,使用默认factory
args:
#限流解析器bean对象
key-resolver: '#{@iPKeyResolver}'
#令牌桶每秒填充平均速率,每秒钟只允许一个请求
redis-rate-limiter.replenishRate: 1
#令牌桶容量,允许并发三个个请求
redis-rate-limiter.burstCapacity: 3
- id: homework-openapi
uri: lb://homework-openapi
predicates:
- Path=/homework-openapi/**
filters:
- StripPrefix=1
globalcors:
cors-configurations:
'[/**]': #匹配所有请求
allowCredentials: true
allowedOrigins: "*"
allowedMethods: "*"
allowedHeaders: "*"
3、实现功能
登陆过滤、跨域、服务端响应处理、白名单过滤、验签过滤以及服务通用fallback处理
三、遇到问题
1、版本问题
由于nacos原因,所以采用最新版本的Springboot以及cloud版本
之前Hoxton.SR6版本的springcloud对长连接处理有问题,所以一直出现too many files问题,后来提出issue,问题得到解决,目前用到的版本是:
springboot:2.3.2.RELEASE
springcloud:Hoxton.SR6
nacos:2.2.1.RELEASE
2、分组、包问题
分组:微服务的discovery分组必须和网关分组一致
包:由于webflux和web冲突问题,所以必须提出所有web包
3、客户端请求问题
客户端请求到网关时,如果数据包很大,默认会分两次请求导致问题,可以在客户端做配置处理,HTTP请求框架将 httpclient 设置 Expect: 100-continue =false