前言

分布式系统遇到的问题

在一个高度服务化的系统中,实现一个业务逻辑通常会依赖多个服务;es:商品详情展示服务会依赖商品服务、价格服务、商品评论服务等:
微服务组件 Sentinel - 图1
调用三个依赖服务会共享商品详情服务的线程池;若其中的商品评论服务不可用,则会出现线程池里所有线程都因等待响应而被阻塞,从而造成服务雪崩

服务雪崩效应:因服务提供者的不可用导致服务调用者的不可用,并将不可用逐渐放大的过程,就叫服务雪崩效应
导致服务不可用的原因:程序 bug、大流量请求、硬件故障、缓存击穿等
大流量请求:在秒杀和大促开始前,若准备不充分,瞬间大量请求会造成服务提供者的不可用
硬件故障:可能为硬件损坏造成的服务器主机宕机,网络硬件故障造成的服务提供者的不可访问
缓存击穿:一般发生在缓存应用重启,缓存失效时高并发,所有缓存被清空时,以及短时间内大量缓存失效时;大量的缓存不命中,使请求直击后端,造成服务提供者超负荷运行,引起服务不可用

解决方案

超时机制:
在不做任何处理的情况下,服务提供者不可用会导致消费者请求线程强制等待,而造成系统资源耗尽;加入超时机制,一旦超时就释放资源;由于释放资源速度较快,一定程度上可以抑制资源耗尽的问题

服务限流(资源隔离):
限制请求核心服务提供者的流量,使大流量拦截在核心服务之外,这样可以更好的保证核心服务提供者不出问题,对于一些出问题的服务可以限制流量访问,只分配固定线程资源访问,这样能使整体的资源不至于被出问题的服务耗尽,进而整个系统雪崩

那么服务之间怎么限流,怎么资源隔离?
es:线程池 + 队列,听过信号量的方式;

服务熔断:
远程服务不稳定或网络抖动时暂时关闭,就叫服务熔断
微服务组件 Sentinel - 图2

服务降级:
所谓降级,就是当某个服务熔断之后,服务将不再被调用,此时客户端可以自行准备一个本地的 fallback(回退)回调,返回一个缺省值

Sentinel 分布式系统的流量防卫兵

Sentinel 介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要;Sentinel 是面向分布式服务架构的流量控制组件;主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保证微服务的稳定性
源码地址 官方文档

Sentinel 特征:

  1. 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,es:秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、实时熔断下游不可用应用等
  2. 完备的实时监控:Sentinel 同时提供实时的监控功能;可在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况
  3. 广泛的开源生态:Sentinel 提供开箱即用的与其他开源框架的整合模块,只需引入相应的依赖并进行简单的配置即可
  4. 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展点,可通过实现扩展点快速的定制逻辑

Sentinel 和 Hystrix 对比

Sentinel Hystrix
熔断策略 信号量隔离 线程池隔离/信号量隔离
熔断降级策略 基于响应时间或失败比率 基于失败比率
实时指标实现 滑动窗口 滑动窗口
规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于 QPS,支持基于调用关系的限流 有限的支持
流量整形 支持慢启动、均衡器模式 不支持
系统负载保护 支持 不支持
控制台 开箱即用,可配置规则、查看秒级监控 不完善
常见框架的适配 Servlet、SpringCloud、Dubbo等 Servlet、SpringCloudNetflix

Sentinel 工作原理

基本概念

资源:资源是 Sentinel 的关键概念;它可以是 Java 应用程序中的任何内容;es:应用程序提供的服务,或由应用程序调用的其它应用提供的服务,甚至可以是一段代码;

规则:围绕资源的实时状态设定的规则,可以包括流量控制规则、熔断降级规则以及系统保护规则,所有规则可以动态实时调整

Sentinel 工作主流程

在 Sentinel 中,所有的资源都对应一个资源名称resourceName,每次资源调用都会创建一个Entry对象;Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用Sphuapi 显示创建;Entry 创建的时候同时也会创建一系列功能插槽(slot chain),这些插槽有不同的职责:

  1. NodeSelectorSlot:负责收集资源的路径,并将这些资源的调用路径,以树状结构存储起来,用于根据调用路径来限流降级
  2. ClusterBuilderSlot:用于存储资源的统计信息以及调用者信息,es:该资源的 RT、QPS、thread count 等,这些信息将用作为多维度限流降级的依据
  3. StatisticSlot:用于记录、统计不同维度的 runtime 指标监控信息
  4. FlowSlot:用于根据预设的限流规则以及前面 slot 统计的状态来进行流量控制
  5. AuthoritySlot:根据配置的黑白名单和调用来源信息来做黑白名单控制
  6. DegradeSlot:通过统计信息以及预设的规则来做熔断降级
  7. SystemSlot:通过系统的状态来控制总的入口流量

Sentinel 实战

