上文文章(Hystrix监控平台、DashBoard、Turbine监控、熔断器的状态、微服务网关Zuul)
微服务网关GateWay Zuul网关存在的问题在实际使用中我们会发现直接使用Zuul会存在诸多问题,包括:
- 性能问题
Zuul1x版本本质上就是一个同步Servlet,采用多线程阻塞模型进行请求转发。简单讲,每来
一个请求,Servlet容器要为该请求分配一个线程专门负责处理这个请求,直到响应返回客户
端这个线程才会被释放返回容器线程池。如果后台服务调用比较耗时,那么这个线程就会被
阻塞,阻塞期间线程资源被占用,不能干其它事情。我们知道Servlet容器线程池的大小是有
限制的,当前端请求量大,而后台慢服务比较多时,很容易耗尽容器线程池内的线程,造成 容器无法接受新的请求。
- 不支持任何长连接,如websocket
- SpringCloud Gateway
微服务网关GateWay简介
Zuul 1.x 是一个基于阻塞 IO 的 API Gateway 以及 Servlet;直到 2018 年 5 月,Zuul 2.x(基于
Netty,也是非阻塞的,支持长连接)才发布,但 Spring Cloud 暂时还没有整合计划。Spring Cloud
Gateway 比 Zuul 1.x 系列的性能和功能整体要好。
Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,旨在为微服务架构提供一种简单而有效的统一的 API 路由管理方式,统一访问接口。SpringCloud Gateway 作为 Spring Cloud 生态系中的网关,目标是替代 Netflix ZUUL,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。它是基于Nttey的响应式开发模式。
核心概念
- 路由(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可以对请求和响应进行处理。
org.springframework.cloud spring-cloud-starter-gateway
注意:
SpringCloud Gateway使用的web框架为webflux,和SpringMVC不兼容。引入的限流组件是hystrix。redis底层不再使用jedis,而是lettuce
意思就是说不能存在web的启动器,不然会报错,如下
配置启动类
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);
}
}
编写配置文件
创建 application.yml 配置文件
server:
port: 9091
spring:
application:
name: ebuy-gateway #服务名称
cloud:
gateway:
routes:
# 第一种 不支持负载均衡 http://localhost:9091/product/816753
- id: ebuy-product
uri: http://127.0.0.1:6501
predicates:
- Path=/product
@Bean
public KeyResolver pathKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getPath().toString()
);
}
// @Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getHeaders().getFirst("X-Forwarded-For")
);
}
// @Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getQueryParams().getFirst("user")
);
}
// @Bean
public KeyResolver remoteAddressKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getHostName()
);
}
public static void main(String[] args) {
SpringApplication.run(GatewayServerApplication.class, args);
}
}
注意:以上几种策略只能保存一种,并且beanname对应上面配置文件中的#{@beanName}。
测试 测试根据请求路径的限流将@Bean的的注释标签去掉
然后重启服务
接着请求服务
正常请求:
然后你上面配置文件配置了限流规则,可以尝试快速刷新,可以看到如下:
即是请求地址被限流了!
测试基于用户的限流
同样打开userKeyResolver的@Bean标签,并将配置文件中的beanname修改为userKeyResolver(记得把上面的请求路径的@Bean注释)
然后重启服务
- 不加user参数,直接报错
添加user参数,未被限流的情况
被限流的情况(尝试多次快速刷新)
其他例子不再展示(同上)
查看redis的信息
查看redis的信息
- timestamp:存储的是当前时间的秒数,也就是System.currentTimeMillis() / 1000或者
Instant.now().getEpochSecond()。 - tokens:存储的是当前这秒钟的对应的可用的令牌数量
Spring Cloud Gateway目前提供的限流还是相对比较简单的,在实际中我们的限流策略会有很多种情
况,比如:
- 对不同接口的限流
- 被限流后的友好提示
这些可以通过自定义RedisRateLimiter来实现自己的限流策略,这里我们不做讨论
基于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
@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("ebuy-product") //资源名称 服务名 .setCount(1) // 限流阈值 .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒 ); GatewayRuleManager.loadRules(rules); } } 注: 配置初始化的限流参数中的rules是一个set集合,也就是说你可以加入多个服务名,为多个服务进行限流。(上面的代码只给ebuy-product配置了限流,你可以给ebuy-order等配置限流,前提是你的配置文件中配置了该服务的路由转发。)
修改启动类
修改配置文件注释掉前面配置的基于Filter的限流的Bean
重启服务重启gateway网关服务之后,再次请求 未限流的状态:
尝试快速多次刷新,如下:
但是这样的限流提示太过不够人性化,所以sentinel提供了一种更加友好的限流提示配置,往下看!
自定义异常提示当触发限流后页面显示的是Blocked by Sentinel: FlowException。为了展示更加友好的限流提示,
Sentinel支持自定义异常处理。
您可以在 GatewayCallbackManager 注册回调进行定制:- setBlockHandler :注册函数用于实现自定义的逻辑处理被限流的请求,对应接口为BlockRequestHandler 。默认实现为 DefaultBlockRequestHandler ,当被限流时会返回类似于下面的错误信息: Blocked by Sentinel: FlowException 。
在刚才的配置类中加入如下:
@PostConstruct public void initBlockHandlers() { BlockRequestHandler blockRequestHandler = new BlockRequestHandler() { public MonohandleRequest(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); }
再次重启服务测试此时限流提示为:
参数限流顾名思义:就是不直接给整个微服务资源限流,而是拿到请求的某个参数,对参数进行规则限流。
上面的配置是针对整个路由来限流的,如果我们只想对某个路由的参数做限流,那么可以使用参数限流
方式:
如下:rules.add( new GatewayFlowRule("ebuy-product") //资源名称 服务名 .setCount(1) // 限流阈值 .setIntervalSec(1) // 统计时间窗口,单位是秒,默认是 1 秒 .setParamItem(new GatewayParamFlowItem() .setParseStrategy(SentinelGatewayConstants.PARAM_PARSE_STRATEGY_URL_PARAM) .setFieldName("id")) );
通过指定PARAM_PARSE_STRATEGY_URL_PARAM表示从url中获取参数,setFieldName指定参数名称
图解:
重启服务测试不加限流参数(id),进行请求:
不同于上面的基于Filter限流(不加限流参数直接报错),基于sentinel限流,参数配置之后不加参数不进行限流处理— 添加限流参数(id),进行快速多次刷新:
添加参数之后,对其进行限流处理
自定义API分组@PostConstruct private void initCustomizedApis() { Setdefinitions = new HashSet<>(); //配置 产品服务分组 ApiDefinition api1 = new ApiDefinition("product-api"); api1.setPredicateItems(new HashSet () {{ add(new ApiPathPredicateItem().setPattern("/ebuy-product/product/**"). setMatchStrategy(SentinelGatewayConstants.URL_MATCH_STRATEGY_PREFIX)); }}); definitions.add(api1); GatewayApiDefinitionManager.loadApiDefinitions(definitions); } 图解:
如需配置多个分组,可以创建多个ApiDefinition 然后配置配置初始化的限流参数,如下:
接下来说一下网关的高可用。
网关高可用由于微服务架构中,客户端一切的请求都要经过网关转发请求,所以网关的工作压力是非常大的,所以搭建集群尤其重要。
高可用HA(High Availability)是分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计减少系统不能提供服务的时间。我们都知道,单点是系统高可用的大敌,单点往往是系统高可用最大的风险和敌人,应该尽量在系统设计的过程中避免单点。方法论上,高可用保证的原则是“集群化”,或者叫“冗余”:只有一个单点,挂了服务会受影响;如果有冗余备份,挂了还有其他backup能够顶上。
实现原理图:
使用nginx反向代理我们的服务端我们实际使用 Spring Cloud Gateway 的方式如上图,不同的客户端使用不同的负载将请求分发到后端 的 Gateway,Gateway 再通过HTTP调用后端服务,最后对外输出。因此为了保证 Gateway 的高可用 性,前端可以同时启动多个Gateway 实例进行负载,在 Gateway 的前端使用 Nginx 或者 F5 进行负载 转发以达到高可用性。
修改配置文件直接修改application.yml中的端口,启动两个gateway服务。
如下:
配置nginx然后访问测试下,如下:
下载解压过程不再演示,可以看之前的文章(nginx文章)
配置如下:
代码如下:
upstream gateway{ server 127.0.0.1:9091; server 127.0.0.1:9092; } location / { proxy_pass http://gateway; }
测试然后启动nginx,如下:
这样就启动成功了,nginx的默认端口是80访问服务,如下:
测试关闭一个gateway服务,访问测试(过程不再展示)
执行流程分析流程图:
Spring Cloud Gateway 核心处理流程如上图所示,Gateway的客户端向 Spring Cloud Gateway 发送请求,请求首先被 HttpWebHandlerAdapter 进行提取组装成网关上下文,然后网关的上下文会传递到 DispatcherHandler 。
DispatcherHandler 是所有请求的分发处理器, DispatcherHandler 主要
负责分发请求对应的处理器。比如请求分发到对应的 RoutePredicateHandlerMapping (路由断言处
理映射器)。路由断言处理映射器主要作用用于路由查找,以及找到路由后返回对应的FilterWebHandler 。 FilterWebHandler 主要负责组装Filter链并调用Filter执行一系列的Filter处理,然后再把请求转到后端对应的代理服务处理,处理完毕之后将Response返回到Gateway客户端。本文结束谢谢大家!!!



