一、背景
1.1、网关的背景
在分布式微服务架构中,某个服务可以会有多个实例来去注册到注册中心,那么如何去调用如此多个的服务也成为了一个比较大的问题。
此时客户端去调用服务就会出现以下问题:①客户端访问地址配置问题。②多个服务的认证授权问题,造成鉴权认证功能重复冗余情况。③服务访问量大造成的重构问题。
如何解决上面的问题呢?微服务引入了 网关 的概念,网关为微服务架构的系统提供简单、有效且统一的API路由管理,作为系统的统一入口,提供内部服务的路由中转,给客户端提供统一的服务,可以实现一些和业务没有耦合的公用逻辑,主要功能包含认证、鉴权、路由转发、安全策略、防刷、流量控制、监控日志等。
原本的对应服务客户端去直接调服务实例接口转变为统一走gateway网关来进行服务转发:
1.2、是否有网关的对比
没有网关:客户端直接访问我们的微服务,会需要在客户端配置很多的 ip:port,如果 user-service 并发比较大,则无法完成负载均衡。
试想:若是某个服务实例采用集群,那么我们在进行负载均衡配置时难道也要一一配置真实的服务地址嘛,那这就会出现很大的人力问题。
有网关:客户端访问网关,网关来访问微服务,(网关可以和注册中心整合,通过服务名称找到目标的 ip:prot)这样只需要使用服务名称即可访问微服务,可以实现负载均衡,可 以实现 token 拦截,权限验证,限流等操作 。
好处:使用网关呢,我们就不需要在nginx负载均衡中配置大量的服务实例地址,而是只需要配置gateway网关的集群地址,某个请求通过nginx走到网关,接着在网关进行服务发现+负载均衡去访问某个服务的实例。
1.3、网关的技术实现
本章节介绍其中的Gateway:是 Spring Cloud 官方提供的用来取代 zuul(netflix)的新一代网关组件
Zuul 1.0 : Netflix开源的网关,使用Java开发,基于Servlet架构构建,本质就是 web 组件 web 三大组件(监听器 过滤器 servlet)便于二次开发。因为基于Servlet内部延迟严重,并发场景不友好,一个线程只能处理一次连接请求。
性能:使用的是 BIO(Blocking IO) tomcat7.0 以前都是 BIO 性能一般。
Zuul 2.0 : 采用Netty实现异步非阻塞编程模型,一个CPU一个线程,能够处理所有的请求和响应,请求响应的生命周期通过事件和回调进行处理,减少线程数量,开销较小。
性能:性能好采用的是NIO,AIO 异步非阻塞,基于 spring5.x,springboot2.x 和 ProjectReactor 等技术。
GateWay : 是Spring Cloud的一个全新的API网关项目,替换Zuul开发的网关服务,基于Spring5.0 + SpringBoot2.0 + WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发,性能高于Zuul
Nginx+lua : 性能要比上面的强很多,使用Nginx的反向代码和负载均衡实现对API服务器的负载均衡以及高可用,lua作为一款脚本语言,可以编写一些简单的逻辑,但是无法嵌入到微服务架构中。
Kong : 基于OpenResty(Nginx + Lua模块)编写的高可用、易扩展的,性能高效且稳定,支持多个可用插件(限流、鉴权)等,开箱即可用,只支持HTTP协议,且二次开发扩展难,缺乏更易用的管理和配置方式
二、认识Springcloud Gateway
2.1、简介
Spring Cloud Gateway 是Spring Cloud的一个全新的API网关项目,目的是为了替换掉Zuul1。
技术选型:基于Spring5.0 + SpringBoot2.0 + WebFlux(基于⾼性能的Reactor模式响应式通信框架Netty,异步⾮阻塞模型)等技术开发。
性能方面:性能⾼于Zuul,官⽅测试,Spring Cloud GateWay是Zuul的1.6倍 ,旨在为微服务架构提供⼀种简单有效的统⼀的API路由管理⽅式。
特点:Spring Cloud Gateway 里明确的区分了 Router 和 Filter,并且一个很大的特点是内置了非常多的开箱即用功能,并且都可以通过 SpringBoot 配置或者手工编码链式调用来使用。
比如内置了 10 种 Router,使得我们可以直接配置一下就可以随心所欲的根据 Header、或者 Path、或者 Host、或者 Query 来做路由。
比如区分了一般的 Filter 和全局 Filter,内置了 20 种 Filter 和 9 种全局 Filter,也都可以直接用。当然自定义 Filter 也非常方便。
2.2、gateway的三大核心概念
三个核心:路由、断言、过滤器。
路由(Route):能够与注册中心来结合作动态路由。是GateWay中最基本的组件之一,表示一个具体的路由信息载体,主要有一个ID、一个目标URI、一组断言和一组过滤器来定义,具体如下:
id:路由唯一标识,区别于其他的route。
url: 路由指向的目的地URL,客户端请求最终被转发到的微服务。
order: 用于多个Route之间的排序,数值越小越靠前,匹配优先级越高。
predicate:断言的作用是进行条件判断,只有断言为true,才执行路由。
filter: 过滤器用于修改请求和响应信息。
断言(Predicate):返回一个bool类型,用于表示在不同状态情况下,该请求是否符合要求。输入的类型是一个ServerWebExchange,可以使用其来匹配HTTP请求的任何内容,例如headers、cookie等等。
过滤器(filter):在gateway中分为两种类型filter:①Gateway filter。②Global filter。
效果:能够对请求和响应进行修改处理。
两种路由各自用途:
①:一个是针对某一个路由(路径)的 filter,例如对某一个接口做限流。
②:一个是针对全局的 filter token ip 黑名单。
2.3、gateway的工作流程
执行流程如下:
1、Gateway Client 向 Spring Cloud Gateway 发送请求,请求首先会被 HttpWebHandlerAdapter 进行提取组装成网关上下文。
3、此时网关的上下文会传递到 DispatcherHandler ,它负责将请求分发给 RoutePredicateHandlerMapping。
4、RoutePredicateHandlerMapping 负责路由查找,并根据路由断言判断路由是否可用。
5、如果过断言成功,由 FilteringWebHandler 创建过滤器链并调用。
6、通过特定于请求的 Fliter 链运行请求,Filter 被虚线分隔的原因是Filter可以在发送代理请求之前(pre)和之后(post)运行逻辑。
7、执行所有pre过滤器逻辑。然后进行代理请求。发出代理请求后,将运行“post”过滤器逻辑。
8、处理完毕之后将 Response 返回到 Gateway 客户端。
针对于代理请求pre与post的分别应用场景:
Filter在pre类型的过滤器可以做参数效验、权限效验、流量监控、日志输出、协议转换等。
Filter在post类型的过滤器可以做响应内容、响应头的修改、日志输出、流量监控等
2.4、实际应用的服务架构
若是想要我们的网关达到高可用,那么就需要进行部署集群,并在gateway网关上层配置一个负载均衡层:
2.5、Nginx与Gateway的区别
Nginx:在做路由,负载均衡,限流之前,都有修改 nginx.conf 的配置文件,把需要负载均衡,路由,限流的规则加在里面。
需要手动配置。
gateway :gateway 自动的负载均衡和路由,gateway 和 eureka 高度集成,实现自动的路由,和 Ribbon 结合,实现了负载均衡(
lb),gateway 也能轻易的实现限流和权限验证。
区别:
1、Nginx需要去自行配置路由、负载均衡等规则;gateway有动态路由,可与注册中心结合使用。
2、Nginx(c语言)比gateway(gateway)的性能高一点。
3、Nginx是服务器级别的;Gateway是项目级别的。
比较合适的搭配效果如下:
三、实战1:搭建SpringCloud Gateway服务
项目版本:SpringBoot 2.3.12.RELEASE、SpringCloud Hoxton.SR12。
3.1、搭建基础Gateway服务,实现路由转发(暂无注册中心)
本小节只需要使用一个gateway与一个服务实例即可。
实现目标:网关实现路由转发的效果。(之后小结会进行动态路由的实现)
login-service:登录模块服务
当前该服务仅仅只要引入web模块即可,当前还没有涉及到注册中心。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
controller/LoginController.java:
package com.changlu.loginservice.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.UUID; /** * @Description: * @Author: changlu * @Date: 8:20 PM */ @RestController public class LoginController { @GetMapping("/doLogin") public String doLogin() { return UUID.randomUUID().toString(); } }
gateway-service:网关服务
依赖配置:对应springboot、springcloud版本号在三章节下有说明
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
配置文件:application.yaml
server: port: ${SERVER_PORT:81} # 默认是81端口,可以通过命令行读取参数 -DSERVER_PORT=82 spring: application: name: gateway-server cloud: gateway: enabled: true # 默认开启,只要加了网关依赖 routes: - id: login-service-route # 路由id,保持唯一 uri: http://localhost:8081 # uri predicates: - Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
实现:①开启网关(其实是默认开启的)。②编写路由。(若是要进行转发我们只需要配置好id、uri以及断言匹配路径)
id:表示的该组路由的id,需要是唯一的。
uri:表示匹配到的路由进行转发的地址。
predicates:当前是进行一个路径匹配的uri接口。
目前的话我们无需编写任何代码,就可以使用一个路由转发的效果,接下来我们来进行测试一下!
测试:访问路径http://localhost:81/doLogin,即可在gateway中进行转发到我们的login-service服务实例上。
注意:当前仅仅是路由转发指定的地址,并没有去注册中心拉取服务实例进行访问!
3.2、Gateway的两种路由配置方式(暂无注册中心)
方式一:代码路由方式
可参考官方案例:https://spring.io/projects/spring-cloud-gateway#overview
实现目标:路由转发到百度一下
可以看到百度的路由地址是:https://www.baidu.com/s?wd=123
思路:将/s即之后的内容转发到指定的百度网址,实际上与3.1中配置的大体参数类似
package com.changlu.config; import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @Description: * @Author: changlu * @Date: 1:15 PM */ @Configuration public class RouteConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() //路由 .route("baidu_route", r -> r.path("/s").uri("https://www.baidu.com/")) .build(); } }
ok,此时我们来测试一下:http://localhost:81/s?wd=123
方式二:yaml配置方式
实际上就是3.1中进行配置的内容:
spring: application: name: gateway-server cloud: gateway: enabled: true # 默认开启,只要加了网关依赖 routes: - id: login-service-route # 路由id,保持唯一 uri: http://localhost:8081 # uri predicates: - Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上 - id: user-service-route # 路由id,保持唯一 uri: http://localhost:8082 # uri predicates: - Path=/info/** # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上
对于下面的/info/,就是会匹配所有前缀为/info/*的内容
例如访问http://localhost:81/info/doLogin,实际上就会进行转发http://localhost:8081/info/doLogin
3.3、实现动态路由(搭配注册中心)
配置过程
之前3.1中案例并没有搭配注册中心,我们就以3.1中的案例来进行集成实现!
同样是对前两个进行改造:
两个服务都添加如下依赖及配置:
①引入依赖:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
②配置application.yaml添加eureka连接信息:
# 配置eureka eureka: client: service-url: defaultZone: http://localhost:8761/eureka instance: hostname: localhost instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
③开启服务发现:在启动器类上添加
@EnableEurekaClient //开启服务注册
最后在``gateway-server`服务的yaml配置里进行开启动态路由:
# 服务发现相关配置 discovery: locator: enabled: true # 开启动态路由 开启通用应用名称 找到服务的功能 lower-case-service-id: true # 开启服务名称小写,因为在eureka中默认服务名是大写的
当前我们已经实现了动态路由了,完全就只需要进行配置即可!
测试动态路由
我们来启动网关、生产者服务以及eureka注册中心:
注册中心的代码案例使用之前博文中的eureka案例:
在对login-service与gateway-server都开启了服务注册之后,以及开启了gateway-server的动态路由,我们就可以来实现根据服务名调用指定注册中心中的服务实例了。
再此之前我们进行路由代码配置访问的是:http://localhost:81/doLogin
针对于动态路由,我们在/doLogin前添加一个服务名,例如在eureka中心中login-service注册的服务名为login-service:http://localhost:81/login-service/doLogin
测试一下:ok能够进行测试访问
3.4、非动态路由的配置实现动态路由效果
之前3.1、3.2节是我们进行静态绑定的,那么我们如何来实现静态绑定的方式来达到动态路由的效果呢?
那么我们只需要对对应uri来进行操作即可:
将原本指明服务ip地址以及port端口的更改为负载均衡协议lb://服务名
# uri: http://localhost:8081 # uri uri: lb://login-service # 实现负载均衡
测试一下:
没得问题!
四、实战2:搭建Gateway集群
4.1、搭建详细过程
目标效果:访问nginx,通过nginx来进行负载均衡转发请求到集群gateway中,此时gateway里同样去搭配注册中心进行服务发现来进行负载均衡访问服务实例!下面就开始吧。
准备:nginx服务器+gateway两个不同端口(实际上是不同ip地址)服务+生产者服务实例。
生产者服务实例:使用的是login-service,也就是3.1节中的对外提供了一个接口。
gateway服务实例准备
直接使用的是3.1章节中的gateway,如何创建不同端口的多实例呢?
我们针对yaml配置文件来编写可接收命令传参:
server: port: ${SERVER_PORT:81} # 默认是81端口,可以通过命令行读取参数 -DSERVER_PORT=82
-DSERVER_PORT=82
接着我们来进行启动服务实例:
nginx配置
下载地址:http://nginx.org/en/download.html
我们打开nginx文件夹中的nginx.conf来进行编辑:
第一个框+第二个框:主要目的是为了能够查看到访问请求进行负载均衡的一个日志情况。
第三个框+第四个框时进行负载均衡配置。
配置如下:
log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"' '$connection $upstream_addr ' 'upstream_response_time $upstream_response_time request_time $request_time '; access_log logs/access.log main; upstream www.gateway.com { server localhost:81; server localhost:82; } server { location / { # root html; # index index.html index.htm; # 代理服务地址 proxy_pass http://www.gateway.com; } }
nginx相关的启动、关闭命令:
start nginx # 启动,下载好之后到指定目录下执行启动命令即可 nginx -s quit # 关闭nginx nginx -s reload # 优雅重启 # 若是想要停止服务还可以使用这个命令或者使用任务管理器找到nginx.exe来关闭 taskkill /IM nginx.exe /F # 关闭所有正在启动的nginx服务
4.2、测试集群
接下来我们来访问nginx的80端口:http://localhost/doLogin
那么我们如何来看负载均衡访问服务器的地址呢?
查看nginx的日志:``access.log`即可
可以看到默认的nginx负载均衡是轮训:
注意:默认的日志打印内容是没有访问服务器的地址的,在前面nginx中我是有进行自主配置加上打印的服务器地址这里才会显示的。
五、Predicate断言工厂
核心:Predicate 就是为了实现一组匹配规则,让请求过来找到对应的 Route 进行处理。
5.1、认识断言
在 gateway 启动时会去加载一些路由断言工厂**(判断一句话是否正确 一个 boolean 表达式** ) ,例如我们3.1中搭建的案例在启动时就会出现如下的一些断言信息:
本质:满足条件的返回true放行,不满足的false进行拦截。
介绍:Spring Cloud Gateway 将路由作为 Spring WebFlux HandlerMapping 基础架构的一部分进行匹配。Spring Cloud Gateway 包括许多内置的路由断言工厂。所有这些断言都与 HTTP 请求的不同属性匹配。您可以将多个路由断言可以组合使用。
源码:Spring Cloud Gateway 创建对象时,使用 RoutePredicateFactory 创建 Predicate 对象,Predicate 对象可以赋值给 Route。
5.2、断言详细配置
spring: application: name: gateway-server cloud: gateway: enabled: true # 默认开启,只要加了网关依赖 routes: - id: login-service-route # 路由id,保持唯一 # uri: http://localhost:8081 # uri uri: lb://login-service # 实现负载均衡 predicates: - Path=/doLogin # 匹配路径规则 只要你Path匹配上了/doLogin 就往 uri 转发 并且将路径带上 - After=2020-01-20T17:42:47.789-07:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之后的请求,ZonedDateTime dateTime=ZonedDateTime.now()获得 - Before=2020-06-18T21:26:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定 日期时间之前的请求 - Between=2020-06-18T21:26:26.711+08:00[Asia/Shanghai],2020-06-18T21:32:26.711+08:00[Asia/Shanghai] #此断言匹配发生在指定日期时间之间的请求 - Cookie=name,xiaobai #Cookie 路由断言工厂接受两个参数,Cookie 名称和 regexp(一 个 Java 正则表达式)。此断言匹配具有给定名称且其值与正则表达式匹配的 cookie - Header=token,123456 #头路由断言工厂接受两个参数,头名称和 regexp(一个 Java 正 则表达式)。此断言与具有给定名称的头匹配,该头的值与正则表达式匹配。 - Host=**.bai*.com:* #主机路由断言工厂接受一个参数:主机名模式列表。该模式是一 个 ant 样式的模式。作为分隔符。此断言匹配与模式匹配的主机头 - Method=GET,POST #方法路由断言工厂接受一个方法参数,该参数是一个或多个参数: 要匹配的 HTTP 方法 - Query=username,cxs #查询路由断言工厂接受两个参数:一个必需的 param 和一个 可选的 regexp(一个 Java 正则表达式)。 - RemoteAddr=192.168.1.1/24 #RemoteAddr 路由断言工厂接受一个源列表(最小大小 1), 这些源是 cidr 符号(IPv4 或 IPv6)字符串,比如 192.168.1.1/24(其中 192.168.1.1 是 IP 地址,24 是子网掩码)。
其他额外的包含有权重属性:下面
80%的请求,由 https://weighthigh.org 这个 url 去处理 20%的请求由 https://weightlow.org 去处理 spring: application: name: gateway-server cloud: gateway: enabled: true routes: - id: weight_high uri: https://weighthigh.org predicates: - Weight=group1, 2 # 权重 - id: weight_low uri: https://weightlow.org predicates: - Weight=group1, 8 # 权重
5.3、实战3:配置一个After断言
效果:指定某个接口只能在指定时间后才能够进行访问,否则无法访问,报出404异常。
配置内容如下:- After=2022-07-29T17:23:21.719+08:00[Asia/Shanghai]
可以看到当前时间是四点多,配置After是五点多,也就是说这个接口在五点多才能够访问,再此之前不能够访问!
测试效果:
5.4、自定义断言器
自定义步骤
1、编写一个断言工厂类:注意工厂类的名字尽量为xxxRoutePredicateFactory,因为之后配置文件要进行配置
package com.changlu.config; import lombok.Data; import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import java.util.function.Predicate; /** * @Description: * @Author: changlu * @Date: 4:38 PM */ @Component public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> { public CheckAuthRoutePredicateFactory() { super(Config.class); } //此时Config也就是自定义配置的一些参数 @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { System.out.println("当前进入到CheckAuthRoutePredicateFactory:" + config.getName()); return config.getName().equals("changlu"); }; } @Data static class Config { private String name; } }
2、yaml来进行配置
-name表示的是工厂名称。 args.name:其中的name就是工厂参数。 - name: CheckAuth #自定义路由断言工厂的名称xxxRoutePredicateFactory,这个xxx就是在这里指明 args: name: changlu1 #传入到自定义路由断言工厂的参数
测试
果然由于配置文件的name与在断言方法类中的值不一致,此时该接口就访问不到了
将name修改为changlu,再次尝试一下: