Alibaba Sentinel熔断、限流使用说明

说明

Sentinel官方文档写的不是很详细,坑比较多。本人主要是改了Sentinel的控制台release版本源码,测试环境又使用的是我改源码后打包的jar,有两个功能支持不是很好(参数限流和黑白名单授权),如果有其他问题,请联系我。但是基本上95%的工作都已经被我封装了,大家只需不到5%的工作量,当前熔断、降级支持Hystrix方案,所以以前的代码可以几乎不动(有少量问题下面会说)。

Sentinel控制台:
http://192.168.128.36:8081/#/dashboard/metric/gateway-server
用户名/密码:myyshop

引入pom包

引入下述包即可使用sentinel

        <!-- sentinel自定义封装组件 -->
        <dependency>
            <groupId>com.myyshop.framework</groupId>
            <artifactId>sentinel-spring-boot-starter</artifactId>
            <version>1.0.0-RELEASE</version>
        </dependency>

其他基础包引用版本升级和现有影响请严格按照Confluence->基础组件变更记录->变更时间2020-10-14修改,Confluence地址为:

http://192.168.128.17:8090/pages/viewpage.action?pageId=3768400

Nacos配置

需要引入 global-public-share.yml 共享配置,这样即可开启 Sentinel 和 Nacos 持久化

  spring:
    cloud:
      config:
        server-addr: 192.168.128.20
        file-extension: yml
        group: uaa-center-server
        namespace: 1c53b59e-9f3d-44a4-b2d7-5faaea25b3c6 # dev 环境的命名空间
        extension-configs:
          - data-id: global-public-share.yml
            group: share
            refresh: true

配置Sentinel控制台链路

bootstrap-dev.yml.example 全部配置如下:

spring:
  cloud:
    sentinel:
      transport:
        client-ip: 192.168.128.41  #如果docker 在20上就配置成20。prod 和 test不需要指定该参数
        port: 8733    #这个端口配置不能跟其他服务重复

bootstrap-prod.yml.examplebootstrap-test.yml.example 跟上述dev配置类似,只是不需要指定client-ip,端口跟上面一样

下面注意两点:

  • client-ip 看自己负责微服务所在开发环境ip(参照下图client-ip)
  • port 去 端口总览 -> sentinel端口 找自己的负责微服务端口,端口总览地址点击
client-ip

注: 如果还看不懂的,参照uaa-center-server服务下bootstrap-dev.yml.example、bootstrap-prod.yml.example 和 bootstrap-test.yml.example

使用Feign+Hystrix处理熔断、降级

用过Hystrix的都知道熔断、降级需要指定具体降级类(也就是fallback),同时加入@Component注解,否则项目启动报错当前Sentinel兼容Hystrix的降级类。而现在大家代码里的fallback根本形同虚设,不管任何异常都不会走进去,当然造成这个现象也有一些历史原因的,本来要用Hystrix的,后面又决定不用了,原因是Hystrix已经闭源了,开源社区已经不活跃的组件,已经没有使用的必要。然后想着后面用Sentinel这个功能就会生效。

目前大家基本都是使用Feign调接口,使用方法跟大家现有Feign一样,几乎不需要改动,害怕大家看不懂,直接贴个例子:

接口端

@FeignClient(value= ServiceNameConstants.DS_USER_PLATFORM, fallbackFactory = DsUserFeignClientFallback.class)
public interface DsUserFeignClient {
    @GetMapping("/api/users/mail/{mail}")
    Result<LoginAppUser> findUserByMail(@RequestParam("mail") @PathVariable("mail") String mail);
}

Fallback类

必须加@Component注解,否则项目不能启动, 这点Hystrix跟Sentinel是一样的

@Component    //注意:必须加@Component注解,否则项目不能启动
public class DsUserFeignClientFallback implements FallbackFactory<DsUserFeignClient> {
    @Override
    public DsUserFeignClient create(Throwable throwable) {
        return new DsUserFeignClient() {
            @Override
            public Result findUserByMail(String mail) {
                log.error("通过邮箱查询用户异常:{}", mail, throwable);
                throw new BusinessException(BusinessExceptionEnum.BUSINESS_CALLBACK_SERVICE_ERROR.getCode()
                , BusinessExceptionEnum.BUSINESS_CALLBACK_SERVICE_ERROR.getMsg() + ServiceNameConstants.DS_USER_PLATFORM + ", 接口方法名:findUserByMail");
            }
        };
    }
}

