发布时间:2024-07-03 10:01
章节知识点
过去的互联网:
1:用户量不多
2:并发低
3:数据少
现在的互联网:
1:用户多
2:并发高
3:数据庞大
互联网架构从简到繁的演进经历了单体架构、分布式架构、SOA架构、微服务架构以及最新的service mesh的演进过程。
1)概念
早期互联网产品用户量少,并发量低,数据量小,单个应用服务器可以满足需要,这就是最早互联网架构。我们用一句话总结什么是单体架构:将业务的所有功能集中在一个项目中开发,部署为一个节点。
2)架构图
3)优缺点
#优点:
1)架构简单
2)部署成本低
#缺点:
1)耦合度高
1)概念
根据业务功能对系统进行拆分,每个业务模块称为一个服务。
2)架构图
3)优缺点
#优点
1)降低服务耦合度
2)有利于服务升级拓展
#缺点
1)维护成本增加
2)服务间调用复杂度增加
4)需要解决的问题
1)服务拆分粒度如何?
2)服务之间如何实现调用?
3)服务关系如何管理?
1)概念
微服务是系统架构的一种设计风格,将一个原本独立的服务拆分成多个小型服务,每个服务独立运行在在各自的进程中,服务之间通过 HTTP RESTful API 进行通信.每个小型的服务都围绕着系统中的某个耦合度较高的业务进行构建。
#微服务是一种经过良好设计的分布式架构方案,而全球的互联网公司都在积极尝试自己的微服务落地方案。其中在java领域最引人注目的是SpringCloud提供的方案。
2)架构图
3)微服务架构特征
单一职责:微服务拆分粒度更小,每个服务都应对唯一的业务能力,做到单一职责
自治:团队独立、技术独立、数据独立,独立部署和交付
面向服务:服务提供统一标准的接口,与语言无关、与技术无关
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
SpringCloud是目前国内使用最广泛的微服务技术栈。官网地址:https://spring.io/projects/spring-cloud。
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验:
单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统,后台管理系统,ERP,OA 中小级企业级应用
分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目,例如:京东、淘宝
微服务:一种良好的分布式架构方案
优点:拆分粒度更小、服务更独立、耦合度更低
缺点:架构非常复杂,运维、监控、部署难度提高
SpringCloud:SpringCloud是微服务架构的一站式解决方案,集成了各种优秀微服务功能组件
章节知识点
案例说明:管理员查询订单详情->根据订单id查询订单的同时,把订单所属的用户信息一起返回,如下图:
将资料\工程代码\springcloud-parent\sql脚本
中的cloud-order.sql
和cloud-user.sql
分别导入到两个数据库中。
将资料\工程\springcloud-parent
导入到IDEA中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgzxVCJt-1650383592028)(assets/image-20211027154853135.png)]
查询某用户详情信息:http://localhost:18081/user/1
查询某订单详情信息:http://localhost:18082/order/101
1)RestTemplate介绍
RestTemplate 是spring家族中一款基于http协议的组件(HttpURLConnection),他的作用就是:用来实现基于http的协议方式的服务之间的通信(也就是远程服务调用)。
RestTemplate 采用同步方式执行 HTTP 请求,底层使用 JDK 原生 HttpURLConnection API 。
#概念总结:RestTemplate是spring提供的一个用来模拟浏览器发送请求和接收响应的一个类,它能基于Http协议实现远程调用。
2)注册RestTemplate
在itheima-order
的OrderApp中注册
RestTemplate`:
3)远程调用
修改itheima-order
中的OrderServiceImpl
的findById
方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lkq1f5QT-1650383592029)(assets/image-20211027154731989.png)]
4)测试
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9YHxcTbx-1650383592030)(assets/1635929466603.png)]
服务提供者:一次业务中,被其它微服务调用的服务。(提供接口给其它微服务)
服务消费者:一次业务中,调用其它微服务的服务。(调用其它微服务提供的接口)
在上面案例中itheima-order
调用了itheima-user
提供的接口,所以itheima-order
是服务消费者,itheima-user
是服务提供者。
下面是RestTemplate部分源码,我们可以看到执行过程中采用了Http请求。
沿着RestTemplate.doExecute()
往下看相关源码:
一直往后跟踪,在SimpleBufferingClientHttpRequest类中的executeInternal方法中,可以发现会调用sun.net.www.protocol.http.HttpURLConnection.connect()
实现远程调用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sNs8HbvF-1650383592030)(assets/image-20211027154500316.png)]
按照上面调用流程,消费者调用服务者存在很多问题:
1:服务消费者该如何获取服务提供者的地址信息?
2:如果有多个服务提供者,消费者该如何选择?
3:消费者如何得知服务提供者的健康状态?
章节知识点
Eureka注册中心如何解决上面的问题?
Eureka工作原理
#1:消费者该如何获取服务提供者具体信息?
服务提供者启动时向eureka注册自己的信息
eureka保存这些信息
消费者根据服务名称向eureka拉取提供者信息
#2:如果有多个服务提供者,消费者该如何选择?
服务消费者利用负载均衡算法,从服务列表中挑选一个
#3:消费者如何感知服务提供者健康状态?
服务提供者会每隔30秒向EurekaServer发送心跳请求,报告健康状态
EurekaServer在90秒内没有接收到某个微服务节点的心跳,EurekaServer将会注销该微服务的节点
消费者就可以拉取到最新的信息
搭建EurekaServer服务步骤如下:
1)pom.xml
创建项目itheima-eurekaserver
,引入spring-cloud-starter-netflix-eureka-server
的依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
dependencies>
2)启动类
创建启动类com.itheima.EurekaServerApp,代码如下:
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApp {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApp.class,args);
}
}
3)核心配置文件application.yml
server:
port: 8001 #端口号
spring:
application:
name: eureka-server # 应用名称,会在Eureka中作为服务的id标识(serviceId)
eureka:
client:
register-with-eureka: false #是否将自己注册到Eureka中
fetch-registry: false #是否从eureka中获取服务信息
service-url:
defaultZone: http://localhost:8001/eureka
此时我们访问EurekaServer地址http://localhost:8001/
,效果如下:
将itheima-user服务注册到EurekaServer步骤如下:
1)pom.xml
在itheima-user
添加如下依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2)修改application.yml
修改itheima-user
的application.yml
,添加如下配置:
...
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
#以IP地址注册到服务中心
prefer-ip-address: true
#服务向eureka注册时,注册名默认:“IP名:应用名:应用端口名”
#现在配置:注册名:应用名:端口:项目版本号
instance-id: ${spring.application.name}:${server.port}:@project.version@
prefer-ip-address:true 效果图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ki5fM1yS-1650383592031)(assets/1636690773904.png)]
prefer-ip-address:flase 效果图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N4H9csP4-1650383592031)(assets/1636690910445.png)]
3)多实例启动
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9XPohZeB-1650383592032)(assets/1635929398644.png)]
分别启动3个服务配置,Eureka(http://localhost:8001/)信息如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VNPIgp8g-1650383592032)(images\1628586175690.png)]
itheima-order
虽然是消费者,但与itheima-user
一样都是eureka
的client
端,同样可以实现服务注册:
在itheima-order
项目引入spring-cloud-starter-netflix-eureka-client
的依赖
1)pom.xml
在itheima-order
的pom.xml
中引入如下依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2)修改application.yml
修改itheima-order
的application.yml
,添加如下配置:
server:
port: 18082
spring:
application:
name: itheima-order
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/springcloud-order?characterEncoding=UTF-8&&serverTimezone=GMT
username: root
password: 123456
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}:@project.version@
在itheima-order
完成服务拉取实现远程调用,服务拉取是基于服务名称获取服务列表,然后在对服务列表做负载均衡。
修改itheima-order
的OrderServiceImpl的代码,修改访问的url路径,用服务名代替ip、端口,代码如下:
在itheima-order项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:
在itheima-order工程启动类OrderApp中,开启负载均衡
/***
* 注册RestTemplate
*/
@Bean
@LoadBalanced//开启负载均衡
public RestTemplate restTemplate(){
return new RestTemplate();
}
我们访问http://localhost:18082/order/101
测试效果如下:
服务注册时默认使用的是主机名,如果我们想用ip进行注册,可以在客户端(提供者与消费者)中的application.yml添加配置:
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}:@project.version@
lease-renewal-interval-in-seconds: 30 #心跳周期,默认是30秒
lease-expiration-duration-in-seconds: 90 #心跳失败最长超时间,默认90秒
itheima-eurekaserver 服务端,可以关闭保护机制
eureka:
...
server:
enable-self-preservation: false # false关闭保护机制。 15分钟内,如果心跳成功率<85%,则启动保护(服务提供者列表不再变化)
搭建EurekaServer
服务注册
服务发现
章节知识点
Ribbon是什么?
Ribbon是Netflix发布的负载均衡器,有助于控制HTTP客户端行为。为Ribbon配置服务提供者地址列表后,Ribbon就可基于负载均衡算法,自动帮助服务消费者请求。
概念:Ribbon是基于Http协议请求的客户端负载均衡器,能实现很丰富的负载均衡算法。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ISbd8Zqj-1650383592033)(images\1628599474712.png)]
负载均衡流程如上图所示:
1:用户发起请求,会先到达itheima-order服务
2:itheima-order服务通过Ribbon负载均衡器从eurekaserver中获取服务列表
3:获取了服务列表后,轮询(负载均衡算法)调用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4BcSIUwp-1650383592033)(…/…/…/…/…/坚果云共享文件夹/我的坚果云/java study/images/1628599691173.png)]
轮询调用会涉及到很多负载均衡算法,负载均衡算法比较多,关系图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t67wJCaq-1650383592033)(images\1628599720713.png)]
Ribbon的负载均衡算法策略如下表:
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表来选择服务器。 |
AvailabilityFilteringRule | 对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的 属性进行配置。 |
WeightedResponseTimeRule | 为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule【默认】 | 以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。它是Ribbon默认的负载均衡规则。 |
BestAvailableRule | 忽略哪些短路的服务器,并选择并发数较低的服务器。 |
RandomRule | 随机选择一个可用的服务器。 |
RetryRule | 重试机制的选择逻辑 |
Ribbon负载均衡算法的使用有2种方式
代码方式
注册IRule接口的实现类(负载均衡算法):在itheima-order
的启动类中添加如下负载均衡注册代码:
/**
* 随机负载均衡算法
* @return
*/
@Bean
public IRule randomRule() {
return new RandomRule();
}
配置方式
为指定服务配置负载均衡算法:在itheima-order
的核心配置文件中添加如下配置:
#注意配置到跟节点
#指定服务使用指定负载均衡算法
itheima-user:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载均衡规则
从懒加载 变为 饥饿加载
Ribbon默认是采用懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载则会在项目启动时创建,降低第一次访问的耗时,在itheima-order
的核心配置文件中,添加如下配置开启饥饿加载:
#注意配置到跟节点
#饥饿加载
ribbon:
eager-load:
clients: itheima-user #开启饥饿加载
enabled: true #指定对user这个服务饥饿加载
Ribbon负载均衡规则
负载均衡自定义方式
饥饿加载
章节知识点
先来看我们以前利用RestTemplate发起远程调用的代码:
User user = restTemplate.getForObject("http://itheima-user/user/"+orderInfo.getUserId(), User.class);
存在下面的问题:
上面RestTemplate存在的问题可以使用Feign解决,那么什么是Feign?
Feign是一个声明式的http客户端,官方地址:https://github.com/OpenFeign/feign
其作用就是帮助我们优雅的实现http请求的发送,解决上面提到的问题。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X9SQDu18-1650383592034)(images\1628603540688.png)]
定义和使用Feign客户端的步骤如下:
1:引入依赖包 spring-cloud-starter-openfeign
2:添加注解@EnableFeignClients开启Feign功能
3:定义远程调用接口,在接口中指明远程调用的【服务名字】、【方法签名】
4:注入接口,执行远程调用(接口)
1)引入依赖
在itheima-order
中引入如下依赖:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
2)开启Feign功能
在itheima-order
的启动类OrderApplication
添加@EnableFeignClients
注解开启Feign功能,代码如下:
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//...其他略
}
3)定义远程调用接口
在itheima-order
中创建接口UserClient
,代码如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CO0IYucn-1650383592035)(assets/1636707016650.png)]
上图代码如下:在itheima-order工程中添加
package com.itheima.client;
import com.itheima.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* order调用user服务(代替了 String url = "http://itheima-user/user/" + orderInfo.getUserId();)
* 1.接口上使用@FeignClient(value="被调用服务名")
* 2.定义被调用接口中的方法(基于被调用的controller编写)
* 2.1 requestMapping中的路径必须是全路径(controller类上的+方法上的)
* 2.2 使用PathVariable注解,必须取别名
*/
@FeignClient(value = "itheima-user")
public interface UserClient {
/**
* 调用用户微服中controller的方法
*/
@GetMapping(value = "/user/{id}")
public User one(@PathVariable(value = "id") Long id);
}
主要是基于SpringMVC的注解来声明远程调用的信息,比如:
4)远程调用
修改itheima-order
的OrderServiceImpl.one()
方法,执行远程调用,代码如下:
@Autowired
private UserClient userClient;
/**
* 根据ID查询订单信息
*/
@Override
public OrderInfo findById(Long id) {
//1.查询订单
OrderInfo orderInfo = orderDao.selectById(id);
//2.根据订单查询用户信息->需要调用 【item-user】 服务
User user = userClient.one(orderInfo.getUserId());
//3.封装user
orderInfo.setUser(user);
//4.返回订单信息
return orderInfo;
}
Feign运行自定义配置来覆盖默认配置,可以修改的配置如下:
类型 | 作用 | 说明 |
---|---|---|
feign.Logger.Level | 修改日志级别 | 包含四种不同的级别:NONE、BASIC、HEADERS、FULL |
feign.codec.Decoder | 响应结果的解析器 | http远程调用的结果做解析,例如解析json字符串为java对象 |
feign.codec.Encoder | 请求参数编码 | 将请求参数编码,便于通过http请求发送 |
feign. Contract | 支持的注解格式 | 默认是SpringMVC的注解 |
feign. Retryer | 失败重试机制 | 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试 |
NONE:默认的,不显示任何日志
BASIC:仅记录请求方法、URL、响应状态码以及执行时间
HEADERS:除了BASIC中定义的信息以外,还有请求和响应的头信息
FULL:除了HEADERS中定义的信息之外,还有请求和响应的正文及元数据
SpringBoot日志配置;
要想让Feign日志生效,得结合着SpringBoot的日志配置一起使用
SpringBoot日志配置
logging:
level:
# feign 日志以什么级别监控哪个接口
com.itheima: debug
配置Feign日志有两种方式:
配置文件方式
全局生效
feign:
client:
config:
default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL #日志级别
局部生效
feign:
client:
config:
itheima-user: #指定服务
loggerLevel: FULL #日志级别
代码方式
注册日志级别
/**
* 注册日志级别
* @return
*/
@Bean
public Logger.Level feignLogLevel() {
return Logger.Level.FULL;
}
全局生效
#如果是全局配置,则把它放到@EnableFeignClients这个注解中
@EnableFeignClients(defaultConfiguration = FeignClientConfiguration.class)
局部生效
#如果是局部配置,则把它放到@FeignClient这个注解中
@FeignClient(value = "itheima-user",configuration = FeignClientConfiguration.class)
Feign底层的客户端实现:
因此优化Feign的性能主要包括:
Feign切换Apache HttpClient步骤如下:
1:引入依赖
2:配置连接池
1)引入依赖
在itheima-order
中引入如下依赖:
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
2)配置连接池
在itheima-order
的核心配置文件application.yml
中添加如下配置:
feign:
client:
config:
default: #这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: BASIC #日志级别
itheima-user: #指定服务
loggerLevel: BASIC #日志级别
httpclient:
enabled: true #开启feign对HttpClient的支持
max-connections: 200 #最大的连接数
max-connections-per-route: 50 #每个路径的最大连接数
方式一(继承):给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F8hh6BO2-1650383592036)(assets/1636819395042.png)]
方式二(抽取):将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5mEX8XQ7-1650383592036)(images\1628608818706.png)]
Feign最佳实现流程如上图所示:
实现最佳实践方式二的步骤如下:
1:创建itheima-api,然后引入feign的starter依赖 itheima-user依赖
2:将itheima-order中编写的UserClient复制到itheima-api项目中
3:在itheima-order中引入itheima-api的依赖
4:重启测试
1)引入依赖
创建itheima-api,然后引入feign的starter依赖 itheima-user依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
<dependency>
<groupId>com.itheimagroupId>
<artifactId>itheima-pojoartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
dependencies>
2)编写的UserClient
将itheima-order中编写的UserClient复制到itheima-api项目中
package com.itheima.client;
import com.itheima.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* order调用user服务(代替了 String url = "http://itheima-user/user/" + orderInfo.getUserId();)
* 1.接口上使用@FeignClient(value="被调用服务名")
* 2.定义被调用接口中的方法(基于被调用的controller编写)
* 2.1 requestMapping中的路径必须是全路径(controller类上的+方法上的)
* 2.2 使用PathVariable注解,必须取别名
*/
@FeignClient(value = "itheima-user")
public interface UserClient {
/**
* 调用用户微服中controller的方法
*/
@GetMapping(value = "/user/{id}")
public User one(@PathVariable(value = "id") Long id);
}
3)在itheima-order中引入itheima-api的依赖
<dependency>
<groupId>com.itheimagroupId>
<artifactId>itheima-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
4)当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。
有两种方式解决:
方式一:指定FeignClient所在包
@EnableFeignClients(basePackages = "com.itheima.user.feign")
方式二:指定FeignClient字节码
@EnableFeignClients(clients = {UserClient.class})
Feign的使用步骤
Feign的日志配置:
方式二是java代码配置Logger.Level这个Bean
Feign的优化
Feign的最佳实践:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g9CkbEz3-1650383592036)(images/1641084401666.png)]
在SpringCloud中网关的实现包括两种:
Zuul是基于Servlet的实现,功能不强,性能较低,是阻塞式 。
SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Gateway网关是我们服务的守门神,所有微服务的统一入口。
网关的核心功能特性:
权限控制:网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截。
路由和负载均衡:一切请求都必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡。
限流:当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大。****
跨域
下面,我们就演示下网关的基本路由功能。基本步骤如下:
1)创建微服务网关并引入依赖
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud-parentartifactId>
<groupId>com.itheimagroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>itheima-gatewayartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
project>
2)配置文件application.yml
server:
# 网关端口
port: 7001
spring:
application:
name: gateway
cloud:
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:18081 # 路由的目标地址 http就是固定地址
uri: lb://itheima-user # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
eureka:
client:
service-url:
# EurekaServer的地址
defaultZone: http://localhost:8001/eureka
instance:
prefer-ip-address: true
instance-id: ${spring.application.name}:${server.port}:@project.version@
lease-renewal-interval-in-seconds: 30 #心跳周期,默认是30秒
lease-expiration-duration-in-seconds: 90 #心跳失败最长超时间,默认90秒
我们将符合Path
规则的一切请求,都代理到 uri
参数指定的地址。
本例中,我们将 /user/**
开头的请求,代理到lb://itheima-user
,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
3)创建启动类com.itheima.GatewayApplication
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4)路由测试
重启网关,访问http://localhost:7001/user/1时,符合/user/**
规则,请求转发到uri:http://user/user/1,得到了结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6rg9eth7-1650383592037)(images/1636863455306.png)]
思考:
比如所有以/order
开始的请求交给itheima-order
服务,如何配置itheima-order
服务的路由请求?
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由
org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=.somehost.org,.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 |
我们只需要掌握Path这种路由工程就可以了。
上面使用案例如下:
spring:
application:
name: gateway
cloud:
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:18081 # 路由的目标地址 http就是固定地址
uri: lb://user # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
# 这里user最好和路径中的路径保持一致,如果不一致,需要配置:
#filters:
# - StripPrefix= 1 见下图
- id: order-service
uri: lb://order
predicates:
- Path=/order/**
- After=2020-12-30T23:59:59.789+08:00[Asia/Shanghai]
- Before=2022-12-30T23:59:59.789+08:00[Asia/Shanghai]
- Between=2020-12-30T23:59:59.789+08:00[Asia/Shanghai],2022-12-30T23:59:59.789+08:00[Asia/Shanghai]
- Cookie=uname, itheima
- Header=X-Request-Id, \d+
- Host=localhost:7001
- Method=GET,POST
- Query=uname,zhangsan
- RemoteAddr=127.0.0.1/16
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BTntBrX0-1650383592037)(images/image-20220105092125175.png)]
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
Spring提供了31种不同的路由过滤器工厂。例如:
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
下面我们以AddRequestHeader 为例来讲解。
需求:给所有进入itheima-order的请求添加一个请求头:Heima=szheima121 nb!
1)只需要修改gateway服务的application.yml文件,添加路由过滤即可:
spring:
cloud:
gateway:
routes: # 网关路由配置
- id: order-service # 路由id,自定义,只要唯一即可
#uri: http://127.0.0.1:18082 # 路由的目标地址 http就是固定地址
uri: lb://itheima-order # 路由的目标地址 lb就是负载均衡,后面跟服务名称
filters: # 过滤器
- AddRequestHeader=Heima,szheima119 nb! # 添加请求头
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/order/** # 这个是按照路径匹配,只要以/user/开头就符合要求
**注意:**当前过滤器写在order-service路由下,因此仅仅对访问order-service的请求有效。
2)修改itheima-order中OrderController
/**
* 查询订单信息
*/
@GetMapping(value = "/{id}")
public OrderInfo one(@PathVariable(value = "id") Long id, @RequestHeader("Heima") String head){
System.out.println("*********head********"+head);
return orderService.findById(id);
}
1)如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:
spring:
cloud:
gateway:
default-filters: # 默认过滤项
- AddRequestHeader= Heima,szheima119 nb!
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
#uri: http://127.0.0.1:18081 # 路由的目标地址 http就是固定地址
uri: lb://itheima-user # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
- id: order-service # 路由id,自定义,只要唯一即可
#uri: http://127.0.0.1:18082 # 路由的目标地址 http就是固定地址
uri: lb://itheima-order # 路由的目标地址 lb就是负载均衡,后面跟服务名称
# filters: # 过滤器
# - AddRequestHeader=Heima,szheima119 nb! # 添加请求头
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/order/** # 这个是按照路径匹配,只要以/user/开头就符合要求
过滤器的作用是什么?
① 对路由的请求或响应做加工处理,比如添加请求头
② 配置在路由下的过滤器只对当前路由的请求生效
③default-filters的作用是什么? 对所有路由都生效的过滤器
上一节学习的过滤器,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
定义方式是实现GlobalFilter接口。
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
在filter中编写自定义逻辑,可以实现下列功能:
需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:
如果同时满足则放行,否则拦截
实现:在gateway中定义一个过滤器:
package com.itheima.gateway.filters;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* 自定义全局过滤器
*/
@Component
public class AuthorizeFilter implements GlobalFilter {
/**
* -参数中是否有authorization,
* - authorization参数值是否为admin
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpResponse response = exchange.getResponse();
//1 获取请求头中authorization
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
//2 校验是否为admin
String authorization = queryParams.getFirst("authorization");
//3 如果是则放行
if("admin".equals(authorization)){
return chain.filter(exchange);
}
//4 如果不是则 禁止访问
//4.1 设置状态码
response.setStatusCode(HttpStatus.UNAUTHORIZED);
//4.2 设置请求已经完成
return response.setComplete();
}
}
响应提示信息:
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
return response.writeWith(Flux.just(response.bufferFactory().wrap("请您先登录!".getBytes(Charset.forName("UTF-8")))));
测试:
http://localhost:7001/order/101?authorization=admin
http://localhost:7001/user/1?authorization=admin
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Hk4TwVh-1650383592038)(images/1636990785234.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqNhijdD-1650383592038)(images/1636990964684.png)]
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器:
排序的规则是什么呢?
详细内容,可以查看源码:
org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#getFilters()
方法是先加载defaultFilters,然后再加载某个route的filters,然后合并。
org.springframework.cloud.gateway.handler.FilteringWebHandler#handle()
方法会加载全局过滤器,与前面的过滤器合并后根据order排序,组织过滤器链
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Uwr9fcjs-1650383592039)(images/1636984290899.png)]
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
找到资料nginx-1.18.0跨域测试.7z 解压放到没有空格和中文目录下,启动并访问。
可以在浏览器控制台看到下面的错误:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-niPRNgTw-1650383592039)(images/1636986996376.png)]
从localhost:8090访问localhost:7001,端口不同,显然是跨域的请求。
在gateway服务的application.yml文件中,添加下面的配置:
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许所有跨域请求
- "*"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
1.为什么需要网关?
2.掌握gateway网关搭建
会配置网关路由过滤器
会配置网关全局过滤器
会解决跨域问题
国内公司一般都推崇阿里巴巴的技术,比如注册中心,SpringCloudAlibaba也推出了一个名为Nacos的注册中心。
Nacos是阿里巴巴的产品,现在是SpringCloud中的一个组件。相比Eureka功能更加丰富,在国内受欢迎程度较高。
安装方式可以参考课前资料《Nacos安装指南.md》
nacos访问地址:http://localhost:8848/nacos
控制台账号:nacos 密码:nacos
Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册、服务发现规范。因此使用Nacos和使用Eureka对于微服务来说,并没有太大区别。
主要差异在于:
1)引入依赖
在springcloud-parent
父工程的pom文件中的
中引入SpringCloudAlibaba的依赖:
<dependencyManagement>
<dependencies>
.......
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.6.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
然后在itheima-user和itheima-order中的pom文件中引入nacos-discovery依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
注意:不要忘了注释掉eureka的依赖。
2)配置nacos地址
在user-service和order-service的application.yml中添加nacos地址:
spring:
cloud:
nacos:
server-addr: localhost:8848
注意:不要忘了注释掉eureka的地址
3)itheima-order修改OrderController
去掉请求头参数
package com.itheima.controller;
import com.itheima.order.pojo.OrderInfo;
import com.itheima.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping(value = "/order")
public class OrderController {
@Autowired
private OrderService orderService;
/**
* 查询订单信息
*/
@GetMapping(value = "/{id}")
public OrderInfo one(@PathVariable(value = "id") Long id){
return orderService.findById(id);
}
}
4)重启
重启微服务后,登录nacos管理页面,可以看到微服务信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bWdUTVS1-1650383592040)(images/1636996840889.png)]
5)测试
访问:http://localhost:18082/order/101 http://localhost:18081/user/1
一个服务可以有多个实例,例如我们的itheima-user,可以有:
假如这些实例分布于全国各地的不同机房,例如:
Nacos就将同一机房内的实例 划分为一个集群。
也就是说,itheima-user是服务,一个服务可以包含多个集群,如深圳、杭州,每个集群下可以有多个实例,形成分级模型,如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fO4QayGT-1650383592040)(images/1636997762904.png)]
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Wa0eUXgr-1650383592041)(images/1636997936128.png)]
深圳机房内的itheima-order应该优先访问同机房的itheima-user。
修改itheima-user的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SZ # 集群名称
重启两个user-service实例后,我们可以在nacos控制台看到下面结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mmp4fgqQ-1650383592041)(images/1636998478729.png)]
我们再次复制一个itheima-user启动配置,添加属性:
-Dserver.port=38081 -Dspring.cloud.nacos.discovery.cluster-name=HZ
配置如图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7ets58M-1650383592042)(images/1636998675210.png)]
启动UserApp3后再次查看nacos控制台:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iV5p2BSA-1650383592042)(images/1636998903744.png)]
默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡。
因此Nacos中提供了一个NacosRule
的实现,可以优先从同集群中挑选实例。
1)给order-service配置集群信息
修改order-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SZ # 集群名称
2)修改负载均衡规则
修改itheima-order的application.yml文件,修改负载均衡规则:
itheima-user:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
实际部署中会出现这样的场景:
服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重:
在弹出的编辑窗口,修改权重:
注意:如果权重修改为0,则该实例永远不会被访问
Nacos提供了namespace来实现环境隔离功能。
默认情况下,所有service、data、group都在同一个namespace,名为public:
我们可以点击页面新增按钮,添加一个namespace:
然后,填写表单:
就能在页面看到一个新的namespace:
给微服务配置namespace只能通过修改配置来实现。
例如,修改itheima-order的application.yml文件:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SZ
namespace: devnamespace # 命名空间,填ID
重启itheima-order后,访问控制台,可以看到下面的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HtkYMkOW-1650383592043)(images/1631822989268.png)]
此时访问itheima-order,因为namespace不同,会导致找不到user,控制台会报错:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-50Apeui0-1650383592043)(images/image-20210714000941256.png)]
在itheima-user修改配置:
-Dspring.cloud.nacos.discovery.namespace=devnamespace
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YuAUMgZW-1650383592044)(images/1637003839496.png)]
重启itheima-user后,访问控制台,可以看到下面的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-reeYmk0K-1650383592044)(images/1637003966048.png)]
此时访问itheima-user,因为namespace相同,找到itheima-user:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rvxaRKGo-1650383592044)(images/1637004043157.png)]
Nacos的服务实例分为两种l类型:
配置一个服务实例为永久实例:
spring:
cloud:
nacos:
discovery:
ephemeral: false # 设置为非临时实例
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wFPFleZC-1650383592045)(images/1631823118790.png)]
Nacos除了可以做注册中心,同样可以做配置管理来使用。
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IQk8qZjX-1650383592045)(images/image-20210714164426792.png)]
Nacos一方面可以将配置集中管理,另一方可以在配置变更时,及时通知微服务,实现配置的热更新。
如何在nacos中管理配置呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HFRoS8Ei-1650383592045)(images/1631823734187.png)]
然后在弹出的表单中,填写配置信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-34Ww3q5B-1650383592046)(images/1637002210728.png)]
配置内容:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DpV4Ebfb-1650383592046)(images/1637032854943.png)]
注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。
微服务要拉取nacos中管理的配置,并且与本地的application.yml
配置合并,才能完成项目启动。
但如果尚未读取application.yml
,又如何得知nacos地址呢?
因此spring引入了一种新的配置文件:bootstrap.yaml
文件,会在application.yml
之前被读取,流程如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIP32sQS-1650383592046)(images/L0iFYNF.png)]
1)引入nacos-config依赖
首先,在user中,引入nacos-config的客户端依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
2)修改application.yaml
将application.yaml 修改为 bootstrap.yaml,并修改配置完整内容如下:
server:
port: 18081
spring:
application:
name: itheima-user
profiles:
active: dev #开发环境,这里是dev
datasource:
driverClassName: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud_user?characterEncoding=UTF8&&serverTimezone=Asia/Shanghai
username: root
password: admin
cloud:
nacos:
server-addr: localhost:8848
config:
file-extension: yaml # 文件后缀名
server-addr: localhost:8848 #nacos配置中心地址
namespace: devnamespace
discovery:
cluster-name: SZ
#ephemeral: false # 设置为非临时实例
namespace: devnamespace
这里会根据spring.cloud.nacos.server-addr获取nacos地址,再根据
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
作为文件id,来读取配置itheima-user-dev.yaml
:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N6YCnVon-1650383592047)(images/1637032645231.png)]
3)读取nacos配置
在user-service中的UserController中,读取uname配置:
@Value("${user.uname}")
private String uname;
@GetMapping("name")
public String now(){
return "********"+uname+"*********";
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RKYDYfqM-1650383592047)(images/1637032872733.png)]
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
要实现配置热更新,可以使用两种方式:
在@Value注入的变量所在类上添加注解@RefreshScope:
@RestController
@RequestMapping(value = "/user")
@RefreshScope //刷新配置
public class UserController {
使用@ConfigurationProperties注解代替@Value注解(此处需要多加个@Setter )。
在itheima-user服务中处理如下:
@RestController
@RequestMapping(value = "/user")
//@RefreshScope //注释掉
@ConfigurationProperties(prefix = "user") //配置读取注解,并未uname添加set方法
@Setter
public class UserController {
@Autowired
private UserService userService;
//@Value("${user.uname}") //去掉@Value("${xx}")
private String uname;
@GetMapping("name")
public String now(){
return "********"+uname+"*********";
}
........
}
其实微服务启动时,会去nacos读取多个配置文件,例如:
[spring.application.name]-[spring.profiles.active].yaml
,例如:itheima-user-dev.yaml[spring.application.name].yaml
,例如:itheima-user.yaml而[spring.application.name].yaml
不包含环境,因此可以被多个环境共享(dev test pro环境)。
下面我们通过案例来测试配置共享
1)添加一个环境共享配置
我们在nacos dev 环境 itheima-user.yaml文件:
用的什么环境就在那个环境中增加
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NjEldmmD-1650383592048)(images/1637038248474.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LgzQwezF-1650383592048)(images/image-20220304170355252.png)]
2)在user-service中读取共享配置
在user-service服务中,修改UserController
@RestController
@RequestMapping(value = "/user")
//@RefreshScope //注释掉
@ConfigurationProperties(prefix = "user")
@Setter
public class UserController {
@Autowired
private UserService userService;
//@Value("${user.uname}")
private String uname;
private String sysname;
@GetMapping("name")
public String now(){
return "********"+uname+"*********"+sysname+"*********";
}
............
}
3)运行两个UserApp,使用不同的profile
UserApp(18081)使用的profile是dev,UserApp2(28081)使用的profile是test。
修改UserApp2这个启动项,改变其profile值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wql0qOJB-1650383592048)(images/1637038773343.png)]
启动UserApp和UserApp2,访问http://localhost:18081/user/name,结果:
访问http://localhost:28081/user/name,结果:
可以看出来,不管是dev,还是test环境,都读取到了sysname这个属性的值。
4)配置共享的优先级
当nacos、服务本地同时出现相同属性时,优先级有高低之分:
itheima-user-dev.yaml > itheima-user.yaml > application.yml
Nacos生产环境下一定要部署为集群状态,部署方式参考课前资料中的文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bAZ60Y8K-1650383592049)(images/1641170823623.png)]
微服务中,服务间调用关系错综复杂,一个微服务往往依赖于多个其它微服务。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-etHRWTxJ-1650383592049)(images/1533829099748.png)]
如图,如果服务提供者I发生了故障,当前的应用的部分业务因为依赖于服务I,因此也会被阻塞。此时,其它不依赖于服务I的业务似乎不受影响。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4ukPqHcI-1650383592050)(images/1533829198240.png)]
但是,依赖服务I的业务请求被阻塞,用户不会得到响应,则tomcat的这个线程不会释放,于是越来越多的用户请求到来,越来越多的线程会阻塞:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t6psf2Vp-1650383592050)(images/1533829307389.png)]
服务器支持的线程和并发数有限,请求一直阻塞,会导致服务器资源耗尽,从而导致所有其它服务都不可用,那么当前服务也就不可用了。
那么,依赖于当前服务的其它服务随着时间的推移,最终也都会变的不可用,形成级联失败,雪崩就发生了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NpL8fyYb-1650383592050)(images/image-20210715172710340.png)]
解决雪崩问题的常见方式有四种:
•超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止等待
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYFBiPSd-1650383592051)(images/image-20210715172820438.png)]
方案2:仓壁模式(线程池隔离)
仓壁模式来源于船舱的设计:
船舱都会被隔板分离为多个独立空间,当船体破损时,只会导致部分空间进入,将故障控制在一定范围内,避免整个船体都被淹没。
于此类似,我们可以限定每个业务能使用的线程数,避免耗尽整个tomcat的资源,因此也叫线程隔离。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uH3v36Hn-1650383592051)(images/image-20210715173215243.png)]
断路器模式:由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。
断路器会统计访问某个服务的请求数量,异常比例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WaI90x6H-1650383592051)(images/image-20210715173327075.png)]
当发现访问服务D的请求异常比例过高时,认为服务D有导致雪崩的风险,会拦截访问服务D的一切请求,形成熔断:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nEvc4Pop-1650383592052)(images/image-20210715173428073.png)]
流量控制:限制业务访问的QPS(每秒处理请求的多少),避免服务因流量的突增而故障。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G2YP4340-1650383592052)(images/image-20210715173555158.png)]
什么是雪崩问题?
如何避免因瞬间高并发流量而导致服务故障?
如何避免因服务故障引起的雪崩问题?
在SpringCloud当中支持多种服务保护技术:
早期比较流行的是Hystrix框架,但目前国内实用最广泛的还是阿里巴巴的Sentinel框架,这里我们做下对比:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZH3bSEQ9-1650383592052)(images/1637051375716.png)]
Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html
Sentinel 具有以下特征:
•丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
•完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
•广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
•完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
1)下载
sentinel官方提供了UI控制台,方便我们对系统做限流设置。大家可以在GitHub下载。
课前资料也提供了下载好的jar包:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmvC4C6S-1650383592053)(images/image-20210715174252531.png)]
2)运行
将jar包放到任意非中文目录,执行命令:
java -jar sentinel-dashboard-1.8.1.jar
如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:
配置项 | 默认值 | 说明 |
---|---|---|
server.port | 8080 | 服务端口 |
sentinel.dashboard.auth.username | sentinel | 默认用户名 |
sentinel.dashboard.auth.password | sentinel | 默认密码 |
例如,修改端口:
java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar
3)访问
访问http://localhost:8090页面,就可以看到sentinel的控制台了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zl6p6XFO-1650383592053)(images/image-20210715190827846.png)]
需要输入账号和密码,默认都是:sentinel
登录后,发现一片空白,什么都没有:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f9EwLytk-1650383592053)(images/1637225046736.png)]
这是因为我们还没有与微服务整合。
我们在itheima-order中整合sentinel,并连接sentinel的控制台,步骤如下:
1)引入sentinel依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
2)配置控制台
修改application.yaml文件,添加下面内容:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
3)访问itheima-order的任意端点
打开浏览器,访问http://localhost:18082/order/101,这样才能触发sentinel的监控。
然后再访问sentinel的控制台,查看效果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dJD6uwar-1650383592054)(images/1637057546846.png)]
雪崩问题虽然有四种方案,但是限流是避免服务因突发的流量而发生故障,是对微服务雪崩问题的预防。我们先学习这种模式。
当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源。
默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点(Endpoint)就是调用链路中的一个资源。
例如,我们刚才访问的itheima-order中的OrderController中的端点:/order/{id}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKy9xh00-1650383592054)(images/1637063254238.png)]
流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:
点击资源/order/{id}后面的流控按钮,就可以弹出表单。
表单中可以填写限流规则,如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1UtsiOP-1650383592054)(images/1637063385702.png)]
其含义是限制 /order/{id}这个资源的单机QPS为2,即每秒只允许2次请求,超出的请求会被拦截并报错。
需求:给 /order/{id}这个资源设置流控规则,QPS不能超过 5,然后测试。
1)首先在sentinel控制台添加限流规则
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JqLRRJQy-1650383592054)(images/1637063536857.png)]
2)利用jmeter测试
如果没有用过jmeter,可以参考课前资料提供的文档《Jmeter快速入门.md》
课前资料提供了编写好的Jmeter测试样例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e8Wny9PK-1650383592055)(images/image-20210715200431615.png)]
打开jmeter,导入课前资料提供的测试样例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hvoJjpKj-1650383592055)(images/image-20210715200537171.png)]
选择:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HjgjBmcd-1650383592055)(images/image-20210715200635414.png)]
20个用户,2秒内运行完,QPS是10,超过了5.
选中流控入门,QPS<5
右键运行:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hcSLqOn2-1650383592055)(images/image-20210715200804594.png)]
注意,不要点击菜单中的执行按钮来运行。
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wEuX6Qw2-1650383592056)(images/image-20210715200853671.png)]
可以看到,成功的请求每次只有5个
在添加限流规则时,点击高级选项,可以选择三种流控模式:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JS6aoN0o-1650383592056)(images/1637227103023.png)]
快速入门测试的就是直接模式。
关联模式:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
配置规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BfvfKeHA-1650383592056)(images/image-20210715202540786.png)]
语法说明:当/write资源访问量触发阈值时,就会对/read资源限流,避免影响/write资源。
使用场景:比如用户支付时需要修改订单状态,同时用户要查询订单。查询和修改操作会争抢数据库锁,产生竞争。业务需求是优先支付和更新订单的业务,因此当修改订单业务触发阈值时,需要对查询订单业务限流。
需求说明:
在OrderController新建两个端点:/order/query和/order/update,无需实现业务
配置流控规则,当/order/ update资源被访问的QPS超过5时,对/order/query请求限流
1)定义/order/query端点,模拟订单查询
@GetMapping("/query")
public String queryOrder() {
return "查询订单成功";
}
2)定义/order/update端点,模拟订单更新
@GetMapping("/update")
public String updateOrder() {
return "更新订单成功";
}
重启服务,查看sentinel控制台的簇点链路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5p5xOe73-1650383592057)(images/1637067169394.png)]
3)配置流控规则
对哪个端点限流,就点击哪个端点后面的按钮。我们是对订单查询/order/query限流,因此点击它后面的按钮:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6e4H9x1l-1650383592057)(images/image-20210716101934499.png)]
在表单中填写流控规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhkXcn49-1650383592057)(images/1637067347687.png)]
4)在Jmeter测试
选择《流控模式-关联》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TSpD7vP1-1650383592058)(images/image-20210716102416266.png)]
可以看到1000个用户,100秒,因此QPS为10,超过了我们设定的阈值:5
查看http请求:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tKaTF2na-1650383592058)(images/image-20210716102532554.png)]
请求的目标是/order/update,这样这个断点就会触发阈值。
但限流的目标是/order/query,我们在浏览器访问,可以发现确实被限流了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fALRtOvC-1650383592058)(images/1637067592843.png)]
5)总结
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5cvFyHCu-1650383592058)(images/image-20210716103143002.png)]
链路模式:只针对从指定链路访问到本资源的请求做统计,判断是否超过阈值。
配置示例:
例如有两条请求链路:
/test1 --> /common
/test2 --> /common
如果只希望统计从/test2进入到/common的请求,则可以这样配置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmmqfOE0-1650383592059)(images/image-20210716103536346.png)]
实战案例
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
步骤:
在itheima-order服务中,添加一个queryGoods方法:
OrderService接口
/**
* 查询商品方法
*/
public void queryGoods();
OrderServiceImpl类
/**
* 查询商品方法
*/
public void queryGoods(){
System.out.println("查询商品");
}
在itheima-order的OrderController中,修改/order/query端点的业务逻辑:
@GetMapping("/query")
public String queryOrder() {
// 查询商品
orderService.queryGoods();
// 查询订单
System.out.println("查询订单");
return "查询订单成功";
}
在order-service的OrderController中,修改/order/save端点,模拟新增订单:
@GetMapping("/save")
public String saveOrder() {
// 查询商品
orderService.queryGoods();
// 查询订单
System.err.println("新增订单");
return "新增订单成功";
}
默认情况下,OrderServiceImpl中的方法是不被Sentinel监控的,需要我们自己通过注解来标记要监控的方法。
给OrderServiceImpl的queryGoods方法添加@SentinelResource注解:
@SentinelResource("goods")
public void queryGoods(){
System.err.println("查询商品");
}
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。
我们需要关闭这种对SpringMVC的资源聚合,修改itheima-order服务的application.yml文件:
spring:
cloud:
sentinel:
web-context-unify: false # 关闭context整合
重启服务,访问/order/query和/order/save,可以查看到sentinel的簇点链路规则中,出现了新的资源:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2IGC3Ht-1650383592059)(images/1637069244914.png)]
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nECg0X1Y-1650383592059)(images/1637069568361.png)]
只统计从/order/query进入/goods的资源,QPS阈值为2,超出则被限流。
选择《流控模式-链路》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVJef437-1650383592060)(images/image-20210716105612312.png)]
可以看到这里200个用户,50秒内发完,QPS为4,超过了我们设定的阈值2
一个http请求是访问/order/save:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kh22muNc-1650383592060)(images/image-20210716105812789.png)]
运行的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-s6179LNU-1650383592060)(images/image-20210716110027064.png)]
完全不受影响。
另一个是访问/order/query:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZxsNcQBh-1650383592061)(images/image-20210716105855951.png)]
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kWQmnDhs-1650383592061)(images/image-20210716105956401.png)]
每次只有2个通过。
流控模式有哪些?
•直接:对当前资源限流
•关联:高优先级资源触发阈值,对低优先级资源限流。
•链路:阈值统计时,只统计从指定资源进入当前资源的请求,是对请求来源的限流
在流控的高级选项中,还有一个流控效果选项:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yBPpHrP9-1650383592061)(images/image-20210716110225104.png)]
流控效果是指请求达到流控阈值时应该采取的措施,包括三种:
快速失败:达到阈值后,新的请求会被立即拒绝并抛出FlowException异常。是默认的处理方式。
warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值。
排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长
阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机。
warm up也叫预热模式,是应对服务冷启动的一种方案。请求阈值初始值是 maxThreshold(最大阈值) / coldFactor(冷因子),持续指定时长后,逐渐提高到maxThreshold值。而coldFactor的默认值是3.
例如,我设置QPS的maxThreshold为10,预热时间为5秒,那么初始阈值就是 10 / 3 ,也就是3,然后在5秒后逐渐增长到10.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LCWngnA5-1650383592061)(images/image-20210716110629796.png)]
案例
需求:给/order/{id}这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-de9McHbw-1650383592062)(images/1637070723519.png)]
选择《流控效果,warm up》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k0U7SEAl-1650383592062)(images/image-20210716111136699.png)]
QPS为10.
刚刚启动时,大部分请求失败,成功的只有3个,说明QPS被限定在3:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z0cogyoV-1650383592062)(images/image-20210716111303701.png)]
随着时间推移,成功比例越来越高:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DGwCYUVV-1650383592062)(images/image-20210716111404717.png)]
到Sentinel控制台查看实时监控:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LCmrk6qX-1650383592063)(images/1637071036729.png)]
当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常。
而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如:QPS = 5,意味着每200ms处理一个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常。
那什么叫做预期等待时长呢?
比如现在一下子来了12 个请求,因为每200ms执行一个请求,那么:
现在,第1秒同时接收到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7fgxcqoB-1650383592063)(images/image-20210716113147176.png)]
如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2PSW9Dh9-1650383592064)(images/image-20210716113426524.png)]
平滑的QPS曲线,对于服务器来说是更友好的。
案例
需求:给/order/{id}这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C8fGMsNU-1650383592064)(images/1637072328761.png)]
选择《流控效果,队列》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZqGpW1S-1650383592065)(images/image-20210716114243558.png)]
QPS为15,已经超过了我们设定的10。
如果是之前的 快速失败、warmup模式,超出的请求应该会直接报错。
但是我们看看队列模式的运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h23YIZ1q-1650383592065)(images/image-20210716114429361.png)]
全部都通过了。
再去sentinel查看实时监控的QPS曲线:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Z5SFH2q-1650383592066)(images/image-20210716114522935.png)]
QPS非常平滑,一致保持在10,但是超出的请求没有被拒绝,而是放入队列。因此响应时间(等待时间)会越来越长。
当队列满了以后,才会有部分请求失败:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqstQlaA-1650383592066)(images/image-20210716114651137.png)]
流控效果有哪些?
快速失败:QPS超过阈值时,拒绝新的请求
warm up: QPS超过阈值时,拒绝新的请求;QPS阈值是逐渐提升的,可以避免冷启动时高并发导致服务宕机。
排队等待:请求会进入队列,按照阈值允许的时间间隔依次执行请求;如果请求预期等待时长大于超时时间,直接拒绝
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值。
例如,一个根据id查询商品的接口:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fp6QCape-1650383592066)(images/image-20210716115014663.png)]
访问/goods/{id}的请求中,id参数值会有变化,热点参数限流会根据参数值分别统计QPS,统计结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ahGFr22G-1650383592067)(images/image-20210716115131463.png)]
当id=1的请求触发阈值被限流时,id值不为1的请求不受影响。
配置示例:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CDbQ7mNI-1650383592067)(images/image-20210716115232426.png)]
代表的含义是:对hot这个资源的0号参数(第一个参数)做统计,每1秒相同参数值的请求数不能超过5
案例需求:给/order/{orderId}这个资源添加热点参数限流,规则如下:
•默认的热点参数规则是每1秒请求量不超过2
•给102这个参数设置例外:每1秒请求量不超过4
•给103这个参数设置例外:每1秒请求量不超过10
注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源
给itheima-order中的OrderController中的/order/{id}资源添加注解:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AHgwiyBy-1650383592067)(images/1637239535875.png)]
访问该接口,可以看到我们标记的hot资源出现了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bzts8CEU-1650383592068)(images/1637074359196.png)]
注意:这里不要点击hot后面的按钮,页面有BUG
点击左侧菜单中热点规则菜单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MUNRaj18-1650383592068)(images/image-20210716120319009.png)]
点击新增,填写表单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RDv9D4xd-1650383592068)(images/image-20210716120536714.png)]
选择《热点参数限流 QPS1》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vd0haR7P-1650383592069)(images/image-20210716120754527.png)]
这里发起请求的QPS为5.
包含3个http请求:
普通参数,QPS阈值为2
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SIBSyRqi-1650383592069)(images/image-20210716120840501.png)]
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvXmVgT0-1650383592069)(images/image-20210716121105567.png)]
例外项,QPS阈值为4
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8vDCsJpc-1650383592070)(images/image-20210716120900365.png)]
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZk2etEt-1650383592070)(images/image-20210716121201630.png)]
例外项,QPS阈值为10
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0LqfXuL9-1650383592070)(images/image-20210716120919131.png)]
运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GRJ51Nk6-1650383592071)(images/image-20210716121220305.png)]
限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。
而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
线程隔离之前讲到过:调用者在调用服务提供者时,给每个调用的请求分配独立线程池,出现故障时,最多消耗这个线程池内资源,避免把调用者的所有资源耗尽。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pRc7JUpg-1650383592071)(images/image-20210715173215243.png)]
熔断降级:是在调用方这边加入断路器,统计对服务提供者的调用,如果调用的失败比例过高,则熔断该业务,不允许访问该服务的提供者了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YrnMq0Wr-1650383592071)(images/image-20210715173428073.png)]
可以看到,不管是线程隔离还是熔断降级,都是对客户端(调用方)的保护。需要在调用方 发起远程调用时做线程隔离、或者服务熔断。
而我们的微服务远程调用都是基于Feign来完成的,因此我们需要将Feign与Sentinel整合,在Feign里面实现线程隔离和服务熔断。
SpringCloud中,微服务调用都是通过Feign来实现的,因此做客户端保护必须整合Feign和Sentinel。
修改OrderService的application.yml文件,开启Feign的Sentinel功能:
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
业务失败后,不能直接报错,而应该返回用户一个友好提示或者默认结果,这个就是失败降级逻辑。
给FeignClient编写失败后的降级逻辑
①方式一:FallbackClass,无法对远程调用的异常做处理
②方式二:FallbackFactory,可以对远程调用的异常做处理,我们选择这种
这里我们演示方式二的失败降级处理。
1)在feing-api项目中定义类,实现FallbackFactory:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5RLWACNS-1650383592072)(images/1637242862791.png)]
代码:
package com.itheima.client.fallback;
import com.itheima.client.UserClient;
import com.itheima.user.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 自定义失败降级处理
*/
@Slf4j
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
/**
* 业务处理
* @param throwable
* @return
*/
@Override
public UserClient create(Throwable throwable) {
return new UserClient() {
@Override
public User one(Long id) {
log.error("查询用户异常", throwable);
User user = new User();
user.setAddress("xx");
user.setUsername("yu");
return user;
}
};
}
}
2)在feing-api项目中的UserClient接口中使用UserClientFallbackFactory:
package com.itheima.client;
import com.itheima.client.fallback.UserClientFallbackFactory;
import com.itheima.user.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* 将String url = "http://itheima-user/user/"+orderInfo.getUserId();
* 替换成当前接口
* 1.通过@FeignClient("被调用的服务名称")
* 2.定义方法 需要跟 被调用的微服务要一样
* 2.1.路径必须是被调用微服务的完整路径
* 2.2.参数一定要加别名
*/
@FeignClient(value = "itheima-user",fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
/***
* 根据id查询用户详情
*/
@GetMapping(value = "/user/{id}")
public User one(@PathVariable(value = "id") Long id);
}
重启后,访问一次订单查询业务,然后查看sentinel控制台,可以看到新的簇点链路:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-45QfO6JE-1650383592072)(images/1637243095378.png)]
Sentinel支持的雪崩解决方案:
Feign整合Sentinel的步骤:
线程隔离有两种方式实现:
线程池隔离
信号量隔离(Sentinel默认采用)
如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ILPNNpvb-1650383592072)(images/image-20210716123036937.png)]
线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
两者的优缺点:
扇出:请求到A微服务,A微服务依赖N个微服务,请求到A就会扇出N个微服务
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zeP58zbs-1650383592073)(images/image-20210716123240518.png)]
用法说明:
在添加限流规则时,可以选择两种阈值类型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJgkyhki-1650383592073)(images/1637243320525.png)]
QPS:就是每秒的请求数,在快速入门中已经演示过
线程数:是该资源能使用用的tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
案例需求:给 order-service服务中的UserClient的查询用户接口设置流控规则,线程数不能超过 2。然后利用jemeter测试。
选择feign接口后面的流控按钮:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-my5XXgnC-1650383592073)(images/1637243486672.png)]
填写表单:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jzP22x0c-1650383592074)(images/1637244456134.png)]
选择《阈值类型-线程数<2》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sepmclJp-1650383592074)(images/image-20210716124229894.png)]
一次发生10个请求,有较大概率并发线程数超过2,而超出的请求会走之前定义的失败降级逻辑。
查看运行结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FGSw7CUH-1650383592074)(images/image-20210716124147820.png)]
发现虽然结果都是通过了,不过部分请求得到的响应是降级返回的null信息。
线程隔离的两种手段是?
信号量隔离
线程池隔离
信号量隔离的特点是?
线程池隔离的特点是?
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rB0s9j20-1650383592074)(images/image-20210716130958518.png)]
状态机包括三个状态:
断路器熔断策略有三种:慢调用、异常比例、异常数
慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gnuHmVII-1650383592075)(images/image-20210716145934347.png)]
解读:RT超过500ms的调用是慢调用,统计最近10000ms内的请求,如果请求量超过10次,并且慢调用比例不低于0.5,则触发熔断,熔断时长为5秒。然后进入half-open状态,放行一次请求做测试。
案例
需求:给 UserClient的查询用户接口设置降级规则,慢调用的RT阈值为50ms,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5
修改user-service中的/user/{id}这个接口的业务。通过休眠模拟一个延迟时间:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9W3Jl9yu-1650383592075)(images/1637245124539.png)]
此时,orderId=101的订单,关联的是id为1的用户,调用时长为60ms:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K4K6G40l-1650383592075)(images/image-20210716150510956.png)]
orderId=102的订单,关联的是id为2的用户,调用时长为非常短;
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F7JaWFUi-1650383592076)(images/image-20210716150605208.png)]
下面,给feign接口设置降级规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R1UfRxf1-1650383592076)(images/image-20210716150654094.png)]
规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gT7cBSRo-1650383592080)(images/1637246218366.png)]
超过50ms的请求都会被认为是慢请求
在浏览器访问:http://localhost:18082/order/101,快速刷新5次,可以发现:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-91TSNYUC-1650383592081)(images/image-20210716150911004.png)]
触发了熔断,请求时长缩短至5ms,快速失败了,并且走降级逻辑,返回的null
在浏览器访问:http://localhost:18082/order/102,竟然也被熔断了:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJBGPtMr-1650383592082)(images/image-20210716151107785.png)]
异常比例或异常数:统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
例如,一个异常比例设置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e7HZ2K1P-1650383592082)(images/image-20210716131430682.png)]
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于0.4,则触发熔断。
一个异常数设置:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzThehhW-1650383592082)(images/image-20210716131522912.png)]
解读:统计最近1000ms内的请求,如果请求量超过10次,并且异常比例不低于2次,则触发熔断。
案例
需求:给 UserClient的查询用户接口设置降级规则,统计时间为1秒,最小请求数量为5,失败阈值比例为0.4,熔断时长为5s
首先,修改user-service中的/user/{id}这个接口的业务。手动抛出异常,以触发异常比例的熔断:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wLzcBmBL-1650383592083)(images/1637246048801.png)]
也就是说,id 为 2时,就会触发异常
下面,给feign接口设置降级规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8dTGtdg8-1650383592083)(images/image-20210716150654094.png)]
规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wlJ6yndZ-1650383592083)(images/1637246261038.png)]
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
在浏览器快速访问:http://localhost:18082/order/102,快速刷新5次,触发熔断:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xfuBQ1aw-1650383592084)(images/image-20210716151722916.png)]
此时,我们去访问本来应该正常的103:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NsvUtfzQ-1650383592084)(images/image-20210716151844817.png)]
授权规则可以对请求方来源做判断和控制。
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
白名单:来源(origin)在白名单内的调用者允许访问
黑名单:来源(origin)在黑名单内的调用者不允许访问
点击左侧菜单的授权,可以看到授权规则:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DVH1tBPL-1650383592085)(images/image-20210716152010750.png)]
资源名:就是受保护的资源,例如/order/{id}
流控应用:是来源者的名单,
比如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YhnESv20-1650383592085)(images/image-20210716152349191.png)]
我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)。
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
public interface RequestOriginParser {
/**
* 从请求request对象中获取origin,获取方式自定义
*/
String parseOrigin(HttpServletRequest request);
}
这个方法的作用就是从request对象中,获取请求者的origin值并返回。
默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。
因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。
例如order-service服务中,我们定义一个RequestOriginParser的实现类:
package com.itheima.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
/**
* 自定义接口的实现,让不同的请求,返回不同的origin
*/
@Component
public class HeaderOriginParser implements RequestOriginParser {
/**
* 获取请求头值 为空返回blank
* @param httpServletRequest
* @return
*/
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
// 1.获取请求头
String origin = httpServletRequest.getHeader("origin");
// 2.非空判断
if(StringUtils.isEmpty(origin)){
origin = "blank";
}
return origin;
}
}
我们会尝试从request-header中获取origin值。
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。
这个需要利用之前学习的一个GatewayFilter来实现,AddRequestHeaderGatewayFilter。
1)修改gateway服务中的application.yml,添加一个defaultFilter:
spring:
application:
name: gateway
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SZ #集群设置名称
namespace: devnamespace #环境隔离 命名空间ID
gateway:
default-filters: #全局过滤器配置 针对所有的微服务
- AddRequestHeader=Heima,shenzhen119 nb!!!
- AddRequestHeader=origin,gateway
routes:
# ...略
从gateway路由的所有请求都会带上origin头,值为gateway。而从其它地方到达微服务的请求则没有这个头。
注意:删除eureka配置
2)添加nacos起步依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
注意:删除eureka依赖
接下来,我们添加一个授权规则,放行origin值为gateway的请求。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DFO8ZNjG-1650383592085)(images/image-20210716153250134.png)]
配置如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4gQiSC4K-1650383592086)(images/1637247706908.png)]
现在,我们直接跳过网关,访问order-service服务:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NP2hC5Dj-1650383592086)(images/1637247818494.png)]
通过网关访问:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6IGDAnN-1650383592087)(images/1637247863623.png)]
默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截。
而如果要自定义异常时的返回结果,需要实现BlockExceptionHandler接口:
public interface BlockExceptionHandler {
/**
* 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
*/
void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}
这个方法有三个参数:
这里的BlockException包含多个不同的子类:
异常 | 说明 |
---|---|
FlowException | 限流异常 |
ParamFlowException | 热点参数限流的异常 |
DegradeException | 降级异常 |
AuthorityException | 授权规则异常 |
SystemBlockException | 系统规则异常 |
下面,我们就在order-service定义一个自定义异常处理类:
package com.itheima.sentinel;
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
String msg = "未知异常";
int status = 429;
if (e instanceof FlowException) {
msg = "请求被限流了";
} else if (e instanceof ParamFlowException) {
msg = "请求被热点参数限流";
} else if (e instanceof DegradeException) {
msg = "请求被降级了";
} else if (e instanceof AuthorityException) {
msg = "没有权限访问";
status = 401;
}
response.setContentType("application/json;charset=utf-8");
response.setStatus(status);
response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
}
}
重启测试,在不同场景下,会返回不同的异常消息.
限流:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iibATPoX-1650383592087)(images/1637248135372.png)]
授权拦截时:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ax90PjPb-1650383592087)(images/1637248166833.png)]
现在,sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在生产环境下,我们必须确保这些规则的持久化,避免丢失。
规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:
pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1iDjGPmJ-1650383592088)(images/image-20210716154155238.png)]
push模式:控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7yBvwfp-1650383592088)(images/image-20210716154215456.png)]
详细步骤可以参考课前资料的《sentinel规则持久化》:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NvfIYqoF-1650383592089)(images/image-20210716154255466.png)]