Entry entry = null;
// 务必保证 finally 会被执行
try {
  // 资源名可使用任意有业务语义的字符串  开启资源的保护
  entry = SphU.entry("自定义资源名");
  // 被保护的业务逻辑    method
  // do something...
} catch (BlockException ex) {
  // 资源访问阻止,被限流或被降级   Sentinel定义异常  流控规则,降级规则,热点参数规则。。。。   服务降级(降级规则)
  // 进行相应的处理操作
} catch (Exception ex) {
  // 若需要配置降级规则,需要通过这种方式记录业务异常    RuntimeException     服务降级   mock  feign:fallback 
  Tracer.traceEntry(ex, entry);
} finally {
  // 务必保证 exit,务必保证每个 entry 与 exit 配对
  if (entry != null) {
    entry.exit();
  }

Sentinel 资源保护的方式

引入依赖:

<dependency>
     <groupId>com.alibaba.csp</groupId>
     <artifactId>sentinel-core</artifactId>
     <version>1.8.0</version>
</dependency>

模拟代码:

@RestController
@Slf4j
public class HelloController {

    private static final String RESOURCE_NAME = "hello";

    @RequestMapping(value = "/hello")
    public String hello() {
        Entry entry = null;
        try {
            // 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
            entry = SphU.entry(RESOURCE_NAME);
            // 被保护的业务逻辑
            String str = "hello world";
            log.info("====="+str);
            return str;
        } catch (BlockException e1) {
            // 资源访问阻止,被限流或被降级
            //进行相应的处理操作
            log.info("block!");
        } catch (Exception ex) {
            // 若需要配置降级规则,需要通过这种方式记录业务异常
            Tracer.traceEntry(ex, entry);
        } finally {
            if (entry != null) {
                entry.exit();
            }
        }
        return null;
    }

    /**
     * 定义流控规则
     */
    @PostConstruct
    private static void initFlowRules(){
        List<FlowRule> rules = new ArrayList<>();
        FlowRule rule = new FlowRule();
        //设置受保护的资源
        rule.setResource(RESOURCE_NAME);
        // 设置流控规则 QPS
        rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
        // 设置受保护的资源阈值
        // Set limit QPS to 20.
        rule.setCount(1);
        rules.add(rule);
        // 加载配置好的规则
        FlowRuleManager.loadRules(rules);
    }
}

缺点: 业务侵入性强,需要在 Controller 写入非业务代码 配置不灵活,若需要添加新的受保护的资源,需要手动添加 init 方法来添加流控规则

@SentinelResource注解实现

@SentinelResource 注解用来表示资源是否被限流、降级 BlockHandler:定义当资源内部发生了 BlockException 应该进入的方法(捕获的是 Sentinel 定义的异常) Fallback:定义的是资源内部发生了 Throwable 应该进入的方法 ExceptionsTolgnore:配置 fallback 可以忽略的异常 源码入口:com.alibaba.csp.sentinel.annotation.aspectj.SentinelResourceAspect

引入依赖:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-annotation-aspectj</artifactId>
    <version>1.8.0</version>
</dependency>

配置切面支持:

@Configuration
public class SentinelAspectConfiguration {

    @Bean
    public SentinelResourceAspect sentinelResourceAspect() {
        return new SentinelResourceAspect();
    }
}

模拟代码,添加@SentinelResource,并配置 blockHandler 和 fallback:

@RequestMapping(value = "/findOrderByUserId/{id}")
@SentinelResource(value = "findOrderByUserId",
                  fallback = "fallback",fallbackClass = ExceptionUtil.class,
                  blockHandler = "handleException",blockHandlerClass = ExceptionUtil.class
                 )
public R  findOrderByUserId(@PathVariable("id") Integer id) {
    //ribbon实现
    String url = "http://mall-order/order/findOrderByUserId/"+id;
    R result = restTemplate.getForObject(url,R.class);

    if(id==4){
        throw new IllegalArgumentException("非法参数异常");
    }
    return result;
}

ExceptionUtil,注:若指定了 class,方法必须是 static 方法:

public class ExceptionUtil {

    public static R fallback(Integer id,Throwable e){
        return R.error(-2,"===被异常降级啦===");
    }

    public static R handleException(Integer id, BlockException e){
        return R.error(-2,"===被限流啦===");
    }
}

流控规则设置可以通过 Sentinel dashboard 配置,客户端需引入 Transport 模块与 Sentinel 控制台进行通信:

<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.8.0</version>
</dependency>

启动 Sentinel 控制台:

#启动控制台命令
java -jar sentinel-dashboard-1.8.0.jar

可通过如下参数进行配置: -Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名 -Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码 默认用户名/密码:sentinel/sentinel -Dserver.servlet.session.timeout=7200 指定 Spring Boot 服务端 session 的过期时间,单位 s,默认 30 分钟

访问 http://localhost:8080/#/login
image.png
Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包,所以要确保客户端有访问量
image.png

Spring Cloud Alibaba 整合 Sentinel

引入依赖:

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加 yml 配置,设置 Sentinel 控制台地址:

添加 Sentinel 后,需暴露 /actuator/sentinel 端点,而 SpringBoot 默认是没有暴露该端点的,所以需要设置;测试 http://localhost:8800/actuator/sentinel

server:
  port: 8800

spring:
  application:
    name: mall-user-sentinel-demo
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

    sentinel:
      transport:
        # 添加sentinel的控制台地址
        dashboard: 127.0.0.1:8080
        # 指定应用与Sentinel控制台交互的端口,应用本地会起一个该端口占用的HttpServer
        # port: 8719

#暴露actuator端点   
management:
  endpoints:
    web:
      exposure:
        include: '*'

在 Sentinel 控制台中设置流控规则:

  1. 资源名:接口的 api
  2. 针对来源:默认 default,当多个微服务都调用这个资源时,可以配置微服务名来对指定的微服务设置阈值
  3. 阈值类型:分为 QPS 和线程数
  4. QPS 类型:只得是每秒访问接口的次数 > 10 就进行限流
  5. 线程数:为接收请求该资源分配的线程数 > 10 就进行限流

image.png