目录
码字不易,喜欢就点个关注❤,持续更新技术内容。相关资料请私信。
1 Nacos服务集群部署
1.1 服务集群部署介绍
我们都知道每个服务可以有多个实例,也就是将服务部署到多台服务器上。还可以根据不同地区可以分为不同的服务集群。这样的服务集群部署方案让多个实例并行地计算工作,提升整体的计算能力。
服务集群部署的优缺点:
-
优点:自下向上,缓解单个服务器承载能力,提升整体的计算能力,也可以缓解某个服务器的硬件或者软件故障问题。
-
缺点:自上向下,服务部署在多个服务器上,如果服务出现了程序问题,那么就需要重新进行服务部署等待问题。当然随着云原生无服务的发展,开发者无需管理服务器等基础设施了。
1.2 Nacos服务集群部署
首先还是在服务配置文件中进行修改,在nacos下添加如下配置:
discovery:
cluster-name: Wuhan # 配置集群名称
将整个微服务项目运行起来,打开nacos注册中心的控制页面,点击服务详情可以看到在Guangzhou集群启动的两个服务实例:
2 Nacos中服务集群的负载均衡
Nacos也集成Ribbon负载均衡组件,在搭建Eureka时讲过。
2.1 服务集群优先级
在服务访问的过程中,一般会选择从较近的服务集群挑选一个服务实例,所以我们可以为服务集群设置优先级。首先我们先将订单服务部署在Wuhan服务集群,将用户服务部署在Wuhan集群和Guangzhou集群。
首先发起多次访问,可以看到部署在Wuhan集群和Guangzhou集群的用户服务实例都被访问到了:
接下来配置集群的访问优先级,和之前Ribbon负载均衡策略配置一样,这也是一种负载均衡策略的配置,在orderservice配置文件中专门为userservice远程调用服务配置负载均衡策略NacosRule,这个规则默认优先访问本地集群:
# 针对专门的服务配置优先级
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
再进行多次访问,查看控制台的日志,可以看到只有部署在Wuhan集群的服务实例被访问,说明设置的策略规则生效了:
该策略优先访问本地集群,那么如果本地集群的机器都发生了故障下线了,再次访问调用该服务的接口时,会访问失败还是跨集群访问呢?
将用户服务部署在本地集群的服务器关闭下线:
然后访问订单服务的订单查询接口,该服务进行远程调用用户服务的用户查询接口,可以看到在订单服务中发生几次跨集群调用,且可以获得响应数据,说明还是可以调用的。
2.2 服务实例优先级
在服务部署时,服务要部署到不同的服务器上,不同的服务器的性能由于一些原因性能各有不同,所以我部署服务后可以为对应的服务实例设置权重(0-1),值越小权重越小,访问的概率也就越小。权重设置为0则完全不会被访问。
如下图在Wuhan集群部署两个服务实例,将一个服务实例的权重设置为0.2。这样当我们访问订单服务接口远程调用用户服务的查询接口时访问到8080服务器概率更大些。
2.3 环境隔离
Nacos服务存储和数据存储的最外层都是一个名为namespace的命名空间,用来做最外层隔离。public命名空间是nacos默认的保留空间。
命名空间的特征和作用:
-
每个命名空间都有一个唯一的ID
-
命名空间用来做环境隔离
-
不同命名空间下的服务不可见
启动的服务也都默认同属于public命名空间。
我们可以打开nacos的命名空间,新建命名空间,命名空间ID可以通过UUID自动随机生成,每个namespace都有一个唯一的ID:
成功新建一个命名空间,此时命名空间下还没有任何服务,此时将自动生成的ID复制下来。
接下来还是在服务的配置文件中spring下修改,我们在订单服务的命名空间粘贴刚刚复制的ver命名空间的ID:
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos服务地址
discovery:
cluster-name: Wuhan # 配置集群名称
namespace: 57760f8c-59ee-4768-b342-d781509e9a32 # 命名空间的ID
打开nacos服务控制页面,可以看到在public下只有userservice一个服务了,之前的orderservice去哪了呢。
打开右侧的ver命名空间,刚刚为订单服务设置的命名空间生效了:
试着访问订单服务的订单查询接口,远程调用处在不同命名空间的用户服务。后端发生了错误,在远程调用时发生了域名解析错误。所以说不同namespace下的服务不可见。
3 Nacos与Eureka的总结对比
3.1 注册中的的细节分析
当服务消费者需要调用服务提供者时,需要向注册中心拉取服务地址列表,拉取的动作不是每次都要做的,服务消费者会将拉取的服务信息缓存,当发起远程调用时直接从服务列表缓存中提取就行。但这会导致一个问题出现,如果调用的这个服务提供者实例挂了怎么办,那么就会导致远程调用失败。
首先服务提供者就采用了心跳检测发送健康状态。临时服务实例每隔一段时间会向服务注册中心发送一次心跳检测,当服务中心检测不到心跳之后会将该服务实例剔除掉。这是临时实例的心跳检测。而非临时实例,注册中心会通过http/tcp向微服务客户端主动发起健康检查,如果15秒内无心跳且健康检查失败则认为实例不健康,如果30秒内健康检查失败则剔除实例。
除了服务消费者定时拉取服务列表之外,nacos服务注册中心还会将新的服务列表主动推送到服务消费者。这就保证了服务列表的即时更新。
3.2 临时实例和非临时实例
服务注册到Nacos时,可以选择注册为临时或非临时实例,同样地在订单服务的配置文件中spring下添加如下配置:
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos服务地址
discovery:
cluster-name: Wuhan # 配置集群名称
ephemeral: false # 设置为非临时实例
重新启动订单服务,可以看到,订单实例变成了非临时实例。
试着把订单服务关掉,过一段时间再刷新nacos控制页面,可以看到订单服务掉线了,但是不会被剔除,nacos会一直等着它上线,除非我们在控制台手动删除了。
3.3 总结
-
Nacos和Eureka的共同特点:
都支持服务注册和服务拉取
都支持服务提供者通过心跳方式进行健康检测
-
Nacos和Ereka的区别:
Nacos支持服务中心主动检测提供者健康状态:临时实例采用心跳检测,非临时实例采用主动检测模式。
临时实例心跳不正常会被剔除,而非临时实例则不会被剔除
Nacos支持服务列表变更的消息推送模式push,服务列表更新更及时
Nacos集群默认采用AP方式,存在非临时实例时采用CP模式。Eureka采用AP模式
4 Nacos配置管理
4.1 Nacos配置服务添加
在企业生产环境下,一个大型分布式微服务系统会有很多微服务子项目,每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境,服务相关的一些配置文件如果需要做修改,那么需要对每个服务配置文件进行修改,然后重启服务,相当的麻烦。怎么对这些微服务配置进行管理呢?
我们可以通过统一的配置管理服务记录微服务重要的配置信息,服务启动时会从配置中心读取配置信息。只需要在配置中心修改配置信息,配置管理服务会自动通知其他微服务读取新的配置信息。这样也就完成了服务的热部署。
Nacos同时实现了注册中心、配置管理服务中心功能,注册发现、读取配置都是在Nacos上完成。点进nacos服务的配置管理控制台页面,点击加号添加配置管理服务。
在表单中,Data ID是要管理的配置文件名,放在配置中心的配置文件名必须唯一,一般先写相应服务名称,然后是开发环境dev。配置内容填写想要统一管理的配置信息。点击发布新建完成。
4.2 配置拉取
上面添加了配置管理服务,只完成了把配置交给nacos,接下来就是微服务如何拉取和读取配置服务了。
我们先来看一下以前的配置文件是怎样读取的:
项目启动 -> 读取本地配置文件application.yml -> 创建spring容器 -> 加载bean
而添加nacos配置服务管理后,配置文件的读取变成了这样:
项目启动 -> 读取bootstrap.yml中的nacos地址 -> 读取nacos中配置文件 -> 读取本地配置文件application.yml -> 创建spring容器 -> 加载bean
1.引入nacos的配置管理客户端依赖:
<!--nacos的配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
2.在微服务项目中的resource目录下添加一个bootstrap.yml文件,这个文件是引导文件,优先级高于application.yml,与nacos注册和发现中心相关的配置信息都应该放到bootstrap.yml中:
spring:
application:
name: userservice # 配置服务名称和eureka地址,方便注册
profiles:
active: dev
cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos地址
discovery:
cluster-name: Wuhan # 配置集群名称
config:
file-extension: yaml # 文件后缀名
4.2.1 测试
在控制器中利用@Value注解读取配置,然后定义一个Get接口,访问这个接口时返回以读取到的配置格式化的当前时间,
@RestController
@RequestMapping("/user")
public class UserController {
@Value("${pattern.dateformat}")
private String dateFormat;
@GetMapping("time")
public String time() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat));
}
}
启动服务,访问用户服务下获取时间的接口,可以看到用户服务的两个服务实例都可以获取到nacos配置管理服务中的配置信息:
4.2.2 总结
-
在Nacos配置管理服务中添加配置文件
-
在微服务中引入nacos的配置依赖
-
在微服务中添加bootstrap.yml,配置nacos地址、项目环境、服务名称、文件后缀名。这些决定程序启动时去配置中心读取的文件。
4.3 配置热部署
热部署能实现代码和服务配置信息的自动刷新。Nacos中的配置文件变更后,微服务无需重启就可以感知。不过需要通过下面两种配置实现:
1.方式一是在@Value注入的变量所在类上添加注解@RefreshScope,如之前进行配置信息读取的类。
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
// 从配置文件中获取配置信息进行注入
@Value("${pattern.dateformat}")
private String dateFormat;
@GetMapping("time")
public String time() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateFormat));
}
}
2.方式二是使用@ConfigurationProperties注解,新建一个配置类专门完成属性的加载,方便多个属性的注入:
@Data
@Component
@ConfigurationProperties(prefix = "pattern") // 前缀名与属性名拼接就完成属性的注入
public class PatternProperties {
private String dateformat;
private String envSharedValue;
private String name;
}
@Slf4j
@RestController
@RequestMapping("/user")
//@RefreshScope
public class UserController {
// @Value("${pattern.dateformat}")
// private String dateFormat;
// 直接注入配置类,完成全部属性的读取和注入
@Autowired
private PatternProperties properties;
@GetMapping("time")
public String time() {
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(properties.getDateformat()));
}
}
4.3.1 测试
1.方式一测试:
重启服务,然后访问该接口:
在nacos配置管理服务控制台直接编辑修改配置信息,然后发布:
直接刷新重新访问服务,刚刚修改的配置实现了自动刷新:
2.方式二测试:
修改之前的配置,然后发布:
刷新重新访问服务,刚刚修改的配置实现了自动刷新:
4.3.2 总结
nacos配置修改后,微服务可以实现热更新,有两种方式:
-
通过@Value注解注入,结合@RefreshScope来刷新
-
通过@ConfigurationProperties注入,自动刷新
注意:
-
不是所有的配置都适合放到配置中心,维护起来比较麻烦
-
建议将一些关键参数,要运行时调整的参数放到nacos配置中心,一般都是自定义配置
4.4 Nacos集群搭建
假设搭建三个服务节点的nacos集群:
因为现在只是学习测试阶段,所以三个nacos服务节点就不另找三台服务器了,所以就在同一机器上为三个nacos服务节点配置三个端口:
服务节点 | IP | PORT |
---|---|---|
nacos1 | localhost | 8845 |
nacos2 | localhost | 8847 |
nacos3 | localhost | 8850 |
集群搭建步骤:
-
搭建MySQL集群并初始化数据库表
-
下载解压nacos
-
修改集群配置(节点信息)、数据库配置
-
分别启动多个nacos节点
-
nginx反向代理
4.4.1 初始化数据库
nacos默认数据存储在内嵌数据库Derby中,不属于生产可用的数据库。官方推荐的最佳实际开发是搭建带有主从模式的高可用数据库集群。这里以点数据库为例。在数据库中新建nacos数据库。创建数据库表。
4.4.2 配置nacos集群
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf。首先在其中添加每个nacos服务节点的地址:
#it is ip
#在生产环境下就是三台机器的真实地址
127.0.0.1:8845
127.0.0.1:8847
127.0.0.1:8850
其次在application.properties文件中将端口改为8849作为第一个nacos服务节点,并修改数据库配置:
复制出另外两个服务节点,并分别将端口设置为8850和8851作为第二和第三个服务节点:
4.4.3 启动nacos集群
执行每个nacos服务节点的startup.cmd执行文件,不加-m standalone,默认以集群的方式启动:
4.4.4 Nginx负载均衡
要通过Nginx实现反向代理,首先解压nginx压缩包后,打开conf目录下nginx.conf的配置文件,在http中添加以下配置:
server {
listen 80;
server_name localhost;
location /nacos {
proxy_pass http://nacos-cluster;
}
}
upstream nacos-cluster {
server 127.0.0.1:8845;
server 127.0.0.1:8847;
server 127.0.0.1:8850;
}
直接点击nginx.exe可执行文件,打开后台任务管理器可以看到nginx启动成功:
那启动项目后如何将服务注册到Nginx实现负载均衡的nacos服务节点上呢,直接在微服务项目配置文件中修改nacos服务地址就行,默认为80端口的Nginx,Nginx会对三个nacos服务节点实现负载均衡。然后启动服务:
server-addr: 127.0.0.1 # nacos地址
最后直接访问localhost/nacos进入nacos服务控制页面。(我这里将nacos升级到了nacos2,SpringBoot版本坐标为2.3.12.RELEASE,SpringCloud版本为Hoxton.SR12,SpringCloudAlibaba版本为2.2.7.RELEASE)
4.4.5 总结
集群搭建步骤:
-
搭建MySQL集群并初始化数据库表
-
下载解压nacos
-
修改集群配置(服务节点信息)、数据库配置
-
分别启动多个nacos服务节点
-
nginx反向代理实现负载均衡
5 Feign远程调用代理组件
在微服务开发中,服务注册发现使用nacos实现,负载均衡使用ribbon实现,但是现有技术体系下的服务间调用存在以下问题,也是为什么我们需要使用Feign的原因:
-
编程体验不统一,代码可读性差
-
参数URL复杂,后期难于维护
-
难以响应需求的变化
所以在主流的微服务架构里,一般会在微服务植入Feign这种服务调用的代理组件。通过这个中间件的封装http请求和然后通过代理发送http请求,方便其他服务的调用。另外,Feign是基于Ribbon和Hystrix的声明式服务调用组件,在请求的底层其实被Ribbon的LoadBalancerInterceptor进行拦截解析服务,拉取服务,并在客户端实现负载均衡。
5.1 Feign介绍
首先先看一下之前基于RestTemplate发送http请求的远程接口调用,通过RestTemplate定义的服务接口地址是写死的,在启动类中的RestTemplate注入方法上添加@LoadBalanced注解后就可以实现自动解析,然后拉取服务,实现负载均衡。这种方式代码代码可读性差,URL参数复杂不好维护。
// 利用RestTemplate发起http请求,查询用户
// 1.url路径
String url = "http://userservice/user/" + order.getUserid();
// 2.发送http请求,实现远程调用
User user = restTemplate.getForObject(url, User.class);
Feign 是⼀个声明式轻量级的客户端HTTP请求框架。通过 接口 + 注解的方式发起HTTP请求调用,面向接口编程,而不是像Java中通过封装HTTP请求报文的方式直接调用。服务消费方拿到服务提供方的接口,然后像调用本地接口方法⼀样去调用,实际发出的是远程的请求。让我们更加便捷和优雅的去调用基于HTTP的 API,被广泛应用在 Spring Cloud 的解决方案中。
Feign的作用就是减少HTTP调用的复杂性,帮助我们优雅地发送http请求,实现远程调用。
Feign 最早是由 Netflix 公司进行维护的,后来 Netflix 不再对其进行维护,最终 Feign 由社区进行维护,更名为 OpenFeign。
官网地址:GitHub - OpenFeign/feign: Feign makes writing java http clients easier
5.2 Feign的使用
5.2.1 引入和使用
步骤:
引入依赖
添加@EnableFeignClient注解开启Feign功能
编写Feign客户端,FeignClient接口
调用FeignClient中定义的方法替代RestTemplate
首先第一步还是是在xml中添加Feign的依赖坐标,导入依赖。
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后就可以在启动类中添加@EnableFeignClient注解开启Feign功能。
@EnableFeignClients // 开启Feign功能
@MapperScan("com.bree.order.mapper")
@SpringBootApplication
// 可以自定义配置,后面讲
// @EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
编写Feign客户端,封装http请求,实现远程调用微服务接口。基于声明式的来定义远程调用的信息,userservice是进行远程调用的服务,Get是请求方式,还要写请求路径定位服务接口。传递用户id,返回User对象。
@FeignClient("userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") long id);
}
在业务从中注入该Feign客户端接口,直接调用查询用户信息的方法,返回一个用户对象。Feign是基于Ribbon和Hystrix的声明式服务调用组件,在请求的底层其实也是被Ribbon的LoadBalancerInterceptor进行拦截解析,并在客户端实现负载均衡。
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.调用Feign客户端的方法替代通过RestTemplate发送http请求
User user = userClient.findById(order.getUserid());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
启动服务:
访问订单服务调用订单查询接口,可以看到通过Feign进行http请求的封装,然后进行远程调用用户查询接口获取到了用户信息:
5.2.2 自定义配置
Feign可以自定义配置修改默认配置,以下是可以修改的配置(一般就配置一下日志级别):
配置 | 功能说明 |
---|---|
feign.Logger.Level | 修改日志级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | http远程调用的响应结果解析,如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数的编码类型 |
feign.Contract | 注解格式 |
feign.Retryer | 请求失败重试机制,默认没有 |
Feign的自定义配置有两种方式:
方式一:直接在项目配置文件中进行配置。
feign:
client:
config:
default: # default表示全局配置,也可以直接写专门针对的微服务的名称
loggerlevel: FULL # 日志级别
方式二:声明一个Bean,指定日志级别。
public class FeignClientConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return logger.Level.FULL
}
}
然后在启动类加Feign注解时,传入该配置参数,对全局的Feign客户端都生效:
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
或者在Feign客户端的注解中加上configuration = FeignClientConfiguration.class,针对该服务指定配置:
@FeignClient(value = "userservice", configuration = FeignClientConfiguration.class)
5.2.3 Feign性能优化
HttpURLConnection 是 Java 标准库中的一部分,是一种比较低级的方式来执行 HTTP 请求和处理 HTTP 响应。而常见的高级 HTTP 客户端请求库有HttpClient,RestTemplate,OKhttp,更高层次的封装有 Feign、Retrofit。
Feign的http客户端支持 3 种框架;HttpURLConnection、HttpClient、OKhttp;默认是HttpURLConnection。所以Feign的性能优化在于http连接的过程。在于使用高级的http连接池替代默认的URLConnection。
网络连接 | 说明 |
---|---|
URLConnection | Java 中用于建立和管理与远程服务器的连接的类,不支持连接池。 |
ApacheHttpClient | 成熟稳定,功能全面,支持连接池,Feign底层结合了HttpClient。 |
OKHttp | 轻量级,高性能,简单易用,支持连接池。 |
两台服务器建立 http 连接的过程是很复杂的一个过程,涉及到多个数据包的交换。Http 连接需要的 3 次握手 4 次分手开销很大,比较小的 Http 消息来说更大。
传统的 HttpURLConnection 是 JDK 自带的,并不支持连接池,如果要实现连接池的机制,还需要自己来管理连接对象。对于网络请求这种底层相对复杂的操作,如果有可用的其他方案,也没有必要自己去管理连接对象。
HttpClient 相比传统 JDK 自带的 HttpURLConnection,它封装了访问 http 的请求头,参数,内容体,响应等等;它不仅使客户端发送 HTTP 请求变得容易,而且也方便了开发人员测试接口(基于 Http 协议的),即提高了开发的效率,也方便提高代码的健壮性;另外高并发大量的请求网络的时候,还是用“连接池”提升吞吐量。
所以Apache HttpClient 或 OKHttp这种更高级的库就能更好的应对更复杂的网络操作。
选择使用 Apache HttpClient 还是 OKHttp 取决于具体需求和偏好。
-
如果需要一个功能丰富且成熟的库,可以选择 Apache HttpClient。
-
如果需要一个轻量级、现代化的库,并且对性能要求较高,可以选择 OKHttp。
-
无论哪种库,都可以用来执行 HTTP 请求和处理响应,具体选择取决于项目的需求和性能要求。
首先第一步还是引入HttpClient的依赖支持:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>11.8</version>
</dependency>
配置http通信连接池开启HttpClient功能:
feign:
client:
config:
default: # default表示全局配置,也可以直接写专门针对的微服务的名称
loggerlevel: BASIC # 日志级别,BASIC是基本的请求和响应信息
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大连接数,根据业务定义
max-connections-per-route: 50 # 每个请求路径的最大连接数
5.2.4 Feign的实践方案
如果多个消费者调用生成者接口,那么每个消费者都需要重复编写服务调用接口和熔断,有冗余也麻烦。下面介绍抽离服务调用接口和熔断成为API模块。将FeignClient抽取为独立的模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
首先创建一个feign-api服务模块,然后引入Feign客户端依赖:
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在订单服务引入feign的统一api:
<!--引入feign的统一api-->
<dependency>
<groupId>com.bree.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
添加feign的UserClient类,config配置类,User类。
@FeignClient(value = "userservice")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
@Data
public class User {
private Long id;
private String username;
private String address;
}
然后在订单服务启动类中还需要指定扫描加载的UserClient类,因为UserClient类移到了feign-api服务中,扫描不到,就不能加载UserClient进行调用。
@EnableFeignClients(clients = UserClient.class, defaultConfiguration = DefaultFeignConfiguration.class)
6 Gateway服务网关
6.1 服务网关介绍
这时后端微服务架构开发完成了,前端要向后端的API接口发送请求,但前端框架五花八门,无法像后端微服务模块那样植入Feign这种服务调用的代理组件。那前后端如何进行交互呢?这时就需要API服务网关了。
微服务提供的接口功能最终都要接入注册到服务网关,然后服务网关提供统一的出口地址,然后前端可以通过出口地址访问到后端微服务的接口。
API服务网关的主要作用有三个:
-
基本的服务路由请求,以及请求的负载均衡。
-
统一认证鉴权,避免多个服务分散鉴权造成的维护与开发的成本升高。
-
访问日志、限流、过滤、缓存、监控等公共服务也可以在网关上集中完成。
总的来说就是避免分散造成的开发及维护成本。
在SpringCloud中网关的实现有以下两种技术:
-
Zuul:Zuul是基于Servlet实现的,属于阻塞式编程。
-
Gateway:Gateway则是基于Spring5提供的WebFlux,属于响应式编程的实现,具备更好的性能。
6.2 搭建Gateway网关
6.2.1 搭建
搭建服务网关的步骤:
-
创建新的模块,引入SpringCloudGateway的依赖,以及nacos的服务注册发现依赖,因为网关本身也是属于一个微服务,也需要将自己注册到nacos和从naocs拉取服务列表。父工程springCloud中Gateway的依赖没有和springboot兼容版本的该版本,需要自己添加版本型号。
<!-- nacos客户端依赖包 --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!-- Gateway服务网关依赖 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> <version>3.1.8</version> </dependency>
-
添加启动类
@SpringBootApplication public class GatewayApplication { public static void main(String[] args) { SpringApplication.run(GatewayApplication.class, args); } }
-
在服务网关配置文件中添加nacos地址配置,以及路由配置,也就是将各个服务路由注册到服务网关中:
server: port: 10050 #客户端端口 spring: application: name: gateway # 配置服务名称 cloud: nacos: server-addr: 127.0.0.1:8848 # 配置nacos服务地址 gateway: routes: # 服务网关路由注册 - id: user-service # 路由id,自定义,唯一 uri: lb://userservice # 路由目标地址,lb就是负载均衡(也可以写http),后面是对应的服务名称。 predicates: # 断言,布尔表达式,路由断言,也就是路由判断,判断是否符合路由规则 - Path=/user/** - id: order-service # 再注册订单服务的网关路由 uri: lb://orderservice predicates: - Path=/order/**
6.2.2 测试
启动各个微服务,以及API网关服务。访问API网关服务,服务网关基于注册的路由规则进行判断,然后将请求路由转发到对应的服务,最后调用到相关的服务接口:
6.2.3 总结
搭建步骤:
-
创建网关服务项目,引入Gateway网关依赖,以及nacos注册发现中心,
-
在项目配置文件中配置网关服务信息、nacos地址、路由信息注册。路由配置信息有路由id(唯一)、路由转发目标(lb表示负载均衡)、路由规则。还可以配置路由过滤器,对请求和响应进行拦截处理。
6.3 Gateway路由断言工厂
路由断言工厂PredicateFactory的作用:读取用户定义的断言规则条件,对请求做出匹配和判断。如Path = /user/**路由断言规则就是指路由以/user开头的就匹配成功,认为是符合规则请求路由,转发成功。这就API网关Gateway基本的服务路由请求转发和限制功能。
在API网关Gateway配置文件中进行服务路由注册时,写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由转发判断的条件。如Path=/user/**按照路径判断的规则就是gateway中PathRoutePredicateFactory类进行处理判断的。
这样的Predicate断言规则SpringCloudGateway增加到了12种。我们可以打开官方网站仿照书写断言规则。
常见的断言规则如下:
路由断言规则 | 说明 | 示例 |
---|---|---|
Path路径 | 当请求路径为gate、rule开头时才会被转发。 | - Path = /order/** |
Before | 在某个时间之前的请求才会被转发。 | - Before=2035-05-20T08:00:00.000+08:00[Asia/Shanghai] |
After | 在某个时间之后的请求才会被转发。 | - After=2025-05-20T08:00:00.000+08:00[Asia/Shanghai] |
Between | 在某个时间之间的请求才会被转发。 | - Between=2025-05-20T08:00:00.000+08:00[Asia/Shanghai], 2035-05-20T08:00:00.000+08:00[Asia/Shanghai] |
Cookie | 请求必须包含某些Cookie。 | - Cookie=myCookie, newbee* |
Header | 携带指定参数或满足指定的请求头才会匹配转发。 | - Header=token, newbee* |
Host | 必须访问指定host域名。 | - Host=.somehost.org, .newbee.com |
Method | 指定请求方法匹配转发。 | - Method=POST,GET |
6.4 Gateway网关过滤器工厂
6.4.1 过滤器工厂
Gateway网关除了进行服务请求转发和限制的路由断言工厂,还有GetewayFilter过滤器,过滤器可以对进入网关的请求,或者对微服务的响应进行处理,简单来说就是请求拦截器和响应拦截器。
简单说过滤器的作用就是:对服务的请求或者响应进行加工处理,比如添加请求头字段。
过滤器工厂有30多种,在官网种有详细的语法使用说明,以以下几种为例:
过滤器 | 说明 | 示例 |
---|---|---|
AddRequestHeader | 在当前请求消息的请求头中添加某个头字段 | - AddRequestHeader=X-Request-red, blue |
RemoveRequestHeader | 移除当前请求消息的请求头中的某个头字段 | - RemoveRequestHeader=X-Request-Foo |
AddResponseHeader | 在当前响应消息的响应头中添加某个头字段 | - AddResponseHeader=X-Response-Red, Blue |
RemoveResponseHeader | 移除当前响应消息的响应头中的某个头字段 | - RemoveResponseHeader=X-Response-Foo |
... | ... | ... |
6.4.2 过滤器配置
-
在某个服务下配置过滤器只对当前服务的路由请求生效。
-
在Gateway中配置defaultFilters过滤器则对所有服务请求路由都会生效。
通过两种方式,在请求消息的请求头中添加一个name的头字段,可以在接口中通过@RequestHeader获取打印查看,也可以在浏览器的请求消息中查看。
server:
port: 10050 #客户端端口
spring:
application:
name: gateway # 配置服务名称
cloud:
nacos:
server-addr: 127.0.0.1:8848 # 配置nacos服务地址
gateway:
routes: # 服务网关路由注册
- id: user-service # 路由id,自定义,唯一
uri: lb://userservice # 路由目标地址,lb就是负载均衡(也可以写http),后面是对应的服务名称。
predicates: # 断言,布尔表达式,路由断言,也就是路由判断,判断是否符合路由规则
- Path=/user/**
filter: # 局部配置过滤器
- AddRequestHeader=name, My name is WMW
default-filters:
- AddRequestHeader=name, My name is WMW # 配置默认的全局过滤器
6.4.3 自定全局过滤器
与GatewayFilter一样,GlobalFilter全局过滤器的作用也是处理进入网关的请求和微服务的响应。
作用:对所有服务请求都生效的过滤器,并且可以自定义处理逻辑。
区别:GatewayFilter通过配置定义,处理逻辑固定,而GlobalFilter可以自己定义一些复杂的过滤器处理方式。
实现步骤:
-
实现GlobalFilter接口成为过滤器
-
添加@Order注解或实现Ordered接口定义过滤器拦截顺序,过滤器一定要有顺序。
-
编写处理逻辑,如验证一个请求消息中请求头的一个字段。结果是通过和不通过,验证通过是放行到下一个过滤器或者进入服务接口,不通过则直接响应状态码。
自定义GlobalFilter全局过滤器的方式是实现GlobalFilter接口:
@Order(-1) // 顺序注解,越小优先级越高,也可以通过实现Ordered接口返回-1,和注解是一个效果
@Component // 交给Spring初始化为bean,
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求参数
ServerHttpRequest request = exchange.getRequest(); // 先通过上下文获取请求对象
MultiValueMap<String, String> queryParams = request.getQueryParams();
// 获取参数中的token参数
String name = queryParams.getFirst("name"); // 取出第一个匹配的参数
// 判断参数值是否为。。。
if("xxx".equals(name)) {
// 放行
return chain.filter(exchange); // 调用过滤器链中的下一个过滤器
}
// 验证失败,设置失败状态码
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
// 返回响应,结束请求
return exchange.getResponse().setComplete();
}
}
6.4.4 过滤器执行顺序
请求进入网关会碰到三类过滤器:当前服务路由过滤器、默认全局过滤器DefaultFilter、自定义全局过滤器GlobalFilter。
请求进入网关后,会将当前服务路由过滤器、DefaultFilter、GlobalFilter合并到一个过滤器链(集合)中,排序后依次执行每个过滤器。当前服务路由过滤器和DefaultFilter都是同一种类型的过滤器(GatewayFilter),只是作用范围不同。而GlobalFilter过滤器,在网关中都可以被适配成GatewayFIlter类型的过滤器。那么这些过滤器就可以合并到一个过滤器链中(集合中)进行排序。
排序规则order:
由之前自定义全局过滤器GlobalFilter可以知道每一个过滤器都必须指定一个int类型的order值,GlobalFilter可以通过添加Order注解或者实现Ordered接口来指定order值,order值越小,优先级越高,执行顺序越靠前。
而GatewayFilter怎么指定呢,服务路由过滤器和DefaultFilter的order值是由Spring指定的,默认是按照声明顺序从1递增。
这样每一种过滤器都有了order值。order值越小越先执行,order值相同,按照以下顺序执行:DefaultFilter过滤器 > 服务路由过滤器 > GlobalFilter过滤器。
6.5 跨域问题
跨域包括:
-
域名相同,端口不同:localhost:8848、localhost:8080。
而跨域问题:浏览器禁止请求的发起者与服务端的AJAX跨域请求,请求被浏览器拦截的问题。
解决方案:CORS,网关处理跨域问题采用的同样是CORS方案,在API网关服务中的配置文件中进行配置即可:
spring:
cloud:
gateway:
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许跨域请求的网站
- "http://localhost:9524"
- "http://www.wmw.com"
allowedMethods: # 允许的Ajax跨域请求方法
- "GET"
- "POST"
- "PUT"
- "DELETE"
- "OPTIONS"
allowedHeaders: "*" # 允许携带任何请求头的请求
allowedCredentials: true # 允许携带Cookie的请求
maxAge: 604800 # 跨域检测的有效期,即这次跨域检测通过后浏览器不再向服务端发起询问的期限。