这地方有个坑,原因是我们现在把服务调用Feign去拆出一个API项目出来(加注解项目会报错,当然本人有解决方案),但是本人不建议拆个服务出来,具体原因看下面的 其他 标题, 会详细分析。

Feign的fallback与fallbackFactory区别

fallback
fallbackFactory

使用Sentinel处理熔断、降级

本着对大家代码改动最小考虑,以前接口代码怎么样,现在还是一样,以前Feign调用fallback怎么处理(就是上面那个方案),现在还是一样。本人已经对Sentinel相关异常统一进行封装返回,一旦遇到Sentinel相关异常,则会默认抛出我的包装返回。
当然,在实际业务开发中,业务场景千奇百怪,虽然我封装的Sentinel默认返回处理适应大部分场景,但是还是有些业务场景需要指定自己的Sentinel异常。可以在需要特定Sentinel异常的接口加入@SentinelResource 注解,可以参照以下两个例子:

例子1:

  • value 指定resource名称,这个必须加,而且尽量不要跟,其他资源名重复
  • blockHandler 指定Sentinel异常返回的方法,参数必须跟接口一致+BlockException参数 ,需要注意的是,方法必须是跟接口在同一个类中
  • defaultFallback 指定接口提供方的异常 或 BlockException 异常(这个可以替代Hystrix的fallback的功能),需要注意的是,方法必须是跟接口在同一个类中
    @SentinelResource(value = "/uaa/hello", blockHandler = "exceptionHandler2", defaultFallback = "fallback2")
    @GetMapping(value = "/hello")
    public String abcd(@RequestParam(value = "name") String name, int age){
        log.info("name={}",name);
        log.info("age={}",age);
        String content = nacosProviderClient.abc(name, age);
        return "我是feign: " + content;
    }
    public String exceptionHandler2(String name, int age, BlockException ex) {.
        ex.printStackTrace();
        return "Oops, error occurred at "+ name + ","+ age;
    }
    public String fallback2(Throwable e){
        log.info("进入sentinelResource注解测试,进入fallback2,参数b={}", e.getMessage());
        return "defaultFallback";
    }

