1. Sentinel是什么:

官网:https://github.com/alibaba/sentinel

中文版:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

JAVAD熔断实现案例 熔断java框架_学习


JAVAD熔断实现案例 熔断java框架_学习_02


JAVAD熔断实现案例 熔断java框架_学习_03

1.1 其实就是代替Hystrix的功能,解决:

  • 服务熔断
  • 服务降级
  • 服务限流
  • 服务雪崩

1.2 分为两部分:

JAVAD熔断实现案例 熔断java框架_spring_04

2. 下载安装:

文档:https://spring-cloud-alibaba-group.github.io/github-pages/greenwich/spring-cloud-alibaba.html#_spring_cloud_alibaba_sentinel

2.1 jar包下载

  1. 官网github下载jar包;
  2. 在下载目录下启动jar包:java -jar jar包名称;
  3. localhost:8080登录,用户名密码都是sentinel

2.2 docker下载

#拉取sentinel镜像
docker pull bladex/sentinel-dashboard

#运行sentinel(docker里的sentinel是8858端口)
docker run --name sentinel -d -p 8858:8858 bladex/sentinel-dashboard

#把nacos和mysql也启动起来

访问:账密都是sentinel

JAVAD熔断实现案例 熔断java框架_自定义_05


JAVAD熔断实现案例 熔断java框架_限流_06

3. 初始化演示工程:

JAVAD熔断实现案例 熔断java框架_自定义_07

  1. 新建模块cloudalibaba-sentinel-service8401
  2. 添加pom:
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. 添加yml
server:
  port: 8401

spring:
  application:
    name: cloudalibaba-sentinal-service
  cloud:
    nacos:
      discovery:
        #Nacos服务注册中心地址(改成自己的服务器ip地址,本地用localhost)
        server-addr: 10.211.55.26:8848
    sentinel:
      transport:
        #配置Sentin dashboard地址(改成自己的服务器ip地址,本地用localhost)
        dashboard: 10.211.55.26:8858
        # 默认8719端口,假如被占用了会自动从8719端口+1进行扫描,直到找到未被占用的 端口
        port: 8719
        
management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 添加controller
@RestController
public class FlowLimitController {

    @GetMapping("/testA")
    public String testA() {
        return "----testA";
    }

    @GetMapping("/testB")
    public String testB() {
        return "----testB";
    }
}
  1. 测试,启动8401,然后刷新sentinel后台页面,可以看到什么都没有。因为sentinel采用懒加载策略,所以需要调用服务后才在后台显示。
  2. 先访问接口,然后刷新sentinel后台页面:

4. 流控规则:

两种方式:

  1. 直接在流控规则中新建:
  2. 在簇点规则中新建:

流量控制:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_08

4.1 三种流控模式:

4.1.1 直接(默认):

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_09


超过一秒一次(或者设置线程数阙值,访问该api的线程数超过1秒一个),就会被限流,报错系统默认的错误信息:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_10

阕值类型

JAVAD熔断实现案例 熔断java框架_限流_11


QPS是直接挡在外面,而线程数是有多少个线程在处理,放进来后,有线程是空闲状态就对请求进行处理,都没空闲,就限流(关门打狗)。

4.1.2 关联:

当关联的资源达到阈值时,就限流自己。

  • 当与A关联的资源B达到阈值后,就限流A自己。

JAVAD熔断实现案例 熔断java框架_自定义_12

  1. 此时不管调用多少次A都不会限流,而此时超过1秒调用1次B,则会限流A。
  2. 设置postman频繁访问testB:
  3. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_13

  4. 测试A可以看到A又挂了:
  5. JAVAD熔断实现案例 熔断java框架_spring_14

  6. 等线程B访问结束后,A恢复正常。
4.1.3 链路:

多个请求调用了同一个微服务。

4.2 三种流控效果:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_15

4.2.1 快速失败(默认):

直接失败,抛出异常,即上面的流控模式的测试的处理方式。

4.2.2 预热:

JAVAD熔断实现案例 熔断java框架_自定义_16


JAVAD熔断实现案例 熔断java框架_spring_17

公式:阈值除以coldFactor(默认值为3),经过预热时长后才会达到阈值。

解释下:阈值设置为10,预热时长设置为10s,那么在前10秒钟,阈值其实时10/3,每秒限制3个qps,当达到10s后,qps才会提升到10个。

JAVAD熔断实现案例 熔断java框架_学习_18


源码

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_19

作用秒杀系统在开启的瞬间,会有很多流量上来,很可能把系统打死,预热方式就是为了保护系统,把流量慢慢的放进来,慢慢的把阙值增长到设置的阙值

4.2.3 排队等待:

JAVAD熔断实现案例 熔断java框架_限流_20


JAVAD熔断实现案例 熔断java框架_自定义_21


JAVAD熔断实现案例 熔断java框架_限流_22


