Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet;直到 2018 年 5 月,Zuul 2.x(基于Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。Spring Cloud
Gateway 比 Zuul 1.x 系列的性能和功能整体要好。
-
GateWay的简介
Spring Cloud GateWay是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式。
-
GaetWay与Zuul的对比
组件 RPS(request per second) Spring Cloud GateWay Request/sec:3223.38 Zuul1X Request/sec:20800.13 -
GateWay的核心概念
- 路由(route):路由是网关最基础的部分,路由信息由一个ID、一个目的URL、一组断言工厂和一组Filter组成。如果断言为真,则说明请求URL和配置的路由匹配。
- 断言(predicates):Java8中的断言函数,Spring Cloud Gateway中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的断言函数允许开发者去定义匹配来自Http Request中的任何信息,比如请求头和参数等。
- 过滤器(filter):一个标准的Spring webFilter,Spring Cloud Gateway中的Filter分为两种类型,分别是Gateway Filter和Global Filter。过滤器Filter可以对请求和响应进行处理。
创建ebuy-gateway功能模块:
-
导入依赖
gateway模块中一定不要导入Spring MVC中的web依赖,否则会报错而无法启动!!!
ebuy-parent cn.ebuy 1.0-SNAPSHOT ../ebuy-parent/pom.xml 4.0.0 cn.ebuy ebuy-gateway org.springframework.cloud spring-cloud-starter-gateway org.springframework.cloud spring-cloud-starter-netflix-eureka-client 注意:SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容。引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce。
-
配置启动类
package cn.ebuy.gateway; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayServerApplication { public static void main(String[] args) { SpringApplication.run(GatewayServerApplication.class, args); } } -
修改配置文件
server: port: 9091 #端口 spring: #http://127.0.0.1:9011/product/12123 application: name: ebuy-gateway #服务名称 cloud: # 根据路径请求 # gateway: # routes: # - id: ebuy-product #自定义的路由id,保持唯一 # uri: http://127.0.0.1:9011 #目标服务地址 # predicates: #路由条件,Predicates接受一个输入参数,返回一个布尔值结果,该接口包含多种默认方法来讲Predicate组合成其他复杂的逻辑 # - Path=/product/4.获取当前的所有请求头信息 HttpHeaders headers = request.getHeaders(); //5.获取jwt令牌信息 String jwtToken = headers.getFirst("token"); //6.判断当前令牌是否存在, if (StringUtils.isEmpty(jwtToken)){ //如果不存在,则向客户端返回错误提示信息 response.setStatusCode(HttpStatus.UNAUTHORIZED); return response.setComplete(); } */ // 判断当前令牌是否存在 String token=request.getQueryParams().getFirst("token"); if(StringUtils.isEmpty(token)){ //如果不存在,则向客户端返回错误提示信息 response.setStatusCode(HttpStatus.UNAUTHORIZED); System.out.println("过滤器正在工作..."); return response.setComplete(); }else { // 如果令牌合法,则放行 return chain.filter(exchange); } } @Override public int getOrder() { return 0; } }
18.6 网关限流- 自定义全局过滤器需要实现GlobalFilter和Ordered接口。
- 在filter方法中完成过滤器的逻辑判断处理
- 在getOrder方法指定此过滤器的优先级,返回值越大级别越低
- ServerWebExchange 就相当于当前请求和响应的上下文,存放着重要的请求-响应属性、请求实例和响应实例等等。一个请求中的request,response都可以通过ServerWebExchange 获取
- 调用chain.filter继续向下游执行
-
常见的限流算法
-
计数器
计数器限流算法是最简单的一种限流实现方式。其本质是通过维护一个单位时间内的计数器,每次请求计数器加1,当单位时间内计数器累加到大于设定的阈值,则之后的请求都被拒绝,直到单位时间已经过去,再将计数器重置为零。
-
漏桶算法
漏桶算法可以很好地限制容量池的大小,从而防止流量暴增。漏桶可以看作是一个带有常量服务时间的单服务器队列,如果漏桶(包缓存)溢出,那么数据包会被丢弃。 在网络中,漏桶算法可以控制端口的流量输出速率,平滑网络上的突发流量,实现流量整形,从而为网络提供一个稳定的流量。
为了更好的控制流量,漏桶算法需要通过两个变量进行控制:一个是桶的大小,支持流量突发增多时可以存多少的水(burst),另一个是水桶漏洞的大小(rate)。 -
令牌桶算法
令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择选择等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
-
基于Filter的限流
SpringCloudGateway官方就提供了基于令牌桶的限流支持。基于其内置的过滤器工厂RequestRateLimiterGatewayFilterFactory实现。在过滤器工厂中是通过Redis和lua脚本结合的方式进行流量控制。
-
导入redis依赖
org.springframework.cloud spring-cloud-starter-gateway org.springframework.boot spring-boot-starter-data-redis-reactive 2.1.3.RELEASE -
修改application.yml配置文件
server: port: 9091 #端口 spring: #http://127.0.0.1:9011/product/12123 application: name: ebuy-gateway #服务名称 cloud: #根据服务名请求 gateway: globalcors: cors-configurations: '[ @Bean public KeyResolver pathKeyResolver(){ return new KeyResolver() { @Override public Monoresolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getPath().toString()); } }; } // @Bean public KeyResolver ipKeyResolver() { return new KeyResolver() { @Override public Mono resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }; } // @Bean public KeyResolver userKeyResolver(){ return new KeyResolver() { @Override public Mono resolve(ServerWebExchange exchange) { return Mono.just(exchange.getRequest().getQueryParams().getFirst("user")); } }; } } -
测试
配置的是一秒刷新一次,当连续刷新一时,会出现限流。
-
基于Sentinel的限流
Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流。
从 1.6.0 版本开始,Sentinel 提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:- route 维度:即在 Spring 配置文件中配置的路由条目,资源名为对应的 routeId
- 自定义 API 维度:用户可以利用 Sentinel 提供的 API 来自定义一些 API 分组Sentinel 1.6.0 引入了 Sentinel API Gateway Adapter Common 模块,此模块中包含网关限流的规则和自定义 API 的实体和管理逻辑:
① GatewayFlowRule:网关限流规则,针对 API Gateway 的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的参数、Header、来源 IP 等进行定制化的限流。
② ApiDefinition:用户自定义的 API 定义分组,可以看做是一些 URL 匹配的组合。比如我们可以定义一个 API 叫 my_api,请求 path 模式为 /foo/ ** 和 /baz/ ** 的都归到 my_api 这个 API 分组下面。限流的时候可以针对这个自定义的 API 分组维度进行限流。
-
导入依赖
导入Sentinel的相应依赖
com.alibaba.csp sentinel-spring-cloud-gateway-adapter 1.6.3 -
编写配置类
GatewayConfiguration.java
package cn.ebuy.gateway.util; import com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; import com.alibaba.csp.sentinel.adapter.gateway.common.api.GatewayApiDefinitionManager; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.SentinelGatewayFilter; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler; import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager; import com.alibaba.csp.sentinel.adapter.gateway.sc.exception.SentinelGatewayBlockExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.server.ServerResponse; import org.springframework.web.reactive.result.view.ViewResolver; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import java.util.*; @Configuration public class GatewayConfiguration { private final ListviewResolvers; private final ServerCodecConfigurer serverCodecConfigurer; public GatewayConfiguration(ObjectProvider - > viewResolversProvider,
ServerCodecConfigurer serverCodecConfigurer) {
this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SentinelGatewayBlockExceptionHandler sentinelGatewayBlockExceptionHandler() {
return new SentinelGatewayBlockExceptionHandler(viewResolvers, serverCodecConfigurer);
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public GlobalFilter sentinelGatewayFilter() {
return new SentinelGatewayFilter();
}
@PostConstruct
public void initGatewayRules() {
Set
rules = new HashSet<>(); rules.add( new GatewayFlowRule("product-api") //资源名称 .setCount(1) // 限流阈值 .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒 ); GatewayRuleManager.loadRules(rules); } // 自定义异常提示 @PostConstruct public void initBlockHandlers() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { public Mono handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map map = new HashMap<>(); map.put("code", 001); map.put("message", "对不起,接口限流了"); return ServerResponse.status(HttpStatus.OK). contentType(MediaType.APPLICATION_JSON_UTF8). body(BodyInserters.fromObject(map)); } }; GatewayCallbackManager.setBlockHandler(blockRequestHandler); } @PostConstruct private void initCustomizedApis() { Set definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition("product-api") .setPredicateItems(new HashSet () {{ add(new ApiPathPredicateItem().setPattern("/ebuy-product/product @PostConstruct private void initCustomizedApis() { Set definitions = new HashSet<>(); ApiDefinition api1 = new ApiDefinition("product-api") .setPredicateItems(new HashSet () {{ add(new ApiPathPredicateItem().setPattern("/ebuy-product/product/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); ApiDefinition api2 = new ApiDefinition("order-api") .setPredicateItems(new HashSet () {{ add(new ApiPathPredicateItem().setPattern("/ebuy-order/order/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); definitions.add(api1); definitions.add(api2); GatewayApiDefinitionManager.loadApiDefinitions(definitions); }
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。
我们实际使用 Spring Cloud Gateway 的方式如上图,不同的客户端使用不同的负载将请求分发到后端的 Gateway,Gateway 再通过HTTP调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用性,前端可以同时启动多个 Gateway 实例进行负载,在 Gateway 的前端使用 Nginx 或者 F5 进行负载转发以达到高可用性。-
创建多个Gateway工程
server: port: 9091 #端口 spring: #http://127.0.0.1:9011/product/12123 application: name: ebuy-gateway #服务名称 cloud: #根据服务名请求 gateway: globalcors: cors-configurations: '[/**]': # 匹配所有请求 allowedOrigins: "*" #跨域处理 允许所有的域 allowedMethods: # 支持的方法 - GET - POST - PUT - DELETE routes: - id: ebuy-product uri: lb://ebuy-product #uri以lb://开头(lb代表从注册中心获取服务),后面接的就是你需要转发到的服务名称 predicates: - Path=/ebuy-product/** #路由转发,直接将匹配的路由path直接拼接到映射路径url之后 filters: #过滤规则 #重写转发的url,将/ebuy-product/(?.*)重新写为{segment},然后转发到订单微服务 - RewritePath=/ebuy-product/(?.*), /${segment} - id: ebuy-order uri: lb://ebuy-order predicates: - Path=/ebuy-order/** filters: - RewritePath=/ebuy-order/(? .*), /${segment} redis: host: 127.0.0.1 port: 6379 password: 123456 logging: level: cn.ebuy: DEBUG eureka: client: service-url: defaultZone: http://127.0.0.1:8000/eureka/,http://127.0.0.1:9000/eureka/ instance: prefer-ip-address: true # 使用ip地址注册 spring: profiles: gateway01 Server: port: 8080 #服务端口 -------------------------- spring: profiles: gateway02 Server: port: 8081 #服务端口 通过不同的profiles配置启动两个网关服务,请求端口分别为8080和8081。浏览器验证发现效果是一致的。
-
配置nginx
#配置多台服务器(这里只在一台服务器上的不同端口) upstream gateway { server 127.0.0.1:8081; server 127.0.0.1:8080; } #请求转向mysvr 定义的服务器列表 location / { proxy_pass http://gateway; }