例子2:

  • value 同上
  • fallbackClass 指定异常返回的类(跟例子1不同的是,上面的异常方法必须在本类,而fallbackClass 可以指定其它类)
  • blockHandler & defaultFallback 跟例子1解释基本一样,不同的是,异常方法必须写在 fallbackClass 指定的类中
    @GetMapping(value = "/abcd")
    @SentinelResource(value = "abcd", fallbackClass = DefaultBlockFallbackHandler.class, defaultFallback = "defaultFallback")
    public String abcd(String name){
        if(name.equals("abc")){
            throw new IllegalArgumentException("adffdgdfg");
        }
        if(name.equals("bbb")){
            try {
                Thread.sleep(900);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return "aaaaaaasdjkl" + name;
    }

以上两个例子大家可以根据自己业务实际情况,定制自己的异常,也可以不做任何处理,会默认走我封装的返回。此外,我还提供了一个通用异常类(例子2上面那个DefaultBlockFallbackHandler就是我对大家封装的一个通用异常),大家可以按需引用(不引用不会生效),通用异常类在sentinel-spring-boot-starter基础包中:

public class DefaultBlockFallbackHandler {
    public static String defaultFallback(Throwable e){
        if(e instanceof FlowException){
            FlowException ex = (FlowException) e;
            log.error("defaultFallback FlowException,资源信息={}", JSON.toJSONString(ex.getRule()));
            throw new SeataTrBusinessException(BusinessExceptionEnum.BUSINESS_FLOWEXCEPTION);
        } else if(e instanceof DegradeException){
            DegradeException ex = (DegradeException) e;
            log.error("defaultFallback DegradeException,资源信息={}", JSON.toJSONString(ex.getRule()));
            throw new SeataTrBusinessException(BusinessExceptionEnum.BUSINESS_DEGRADEEXCEPTION);
        } else if(e instanceof ParamFlowException){
            ParamFlowException ex = (ParamFlowException) e;
            log.error("defaultFallback ParamFlowException,资源名={},参数={},资源信息={}", ex.getResourceName(), ex.getLimitParam(), JSON.toJSONString(ex.getRule()));
            throw new SeataTrBusinessException(BusinessExceptionEnum.BUSINESS_PARAMFLOWEXCEPTION.getCode(), BusinessExceptionEnum.BUSINESS_PARAMFLOWEXCEPTION.getMsg() + e.getMessage());
        } else if(e instanceof AuthorityException){
            AuthorityException ex = (AuthorityException) e;
            log.error("defaultFallback AuthorityException,资源信息={}", JSON.toJSONString(ex.getRule()));
            throw new SeataTrBusinessException(BusinessExceptionEnum.BUSINESS_AUTHORITYEXCEPTION);
        }
        throw new SeataTrBusinessException(999, "defaultFallback, error = " + e.getClass().getName() + ": " + e.getMessage());
    }
}

无论是Feign+Hystrix方案还是Sentinel方案都需要大家根据自身业务需要去选型,也可以组合用。基本做到大家最少改动,就算大家现在代码什么都不改(除了Feign的fallback要加@Component注解外),也会走我封装的默认处理(默认会抛出异常)。但是还是那句话,结合自身业务场景去用,而不是一味的懒而不做处理(参照我上面Fallback类那块例子按自身业务去返回什么),默认走我抛出的异常。这时调用方就需要跟接口提供方协调代码异常是抛出BusinessException 还是 SeataTrBusinessException(这两种异常下面会进行说明),而不是什么都不处理或简单处理,线上接口出错,接口提供方和接口调用方互相指责

BusinessException 和 SeataTrBusinessException

说明:BusinessException 和 SeataTrBusinessException内部代码几乎一样,只是一个request state返回200,一个返回500

接口提供方代码异常无非就两种场景:

  • 第一种场景是接口提供方抛出异常,调用方不捕获跟着抛出(SeataTrBusinessException)
  • 第二种场景是接口提供方不抛异常(返回的是一个Result.fail对象),接口调用方拿到Result对象,去判断success是否是false,如果是false,进行相应业务错误处理,否则是true拿着接口提供方返回的数据继续执行后续业务代码(BusinessException )

SeataTrBusinessException:接口提供方异常会被抛出,这时调用方不处理(没有走feign fallback或者try cache捕获),也会跟着往上抛而出错。这种适合上面第一种场景。

BusinessException :接口提供方异常不会抛出,调用方拿到的是Result.fail(success=false)的对象,这时调用方可以根据success去判断接下来怎么处理。这种适合上面第二种场景

注意:SeataTrBusinessException如果在分布式事务场景下,只要一方服务出现任何异常(包括调用服务超时),其他服务事务跟着会回滚。而BusinessException只会回滚那个报错的服务事务。

其他

另外我想说的是,不建议大家把服务拆分出一个API项目出来,就比如拿上面的Feign fallback为例,如果服务拆分出来,会导致你不加@Component注解,服务可以调用,但是熔断、降级失效;加了这个注解,你连你自己的服务因为报错都启动不起来。(当然这个我也有解决方案)

Dubbo服务间调用默认是一种二进制TCP协议,它本身就需要握手来传输数据,所以导致他是有状态的,这就是为什么dubbo服务调用是调用对方的service,协议设计上没有足够的前瞻性,不适合做 service-mesh。但是熟悉dubbo的人肯定会想用dubbo这种调用方式,但是这种方式本身就不适合服务间调用(具体可以看dubbo协议介绍)。

而Spring Cloud的走的是HTTP协议,HTTP协议本身就是无状态的(对同一个url请求没有上下文关系),就算服务提供方的服务本身有问题,我调用方最多包一层处理就好了,也总比我依赖你的API POM包,导致我调用方服务都运行不起来好。

所以我的建议是Feign调用就在自身服务里,不要单独拆个API项目给别人用。

链路监控

Sentinel提供链路监控功能,能监控每个请求,或者某个微服务的QPS或响应时间(该功能需要添加网关),所以能充当一部分监控功能。

针对接口的监控
针对微服务监控
image.png