作用用于处理间隔性突发的流量,例如消息队列。例如这样的场景:在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是第一秒直接拒绝多余的请求测试:postman模拟10个请求,可以看到请求每秒一个处理了:

JAVAD熔断实现案例 熔断java框架_spring_23

5. 降级规则:

JAVAD熔断实现案例 熔断java框架_学习_24

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_25


JAVAD熔断实现案例 熔断java框架_自定义_26


JAVAD熔断实现案例 熔断java框架_spring_27


注意sentinel断路器是没有半开状态的

半开状态:半开的状态系统自动去检测是否请求有异常,没有异常就关闭断路器恢复使用,由异常则继续打开断路器不可用。

5.1 平均响应时间:

JAVAD熔断实现案例 熔断java框架_学习_28


JAVAD熔断实现案例 熔断java框架_限流_29


测试:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_30

这个200ms是平均响应时间,而不是某一次的响应时间。
后续停止jmeter了,1s后,访问恢复正常。

5.2 异常比例:

JAVAD熔断实现案例 熔断java框架_spring_31


JAVAD熔断实现案例 熔断java框架_学习_32


测试:

JAVAD熔断实现案例 熔断java框架_spring_33


上述配置意思是:每秒大于等于5个请求,并且每秒内的请求中百分之20都失败了,那么就进入服务降级。请求恢复后,3秒钟之后,退出服务降级继续处理请求。

5.3 异常数:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_34


JAVAD熔断实现案例 熔断java框架_自定义_35


测试:

JAVAD熔断实现案例 熔断java框架_学习_36


一分钟内,如果访问处理出现异常的次数超过5次,熔断降级,进入时间窗口期不处理请求,61秒后退出时间窗口期继续处理请求。(时间窗口必须大于等于60秒,防止再次熔断降级)

6. 热点key限流:

JAVAD熔断实现案例 熔断java框架_限流_37


解释:比如某个商品被经常访问:localhost:80/get?pid=2;那么会对这条访问进行限流,但是不会对localhost:80/get?pid=3进行限流。

6.1 复习兜底方法:

即之前学的hystrix的兜底方法,如果发生了熔断,自定义一个兜底的方法,给用户一个友好提示。

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_38

6.2 限流配置:

  1. 在FlowLimitController中添加:主要是@SentinelResource注解,跟原来的HystrixCommand一样
@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"; // sentinel的默认提示都是: Blocked by Sentinel (flow limiting)
}
  1. 配置:testHotKey这个方法的第一个参数,限制同一个值每秒访问一次,超过就进行限流,调用兜底方法
  2. JAVAD熔断实现案例 熔断java框架_spring_39

  3. 访问热点key即第一个参数,每秒访问一次,正常返回
  4. 如果不设置blockHandler兜底方法,多次请求后,会报出错误页面,不太友好
  5. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_40

  6. 注意:如果程序中有运行异常,并不会进入兜底方法,@SentinelResource+blockHandler注解管的只是热点key的配置(fallback会处理业务异常)
  7. 设置兜底方法:如果每秒的访问请求带有索引为0的参数的数量超过1,进入统计窗口期,然后调用兜底方法
  8. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_41

  9. 1秒后退出统计窗口期,继续处理请求

6.3 参数例外项:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_42


JAVAD熔断实现案例 熔断java框架_学习_43

意思就是:比如当我们的第一个参数,比如上面的p1=10时,我们希望它的阈值是一个特殊值,比如可以达到200

点击热点规则配置的高级配置:

JAVAD熔断实现案例 熔断java框架_学习_44


注意点:参数必须是基本类型或者String

  1. 多次访问值为1:
  2. 多次访问值为vip:

6.4 注意点:

给testHotKey方法添加int i = 1 / 0;异常。

然后重新测试,发现兜底方法不适用于异常,有异常会直接打印到页面。

因为目前的兜底方法,是运用于配置不符合的情况,对于程序出错不关心。

JAVAD熔断实现案例 熔断java框架_自定义_45

7. 系统规则

尽量不用,粒度太高,很容易造成整个系统瘫痪

JAVAD熔断实现案例 熔断java框架_学习_46

7.1 配置系统规则:

  1. 针对整个系统的,每秒访问量超过1(阈值),限流
  2. 测试:略

8. SentnielResource:

8.1 限流演示:

8.1.1 按资源名称限流:
  1. 添加controller
@RestController
public class RateLimitController {

    @GetMapping("/byResource")
    @SentinelResource(value = "byResource",blockHandler = "handleException")
    public CommonResult byResource() {
        return  new CommonResult(200,"按照资源名称限流测试",new Payment(2020L,"serial001"));
    }

