SpringCloud Alibaba Sentinel实现熔断与限流
Hystrix的不足
- 需要我们程序员自己手工搭建监控平台
- 没有一套web界面可一个我们进行更加细粒度化的配置流控、速率控制、服务熔断、服务降级
Sentinel的优势
- 单独一个组件,可以独立出来
- 直接界面化的细粒度统一配置。
https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
下载安装后运行即可
启动成功后则是
案例
创建一个cloudalibaba-sentinel-service8401
写pom
<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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</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>
<scope>test</scope>
</dependency>
<!--自定义的api包-->
<dependency>
<groupId>com.huangyy.cloud2021</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
写yml
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
#默认是8719,加入被占用会自动从8719开始扫描,直至找到未被占用的端口
port: 8719
management:
endpoints:
web:
exposure:
include: '*'
主启动:
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class,args);
}
}
controller
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA(){
return "----------A";
}
@GetMapping("/testB")
public String testB(){
return "----------B";
}
}
Sentinel采用的是懒加载,需要执行一次才可
流控规则
流控:
- QPS(每秒钟的请求数量):当调用该api的QPS达到阈值的时候,进行限流
- 线程数:当调用该api的线程数达到阈值的时候,进行限流
直接:api达到限流条件时,直接限流
阈值1,一秒点一下没事,但一旦点多
关联:当关联的资源达到阈值时,就限流自己,B惹事,A挂了
链路:只记录指定连路上的流量(制定资源从入口资源进来的流量,如果达到阈值,就进行限流)【api级别的针对来源】
快速失败:直接失败,抛异常
Warm Up:根据codeFactor(冷加载因子,默认3)的值,从阈值/codeFactor,经过预热市场,才达到设置的QPS阈值
排队等待:匀速排队
降级规则:
- 慢调用比例 (SLOW_REQUEST_RATIO):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。
- 异常比例 (ERROR_RATIO):当单位统计时长(statIntervalMs)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
- 异常数 (ERROR_COUNT):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。
热点规则:
热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
-
商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
-
用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制
用@SentinelResource注解来设置兜底方法:
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
<version>x.y.z</version>
</dependency>
@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, BlockException exception){
return "---------------deal_testHotKey,o(╥﹏╥)o";
}
参数索引
及参数组的下标,从0开始
假设若期望p1参数当它是某个特殊值时,它的限流值和平时不一样,如p1的值为5时,他的阈值可以达到200
注意:
@SentinelResource处理的是Sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理,但如果是运行时异常,@SentinelResource则不管,后面会讲
系统自适应限流
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 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
在8401多写一个controller类:
按资源名称限流
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource",blockHandler = "handleException")
public CommonResult byResource(){
return new CommonResult(200,"按资源名称限流测试ok",new Payment(2020L,"serial001"));
}
public CommonResult handleException(BlockException exception){
return new CommonResult(444,exception.getClass().getCanonicalName()+"\t服务不可用");
}
}
运行方法一次后,会出现:
添加流控规则
当快速点击时,会返回自定义限流信息
额外问题:当关闭8401时,Sentinel控制台上的信息消失 ,后面会说
按url地址限流
在controller上加:
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")
public CommonResult byUrl(){
return new CommonResult(200,"按url限流测试ok",new Payment(2020L,"serial002"));
}
全局统一的限流兜底方法
创建一个CustomerBlockHandler类
public class CustomerBlockHandler {
public static CommonResult handlerException(BlockException exception){
return new CommonResult(4444,"按客户自定义1,global");
}
public static CommonResult handlerException2(BlockException exception){
return new CommonResult(4444,"按客户自定义2,global");
}
}
然后改controller,blockHandlerClass可指定类,blockHandler指定方法
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class,blockHandler = "handlerException2")
public CommonResult customerBlockHandler(){
return new CommonResult(200,"按客户自定义ok",new Payment(2020L,"serial002"));
}
最终会走2的方法
Sentinel主要有三个核心API
- SphU定义资源
- Tracer定义统计
- ContextUtil定义上下文
服务熔断功能
新建一个9003、9004
写pom
<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>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.huangyy.cloud2021</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>${project.version}</version>
</dependency>
写yml
server:
port: 9003
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848
management:
endpoints:
web:
exposure:
include: '*'
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9003 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9003.class,args);
}
}
controller
@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,"34342154126265126446521"));
hashMap.put(2L,new Payment(2L,"sdadasfafea33343aa34521"));
hashMap.put(3L,new Payment(3L,"34adffaetret454q6446521"));
}
@GetMapping(value = "/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
//模拟从数据库中取出数据
Payment payment=hashMap.get(id);
CommonResult<Payment> result=new CommonResult<>(200,"from mysql,servletPort:"+serverPort,payment);
return result;
}
}
9004和9003一样。
下面是消费者84,pom和上面一样
yml
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
dashboard: localhost:8080
port: 8719
#消费者要去访问的微服务名称(“成功注册进nacos的微服务提供者”)
service-url:
nacos-user-service: http://nacos-payment-provider
主启动
@SpringBootApplication
@EnableDiscoveryClient
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class,args);
}
}
config
@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller
@RestController
@Slf4j
public class CircleBreakerController {
@Value("service-url.nacos-user-service")
private String service_URL;
@Resource
private RestTemplate restTemplate;
@RequestMapping("/consumer/fallback/{id}")
@SentinelResource(value = "fallback")
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("IllegalArgumentException,非法参数异常");
else if (result.getData()==null) throw new NullPointerException("NullPointerException,改id没有对应值,空指针异常");
return result;
}
}
到此,我们实现了轮询的效果
我们这里什么都没配,如果是4,这里是Error Page,非法参数异常
将注释改为:@SentinelResource(value = "fallback",fallback = "handlerFallback")
并写上兜底方法:
public CommonResult<Payment> handlerFallback(@PathVariable("id") Long id,Throwable e){
Payment payment=new Payment(id,"null");
return new CommonResult<>(444,"兜底异常handlerFallback,exception内容"+e.getMessage(),payment);
}
如果id>=4,则会去到友好提示
若将注解改为blockhandler:@SentinelResource(value = "fallback",blockHandler = "blockHandler")
兜底方法改为:
public CommonResult<Payment> blockHandler(@PathVariable("id") Long id, BlockException blockException){
Payment payment=new Payment(id,"null");
return new CommonResult<>(445,"blockHandler限流,exception内容"+blockException.getMessage(),payment);
}
设置降级规则
当id等于4时报错误页面,非法参数。当达到规则,会有友好界面
综上 :fallback管java业务异常,blockHandler管控制台配置违规
fallback和blockHandler都配置:@SentinelResource(value = "fallback",fallback = "handlerFallback",blockHandler = "blockHandler")
若配置流控:
最终结果,都会生效,若进入了限流,则走blockhandler
其他属性:
- exceptionsToIgnore,排除某个异常类
Sentinel整合openfeign
84改pom,加上(之前 我加过了):
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
改yml,加:
feign:
sentinel:
enabled: true
主启动类上加注解:@EnableFeignClients
接口加注解:
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping(value = "/paymentSQL/{id}")
CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);
}
实现类:
@Component
public class PaymentFallbackService implements PaymentService{
@Override
public CommonResult<Payment> paymentSQL(Long id) {
return new CommonResult<>(44444,"服务降级返回,PaymentFallbackService",new Payment(id,"errorSerial"));
}
}
controller:
@Resource
private PaymentService paymentService;
@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id){
return paymentService.paymentSQL(id);
}
持久化
最后的问题,Sentinel的规则在服务器挂了后则会消失
将限流配置跪着持久化进Nacos保存,只需要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效
这里用8401:
改pom(之前我加过 ):
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
改yml:加:
datasource:
ds1:
nacos:
server-addr: localhost:8448
dataId: cloudalibaba-sentinel-service
groupID: DEFAULT_GROUP
data-type: json
rule-type: flow
在nacos中:
- resource:资源名称
- limitApp:来源应用
- grade:阈值类型,0代表线程数,1表示QPS
- count:单机阈值
- strategy:流控模式,0表示直接,1表示关联,2表示链路;
- controlBehavior:流控效果,0表示快速失败,1表示warm Up,2表示排队等待
- clusterMode:是否集群