SpringCloudAlibaba 技术栈—Sentinel
1、什么是sentinel?
Sentinel是一个用于微服务架构的流量管理和控制系统,它通过限制和控制进入系统的流量,来保护系统免受过载和故障的影响,确保服务的稳定性。简而言之,它就是一个帮助微服务在高负载情况下也能稳定运行的工具。在分布式系统中,服务之间的相互调用会生成分布式流量。如何通过组件进行流量防护,并有效控制流量,是分布式系统的技术挑战之一。
什么是服务雪崩
假设我有一个微服务系统,这个系统内包含了 ABCD 四个微服务,这四个服务都是以集群模式构建的。
现在因为访问人数太多了,服务D堵住了,用户调用服务D没反应了。突然D死机了引发一系列的问题,这就是服务雪崩。
解决方案
Sentinel 服务容错的思路
Sentinel 是 Spring Cloud Alibaba 的⼀款服务容错组件,我们也经常把它叫做“防流量哨 兵”。它是阿里巴巴双十一促核心场景的保护神,内置了丰富的服务容错应用场景。它以流 量作为切入点,通过各种内外防控手段达到维持服务稳定性的目的。
在 Sentinel 中,我们可以采用降级和熔断的方式处理内部的异常。 所谓降级,是指当服务调用发生了响应超时、服务异常等情况时,我们在服务内部可以执行一段“降级逻辑”,说人话就是让用户等一下。
而所谓熔断,是指当异常调用量达到一定的判定条件,比如在异常降级和慢调用请求的比例达到⼀个阈值、窗口时间内降级请求达到⼀定数量的情况下,微服务在一段时间内停止对⽬标服务发起调用,所有来访请求直接执行降级逻辑。所以,熔断是“多次服务调用异常”累积的结果。
限流是流量整形流控方案的一种。在 Sentinel 中我们可以根据集群的处理能力,为每个服务设置⼀个限流规则,从 QPS 维度或者并发线程数的维度控制外部的访问流量。⼀旦访问量超过阈值,后续的请求就会被 “fast fail”,这是最为常用的⼀种限流手段。
Sentinel 基本概念
资源
资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码。只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。
规则
围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则。所有规则可以动态实时调整。
Sentinel 的主要特性
特性:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分
控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
注意:
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
Sentinel 是如何工作的
Sentinel 的主要工作机制如下:
- 对主流框架提供适配或者显示的 API,来定义需要保护的资源,并提供设施对资源进行实时统计和调用链路分析。
- 根据预设的规则,结合对资源的实时统计信息,对流量进行控制。同时,Sentinel 提供开放的接口,方便您定义及改变规则。
- Sentinel 提供实时的监控系统,方便您快速了解目前系统的状态。
Sentinel 与 Hystrix、resilience4j 的对比
Sentinel | Hystrix | resilience4j | |
---|---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 | 基于异常比率、响应时间 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) | Ring Bit Buffer |
动态规则配置 | 支持多种数据源 | 支持多种数据源 | 有限支持 |
扩展性 | 多个扩展点 | 插件的形式 | 接口的形式 |
基于注解的支持 | 支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | Rate Limiter |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 | 简单的 Rate Limiter 模式 |
系统自适应保护 | 支持 | 不支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 | 不提供控制台,可对接其它监控系统 |
2、安装Sentinel控制台
可以从Releases · alibaba/Sentinel · GitHub下载最新版本的控制台 jar 包。
后台启动 Sentinel 控制台
注意:
启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。
nohup java -server -Xms64m -Xmx256m -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=192.168.66.100:8080-Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar >> /opt/sentinel.log 2>&1 &
访问 Sentinel 控制台
在浏览器输入http://192.168.66.100:8080即可,登录用户名密码都是sentinel。
3、将应用接入Sentinel
创建testSentinel父项目。添加依赖。
<dependencies>
<!-- SpringCloud 微服务 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringCloud Alibaba 微服务 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- SpringBoot 依赖配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
创建子模块pay-service.添加依赖。
<dependencies>
<!--nacos服务注册中心,主要是为了实现授权服务的-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--sentinel依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--借用监控开放端口-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok注解-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
创建配置文件application.yml
spring:
application:
name: pay-service
cloud:
#Sentinel 控制台地址
sentinel:
transport:
dashboard: 192.168.66.100:8080
# 关闭context整合
web-context-unify: false
# nacos地址,主要是实现授权服务的
nacos:
discovery:
server-addr: 192.168.66.100:8848
server:
port: 8081
创建启动类
@SpringBootApplication
@EnableDiscoveryClient
public class payServiceApplication
{
public static void main( String[] args )
{
SpringApplication.run(payServiceApplication.class, args);
}
}
编写测试controller
@RestController
public class payController {
/*测试*/
@GetMapping("/testA")
public String testA(){
return "testA";
}
}
查看sentinel控制台
注意:
Sentinel采用懒加载,发送一次请求即可localhost:8081/testA。
发送http://192.168.66.100:8080打开面板。
4、流量控制
流量控制就是:监控应用流量的 QPS 或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流量控制设计理念
流量控制有以下几个角度:
- 资源的调用关系:例如资源的调用链路,资源和资源之间的关系;
- 运行指标:例如 QPS、线程池、系统负载等;
- 控制的效果:例如直接限流、冷启动、排队等。
为/testA资源设置流控模式
sentinel默认设置controller层的接口为资源。
表示1秒钟内查询1次就ok,若超过1次,就直接快速失败,报默认错误。
参数:
- 资源名
其实就是我们请求的资源路径
- 针对来源
这个是此流控规则对应那些微服务进行流控管理,一般填写调用方的微服务名称,多个用","分割
- 阈值类型
一般有2中类型,QPS(每秒的最大请求数2)和线程数(并发线程数)
- 单机阈值
单机状态下的最大限制值
- 是否集群
根据实际情况选择
- 直接:
直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来;
- 关联:
统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 链路:
当指定链路上的访问量⼤于某个阈值时,对当前资源进⾏限流,这⾥的“指定链 路”是细化到 API 级别的限流维度
- 快速失败
默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出
FlowException
。
- Warm Up
即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压。
- 排队等待
匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法 。这种方式主要用于处理间隔性突发的流量。
流控模式之直接模式
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。
注意:
若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。
一直点刷新出现这个,这就说明配置生效啦。
流控模式之关联模式
当关联的资源达到阈值时,就限流自己。
流控模式之链路模式
假设有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求流控,并设置限流。
创建PaymentService类并添加一个queryGoods方法:
@Service
public class goodService {
/**
* @SentinelResource表示该资源受sentinel监控
* @return
*/
@SentinelResource("/goods")
public String queryGoods(){
return "the information of goods";
}
}
默认情况下,OrderService中的方法是不被Sentinel监控的,需要我们自己通过注解来标记要监控的方法。给OrderService的queryGoods方法添加@SentinelResource注解
控制层创建方法
@GetMapping("/save")
public String saveOrder() {
// 查询商品
orderService.queryGoods();
// 查询订单
System.err.println("新增订单");
return "新增订单成功";
}
重启服务,访问query和save,可以查看到sentinel的簇点链路规则中,出现了新的资源。
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息
流控效果之预热
预热:项目刚开始上线的时候访问数量很大,缓存很少,数据库压力很大。慢慢把流量开大。
给/payment/index这个资源设置限流,最大QPS为10,利用warm up效果,预热时长为5秒。
5秒钟之内将流量慢慢增大到10
QPS为10.刚刚启动时,大部分请求失败,成功的只有3个,说明QPS被限定在3,随着时间推移,成功比例越来越高。
热点参数限流
什么是热点? 热点即经常访问的数据
例子:
手机热点: 华为手机、苹果手机等
化妆品热点: 迪奥口红、香奈儿香水等
创建GoodsController
@RestController
public class GoodsController {
@SentinelResource("hot") //热点资源标记
@GetMapping("/getGood")
public String getGoods(String goodName){
return goodName;
}
}
热点参数限流规则
jemter测试:
参数是口红的话正好是10个:
线程隔离
限流是一种预防措施,虽然限流可以尽量避免因高并发而引起的服务故障,但服务还会因为其它原因而故障。而要将这些故障控制在一定范围,避免雪崩,就要靠线程隔离(舱壁模式)和熔断降级手段了。
假设1000个用户访问商品服务,但是出现了堵塞,请求迟迟没有返回,将用户服务的资源消耗完了;但是又有些请求向访问支付服务和地址服务等其他的服务,因为线程都用完了,因此造成其他服务不可访问。造成了服务雪崩。我们为每个服务创建个线程池, 只要用户想请求商品服务就需要从商品服务的线程池获取线程才能访问商品服务,这样就不会抢占其他服务的线程了。如果线程池的线程用完了就被限流了。
熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。
就是家里的保险丝,电路出问题了保险丝就烧断了。
断路器控制熔断和放行是通过状态机来完成的
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态。
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 请求成功:则切换到closed状态 请求失败:则切换到open状态
熔断降级策略
慢调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
controller模拟慢调用
@GetMapping("/testC")
public String testC(Integer id) throws InterruptedException {
if (id == 1){
//模拟异常睡眠两秒钟
Thread.sleep(2000);
}
return "testC";
}
超过50ms的请求都会被认为是慢请求,当异常比例达到百分之40,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了。过5秒钟由断路器又打开状态变为半开状态放一部分请求进来。
未使用Jemeter情况下测试/testC接口
使用Jemeter情况下测试/testC接口
后续我停止Jmeter,没有这么大的访问量了,断路器关闭(保险丝恢复),微服务恢复OK。
异常比例
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
当资源每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态。异常比率的阈值范围是 [0.0,1.0]。
/**
* 测试异常比例
* RT 平均响应时间
* @return
*/
@GetMapping("testD")
public String testD(Integer id) {
if (id == 1){
throw new RuntimeException("故意抛出异常,触发异常比例熔断。");
}
return "testD";
}
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
发送请求localhost:8001/testD?id=2
异常数
当资源近1分钟的异常数目超过阈值之后会进行熔断。
/*
* 测试异常数
*/
@GetMapping("/testF")
public String testF()
{
int age = 10/0;
return "------testF 测试异常数";
}
请求http://localhost:8081/testF,第一次访问绝对报错,因为除数不能为零, 我们看到error窗口
但是达到5次报错后,进入熔断后降级。
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
主要是对不走网关的请求的限制。只要请求走网关就会加上权限,不走网关就没权限,无法访问微服务。
我们允许请求从gateway到payment服务,不允许浏览器访问payment,那么白名单中就要填写网关的来源名称(origin)。
@RestController
@RequestMapping("/pay")
public class GoodsController {
@GetMapping("/auth")
public String auth()
{
return "auth";
}
}
添加一个解析请求的类
/*获取origin,如果是从网关过来的请求,网关会给请求加上个请求头origin*/
@Component
public class getOrigin implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String origin = httpServletRequest.getHeader("origin");
if (StringUtils.isEmpty(origin)) {
return "origin is blank";
}
return origin;
}
}
注意:
这个方法的作用就是从request对象中,获取请求者的origin值并返回。默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值default。因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。
先访问一下:localhost:8081/auth
再配置sentinel对/auth资源的授权。
不走网关再访问一下,被拦截住了。
打开Higress网关
systemctl start docker
docker ps -a
docker start b2
访问Higress
先创建路由
添加header设置
只要是通过网关的请求都会被添加一个origin=getway的参数。
走网关访问,没被拦截住。
系统自适应限流
这是个全局概念就是全部的资源作限流。
引入系统自适应限流的主要的目的
- 保证系统不被拖垮
- 在系统稳定的前提下保证系统的吞吐量。
系统规则
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的
maxQps * minRt
估算得出。设定参考值一般是CPU cores * 2.5
。
查看cpu核数
cat /proc/cpuinfo | grep "processor" | wc -l
- CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
top 查看cup使用情况
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
5、SentinelResource注解配置详解之只配置fallback
服务降级功能,但是只是限制后,返回不可控的结果肯定是不行的,我们还要保证调用者在调用那些被限制的服务时候,不管是不是被限制,都要让他们拿到一个合理的结果,而不是扔回去一个异常就完事了。
Sentinel提供了这样的功能,让我们可以另外定义一个方法来代替被限制或异常服务返回数据,这就是fallback和blockHandler。
- fallback:针对Java本身出现的异常进行处理的对应属性。
- blockHandler:针对违反Sentinel控制台配置规则时触发BlockException异常时对应处理的属性
@SentinelResource注解用于定义资源,并提供可选的 BlockException 异常处理(仅处理Sentinel控制台配置相关异常) 和 fallback 配置项(运行时异常以及自定义异常)。
@GetMapping("/testA")
public String testA(Integer id){
if(id==1){
throw new RuntimeException("异常");
}
return "testA";
}
}
不配置fallback之前
@GetMapping("/testA")
@SentinelResource(value = "testB",fallback = "testB")
public String testA(Integer id){
if(id==1){
throw new RuntimeException("异常");
}
return "testA";
}
/*testA出现异常后调用这个方法*/
public String testB(Integer id,Throwable throwable){
return "业务繁忙,请稍等( ̄︶ ̄)↗ ";
}
}
fallback中的参数名称就是testB方法名称。
测试一下:
可以单独创建个类或者是package专门存放这些fallback的方法。
package com.zj.fallback;
public class payFallBack {
/*testA出现异常后调用这个方法*/
public static String testA(Integer id,Throwable throwable){
return "业务繁忙,请稍等( ̄︶ ̄)↗ ";
}
}
@GetMapping("/testA")
@SentinelResource(value = "testA",fallback = "testA",fallbackClass = payFallBack.class)
public String testA(Integer id){
if(id==1){
throw new RuntimeException("异常");
}
return "testA";
}
推荐使用外置的方法,更有条理。
效果是一样的。
6、SentinelResource配置详解之只配置blockHandler
@GetMapping("block")
public String block(Integer id) {
if (id==1){
System.out.println(1/0);
}
return "block";
}
先请求一下:localhost:8081/pay/block?id=2
再配置熔断规则:
出现熔断:
package com.zj.fallback;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import org.springframework.stereotype.Service;
@Service
public class blockHandler {
/*block出现异常后调用这个方法*/
public static String block(Integer id, BlockException throwable){
return "业务繁忙,请稍等>﹏< ";
}
}
@SentinelResource(value = "block",blockHandlerClass = blockHandler.class,blockHandler = "block")
@GetMapping("block")
public String block(Integer id) {
if (id==1){
System.out.println(1/0);
}
return "block";
}
}
7、SentinelResource配置详解之fallback和blockHandler都配置
@SentinelResource(value = "test",blockHandlerClass = blockHandler.class,blockHandler = "block",
fallbackClass = payFallBack.class,fallback = "test")
@GetMapping("test")
public String test(Integer id) {
if (id==1){
System.out.println(1/0);
}
return "test";
}
没有出现熔断异常之前是fallback处理,出现熔断就是blockHander处理。