一、前言:和Hystrix作对比
Hystrix:
- 1、需要我们自己手工搭建监控平台
- 2、没有一套web界面,不可以给我们进行更加细粒度化的配置流控、速率控制、服务熔断、服务降级
Sentinel:
- 1、单独一个组件,可以独立出来
- 2、直接界面化的细粒度统一配置
二、Sentinel(哨兵)是什么
官网:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。主要就是服务雪崩、服务降级、服务熔断、服务限流
三、Sentinel的下载安装运行
Sentinel分为两部分:
核心库(java客户端):不依赖任何框架/库,能够运行于所有java运行环境,同时对Dubbo/Spring Cloud等框架也有较好的支持。
控制台(Dashboard):基于Spring Boot开发,打包后可以直接运行,不需要额外的Tomcat等应用容器。
下载Sentinel的jar包,然后直接java -jar 运行即可 ,Sentinel的用户和密码默认都是Sentinel
四、Sentinel的初始化监控
1、建立一个module被Sentinel保护
2、pom
//sentinel的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
//后面做持久化会用到
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
//添加openfeign的依赖,后面会用到
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3、yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719+1依次扫描
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
4、主启动类
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class,args);
}
}
5、业务类
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "testA";
}
@GetMapping("/testB")
public String testB() {
return "testB";
}
}
6、测试
此时访问一下controller中指定的路径,Sentinel就将对应用进行保护了,因为Sentinel采取的是懒加载,访问localhost:8080也就可以看到应用。
五、Sentinel的流量控制
流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
资源名:唯一名称,默认请求路径
阈值类型/单机阈值:
①QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
②线程数:当调用该api的线程数达到阈值的时候进行限流
流控模式:
①直接:api达到限流条件时,直接限流
②关联:当关联的资源达到阈值时,就限流自己。
③链路:只记录指定链路上的流量,如果达到阈值,就进行限流
流控效果
①快速失败:直接失败,抛异常
②Warm up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热时长,才达到设置的QPS阈值
③排队等待:匀速排队,让请求以匀速的速度通过,阈值类型必须为QPS,否则无效
1、QPS直接快速失败报错
2、QPS和线程作对比
如下图所示,在这里都以设置为1作比较,QPS表示的是每秒的请求数量,如果一旦一秒钟超过了1个,就相当于是有一扇门直接给挡住了,然后直接报错。线程就像是,进去了这扇门,不管有多少数据都可以进来,但是只能一个个的依次的来处理,就像是银行的业务人员,一旦两个想同时访问,不好意思,不行。
3、QPS关联模式快速报错(因为微服务中两个服务之间是有关联的,比如说支付接口挂了,那你下单接口也歇一歇)当与A关联的B达到阈值时,A就限流自己,B惹事A遭殃
@GetMapping("/testA")
public String testA() {
return "testA";
}
@GetMapping("/testB")
public String testB() {
return "testB";
}
4、QPS warm up流控效果
Warm Up(
RuleConstant.CONTROL_BEHAVIOR_WARM_UP
)方式,即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。详细文档可以参考 流量控制 - Warm Up 文档,具体的例子可以参见 WarmUpFlowDemo。
通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:
如下图所示,我们最后想要设置的值是单机阈值是10,但是不想让他一下子到10,而是想让他慢慢的增长,一开始的单机阈值设置为10(单机阈值)/3(冷加载因子)=3.3,5秒(预热时长)之后达到理想单机阈值10。
公式:默认 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。
5、QPS匀速排队
匀速排队
匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER
)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。详细文档可以参考 流量控制 - 匀速器模式,具体的例子可以参见 PaceFlowDemo。
该方式的作用如下图所示:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
如上配置表示不管有多少的请求放进来,一秒钟只会处理一个请求,超时时间设置为了20s,表示如果这个请求在20s之后还没有被处理就会报错。
六、Sentinel的服务降级
1、简介
Sentinel熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速的失败,当资源被降级时,在接下来的降级时间窗口之内,对该资源的调用都自动熔断,而Hystrix则会有一个半开状态,这是不一样的。
RT、异常比例、异常数
2、RT
平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 N 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位)
,那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
如上图所示,设置RT降级规则,RT设置为200毫秒,时间窗口设置为1s,也就是说,如果访问/testB资源进入的N个请求,访问响应的时间都超过200ms了,那么在这个时间窗口期1s内,就会发生服务熔断。
3、异常比例
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= N(可配置),并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
如上图所示,如果一秒钟内有四个请求访问/testB,50%以上的请求,比如说3个请求访问发生错误,就会在1s的时间窗口内发生服务熔断。
4、异常数
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow(时间窗口) 小于 60s,则结束熔断状态后仍可能再进入熔断状态。所以时间窗口一定要大于等于一分钟时间
如上图所示,如果在一分钟内,异常数超过10个,就会发生服务降级。注意时间窗口一定要大于60s。
七、热点参数限流
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
1、在服务类新加配置
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String testHotKey(@RequestParam(value = "p1", required = false) String p1,
@RequestParam(value = "p2", required = false) String p2) {
return "-----testHotKey";
}
public String deal_testHotKey(String p1, String p2, BlockedException e) {
return "------deal_testHotKey failed";
}
2、浏览器访问
此时如果访问这个路径http://localhost:8401/testHotKey?p1=a,因为上图进行了热点限流,参数索引是0,也就是p1,单机阈值为1,也就是1s内访问超过1次就会触发热点限流,而这里就是触发我们写的兜底方法。如果没有兜底方法的话就会是一个很不友好的error界面。而且只要有p1就会进行热点限流
http://localhost:8401/testHotKey?p1=a会进行热点限流
http://localhost:8401/testHotKey?p1=a&p2=b会进行热点限流
http://localhost:8401/testHotKey?p2=b不会进行热点限流
3、参数例外项(VIP)
如上图所示,进行了参数例外项的配置,其实意思就是当索引0下标对应的值也就是p1,当他是5时,阈值QPS可以达到200在进行热点限流,而不再是1。相当于是给p1=5开了一个vip。参数类型必须是八大基本类型和String。
4、系统保护规则
系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量。
Load 自适应(仅对 Linux/Unix-like 机器生效):
系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
CPU usage(1.5.0+ 版本):
当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
平均 RT:
当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
并发线程数:
当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
入口 QPS:
当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
八、@SentinelResource注解
@GetMapping(value = "/byResource")
@SentinelResource(value = "byResource", blockHandler = "handlerException")
public String CommonResult() {
return "success";
}
public String handlerException(BlockException e) {
return "failed" + e;
}
如上图所示,如果每一个方法都要配置一个blockHandler,那么会造成业务和代码高耦合,且造成代码冗余,所以,把处理方法提取出来放到一个类里面,并且可以在这个类里面配置多个方法。
@RestController
public class RateLimitController {
@GetMapping(value = "/extract")
@SentinelResource(value = "extract", blockHandlerClass = CustomerBlockHandler.class, blockHandler = "Handler1")
public String HandlerExtract() {
return "我成功了";
}
}
public class CustomerBlockHandler {
public static String Handler1(BlockException e) {
return "handler1由我来处理";
}
public static String Handler2(BlockException e) {
return "handler2由我来处理";
}
}
如上图所示,这样就可以避免了代码的冗余和业务的耦合。并且能够实现全局的统一处理方法,
@SentinelResource注解其他注意:
注意不可以用private注解
注意Sentinel的三个核心API:
①:sphU定义资源
②:Tracer定义统计
③:contextUtil定义上下文
九、服务熔断功能
一、环境预说
9003和9004只有端口号的差异,举例9003配置:
1、module
2、pom
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
3、yml
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
4、主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class, args);
}
}
5、业务类
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
public static HashMap<Long, Payment> hashMap = new HashMap<>();
static {
hashMap.put(1L, new Payment(1L, "dfadfhasdfgjadsf"));
hashMap.put(2L, new Payment(2L, "nnnnnnnn"));
hashMap.put(3L, new Payment(3L, "mmmmmmm"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
Payment payment = hashMap.get(id);
CommonResult<Payment> result = new CommonResult<>(200, "serverPort:" + serverPort, payment);
return result;
}
}
8402客户端环境:
1、module
2、pom
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
3、yml
server:
port: 8402
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719+1依次扫描
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
4、主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderMain {
public static void main(String[] args) {
SpringApplication.run(OrderMain.class, args);
}
}
5、配置类
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
6、业务类
@RestController
@Slf4j
public class CircleBreakerController {
public static final String SERVICE_URL = "http://nacos-payment-provider";
@Resource
private RestTemplate restTemplate;
@GetMapping(value = "/consumer/fallback/{id}")
public CommonResult<Payment> fallBack(@PathVariable("id") Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("非法参数异常");
} else if (result.getData() == null) {
throw new NullPointerException("没有对应的记录");
}
return result;
}
}
如上所示,客户端在访问时可以实现轮询
二、然后由业务类来进行服务熔断的学习
运用fallback管理程序运行时异常,如下所示添加一个fallback方法,就不再是难看的errorpage页面,而是我们自定义的页面。
@GetMapping(value = "/consumer/fallback/{id}")
@SentinelResource(value = "fallback",fallback = "handFallback")
public CommonResult<Payment> fallBack(@PathVariable("id") Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("非法参数异常");
} else if (result.getData() == null) {
throw new NullPointerException("没有对应的记录");
}
return result;
}
public CommonResult<Payment> handFallback(@PathVariable("id") Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常" + e.getMessage(), payment);
}
三、fallback只会管java产生的异常,blockHandler只会管Sentinel控制台的配置违规
如下这样,既配置blockhandler又配置fallback就可以既处理java异常又处理Sentinel配置违规,如果两个规则都违规时,则会进入blockHandler的处理
。
@GetMapping(value = "/consumer/fallback/{id}")
// @SentinelResource(value = "fallback",fallback = "handFallback")
@SentinelResource(value = "fallback",fallback = "handFallback",blockHandler = "blockHandler")
public CommonResult<Payment> fallBack(@PathVariable("id") Long id) {
CommonResult<Payment> result = restTemplate.getForObject(SERVICE_URL + "/paymentSQL/" + id, CommonResult.class, id);
if (id == 4) {
throw new IllegalArgumentException("非法参数异常");
} else if (result.getData() == null) {
throw new NullPointerException("没有对应的记录");
}
return result;
}
public CommonResult<Payment> handFallback(@PathVariable("id") Long id, Throwable e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(444, "兜底异常" + e.getMessage(), payment);
}
public CommonResult<Payment> blockHandler(@PathVariable("id") Long id, BlockException e) {
Payment payment = new Payment(id, "null");
return new CommonResult<>(445, "blockhandler异常" + e.getMessage(), payment);
}
exceptionsToIgnore表示忽略这个异常,保证这个程序先走通,如下所示,这里是个数组,可以加多个异常VIP
四、Sentinel和OpenFeign
1、修改pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、修改yml
#添加激活Sentinel对Feign支持
feign:
sentinel:
enabled: true
3、主启动类添加注解@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderMain8402 {
public static void main(String[] args) {
SpringApplication.run(OrderMain8402.class, args);
}
}
4、业务类
首先是一个专门调用服务端方法的接口,详细看之前OpenFeign的学习,FeignClient的value对应着服务类的实例名,fallback表示处理降级的类
@FeignClient(value = "nacos-payment-provider", fallback = PaymentServiceImpl.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
@Component
public class PaymentServiceImpl implements PaymentService {
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444, "服务降级返回", new Payment(id, "err"));
}
}
然后在controller就可以直接调用了,而不再需要RestTemplate了
@Resource
private PaymentService paymentService;
@GetMapping(value = "consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
return paymentService.paymentSQL(id);
}
5、测试
将服务端停掉
十、Sentinel的持久化规则
痛点:每次重启服务器,Sentinel的配置就都没有了,导致了每次都要重新配置
解决方法:将限流规则配置到nacos上面,只要刷新8401某个rest地址,Sentinel的流控规则就能看到,只要Nacos不删除配置,就一直有效。
详细步骤:8401示例
1、pom添加依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
2、修改yml(yml一定要格式正确,因为格式不正确,我的Sentinel持久化一直没生效)
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
#Nacos服务注册中心地址
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719+1依次扫描
port: 8719
datasource:
ds1:
nacos:
server-addr: localhost:8848
dataId: cloudalibaba-sentinel-service
groupId: DEFAULT_GROUP
data_type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
在yml中配置的dataId即对应着在如下Nacos中配置的Data ID。
3、在nacos中配置Sentinel流控规则
配置规则:
resource:
资源名,即限流规则的作用对象;
limitApp:
流控针对的调用来源,若为 default 则不区分调用来源;
grade:
限流阈值类型: 0表示线程数,1表示QPS;
strategy:
流控模式,0表示直接,1表示关联,2表示链路;
controlBehavior:
流量控制效果:0快速失败、1Warm Up、2排队等待;
clusterMode:
是否集群。
这时Sentinel中就会一直有我们配置的限流规则了,但是目前来看Sentinel的持久化配置比较复杂,应该是个半成品,期待阿里后期继续完善。