所有博客文件目录索引:博客目录索引(持续更新)
学习视频:SpringCloud 教程 已完结(IDEA 2022.1最新版)4K蓝光画质 微服务开发
PS:本章节中部分图片是直接引用学习课程课件,如有侵权,请联系删除。
当前项目环境版本:springboot 2.3.12.RELEASE
、springcloud alibaba 2.2.7.RELEASE
、SpringCloud Hoxton-SR12
。
Sentinel是Spring Cloud Alibaba的一个重要组件,类似于spring clound的hystrix,与hystrix-dashboard控制台一样,sentinel-dashboard控制台可以提供对流量的实时监控、在线维护流量规则、熔断规则,前提是微服务整合了sentinel。
Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
可以直接跳过2.1部分,看2.2的集成Sentinel组件。
在集成Sentinel组件前,我们先来准备一些服务来为下面部分的demo测试做准备。
服务:Nacos服务(注册中心)、用户服务(含远程调用图书服务)、图书服务。
nacos服务启动
提前运行,并创建服务的命名空间,之后搭建的服务全部注册到这里:
引入依赖:
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.3.12.RELEASEspring-boot.version>
<spring-cloud-alibaba.version>2.2.7.RELEASEspring-cloud-alibaba.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
1、添加服务注册注解:在启动器上SentinelBookserviceApplication
@EnableDiscoveryClient
2、编写配置文件项:application.yaml
server:
port: ${SERVER_PORT:8083}
spring:
application:
name: book-service
cloud:
nacos:
server-addr: localhost:8848
username: nacos
password: nacos
discovery:
# 指定命名空间和对应的组别以及注册的服务名
namespace: 477245fa-d5e1-47e0-9580-4a8e268c3f58
group: DEFAULT_GROUP
service: book-service
sentinel: # sentinel配置
transport:
dashboard: localhost:8858
3、控制器对外的公共接口:BookController
在控制器中对外有两个接口,其中/book/like/{id}
接口是给用户控制器来进行远程调用的。
package com.changlu.sentinelbookservice.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import java.util.Arrays;
import java.util.List;
/**
* @Description:
* @Author: changlu
* @Date: 12:39 PM
*/
@RestController
public class BookController {
@GetMapping("/list")
public List<String> list() {
List<String> books = Arrays.asList("西游记", "水浒传", "红楼梦");
return books;
}
@GetMapping("/book/like/{id}")
public String getUserLikeBook(@PathVariable("id")Long id) {
String book = "哈利波特";
if (id != 1) {
book = "西游记";
}
return book;
}
}
依赖:与2.1.2一致,只不过多了一个远程调用的组件feign:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
1、开启服务注册以及feign包扫描增强注解,在启动器SentinelUserserviceApplication
上添加即可
@EnableDiscoveryClient
@EnableFeignClients //开启feign扫描
2、配置文件:application.yaml
server:
port: ${SERVER_PORT:8081}
spring:
application:
name: user-service
cloud:
nacos: # nacos配置
server-addr: localhost:8848
username: nacos
password: nacos
discovery:
namespace: 477245fa-d5e1-47e0-9580-4a8e268c3f58
group: DEFAULT_GROUP
service: user-service
3、pojo类User:用户对象。com.changlu.sentineluserservice.pojo.User
package com.changlu.sentineluserservice.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Description:
* @Author: changlu
* @Date: 12:31 PM
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Long id;
private String name;
private String sex;
private String hobby;
private String likeBook;
}
4、添加一个feign接口,之后用于调用BookService:com.changlu.sentineluserservice.feign.UserBookFeign
package com.changlu.sentineluserservice.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
* @Description:
* @Author: changlu
* @Date: 12:34 PM
*/
@FeignClient(value = "book-service")
public interface UserBookFeign {
@GetMapping("/book/like/{id}")
String getUserLikeBook(@PathVariable("id")Long id);
}
5、控制器:com.changlu.sentineluserservice.controller.UserController
package com.changlu.sentineluserservice.controller;
import com.changlu.sentineluserservice.feign.UserBookFeign;
import com.changlu.sentineluserservice.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: changlu
* @Date: 12:31 PM
*/
@RestController
public class UserController {
@Autowired
private UserBookFeign userBookFeign;
@GetMapping("/user")
public User getUser() {
User user = new User(1L, "changlu", "男", "打篮球", null);
//远程调用
String userLikeBook = userBookFeign.getUserLikeBook(user.getId());
user.setLikeBook(userLikeBook);
return user;
}
}
启动服务
通过修改配置项来进行启动两个服务,nacos是单独去启动的:
看一下nacos的管理页:
测试服务
测试user-service接口:
测试book-service接口:
ok都没有问题,那么我们接下来来对两个服务集成sentinel!
接下来我们想要对两个服务进行流量监控以及熔断、降级相关操作,就可以在我们的项目中引入sentinel依赖!
Seninel是一个控制台(Dashboard),其基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
其主要提供一个轻量级的控制台,它提供机器发现、单机资源实时监控、集群资源汇总,以及规则管理的功能。
同样Sentinel-dashboard是独立安装和部署的,下载地址:https://github.com/alibaba/Sentinel/releases,目前的话我们下载1.8版本的jar包即可:
接着我们用IDEA来进行配置jar包启动的一些参数:
将其设置在8858端口,对其进行启动,可以看到其本身就是一个springboot项目:
准备动作完成之后,我们即可在项目中集成Sentinel,来让dashboard与其能够进行通讯来实现一个检测、熔断等功能。
1、引入Sentinel依赖:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
2、配置Sentinel的远程地址:application.yaml
spring:
cloud:
sentinel: # sentinel配置
transport:
dashboard: localhost:8858
此时我们就已经集成完成了!
接下来我们去访问Sentinel控制台:http://localhost:8858/#/login
用户名和密码分别都是Sentinel,进入之后你可以看到里面没有任何的服务,这是怎么回事呢?
接着我们尝试访问两个服务的接口:http://localhost:8081/user、http://localhost:8083/list
OK,回到控制台,此时你就可以看到了!
对于一些服务的数据信息,会存储在指定的日志文件中,可以在运行面板上看到路径:
在这里你可以看到统一服务的集群数量以及相应的ip地址、客户端版本以及健康状态,每10秒会对一个实例来进行心跳检测:
在这里你可以看到最近的一个请求信息,包含响应时间:
在簇点链路中你可以看到所有关于该服务在启动时已经访问过的一些api接口,其中可以看到对应该接口的一些信息,同时你可以对制定的接口来进行流控(流量控制)、熔断、热点数据(制定请求参数)以及授权。
是否超过流量阈值的判断,这里介绍4种算法:
1、漏桶算法
顾名思义,就像一个桶开了一个小孔,水流进桶中的速度肯定是远大于水流出桶的速度的,这也是最简单的一种限流思路。
说明:访问服务的TPS比较均匀,不会超出最大的TPS,当访问第三方系统时,对方的接口并发数如果限制TPS,那么相对采用令牌桶比较合适。
好处:有利于提升系统的稳定性。
坏处:面对突发流量时,漏斗算法无法灵活应对,用户需要消耗大量的时间等待请求被处理。
2、令牌桶算法
只能说有点像信号量机制。现在有一个令牌桶,这个桶是专门存放令牌的,每隔一段时间就向桶中丢入一个令牌(速度由我们指定)当新的请求到达时,将从桶中删除令牌,接着请求就可以通过并给到服务,但是如果桶中的令牌数量不足,那么不会删除令牌,而是让此数据包等待。
说明:相对于漏铜算法,令牌桶的算法更具有优势,除了可以控制客户端请求的TPS,令牌桶还有冗余的令牌应对突发流量,当然应对突然流量,底层的微服务也要足够的健壮性,否则服务很容易被瞬时流量压垮。
好处:能够灵活应对突发流量,支持高并发场景,并且对流量的控制很平滑,用户体验好。
坏处:开发者需要通过系统性能、用户习惯等进行令牌桶数量和生成速度的评估。
3、 固定时间窗口算法
我们可以对某一个时间段内的请求进行统计和计数,比如在14:15
到14:16
这一分钟内,请求量不能超过100
,也就是一分钟之内不能超过100
次请求。
可能出现问题复现:
20:09:59的时候来了100个请求 20:10:01的时候又来了100个请求
问题说明:我们其实希望的是60秒内只有100个请求,但是这种情况却是在3秒内出现了200个请求,很明显已经违背了我们的初衷。
优点:每个时间段的总体性能能够较为精准地进行控制,避免系统崩溃。
缺点:如果是在时间范围临界点突发大流量,固定窗口算法就无法发挥其作用,有可能会直接激发降级机制,影响用户的后续使用体验。
4、 滑动时间窗口算法
滑动时间窗口算法更加灵活,它会动态移动窗口,重新进行计算。
说明:避免固定时间窗口的临界问题,但是这样显然是比固定窗口更加耗时的。
好处:通过滑动窗口的方式,将剩余的系统资源交给该时间段的后半部分。
坏处:只能通过控制时间段来控制请求总量,无法平滑地实现请求流量的控制。
进入到簇点链路中,我们选择/user来对其进行流控:
阈值类型任意选择一个,单机阈值指的就是每秒的阈值数,若是我们的某个服务是集群的话,那么就要对其进行勾选,这样的话整个集群的每个实例都会增加其控制!
流控记录如下:选择的分别是QPS、单机阈值、集群模式、流控是直接模式、效果是快速失败
此时我们来尝试进行访问:默认的失败返回内容如下所示
对于机器不可能无限制接收并处理请求,对于高并发的情况下,一定要对其来进行限制,我们可以采用流控的方式。
一旦达到了某个阈值,新的请求就不会再进行处理或者返回预先设定好的内容,这样的话就能够保证服务的可用性。
在管理界面中包含三种流控效果分别是:快速失败、Warm Up、排队等待。
方案一:快速拒绝,既然不再接受新的请求,那么我们可以直接返回一个拒绝信息,告诉用户访问频率过高。
方案二:预热,依然基于方案一,但是由于某些情况下高并发请求是在某一时刻突然到来,我们可以缓慢地将阈值提高到指定阈值,形成一个缓冲保护。
方案三:排队等待,不接受新的请求,但是也不直接拒绝,而是进队列先等一下,如果规定时间内能够执行,那么就执行,要是超时就算了。
下面是不同流控效果的测试:
①快速失败
对user-service服务来设置快速失败,阈值为1:
效果:对于快速失败来说,一旦达到阈值,在指定秒数中的请求就会直接返回限流的页面。
②预热Warm Up
效果:预热的话就会在指定的范围内,例如阈值设置为10,阈值逐渐的在指定的秒数内增加到10,也就是说刚开始可能阈值会从1开始然后逐渐增加。
我们使用jmeter来进行测试:每秒10个请求,发150个请求
可以看到逐步请求能够正常响应的数量在增多:
③排队等待
效果:请求过多时,让请求匀速的进入后台进行处理。采用漏斗算法,控制流量,设置超时时间,超时的则将请求抛弃,返回错误信息。
同样我们来使用jmeter来进行测试:
结果:可以看到10个请求,最后有3个响应失败,说明在一定情况下,部分请求加入队列可以对其延迟处理,用以减轻服务器的压力。
不同流控模式包含:直接、关联、链路。
直接
:只针对当前的接口,达到指定的阈值就会被限流。
关联
:A关联指定接口B,若是该B接口超过阈值时,会导致A接口也被限流。
链路
:更细粒度的限流,可以精确到具体的方法。
关于测试:关联模式我在测得时候无效,目前还不清楚原因。
开启方式
1、添加配置文件配置:
spring:
cloud:
sentinel: # sentinel配置
# 关闭Context收敛,这样被监控方法可以进行不同链路的单独控制
web-context-unify: false
2、在service中的方法级别添加@SentinelResource("getUserLikeBook")
注解
@Service
public class BookServiceImpl implements BookService{
@SentinelResource("getUserLikeBook")
@Override
public String getUserLikeBook(Long userId) {
String book = "哈利波特";
if (userId != 1) {
book = "西游记";
}
return book;
}
}
注意:此时两个controller接口都会调用该service的方法,如下;
ok,接下来我们就能够来测试链路模式了!
我们对这个方法来进行流控,对其选择链路,然后关联/list资源:
模式如下:
Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load作为启发指标,进行自适应系统保护。当系统 load超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
。
CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
这里我们来使用cpu资源率作为示例:
来进行一下压测,可以很明显的看到,当请求数变得很多时,服务的CPU使用率标高达到阈值,就会进行拒绝访问,直接响应异常。
热点规则是根据指定的请求参数类型:
@GetMapping("/testparams")
@SentinelResource("testparams")
public String testParams(@RequestParam(value = "a", required = false)String a,
@RequestParam(value = "b", required = false)String b,
@RequestParam(value = "c", required = false)String c
) {
return "a:" + a + ", b:" + b + ", c:" + c;
}
接下来我们去进行编辑:
上面的是指定某个参数来进行阈值设定,下面则是指定某个参数来设置值与阈值:
这里指的是第2个参数阈值为1,第0个参数若是值为1的话且阈值为1就限流:
测试1:http://localhost:8082/testparams?a=2
参数1值为2,不在阈值限定的范围中,所以不会出现限流的情况。
测试2:http://localhost:8082/testparams?a=2&b=1
抛出的异常类型为:com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException
在簇点链路中可以去选择指定的路径来指定熔断策略:
策略分别包含:
1、慢调用比例:对于指定接口的处理时长达到阈值(最大RT),那么系统会判定它可能出现了服务异常情况,判定为慢调用
,在一个统计时长内,如果请求数目大于最小请求数目,并且被判定为慢调用
的请求比例已经超过阈值,将触发熔断。
2、异常比例:这里判断的是出现异常的比例数。
3、异常数:达到指定的异常数量。
自定义配置
场景:当我们对某个接口来设置流控,当达到阈值,就会返回如下异常信息。
接下来我们要对其进行自定义:对user-service服务来进行增强
1、编写一个自定义限流接口,接口地址也可以自行指定。
package com.changlu.sentineluserservice.controller;
import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description:
* @Author: changlu
* @Date: 9:57 AM
*/
@RestController
public class CommonController {
@RequestMapping("/blocked")
public JSONObject blocked() {
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 403);
jsonObject.put("success", false);
jsonObject.put("message", "您的请求频率过快,请稍后再试!");
return jsonObject;
}
}
2、配置文件添加阻塞页面路径配置:application.yaml
spring:
cloud:
sentinel: # sentinel配置
block-page: /blocked
测试
我们对user-service服务来进行流控:
对/user快速刷新两次之后,就会进行重定向到/blocked地址:
场景:针对于方法级别的流控,一旦超过指定的阈值,那么就会抛出异常。
自定义配置
接下来我们对book-service服务中的service方法来进行自定义异常处理。
1、编写一个与对应方法返回参数一致,额外添加一个BlockException异常类型参数
//替代方案,注意参数和返回值需要保持一致(会一并传入进来),并且参数最后还需要额外添加一个BlockException
public String blocked(Long userId, BlockException e) {
return "限流书籍";
}
2、在@SentinelResource中指定异常处理
//指定blockHandler,也就是被限流之后的替代解决方案,这样就不会使用默认的抛出异常的形式了
@SentinelResource(value = "getUserLikeBook", blockHandler = "blocked")
测试
我们对book-service服务的指定service方法进行流控:
之后我们来去尝试调用user-service的/user接口,该接口包含一个远程调用涉及到getUserLikeBook,快速请求刷新一下:
指定方法流控超越阈值,成功处理异常!
之前我们测试热点参数的时候,可以看到是抛出的异常,而不是走得跳转流控响应,那么如何对热点参数这类的报错进行异常处理呢?
@SentinelResource(value = "testparams",
blockHandler = "handlerTestHotKey"
)
//必须要设置为public,参数要一致,否则会抛出异常
public String handlerTestHotKey(String a, String b, String c, BlockException exception) {
return "对不起, 当前流量过高...";
}
看下访问效果:
场景:若是我们的接口会有一些异常抛出,我们也可以对其来进行自定义处理返回!
示例如下:
@GetMapping("/testparams")
@SentinelResource(value = "testparams",
blockHandler = "handlerTestHotKey",
fallback = "except", //fallback指定出现异常时的替代方案
exceptionsToIgnore = IOException.class //忽略那些异常,也就是说这些异常出现时不使用替代方案
)
public String testParams(@RequestParam(value = "a", required = false)String a,
@RequestParam(value = "b", required = false)String b,
@RequestParam(value = "c", required = false)String c) throws RuntimeException {
throw new RuntimeException("自定义抛出异常!");
}
测试效果:
在整个微服务调用链路出现问题的时候,及时对服务进行降级,以防止问题进一步恶化,如下图所示:
举例:例如A接口远程调用B接口,若是B接口不可用,那么请求迟迟不响应,此时又有大量的客户端去调用接口A,若是对于这种情况不进行及时响应的话,就会造成整条链路的问题恶化。
对于大量请求去调用A,又A调用服务B,此时由于B短时间没有进行响应,那么该线程就会进入阻塞,新来的请求会不断在线程池中开辟新的线程,这种情况会导致线程数不断增加,cpu资源也会被耗尽,对于整个线程不断增加的情况,有一些隔离方案如下:
1、线程池隔离
线程池隔离实际上就是对每个服务的远程调用单独开放线程池,比如服务A要调用服务B,那么只基于固定数量的线程池,这样即使在短时间内出现大量请求,由于没有线程可以分配,所以就不会导致资源耗尽了。
2、 信号量隔离。【Sentinel也正是采用的这种方案实现隔离的。】
信号量隔离是使用Semaphore类实现的,也是限定指定的线程数量能够同时进行服务调用,但是它相对于线程池隔离,开销会更小一些,使用效果同样优秀,也支持超时等。
针对于熔断降级:当下游服务因为某种原因变得不可用或响应过慢时,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务而是快速返回或是执行自己的替代方案,这便是服务降级。
整个过程分为三个状态:
关闭:熔断器不工作,所有请求全部该干嘛干嘛。
打开:熔断器工作,所有请求一律降级处理。
半开:尝试进行一下下正常流程,要是还不行继续保持打开状态,否则关闭。
这个实际上与我们之前学习的Hystrix思路也是一致的。
处理方案就是添加@SentinelResource,然后去指定``blockHandler = “handlerTestHotKey”`,之后就会去进行响应啦。
配置如下:实际上与4.1中的①②是一致的。
@GetMapping("/testparams")
@SentinelResource(value = "testparams",
fallback = "except", //fallback指定出现异常时的替代方案
exceptionsToIgnore = IOException.class //忽略那些异常,也就是说这些异常出现时不使用替代方案
)
public String testParams(@RequestParam(value = "a", required = false)String a,
@RequestParam(value = "b", required = false)String b,
@RequestParam(value = "c", required = false)String c) throws RuntimeException {
throw new RuntimeException("自定义抛出异常!");
}
//参数保持一致,必须要设置为public,否则会抛出异常
public String handlerTestHotKey(String a, String b, String c, BlockException exception) {
return "对不起, 当前流量过高...";
}
配置过程
背景:user-service服务在/user接口中会去调用book-service服务中的某个接口,若是book-service在调用过程中下线了,那么此时使用熔断方案来进行解决。
我们就来对user-service的接口来进行改造:
1、开启feign的sentinel:application.yaml
# 开启feign的sentinel流量治理
feign:
sentinel:
enabled: true
2、实现指定UserBookFeign的接口:com.changlu.sentineluserservice.feign.UserClientFallback
package com.changlu.sentineluserservice.feign;
import org.springframework.stereotype.Component;
/**
* @Description:
* @Author: changlu
* @Date: 11:22 AM
*/
@Component
public class UserClientFallback implements UserBookFeign{
@Override
public String getUserLikeBook(Long id) {
return "熔断替代方案";
}
}
3、在对应的feign接口注解上指定fallback类:
@FeignClient(value = "book-service", fallback = UserClientFallback.class)
测试
首先user与book服务都正常,我们来进行调用user的服务:
接着将book服务下线,再来请求user服务:
此时熔断方案已生效!
在restTemplate上添加注解@SentinelRestTemplate,然后去配置fallbackClass指定的熔断类即可。
@Bean
@LoadBalanced
@SentinelRestTemplate(blockHandler = "handleException", blockHandlerClass = ExceptionUtil.class,
fallback = "fallback", fallbackClass = ExceptionUtil.class) //这里同样可以设定fallback等参数
public RestTemplate restTemplate() {
return new RestTemplate();
}
[1]. SpringBoot集成sentinel
[3]. 7. Alibaba Sentinel 流控效果之排队等待