	//兜底方法
    public CommonResult handleException(BlockException exception) {
        return  new CommonResult(444,exception.getClass().getCanonicalName() + "\t 服务不可用");
    }
}
  1. 配置限流规则:
  2. JAVAD熔断实现案例 熔断java框架_学习_47

  3. 测试:可以看到成功进入了我们的兜底方法
  4. JAVAD熔断实现案例 熔断java框架_spring_48

  5. 关闭我们的服务,刷新流控规则,可以看到,我们的流控规则消失了,说明没有进行持久化
8.1.2 按照url进行限流
  1. controller添加
@GetMapping("/rateLimit/byUrl")
@SentinelResource(value = "byUrl")	//没有兜底方法,系统就用默认的
public CommonResult byUrl() {
    return  new CommonResult(200,"按照byUrl限流测试",new Payment(2020L,"serial002"));
}
  1. 配置添加:
  2. JAVAD熔断实现案例 熔断java框架_学习_49

  3. 测试
  4. JAVAD熔断实现案例 熔断java框架_自定义_50

8.2 存在的问题

上述演示的时候,我们可能会面临的问题:

  1. 如果没有写兜底方法,则是系统默认的,没有体现的我们自己的业务要求
  2. 如果写了兜底方法,我们自定义的处理方法又和业务代码耦合在一块,不直观
  3. 每个业务方法都添加一个兜底的,那代码膨胀加剧
  4. 全局统一的处理方法没有体现

8.3 自定义限流处理逻辑并解耦:

JAVAD熔断实现案例 熔断java框架_限流_51


JAVAD熔断实现案例 熔断java框架_限流_52

  1. 新建myhandler.CustomerBlockHandler自定义限流处理类:
public class CustomerBlockHandler {
    public static CommonResult handlerException(BlockException exception) {
        return  new CommonResult(444,"按照客户自定义限流测试,Glogal handlerException ---- 1");
    }

    public static CommonResult handlerException2(BlockException exception) {
        return  new CommonResult(444,"按照客户自定义限流测试,Glogal handlerException ---- 2");
    }
}
  1. 在RateLimitController中添加:
//CustomerBlockHandler
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
        blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
public CommonResult customerBlockHandler() {
    return  new CommonResult(200,"按照客户自定义限流测试",new Payment(2020L,"serial003"));
}
  1. 测试,不多说

8.4 更多注解说明:

JAVAD熔断实现案例 熔断java框架_限流_53


JAVAD熔断实现案例 熔断java框架_限流_54

9. Sentinel熔断处理:

9.1 新建提供者9003和9004

  1. 添加9003和9004
  2. pom导入nacos和sentinel不多说
  3. yml
server:
  port: 9003

spring:
  application:
    name: nacos-payment-provider
  cloud:
    nacos:
      discovery:
        server-addr: 10.211.55.26:8848  #nacos

management:
  endpoints:
    web:
      exposure:
        include: '*'
  1. 启动类不多说
  2. 接口:
@RestController
public class PaymentController {

    @Value("${server.port}")    //spring的注解
    private  String serverPort;

    public static HashMap<Long, Payment> map = new HashMap<>();
    static {
        map.put(1L,new Payment(1L,"1111"));
        map.put(2L,new Payment(2L,"2222"));
        map.put(3L,new Payment(3L,"3333"));
    }

    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id) {
        Payment payment = map.get(id);
        CommonResult<Payment> result = new CommonResult<>(200,"from mysql,serverPort: " + serverPort,payment);
        return result;
    }
}
  1. 测试,不多说

9.2 新建消费者84

这里远程调用服务使用ribbon来完成。

  1. pom、启动类和上述一样不多说
  2. yml
server:
  port: 84

spring:
  application:
    name: nacos-order-consumer
  cloud:
    nacos:
      discovery:
        server-addr: 10.211.55.26:8848  #nacos
    sentinel:
      transport:
        dashboard: 10.211.55.26:8858    #sentinel
        port: 8719

#消费者将去访问的微服务名称
server-url:
  nacos-user-service: http://nacos-payment-provider

#激活Sentinel对Feign的支持
feign:
  sentinel:
    enabled: true
  1. 启动类:
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class OrderMain84 {
    public static void main(String[] args) {
        SpringApplication.run(OrderMain84.class,args);
    }
}
  1. config:
@Configuration
public class ApplicationContextConfig {

    @Bean
    @LoadBalanced
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
  1. controller:
@RestController
@Slf4j
public class CircleBreakerController {

    public static  final  String SERVICE_URL = "http://nacos-payment-provider";

    @Resource
    private RestTemplate restTemplate;

    @RequestMapping("/consumer/fallback/{id}")
    @SentinelResource(value = "fallback")   //没有配置
    public CommonResult<Payment> fallback(@PathVariable Long id) {
        CommonResult<Payment> result = restTemplate.getForObject(
                SERVICE_URL + "/paymentSQL/" + id,CommonResult.class,id);

        if(id == 4){
            throw new IllegalArgumentException("IllegalArgument,非法参数异常...");
        }else if(result.getData() == null) {
            throw new NullPointerException("NullPointerException,该ID没有对应记录,空指针异常");
        }

        return  result;
    }   
}
  1. 简单测试,略

9.3 测试

之所以测试,是因为@SentinelResource注解中:

