Sentinel
- 在微服务架构下,会涉及到 微服务A需要远程调用其他的微服务B,C,D等的接口,比如商品服务接口,需要调用库存服务数据,秒杀服务数据等,这里就会衍生一个长链路的调用过程,那么一旦下游需要被调用的数据接口出现异常,那么就会导致整个上游的接口都会报错无法使用,所以就需要去做相应的方案措施,避免上游服务异常。
一、熔断、降级、限流
1.熔断
服务熔断 的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。
停止是说,当前服务一旦对下游服务进行熔断,当请求到达时,当前服务不再对下游服务进行调用,而是使用设定好的策略(如构建默认值)直接返回。
暂时是说,熔断后,并不会一直不再调用下游服务,而是以一定的策略(如每分钟调用 10 次,若均返回成功,则增大调用量)试探调用下游服务,当下游服务恢复可用时,自动停止熔断。
2.降级
降级是指当自身服务压力增大时,采取一些手段,增强自身服务的处理能力,以保障服务的持续可用。比如,下线非核心服务以保证核心服务的稳定、降低实时性、降低数据一致性。
为了预防某些功能出现负荷过载或者响应慢的情况,在其内部暂时舍弃一些非核心接口和数据的请求(如评论、积分),而直接返回一个提前准备好的 fallback(退路) 错误处理信息。释放CPU和内存资源,以保证整个系统的稳定性和可用性。
3.限流
限流是指上游服务对本服务请求 QPS 超过阙值时,通过一定的策略(如延迟处理、拒绝处理)对上游服务的请求量进行限制,以保证本服务不被压垮,从而持续提供稳定服务。常见的限流算法有滑动窗口、令牌桶、漏桶等。
二、Sentinel
1.其他组件对比
2.Sentinel介绍
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 的历史:
- 2012 年,Sentinel 诞生,主要功能为入口流量控制。
- 2013-2017 年,Sentinel 在阿里巴巴集团内部迅速发展,成为基础技术模块,覆盖了所有的核心场景。Sentinel 也因此积累了大量的流量归整场景以及生产实践。
- 2018 年,Sentinel 开源,并持续演进。
- 2019 年,Sentinel 朝着多语言扩展的方向不断探索,推出 C++ 原生版本,同时针对 Service Mesh 场景也推出了 Envoy 集群流量控制支持,以解决 Service Mesh 架构下多语言限流的问题。
- 2020 年,推出 Sentinel Go 版本,继续朝着云原生方向演进。
Sentinel 分为两个部分:
- 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
- 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
3. 基本概念及作用
基本概念:
资源:是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。在接下来的文档中,我们都会用资源来描述代码块。
只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则:围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
主要作用:
- 流量控制
- 熔断降级
- 系统负载保护
我们说的资源,可以是任何东西,服务,服务里的方法,甚至是一段代码。使用 Sentinel 来进行资源保护,主要分为几个步骤:
- 定义资源
- 定义规则
- 检验规则是否生效
先把可能需要保护的资源定义好,之后再配置规则。也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。
4.Sentinel应用
官方文档: https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
4.1 Sentinel控制台搭建
下载对应的jar包:https://github.com/alibaba/Sentinel/releases 下载适合自己的版本
通过命令行启动:
java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
启动后需要输入账号密码:sentinel sentinel
登录成功后看到的页面
4.2 服务监控
如果我们需要把自己的服务被Sentinel监控,那么我们需要添加对应的依赖
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
然后需要添加对应的属性信息
然后我们访问请求后,在控制台就可以看到对应的监控信息了
然后我们可以指定简单的限流规则
表示1秒中内只能有一个QPS。超过就限流
测试我们发送请求快一点就会出现限流的处理
4.3 实时监控
实时监控这块需要引入Acuator
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
endpoint端点:
4.4 限流信息自定义
java
@Component
public class SentinelUrlBlockHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
R r = R.error(500,"访问太快,稍后再试!");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(r));
}
}
5.sentinel容器安装
直接在windows中安装Sentinel启动管理还是不太方便,所以我们还是把Sentinel安装在Docker容器中
拉取对应的镜像文件
docker pull bladex/sentinel-dashboard:1.7.2
启动容器
docker run --name sentinel -d -p 8858:8858 -d bladex/sentinel-dashboard:1.7.2
开机自启动
docker update --restart=always sentinel
访问:
对应的客户端的使用:需要调整的配置信息
6.Sentinel监控所有服务
在公共微服务中引入以上说明的两个依赖,使得每个需要进行限流降级操作的微服务进入sentinel监控中。
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
在每一个服务都添加Sentinel的配置 以及 Acuator实时监控的端点配置,这里都是重复的配置信息,也可以配置在nacos配置中心
yaml
spring:
cloud:
sentinel:
transport:
port: 8719
client-ip: 192.168.56.1
dashboard: 192.168.56.100:8858
management:
endpoints:
web:
exposure:
include: '*'
7.熔断降级
https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel
场景一 远程服务调用异常 熔断降级实例: 刷新商品页面详情会调用到多个接口包括远程服务接口
商品服务product中-->>会调用本身服务多个接口方法-->>同时也会通过fegin调用到其他的微服务 比如秒杀服务,假如此时秒杀服务接口异常(异常有很多种,比如我们模拟的时候,可以直接把秒杀服务关了...或者接口代码去抛异常...),那么商品服务的请求链路会因为秒杀服务异常而导致整个接口请求异常,页面报错,那么我们通过代码的方式 设置熔断机制,使得即使fegin远程调用的接口异常返回一个回调相应数据,来确保业务逻辑不报错
1.添加对应的配置
yaml
feign:
sentinel:
enabled: true
2.将涉及fegin远程调用的秒杀服务查询接口添加托底的方法,回调类
java
@Slf4j
@Component
public class SeckillFeignServiceFallback implements SeckillFeignService {
@Override
public R getSeckillSessionBySkuId(Long skuId) {
log.error("熔断降级....SeckillFeignService:{}",skuId);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
3.在商品服务的调用远程服务接口中增加注解的属性值指定前面定义的回调类:fallback = SeckillFeignServiceFallback.class
java
@FeignClient(value = "mall-seckill",fallback = SeckillFeignServiceFallback.class)
public interface SeckillFeignService {
@GetMapping("/seckill/seckillSessionBySkuId")
R getSeckillSessionBySkuId(@RequestParam("skuId") Long skuId);
}
4.设置好回调类信息,需要进一步的优化业务逻辑的响应,避免出现空指针异常,需要先判断返回值的响应代码,如果是正常的r.getCode() == 0再做进一步的调用
java
CompletableFuture<Void> seckillFuture = CompletableFuture.runAsync(() -> {
// 查询商品的秒杀活动
R r = seckillFeignService.getSeckillSessionBySkuId(skuId);
if(r.getCode() == 0){
SeckillVO seckillVO = JSON.parseObject(r.get("data").toString(),SeckillVO.class);
vo.setSeckillVO(seckillVO);
}
}, threadPoolExecutor);
场景二 远程服务调用慢 延时 熔断降级实例:刷新商品页面详情会调用到多个接口包括远程服务接口
与场景一相同,只不过此时我们模拟远程调用接口时长久一些,调用线程睡眠方法,比如设置500毫秒,然后
可以通过我们安装好的sentinel启动访问客户端,访问商品服务接口中调用到的 远程服务接口,点击编辑降级规则,熔断策略设置:慢调用比例, 最大RT200毫秒,比例阈值0.1 ,熔断时长10s,最小请求数5
熔断策略
Sentinel 提供以下几种熔断策略:
- 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
手动配置:客户端的Sentinel版本需要和服务器的版本要保持一致。
java
@ResponseBody
@GetMapping("/seckillSessionBySkuId")
public R getSeckillSessionBySkuId(@RequestParam("skuId") Long skuId){
System.out.println("seckillSessionBySkuId -----------------------");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
SeckillSkuRedisDto dto = seckillService.getSeckillSessionBySkuId(skuId);
return R.ok().put("data",JSON.toJSONString(dto));
}
8.自定义资源
详情看官网
https://github.com/alibaba/Sentinel/wiki/如何使用#定义资源
定义资源
-
方式一:主流框架的默认适配
为了减少开发的复杂程度,我们对大部分的主流框架,例如 Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor 等都做了适配。您只需要引入对应的依赖即可方便地整合 Sentinel。可以参见: 主流框架的适配。 比如前面结合Feign
-
方式二:抛出异常的方式定义资源
SphU 包含了 try-catch 风格的 API。用这种方式,当资源发生了限流之后会抛出 BlockException。这个时候可以捕捉异常,进行限流之后的逻辑处理。示例代码如下:
java
// 1.5.0 版本开始可以利用 try-with-resources 特性(使用有限制)
// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
// 被保护的业务逻辑
// do something here...
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
// 在此处进行相应的处理操作
}
- 方式四:注解方式定义资源
Sentinel 支持通过 @SentinelResource 注解定义资源并配置 blockHandler 和 fallback 函数来进行限流之后的处理。示例:
java
// 原本的业务方法.
@SentinelResource(blockHandler = "blockHandlerForGetUser")
public User getUserById(String id) {
throw new RuntimeException("getUserById command failed");
}
// blockHandler 函数,原方法调用被限流/降级/系统保护的时候调用
public User blockHandlerForGetUser(String id, BlockException ex) {
return new User("admin");
}
带入实际业务接口 秒杀商品信息接口实例:
java
public List<SeckillSkuRedisDto> blockHandler(BlockException blockException){
log.error("限流执行的 blockHandler 方法 ....{}",blockException.getMessage());
return null;
}
/**
* 查询出当前时间内的秒杀活动及对应的商品SKU信息
* @return
*/
@SentinelResource(value = "currentSeckillSkusResources",blockHandler = "blockHandler")
@Override
public List<SeckillSkuRedisDto> getCurrentSeckillSkus() {
// 1.确定当前时间是属于哪个秒杀活动的
long time = new Date().getTime();
try (Entry entry = SphU.entry("getCurrentSeckillSkusResources")) {
// 被保护的业务逻辑
// 从Redis中查询所有的秒杀活动
Set<String> keys = redisTemplate.keys(SeckillConstant.SESSION_CHACE_PREFIX + "*");
for (String key : keys) {
//seckill:sessions1656468000000_1656469800000
String replace = key.replace(SeckillConstant.SESSION_CHACE_PREFIX, "");
// 1656468000000_1656469800000
String[] s = replace.split("_");
Long start = Long.parseLong(s[0]); // 活动开始的时间
Long end = Long.parseLong(s[1]); // 活动结束的时间
if(time > start && time < end){
// 说明的秒杀活动就是当前时间需要参与的活动
// 取出来的是SKU的ID 2_9
List<String> range = redisTemplate.opsForList().range(key, -100, 100);
BoundHashOperations<String, String, String> ops = redisTemplate.boundHashOps(SeckillConstant.SKU_CHACE_PREFIX);
List<String> list = ops.multiGet(range);
if(list != null && list.size() > 0){
List<SeckillSkuRedisDto> collect = list.stream().map(item -> {
SeckillSkuRedisDto seckillSkuRedisDto = JSON.parseObject(item, SeckillSkuRedisDto.class);
return seckillSkuRedisDto;
}).collect(Collectors.toList());
return collect;
}
}
}
} catch (BlockException ex) {
// 资源访问阻止,被限流或被降级
log.error("getCurrentSeckillSkusResources被限制访问了...");
// 在此处进行相应的处理操作
}
return null;
}
9.网关流控
- 前面所做的限流都是基于具体的业务微服务中,在业务代码去限制,而我们微服务架构中也有网关服务Gateway,请求通过网关来转发到具体的微服务中,我们也可以在网关处做限流动作,可以参考官网的指南: 网关流量控制
这里演示一个简单的例子,在网关服务pom文件,导入一个依赖,然后与前面介绍的使用方式一致,直接请求我们的电商首页等接口信息,因为这些都是通过网关转发,然后在sentinel客户端页面刷新,就可以打开看的网关服务的接口信息,请求链路栏目中可以看的API的名称,是网关服务定义的路由id 值如:- id: msbmall_host_route1,点击 流控 按钮 进行相应的 阈值设置即可生效
网关服务依赖引入:
xml
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
<version>2021.1</version>
</dependency>