1、 网关简介
1.1 背景分析
- 当一个大型系统在设计时,经常会被拆分为很多个微服务。那么作为客户端应该如何去调用这许多的微服务。客户端可以直接向微服务发送请求,每个微服务都只有一个公开的请求地址,该url地址可以直接映射到具体的微服务,如果没有网关的存在,我们只能在客户端,记录每个微服务的地址,然后分别去调用。
- 这样的框架会存在诸多的问题,例如:
- 每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,效率会十分低下,这些步骤完全可以抽取出来,在一个地方统一去做一次就行。
- 随着业务越来越复杂,比如淘宝打开一个页面可能会涉及到数百个微服务协同工作,如果每一个微服务都分配一个域名的话,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,这在移动端下会显得非常低效。
- 基于微服务架构中的设计及实现上的问题,为了在项目中简化前端的调用逻辑,同时也简化内部服务之间相互调用的复杂度,更好的保护内部服务,提出了网关的概念。
1.2 网关概述
- 网关本质上提供了一个各种服务访问的入口,并提供服务去接收或者转发所有内部和外部客户端的调用,还有就是权限认证,限流控制等等。
2、Spring Cloud Gateway
Spring Cloud Gateway
是Spring公司基于Spring5.0、Spring Boot 2.0 等技术开发的一个网关组件,它旨在为微服务架构提供一种有效和统一的API入口,负责服务请求路由、组合及协议转换,并且基于Filter链的方式提供了权限认证,监控,限流等功能。- 它是Spring Cloud官方推出的第二代网关框架,定位于取代 Netflix Zuul1.0。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的功能。
- 它是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。它不能在传统的 servlet 容器中工作,也不能构建成 war 包。
2.1 功能特性
- 性能强劲:是第一代网关Zuul的1.6倍。
- 功能强大:内置了很多实用的功能,例如:转发、监控、限流等。
- 动态路由:能够匹配任何请求属性,并且支持路径重写;
- 集成 Spring Cloud 服务注册发现功能(Nacos、Eruka);
- 可集成流控降级功能(Sentinel、Hystrix);
- 可以对路由指定易于编写的 Predicate(断言)和 Filter(过滤器);
2.2 核心概念
- 路由(
Route
):路由是网关中最基础的部分,路由信息包括一个路由id、一个目标url、一组断言工厂、一组过滤器组成。如果断言为真,则请求的URL和配置的路由成功匹配。 - 断言(
Predicate
):Java8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。 - 过滤器(
Filter
):SpringCloud Gateway中的filter分为Gateway FilIer和Global Filter。Filter可以对请求和响应进行处理。
2.3 工作原理
- Gateway Client向Gateway Server发送请求;
- 请求首先会被HttpWebHandlerAdapter进行提取组装成网关上下文;
- 然后网关的上下文会传递到DispatcherHandler,它负责将请求分发给RoutePredicateHandlerMapping;
- RoutePredicateHandlerMapping负责路由查找,并根据路由断言判断路由是否可用;
- 如果断言成功,由FilteringWebHandler创建过滤器链并调用;
- 请求会经过PreFilter–微服务–PostFilter的方法,最终返回响应;
3、 快速开始
- 通过网关作为服务的访问入口,来对系统中的服务进行访问。
- 例如,通过网关服务去访问sca-providers服务。
3.1 导入依赖
-
第一步:创建sca-gateway模块,在pom文件中引入gateway和nacos注册中心的依赖。
- 注意:可能会和spring-webmvc的依赖冲突,需要排除spring-webmvc
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- nacos服务注册与发现 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
3.2 编写配置文件
- 第二步:编写yml配置文件
server: port: 9000 spring: application: name: sca-gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: #配置网关路由规则 - id: route01 #路由id,全局唯一值,用来标识不同的路由信息 uri: http://localhost:8081/ # 客户端请求需要被转发到某个服务的地址 predicates: # 配置断言信息(就是路由转发时需要满足的条件) - Path=/nacos/provider/echo/** #设置路由转发的具体路径 filters: # 网关过滤器,用于修改请求和响应信息 - StripPrefix=1 # 转发之前去掉path中第一层路径,如nacos
3.3 测试
- 第三步:创建启动类
@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
- 第四步:启动项目进行测试。
- 依次启动sca-providers,sca-gateway服务,然后打开浏览器进行测试。
- 依次启动sca-providers,sca-gateway服务,然后打开浏览器进行测试。
4、负载均衡设计
4.1 为什么需要负载均衡
- 网关才是服务访问入口,所有服务都会在网关层面进行底层映射,所以,在访问服务时,要基于服务service id(服务名)去查找对应的服务,让请求从网关层进行均衡转发,以此来平衡服务实例的处理能力。
4.2 Gateway中负载均衡的实现
- 第一步:在项目中添加服务发现依赖。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
- 第二步:修改其配置文件。
server: port: 9000 spring: application: name: sca-gateway cloud: nacos: discovery: server-addr: localhost:8848 gateway: discovery: locator: enabled: true #开启通过服务名查找服务实例的特性,就可以按照网关地址/微服务名称/具体接口路径 的格式去访问 routes: #配置网关路由规则 - id: route01 #路由id,可自己指定一个唯一值 uri: lb://sca-providers # lb指从nacos中按照名称获取微服务,并遵循负载均衡策略 predicates: #匹配请求规则 - Path=/nacos/provider/echo/** #请求路径定义 filters: #网关过滤器,用于对请求规则中的内容进行判断分析以及处理 - StripPrefix=1 #转发之前去掉path中第一层路径,例如nacos
- 建议开发阶段,打开gateway日志,代码如下:
logging: level: org.springframework.cloud.gateway: debug
5、 Predicate 断言工厂
- 当请求gateway的时候, 使用断言对请求进行匹配, 如果匹配成功就路由转发, 如果匹配失败就返回404;
- 断言其本质就是定义路由转发的条件。
- 官网参考
5.1 内置断言工厂
- SpringCloud Gateway提供了一些内置的断言工厂,所有这些断言都与HTTP请求的不同属性进行匹配;
- 基于Datetime类型的断言工厂
- 此类型的断言根据时间判断,主要有三个:
- AfterRoutePredicateFactory:判断请求日期是否晚于指定日期。
- BeforeRoutePredicateFactory:判断请求日期是否早于指定日期。
- BetweenRoutePredicateFactory:判断请求日期是否在指定时间段内。
- After=2021-08-02T00:00:00.000+00:00[Asia/Shanghai] # 请求时间在设定时间之后才能访问 - Before=2021-08-12T00:00:00.000+08:00[Asia/Shanghai] # 请求时间在设定时间之前才能访问 ``
- 在请求时,如果与配置的时间不吻合,就会返回404 not found。
- 此类型的断言根据时间判断,主要有三个:
- 基于Header的断言工厂
- HeaderRoutePredicateFactory:接收两个参数,标题名称和正则表达式;
- 判断请求头Header是否具有给定名称,且值与正则表达式匹配。
- Header=X-Request-Id,\d+ #判断请求头是否具有给定名称且值与正则表达式匹配
- 基于Method请求方法的断言工厂
- MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
- Method=GET #设定请求方式为GET
- MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。
- 基于Query请求参数的断言工厂
- QueryRoutePredicateFactory:接收两个参数,请求参数和正则表达式;
- 判断请求参数是否具有给定名称,且值与正则表达式匹配。
- Query=pagesize,10 #接收请求参数param和给定值,判断是否匹配
- 基于远程地址的断言工厂
- RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中;
- RemoteAddr=192.168.1.1/24
- RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中;
- 基于Cookie的断言工厂
- CookieRoutePredicateFactory:接收两个参数,cookie 名字和一个正则表达式。
- 判断请求cookie是否具有给定名称,且值与正则表达式匹配。
-Cookie=chocolate, ch.
- 基于Host的断言工厂
- HostRoutePredicateFactory:接收一个参数,判断请求的Host是否满足匹配规则
-Host=**.testhost.org
- HostRoutePredicateFactory:接收一个参数,判断请求的Host是否满足匹配规则
- 基于Path请求路径的断言工厂
- PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
-Path=/foo/{segment}
- PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。
- 基于路由权重的断言工厂
- WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
routes: -id: weight_route1 uri: host1 predicates: -Path=/product/** -Weight=group3, 1 -id: weight_route2 uri: host2 predicates: -Path=/product/** -Weight= group3, 9
- WeightRoutePredicateFactory:接收一个[组名,权重], 然后对于同一个组内的路由按照权重转发
5.2 Predicate应用案例
- 配置ymal文件
server: port: 9000 spring: application: name: sca-gateway cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: discovery: locator: enabled: true routes: - id: route01 uri: lb://sca-providers predicates: - Path=/nacos/provider/echo/** #请求路径定义 - After=2021-08-02T00:00:00.000+00:00[Asia/Shanghai] - Before=2021-08-12T00:00:00.000+08:00[Asia/Shanghai] - Header=X-Request-Id,\d+ #判断请求头是否具有给定名称且值与正则表达式匹配 - Method=GET #设定请求方式为GET - Query=pagesize,10 #接收请求参数param和给定值,判断是否匹配 filters: - StripPrefix=1
5.3 自定义路由断言工厂
- 自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法。
- 在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
- 必要条件:
- 必须为Spring的组件Bean;
- 类名必须加上
RoutePredicateFactory
作为结尾; - 类必须继承
AbstractRoutePredicateFactory
; - 类中必须声明一个静态内部类 ,并定义相关属性来接收配置文件中对应的断言信息;
- 需要结合
shortcutFieldOrder
进行绑定; - 通过apply进行逻辑判断 true就是匹配成功 false匹配失败;
- 第一步:创建自定义断言工厂类:
@Component @Slf4j //注意:类名为 xxxxRoutePredicateFactory public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> { public CheckAuthRoutePredicateFactory() { super(Config.class); } //声明静态内部类,接收断言信息 public static class Config { private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("name"); } @Override public Predicate<ServerWebExchange> apply(Config config) { return new GatewayPredicate() { @Override public boolean test(ServerWebExchange serverWebExchange) { if(config.getName().equals("admin")){ return true; } return false; } }; } }
- 第二步:配置yml文件
spring: cloud: gateway: routes: - id: order_route #路由ID,全局唯一 uri: http://localhost:8020 #目标微服务的请求地址和端口 predicates: # 测试:http://localhost:8888/order/findOrderByUserId/1 - Path=/order/** #Path路径匹配 # 自定义CheckAuth断言工厂 - CheckAuth=admin
6、GatewayFilter 过滤器工厂
- 过滤器(Filter)就是在请求传递过程中,对请求和响应做一个处理。
- Gateway 的Filter从作用范围可分为两种:
GatewayFilter
与GlobalFilter
。- (1)GatewayFilter:应用于单个路由或者一个分组的路由上,称为 局部过滤器;
- (2)GlobalFilter:应用到所有的路由上,称为 全局过滤器;
6.1 局部过滤器
- 在SpringCloud Gateway中内置了很多不同类型的网关路由过滤器(
GatewayFilter
),可以进行一些业务逻辑处理器,比如添加剔除响应头,添加/去除参数等; - 官网参考
- AddRequestHeader:
- 作用:为原始请求添加Header
- 参数:Header的名称及值
- 例如:为原始请求添加一个名称为:X-Request-Foo,值为:Bar的请求头。
spring: cloud: gateway: routes: - id: add_request_header_route uri: https://example.org filters: - AddRequestHeader=X-Request-Foo, Hello
- AddRequestParameter:
- 作用:为原始请求添加请求参数及值。
- 参数:参数名称及值。
- 例如:为原始请求添加一个名称为:foo,值为:bar的参数。
spring: cloud: gateway: routes: - id: add_request_parameter_route uri: https://example.org filters: - AddRequestParameter=foo, bar
- PrefixPath:
- 作用:为原始的请求路径添加统一前缀路径
- 参数:前缀路径
- 例如:访问${GATEWAY_URL}/hello 会转发到uri:mypath/hello。
spring: cloud: gateway: routes: - id: prefixpath_route uri: https://example.org filters: - PrefixPath=/mypath
- RequestSize:
- 作用:设置允许接收最大请求包的大小
- 参数:请求包大小,单位为字节,默认值为5M
- 例如:如果请求包大小超过设置的值,则会返回 413 Payload Too Large以及一个errorMessage。
spring: cloud: gateway: routes: - id: request_size_route uri: http://localhost:8080/upload predicates: - Path=/upload filters: - name: RequestSize args: # 单位为字节 maxSize: 5000000
- RedirectTo:
- 作用:将原始请求重定向到指定的URL
- 参数:http状态码及重定向的url
- 例如:将原始请求重定向到百度
spring: cloud: gateway: #设置路由:路由id、路由到微服务的uri、断言 routes: - id: order_route #路由ID,全局唯一 uri: http://localhost:8020 #目标微服务的请求地址和端口 #配置过滤器工厂 filters: - RedirectTo=302, https://www.baidu.com/ #重定向到百度
- 自定义过滤器工厂:
- 类需要继承
AbstractNameValueGatewayFilterFactory
; - 类名必须要以
GatewayFilterFactory
结尾并且交给spring管理;
@Component @Slf4j public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory { @Override public GatewayFilter apply(NameValueConfig config) { return (exchange, chain) -> { log.info("调用CheckAuthGatewayFilterFactory===" + config.getName() + ":" + config.getValue()); return chain.filter(exchange); }; } }
- 配置自定义的过滤器工厂:
spring: cloud: gateway: #设置路由:路由id、路由到微服务的uri、断言 routes: - id: order_route #路由ID,全局唯一 uri: http://localhost:8020 #目标微服务的请求地址和端口 #配置过滤器工厂 filters: - CheckAuth=fox,男
- 类需要继承
6.2 全局过滤器
- 全局过滤器(
GlobalFilter
)作用于所有路由,无需配置。在系统初始化时加载,并作用在每个路由上。通过全局过滤器可以实现对权限的统一校验,安全性验证等功能。 - 一般内置的全局过滤器已经可以完成大部分的功能,但是对于企业开发的一些业务功能处理,还是需要我们自己编写过滤器来实现。
- 自定义全局过滤器:
- 例如,当客户端第一次请求服务时,服务端对用户进行信息认证(登录) ,通过数据库的比对,来验证登录信息是否正确来判断是否过滤信息。
/** *定义一个全局过滤器,模拟统一认证,Spring Cloud Gateway规范中,要求所有的全局过滤器必须实现GlobalFilter接口 */ public class AuthGlobalFilter implements GlobalFilter, Ordered { /** * 对请求进行过滤 * @param exchange (网关层面的过滤器,通过此对象获取请求,响应对象) * @param chain 过滤链(这个过滤链中包含0个或者多个过滤器) * @return Mono 代表Spring WebFlux技术中的0个或者一个响应序列 */ // http://localhost:9000/nacos/provider/echo/9000?username=admin @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { //1.获取请求对象 ServerHttpRequest request = exchange.getRequest(); ServerHttpResponse response = exchange.getResponse(); //2.获取请求参数数据,并进行数据响应 String username = request.getQueryParams().getFirst("username"); if(!"admin".equals(username)){ System.out.println("用户名不匹配,认证失败"); //设置状态码 response.setStatusCode(HttpStatus.UNAUTHORIZED);//401 //终止请求继续传递 return response.setComplete(); } //调用chain.filter继续向下执行 return chain.filter(exchange); } @Override public int getOrder() { return -200;//数字越小优先级越高 } }
- 启动Gateway服务,假如在访问中的url不带user="admin”这个参数,就会出现异常。
6.3 开启网关日志及跨域配置
- 要启用 Reactor Netty 访问日志,请设置
- -Dreactor.netty.http.server.accessLogEnabled=true
- Gateway跨域配置(CORS Configuration)
- 官网参考
- 通过yaml配置文件:
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedMethods: - GET - POST
- 通过Java配置类:
@Configuration public class CorsConfig { @Bean public CorsWebFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedMethod("*"); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser()); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }
7、Gateway整合Sentinel流控降级
- 网关是所有请求的公共入口,所以可以在网关进行限流,而且限流的方式也很多,我们可以采用Sentinel组件来实现网关的限流。Sentinel支持对SpringCloud Gateway、Zuul等主流网关进行限流。参考网址。
7.1 限流快速入门
- 第一步:添加依赖,在原有GateWay的依赖基础上再添加如下两个依赖。
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency>
- 第二步:添加sentinel的连接信息以及路由规则。
routes: #配置网关路由规则 - id: route01 #路由id,可自己指定一个唯一值 uri: lb://sca-providers #lb这里表示负载均衡,sca-providers为注册中心的服务名 predicates: #匹配请求规则 - Path=/nacos/provider/echo/** #请求路径定义 sentinel: transport: dashboard: localhost:8180 #Sentinel 控制台地址 port: 8092 #客户端监控API的端口 eager: true #取消Sentinel控制台懒加载,即项目启动即连接
- 第三步:启动网关项目,检测sentinel控制台的网关菜单。
- 启动时,添加sentinel的JVM参数,通过此菜单可以让网关服务在sentinel控制台显示不一样的菜单,代码如下:
-Dcsp.sentinel.app.type=1
- 启动时,添加sentinel的JVM参数,通过此菜单可以让网关服务在sentinel控制台显示不一样的菜单,代码如下:
- Sentinel控制台启动以来,界面如下图所示。
- 说明:假如没有发现请求链路,API管理,则可以关闭网关项目,关闭sentinel,重启。
- 第四步:在sentinel面板中设置限流策略。
- 第五步:通过url进行访问检测是否实现了限流操作
7.2 基于请求属性限流。
- 定义指定routeId的基于属性的限流策略如图所示:
- 通过postman进行测试分析:
7.3 自定义API维度限流
- 自定义API分组,是一种更细粒度的限流规则定义,它允许我们利用sentinel提供的API,将请求路径进行分组,然后在组上设置限流规则。
- 第一步:新建API分组,如图所示:
- 第二步:新建分组流控规则,如图所示:
- 第三步:进行访问测试,如图所示
- 第一步:新建API分组,如图所示:
7.4 自定义流控网关返回值
- 定义配置类,设计流控返回值,代码如下:
@Configuration public class GatewayConfig { public GatewayConfig(){ GatewayCallbackManager.setBlockHandler( new BlockRequestHandler() { @Override public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) { Map<String,Object> map=new HashMap<>(); map.put("state",429); map.put("message","two many request"); String jsonStr=JSON.toJSONString(map); return ServerResponse.ok().body(Mono.just(jsonStr),String.class); } }); } }
- 其中,Mono是一个发出(emit)0-1个元素的Publisher.
8.、总结
8.1 重点分析
- 网关(Gateway)诞生的背景?(第一:统一微服务访问的入口,第二:对系统服务进行保护,第三进行统一的认证,授权,限流)
- 网关的选型?(Netifix Zuul,Spring Cloud Gateway,…)
- Spring Cloud Gateway的入门实现(添加依赖,路由配置,启动类)
- Spring Cloud Gateway中的负载均衡?(网关服务注册,服务的发现,基于uri:lb://服务id方式访问具体服务实例)
- Spring Cloud Gateway中的断言配置?(掌握常用几个就可,用时可以通过搜索引擎去查)
- Spring Cloud Gateway中的过滤器配置?(掌握过滤器中的两大类型-局部和全局)
- Spring Cloud Gateway中的限流设计?(Sentinel)
8.2 FAQ分析
- Gateway在互联网架构中的位置?(nginx->gateway–>微服务–>微服务)
- Gateway底层负载均衡的实现?(Ribbon)
- Gateway应用过程中设计的主要概念?(路由id,路由uri,断言,过滤器)
- Gateway中你做过哪些断言配置?(after,header,path,cookie,…)
- Gateway中你用的过滤器有哪些?(添加前缀,去掉前缀,添加请求头,…,负载均衡,…)
- 503 异常?(服务不可用,检查你调用的服务是否启动ok,路由uri写的是否正确)
- 启动时解析.yml配置文件异常(格式没有对齐,单词写错)