  • fallback管运行异常
  • blockHandler管的是配置违规
9.3.1 无配置测试:

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_55


JAVAD熔断实现案例 熔断java框架_学习_56


JAVAD熔断实现案例 熔断java框架_spring_57


JAVAD熔断实现案例 熔断java框架_限流_58

9.3.2 只配置fallback:
  1. 修改@SentinelResource注解,添加fallback方法:
//@SentinelResource(value = "fallback")
@SentinelResource(value = "fallback",fallback ="handlerFallback")
//。。。


public CommonResult handlerFallback(@PathVariable Long id,Throwable e) {
    Payment payment = new Payment(id,"null");
    return new CommonResult(444,"异常handlerFallback,exception内容: " + e.getMessage(), payment);
}
  1. 测试:
  2. 结论:运行异常被兜底
9.3.3 只配置blockHandler:
  1. 修改注解,添加blockHandler方法
@SentinelResource(value = "fallback",blockHandler = "blockHandler")
// ...

public CommonResult blockHandler(@PathVariable Long id,BlockException e) {
    Payment payment = new Payment(id,"null");
    return new CommonResult(444,"blockHandler-sentinel 限流,BlockException: " + e.getMessage(), payment);
}
  1. 在管理端中配置降级配置:
  2. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_59

  3. 测试id=5
  4. JAVAD熔断实现案例 熔断java框架_学习_60

  5. 多次测试:可以看到并没有进入空指针异常,而是服务被降级
  6. JAVAD熔断实现案例 熔断java框架_限流_61

  7. 结论:会抛出程序异常,如果触发配置异常会被降级
9.3.4 同时配置:
  1. 修改注解:
@SentinelResource(value = "fallback",fallback ="handlerFallback",blockHandler = "blockHandler")
  1. 多次调用id=1
  2. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_62

  3. 测试id=5
  4. JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_63

  5. 多次测试id=5
  6. JAVAD熔断实现案例 熔断java框架_限流_64

  7. 结论:当@SentinelResource注解fallback和blockHandler都指定后,然后同时符合,优先执行blockHandler兜底方法

9.4 异常忽略:

如果出现指定异常,不会进入兜底方法,会直接报出异常。

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_65

  1. 修改注解:
@SentinelResource(value = "fallback", 
            fallback ="handlerFallback", 
            blockHandler = "blockHandler", 
            exceptionsToIgnore = {IllegalArgumentException.class})
    //如果出现exceptionsToIgnore中的异常,不运行fallback兜底方法。
  1. 测试

9.5 openfeign完成远程调用

上述示例远程调用是通过ribbon来完成的,这里我们使用openfeign来完成

  1. 修改84,添加pom、yml、启动类注解支持
  2. 添加service接口
// fallback为降级后调用的服务
@FeignClient(value = "nacos-payment-provider",fallback = PaymentFallbackService.class)
public interface PaymentService {
    @GetMapping(value = "/paymentSQL/{id}")
    public CommonResult<Payment> paymentSQL(@PathVariable("id") Long id);

}
  1. 添加实现类
@Component
public class PaymentFallbackService implements PaymentService{
    
    @Override
    public CommonResult<Payment> paymentSQL(Long id) {
        return new CommonResult<>(444,"服务降级返回,---PaymentFallbackService",new Payment(id,"ErrorSerial"));
    }
}
  1. controller中:
//======= OpenFeign
@Resource
private PaymentService paymentService;

@GetMapping(value = "/consumer/paymentSQL/{id}")
public CommonResult< Payment > paymentSQL(@PathVariable("id") Long id){
    return paymentService.paymentSQL(id);
}
  1. 测试

10. 规则持久化:

之前我们看到,一旦我们重启应用,sentinel规则将消失,生产环境需要将配置规则进行持久化。

10.1 解决:

将限流配置规则持久化进Nacos保存,只要刷新8401某个rest地址,sentinel控制台的流控规则就能看到,只要Nacos里面的配置不删除,针对8401上sentinel上的流控规则持续有效。

JAVAD熔断实现案例 熔断java框架_JAVAD熔断实现案例_66

  1. pom添加:
<!-- SpringCloud ailibaba sentinel-datasource-nacos 持久化需要用到-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. yml的sentinel中添加datasource配置:
sentinel:
   transport:
     #配置Sentin 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
  1. nacos添加配置:
  2. 测试,略
  3. 停止后再次启动,发现配置还在

11. 熔断框架比较:

JAVAD熔断实现案例 熔断java框架_spring_67