Sentinel 微服务保护

发布时间 2023-12-01 15:05:19作者: 安浩阳

Sentinel 微服务保护

​#Sentinel#​

本文章为个人笔记,原文章来源于www.cnblogs.com/xiegongz...


Sentinel是阿里巴巴开源的一款微服务流量控制组件。官网地址:https://sentinelguard.io/zh-cn/index.html

雪崩问题与解决方式

所谓的雪崩指的是:微服务之间相互调用,调用链中某个微服务出现问题了,导致整个服务链的所有服务也跟着出问题,从而造成所有服务都不可用

image

解决方式:

  1. 超时处理:是一种临时方针,即设置定时器,请求超过规定的时间就返回错误信息,不会无休止等待

    image

    缺点:在超时时间内,还未返回错误信息内,服务未处理完,请求激增,一样会导致后面的请求阻塞​

  2. 线程隔离:也叫舱壁模式,即限定每个业务能使用的线程数,避免耗尽整个tomcat的资源

    image

    缺点:会造成一定资源的浪费。明明服务已经不可用了,还占用固定数量的线程

  3. 熔断降级

    1. 熔断: 由“断路器”统计业务执行的异常比例,如果超出“阈值”则会熔断/暂停该业务,拦截访问该业务的一切请求,后续搞好了再开启。从而做到在流量过大时(或下游服务出现问题时),可以自动断开与下游服务的交互,并可以通过自我诊断下游系统的错误是否已经修正,或上游流量是否减少至正常水平来恢复自我恢复。熔断更像是自动化补救手段,可能发生在服务无法支撑大量请求或服务发生其他故障时,对请求进行限制处理,同时还可尝试性的进行恢复
    2. 降级: 丢车保帅。针对非核心业务功能,核心业务超出预估峰值需要进行限流;所谓降级指的就是在预计流量峰值前提下,整体资源快不够了,忍痛将某些非核心服务先关掉,待渡过难关,再开启回来
  4. 限流: 也叫流量控制。指的是限制业务访问的QPS,避免服务因流量的突增而故障。是防御保护手段,从流量源头开始控制流量规避问题

    image

限流是对服务的保护,避免因瞬间高并发流量而导致服务故障,进而避免雪崩。是一种预防措施

超时处理、线程隔离、降级熔断是在部分服务故障时,将故障控制在一定范围,避免雪崩。是一种补救措施

服务保护技术对比

在SpringCloud当中支持多种服务保护技术:

早期比较流行的是Hystrix框架(后面这叼毛不维护、不更新了),所以目前国内实用最广泛的是阿里巴巴的Sentinel框架

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

安装sentinel

  1. 下载:https://github.com/alibaba/Sentinel/releases 是一个jar包,这是sentinel的ui控制台,下载了放到“非中文”目录中

  2. 运行

    java -jar sentinel-dashboard-1.8.1.jar
    

如果要修改Sentinel的默认端口、账户、密码,可以通过下列配置:

配置项 默认值 说明
server.port 8080 服务端口
sentinel.dashboard.auth.username sentinel 默认用户名
sentinel.dashboard.auth.password sentinel 默认密码

例如,修改端口:

java -Dserver.port=8090 -jar sentinel-dashboard-1.8.1.jar
  1. 访问。如http://localhost:8080,用户名和密码都是sentinel

入手sentinel

  1. 依赖

    <!--sentinel-->
    <dependency>
        <groupId>com.alibaba.cloud</groupId> 
        <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    </dependency>
    
  2. YAML配置

    server:
      port: 8088
    spring:
      cloud: 
        sentinel:
          transport:
    # 		sentinel的ui控制台地址
            dashboard: localhost:8080
    
  3. 然后将服务提供者、服务消费者、网关、Feign……启动,发送请求即可在前面sentinel的ui控制台看到信息了

    image

限流 / 流量控制

雪崩问题虽然有四种方案,但是限流是避免服务因突发的流量而发生故障,是对微服务雪崩问题的预防,因此先来了解这种模式,但在了解这个之前先了解一下限流算法

限流算法

固定窗口计数器算法

  1. 将时间划分为多个窗口,窗口时间跨度称为Interval
  2. 每个窗口维护一个计数器,每有一次请求就将计数器 +1,限流就是设置计数器阈值
  3. 如果计数器超过了限流阈值,则超出阈值的请求都被丢弃

image

但是有个缺点:时间是不固定的。如0 - 1000ms是QPS(1秒内的请求数),这样来看没有超过阈值,可是:4500 - 5500ms也是1s啊,这是不是也是QPS啊,像下面这样就超出阈值了,服务不得干爬了

image

滑动窗口计数器算法

在固定窗口计数器算法的基础上,滑动窗口计数器算法会将一个窗口划分为n个更小的区间,如:

  1. 窗口时间跨度Interval为1秒;区间数量 n = 2 ,则每个小区间时间跨度为500ms
  2. 限流阈值依然为3,时间窗口(1秒)内请求超过阈值时,超出的请求被限流
  3. 窗口会根据当前请求所在时间(currentTime)移动,窗口范围是从(currentTime-Interval)之后的第一个时区开始,到currentTime所在时区结束

令牌桶算法

  1. 以固定的速率生成令牌,存入令牌桶中,如果令牌桶满了以后,多余令牌丢弃
  2. 请求进入后,必须先尝试从桶中获取令牌,获取到令牌后才可以被处理
  3. 如果令牌桶中没有令牌,则请求等待或丢弃

image

也有个缺点:

  1. 假如限流阈值是1000个请求
  2. 假设捅中只能放1000个令牌,前一秒内没有请求,但是令牌已经生成了,放入了捅中
  3. 之后下一秒来了2000个请求,可捅中前一秒生成了1000令牌,所以可以有1000个请求拿到令牌,从而放行,捅中没令牌了
  4. 然后当前这一秒就要生成令牌,这样另外1000个请求也可以拿到令牌
  5. 最后2000个请求都放行了,服务又干爬了

漏桶算法

是对令牌桶算法做了改进:可以理解成请求在桶内排队等待

  1. 将每个请求视作"水滴"放入"漏桶"进行存储
  2. "漏桶"以固定速率向外"漏"出请求来执行,如果"漏桶"空了则停止"漏水”
  3. 如果"漏桶"满了则多余的"水滴"会被直接丢弃

image

限流算法对比

因为计数器算法一般都会采用滑动窗口计数器,所以这里只对比三种算法

对比项 滑动时间窗口 令牌桶 漏桶
能否保证流量曲线平滑 不能,但窗口内区间越小,流量控制越平滑 基本能,在请求量持续高于令牌生成速度时,流量平滑。但请求量在令牌生成速率上下波动时,无法保证曲线平滑 能,所有请求进入桶内,以恒定速率放行,绝对平滑
能否应对突增流量 不能,徒增流量,只要高出限流阈值都会被拒绝。 能,桶内积累的令牌可以应对突增流量 能,请求可以暂存在桶内
流量控制精确度 低,窗口区间越小,精度越高

簇点链路

image

簇点链路: 就是项目内的调用链路,链路中被监控的每个接口就是一个“资源”

当请求进入微服务时,首先会访问DispatcherServlet,然后进入Controller、Service、Mapper,这样的一个调用链就叫做簇点链路。簇点链路中被监控的每一个接口就是一个资源

默认情况下sentinel会监控SpringMVC的每一个端点(Endpoint,也就是controller中的方法),因此SpringMVC的每一个端点就是调用链路中的一个资源

例如下图中的端点:/order/{orderId}

image

流控、熔断等都是针对簇点链路中的资源来设置的,因此我们可以点击对应资源后面的按钮来设置规则:

  1. 流控:流量控制
  2. 降级:降级熔断
  3. 热点:热点参数限流
  4. 授权:请求的权限控制

入门流控

  1. 点击下图按钮

image

  1. 设置基本流控信息

    image

    上图的含义:限制 /order/{orderId} 这个资源的单机QPS为1,即:每秒只允许1次请求,超出的请求会被拦截并报错

流控模式的分类

​​image​​

在添加限流规则时,点击高级选项,可以选择三种流控模式

  1. 直接模式:一句话来说就是“对当前资源限流”。统计当前资源的请求,当其触发阈值时,对当前资源直接限流。上面这张图就是此种模式。这也是默认的模式。采用的算法就是滑动窗口算法
  2. 关联模式:一句话来说就是“高优先级触发阈值,对低优先级限流”。统计与当前资源A “相关” 的另一个资源B,A资源触发阈值时,对B资源限流
    如:在一个Controller中,一个高流量的方法和一个低流量的方法都调用了这个Controller中的另一个方法,为了预防雪崩问题,就对低流量的方法进行限流设置
    适用场景:两个有竞争关系的资源,一个优先级高,一个优先级低,优先级高的触发阈值时,就对优先级低的进行限流
  3. 链路模式:一句话来说就是“对请求来源做限流”。统计从“指定链路”访问到本资源的请求,触发阈值时,对指定链路限流
    如:两个不同链路的请求,如需要读库和写库,这两个请求都调用了同一个服务/资源/接口,所以为了需求考虑,可以设置读库达到了阈值就进行限流

示例:

  1. 关联模式: 对谁进行限流,就点击谁的流控按钮进行设置

    image

    上图含义:当 /order/update 请求单机达到 每秒1000 请求量的阈值时,就会对 /order/query 进行限流,从而避免影响 /order/update 资源

  2. 链路模式: 请求链路访问的是哪个资源,就点击哪个资源的流控按钮进行配置
    ​​image
    上图含义:只有来自 /user/queryGoods 链路的请求来访问 /order/queryGoods 资源时,每秒请求量达到1000,就会对 /user/queryGoods 进行限流

    链路模式的注意事项:

    1. 默认情况下,Service中的方法是不被Sentinel监控的,想要Service中的方法也被Sentinel监控的话,则需要我们自己通过 @SentinelResource("起个名字 或 像controllerz中请求路径写法") 注解来标记要监控的方法

    2. 链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,进行了context整合,所以会导致链路模式失效。因此需要关闭一个context整合设置:

      spring:
        cloud:
          sentinel:
            web-context-unify: false # 关闭context整合
      

      同一个root资源指的是:

      ​​image​​

流控效果及其分类

流控效果:指请求达到流控阈值时应该采取的措施

分类

image

  1. 快速失败:达到阈值后,新的请求会被立即拒绝并抛出 FlowException异常。是默认的处理方式
  2. warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值
  3. 排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长

warn up 预热模式

warm up:预热模式,对超出阈值的请求同样是拒绝并抛出异常。但这种模式阈值会动态变化,从一个较小值逐渐增加到最大阈值

阈值一般是一个微服务能承担的最大QPS,但是一个服务刚刚启动时,一切资源尚未初始化(冷启动),如果直接将QPS跑到最大值,可能导致服务瞬间宕机

warm up也叫预热模式,是应对服务冷启动的一种方案

请求阈值初始值 = maxThreshold / coldFactor
  • maxThreshold 就是设置的QPS数量。持续指定时长后,逐渐提高到maxThreshold值。
  • coldFactor 预热因子,默认值是3

image

排队等待

排队等待:让所有的请求按照先后次序排队执行,两个请求的间隔不能小于指定时长

采用的算法:基于漏桶算法

当请求超过QPS阈值时,快速失败和warm up 会拒绝新的请求并抛出异常

而排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝

image

QPS = 5,那么 1/5(个/ms) = 200(个/ms),意味着每200ms处理1个队列中的请求;timeout = 2000,意味着预期等待时长超过2000ms的请求会被拒绝并抛出异常

那什么叫做预期等待时长呢?

image

如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑

平滑的QPS曲线,对于服务器来说是更友好的

热点参数限流

之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值

热点参数限流是分别统计参数值相同的请求,判断是否超过QPS阈值

采用的算法: 令牌桶算法

注意事项:热点参数限流对默认的SpringMVC资源无效,需要利用@SentinelResource注解标记资源,例如:

image

image

但是配置时不要通过上面按钮点击配置,会有BUG,而是通过下图中的方式:

image

所谓的参数值指的是

image

id参数值会有变化,热点参数限流会根据参数值分别统计QPS

当id=1的请求触发阈值被限流时,id值不为1的请求不受影响

全局参数限流

就是基础设置,没有加入高级设置的情况

image

上图含义:对于来访问hot资源的请求,每1秒相同参数值的请求数不能超过10000

热点参数限流

刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都限定为10000

而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。那就需要配置热点参数限流的高级选项了

image

上图含义:对于来访问hot资源的请求,id=110时的QPS阈值为30000,id=4132443时的QPS阈值为50000,id为其他的则QPS阈值为10000

Seatinel限流与Gateway限流的差异

Gateway则采用了基于Redis实现的令牌桶算法。而Sentinel内部所有算法都有:

  1. 默认限流模式是基于滑动时间窗口算法
  2. 排队等待的限流模式则基于漏桶算法
  3. 而热点参数限流则是基于令牌桶算法

Sentinel整合Feign

Sentinel是做服务保护的,而在微服务中调来调去是常有的事,要远程调用就离不开Feign

  1. 修改配置,开启sentinel功能: 在服务“消费方”的feign配置添加如下配置内容
feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持
  1. feign-client中编写失败降级逻辑: 后面的流程就是前面玩Fengn时失败降级的流程
package com.zixieqing.feign.fallback;

import com.zixieqing.feign.clients.UserClient;
import com.zixieqing.feign.pojo.User;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;

/**
 * userClient失败时的降级处理
 *
 * <p>@author       : ZiXieqing</p>
 */

@Slf4j
public class UserClientFallBackFactory implements FallbackFactory<UserClient> {
    @Override
    public UserClient create(Throwable throwable) {
        return new UserClient() {
            /**
             * 重写userClient中的方法,编写失败时的降级逻辑
             */
            @Override
            public User findById(Long id) {
                log.info("userClient的findById()在进行 id = {} 时失败", id);
                return new User();
            }
        };
    }
}
  1. 将失败降级逻辑的类丢给Spring容器
@Bean
public UserClientFallBackFactory userClientFallBackFactory() {
    return new UserClientFallBackFactory();
}
  1. 在相关feign-client定义处使用fallbackFactory回调函数即可
package com.zixieqing.feign.clients;


import com.zixieqing.feign.fallback.UserClientFallBackFactory;
import com.zixieqing.feign.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "userservice",fallbackFactory = UserClientFallBackFactory.class)
public interface UserClient {

    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}
  1. 调用,失败时就会进入自定义的失败逻辑中
package com.zixieqing.order.service;

import com.zixieqing.feign.clients.UserClient;
import com.zixieqing.feign.pojo.User;
import com.zixieqing.order.mapper.OrderMapper;
import com.zixieqing.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.用Feign远程调用
        User user = userClient.findById(order.getId());
        // 3.封装user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}

离与降级

线程隔离

线程隔离有两种方式实现:

  1. 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
    优点:

    • 支持主动超时:也就是调用进行逻辑处理时超过了规定时间,直接噶了,不再让其继续处理
    • 支持异步调用:线程池隔离了嘛,彼此不干扰,因此可以异步了

    缺点:造成资源浪费。明明被调用的服务都出问题了,还占用固定的线程池数量
    适用场景:低扇出。MQ中扇出交换机的那个扇出,也就是较少的请求量,扇出/广播到很多服务上

  2. 信号量隔离(Sentinel默认采用):不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求
    优点:轻量级、无额外开销
    缺点:不支持主动超时、不支持异步调用
    适用场景:高频调用、高扇出

image

配置Sentinel的线程隔离-信号量隔离

在添加限流规则时,可以选择两种阈值类型:

image

熔断降级

熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求

断路器控制熔断和放行是通过状态机来完成的:

image

断路器熔断策略有三种:慢调用、异常比例、异常数

状态机包括三个状态:

  • Closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态

  • Open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态默认5秒后会进入half-open状态

  • Half-Open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。

    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

断路器熔断策略:慢调用

慢调用:业务的响应时长(RT)大于指定时长的请求认定为慢调用请求

在指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断

image

上图含义:

  1. 响应时间为500ms的即为慢调用

  2. 如果1000ms内有100次请求,且慢调用比例不低于0.05(即:100*0.05=5个慢调用),则触发熔断(暂停该服务)

  3. 熔断时间达到1s进入half-open状态,然后放行一次请求测试

    1. 成功则进入Closed状态关闭断路器
    2. 失败则进入Open状态打开断路器,继续像前面一样开始统计RT=500ms,1s内有100次请求……………..

断路器熔断策略:异常比例 与 异常数

  1. 异常比例

image

上图含义:在1s内,若是请求数量不低于100个,且异常比例不低于0.08(即:100*0.08=8个有异常),则触发熔断,熔断时长达到1s就进入half-open状态

  1. 异常数: 直接敲定有多少个异常数量就触发熔断

image

授权规则

授权规则可以对请求方来源做判断和控制

授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式:

  1. 白名单:来源(origin)在白名单内的调用者允许访问
  2. 黑名单:来源(origin)在黑名单内的调用者不允许访问

image

  • 资源名:就是受保护的资源,例如 /order/

  • 流控应用:是来源者的名单

    • 如果是勾选白名单,则名单中的来源被许可访问
    • 如果是勾选黑名单,则名单中的来源被禁止访问

image

我们允许请求从gateway到order-service,不允许浏览器访问order-service,那么白名单中就要填写网关的来源名称(origin)

但是上图中怎么区分请求是从网关来的还是浏览器来的?在微服务中的想法是所有请求只能走网关,然后由网关路由到具体的服务,直接访问服务应该阻止才对,像下面直接跳过网关去访问服务,应该不行才对

image

要做到就需要使用授权规则了:

  1. 网关授权拦截:针对于别人不知道内部服务接口的情况可以拦截成功
  2. 服务授权控制/流控应用控制:针对“内鬼“ 或者 别人知道了内部服务接口,我们限定只能从哪里来的请求才能访问该服务,否则直接拒绝

流控应用怎么控制的?

下图中的名字怎么定义?

image

需要实现 RequestOriginParser 这个接口的 parseOrigin() 来获取请求的来源从而做到

public interface RequestOriginParser {
    /**
     * 从请求request对象中获取origin,获取方式自定义
     */
    String parseOrigin(HttpServletRequest request);
}

示例:

  1. 在需要进行保护的服务中编写请求来源解析逻辑
package com.zixieqing.order.intercepter;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;

/**
 * 拦截请求,允许从什么地方来的请求才能访问此微服务
 *
 * <p>@author       : ZiXieqing</p>
 */

@Component
public class RequestInterceptor implements RequestOriginParser {
    @Override
    public String parseOrigin(HttpServletRequest request) {
        // 获取请求中的请求头 可自定义
        String origin = request.getHeader("origin");
        if (StringUtils.isEmpty(origin))
            origin = "black";

        return origin;
    }
}
  1. 在网关中根据2中 parseOrigin() 的逻辑添加相应的东西

image

  1. 添加流控规则:不要在簇点链路中选择相应服务来配置授权,会有BUG

image

经过上面的操作之后,要进入服务就只能通过网关路由过来了,不是从网关过来的就无法访问服务

自定义异常

默认情况下,发生限流、降级、授权拦截时,都会抛出异常到调用方。异常结果都是flow limmiting(限流)。这样不够友好,无法得知是限流还是降级还是授权拦截

而如果要自定义异常时的返回结果,需要实现 BlockExceptionHandler 接口:

public interface BlockExceptionHandler {
    /**
     * 处理请求被限流、降级、授权拦截时抛出的异常:BlockException
     *
     * @param e 被sentinel拦截时抛出的异常
     */
    void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception;
}

这里的BlockException包含多个不同的子类:

异常 说明
FlowException 限流异常
ParamFlowException 热点参数限流的异常
DegradeException 降级异常
AuthorityException 授权规则异常
SystemBlockException 系统规则异常

示例:

  1. 在需要的服务中实现 BlockExceptionHandler 接口
package com.zixieqing.order.exception;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 自定义sentinel的各种异常处理
 *
 * <p>@author       : ZiXieqing</p>
 */

@Component
public class SentinelExceptionHandler implements BlockExceptionHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, BlockException e) throws Exception {
        String msg = "未知异常";
        int status = 429;

        if (e instanceof FlowException) {
            msg = "请求被限流了";
        } else if (e instanceof ParamFlowException) {
            msg = "请求被热点参数限流";
        } else if (e instanceof DegradeException) {
            msg = "请求被降级了";
        } else if (e instanceof AuthorityException) {
            msg = "没有权限访问";
            status = 401;
        }

        response.setContentType("application/json;charset=utf-8");
        response.setStatus(status);
        response.getWriter().println("{\"msg\": " + msg + ", \"status\": " + status + "}");
    }
}
  1. 重启服务,不同异常就会出现不同结果了

规则持久化

在默认情况下,sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在生产环境下,我们必须确保这些规则的持久化,避免丢失

规则是否能持久化,取决于规则管理模式,sentinel支持三种规则管理模式:

  1. 原始模式:Sentinel的默认模式,将规则保存在内存,重启服务会丢失
  2. pull模式
  3. push模式

pull模式

pull模式:控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则

缺点:服务之间的规则更新不及时。因为是定时去读取,在时间还未到时,可能规则发生了变化

image

push模式

push模式:控制台将配置规则推送到远程配置中心(如Nacos)。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新

image

使用push模式实现规则持久化

在想要进行规则持久化的服务中引入如下依赖:

<!--sentinel规则持久化到Nacos的依赖-->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

配置此服务的YAML文件,内容如下:

spring:
  cloud:
    sentinel:
      datasource:
        flow: # 流控规则持久化
          nacos:
            server-addr: localhost:8848 # nacos地址
            dataId: orderservice-flow-rules
            groupId: SENTINEL_GROUP
            rule-type: flow # 还可以是:degrade 降级、authority 授权、param-flow 热点参数限流
#        degrade:  # 降级规则持久化
#          nacos:
#            server-addr: localhost:8848 # nacos地址
#            dataId: orderservice-degrade-rules
#            groupId: SENTINEL_GROUP
#            rule-type: degrade
#        authority:  # 授权规则持久化
#          nacos:
#            server-addr: localhost:8848 # nacos地址
#            dataId: orderservice-authority-rules
#            groupId: SENTINEL_GROUP
#            rule-type: authority
#        param-flow: # 热电参数限流持久化
#          nacos:
#            server-addr: localhost:8848 # nacos地址
#            dataId: orderservice-param-flow-rules
#            groupId: SENTINEL_GROUP
#            rule-type: param-flow

修改sentinel的源代码

因为阿里的sentinel默认采用的是将规则内容存到内存中的,因此需要改源码

  1. 使用git克隆sentinel的源码,之后IDEA等工具打开
git clone https://github.com/alibaba/Sentinel.git
  1. 修改nacos依赖。在sentinel-dashboard模块的pom文件中,nacos的依赖默认的scope是test,那它只能在测试时使用,所以要去除 scope 标签
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
  1. 添加nacos支持。在sentinel-dashboard的test包下,已经编写了对nacos的支持,我们需要将其拷贝到src/main/java/com/alibaba/csp/sentinel/dashboard/rule 下

image

  1. 修改nacos地址,让其读取application.properties中的配置

image

  1. 在sentinel-dashboard的application.properties中添加nacos地址配置
nacos.addr=127.0.0.1:8848	# ip和port改为自己想要的即可
  1. 配置nacos数据源

image

  1. 修改前端

image

  1. 重现编译打包Sentinel-Dashboard模块

image

  1. 重现启动sentinel即可
java -jar -Dnacos.addr=127.0.0.1:8848 sentinel-dashboard.jar

补充:Sentinel基础知识

Sentinel实现限流、隔离、降级、熔断等功能,本质要做的就是两件事情:

  • 统计数据:统计某个资源的访问数据(QPS、RT等信息)
  • 规则判断:判断限流规则、隔离规则、降级规则、熔断规则是否满足

这里的资源就是希望被Sentinel保护的业务,例如项目中定义的controller方法就是默认被Sentinel保护的资源

ProcessorSlotChain

实现上述功能的核心骨架是一个叫做ProcessorSlotChain的类。这个类基于责任链模式来设计,将不同的功能(限流、降级、系统保护)封装为一个个的Slot,请求进入后逐个执行即可

image

责任链中的Slot也分为两大类:

  • 统计数据构建部分(statistic)

    • NodeSelectorSlot:负责构建簇点链路中的节点(DefaultNode),将这些节点形成链路树
    • ClusterBuilderSlot:负责构建某个资源的ClusterNode,ClusterNode可以保存资源的运行信息(响应时间、QPS、block 数目、线程数、异常数等)以及来源信息(origin名称)
    • StatisticSlot:负责统计实时调用数据,包括运行信息、来源信息等
  • 规则判断部分(rule checking)

    • AuthoritySlot:负责授权规则(来源控制)
    • SystemSlot:负责系统保护规则
    • ParamFlowSlot:负责热点参数限流规则
    • FlowSlot:负责限流规则
    • DegradeSlot:负责降级规则

Node

Sentinel中的簇点链路是由一个个的Node组成的,Node是一个接口,包括下面的实现:

image

所有的节点都可以记录对资源的访问统计数据,所以都是StatisticNode的子类

按照作用分为两类Node:

  • DefaultNode:代表链路树中的每一个资源,一个资源出现在不同链路中时,会创建不同的DefaultNode节点。而树的入口节点叫EntranceNode,是一种特殊的DefaultNode
  • ClusterNode:代表资源,一个资源不管出现在多少链路中,只会有一个ClusterNode。记录的是当前资源被访问的所有统计数据之和。

DefaultNode记录的是资源在当前链路中的访问数据,用来实现基于链路模式的限流规则。ClusterNode记录的是资源在所有链路中的访问数据,实现默认模式、关联模式的限流规则。

例如:我们在一个SpringMVC项目中,有两个业务:

  • 业务1:controller中的资源/order/query​访问了service中的资源/goods
  • 业务2:controller中的资源/order/save​访问了service中的资源/goods

创建的链路图如下:

image

Entry

默认情况下,Sentinel会将controller中的方法作为被保护资源,那么问题来了,我们该如何将自己的一段代码标记为一个Sentinel的资源呢?前面是用了 @SentinelResoutce 注解来实现的,那么这个注解的原理是什么?要搞清这玩意儿,那就得先来了解Entry这个吊毛玩意儿了

Sentinel中的资源用Entry来表示。声明Entry的API示例:

// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串。
try (Entry entry = SphU.entry("resourceName")) {
  // 被保护的业务逻辑
  // do something here...
} catch (BlockException ex) {
  // 资源访问阻止,被限流或被降级
  // 在此处进行相应的处理操作
}

打开sentinel控制台,查看簇点链路:

​​image​​

@SentinelResoutce 注解标记资源

通过给方法添加@SentinelResource注解的形式来标记资源:

image

这是怎么实现的?

Sentinel依赖中有自动装配相关的东西,spring.factories声明需要就是自动装配的配置类,内容如下:

image

我们来看下SentinelAutoConfiguration​这个类:

image

可以看到,在这里声明了一个Bean,SentinelResourceAspect​:

/**
 * Aspect for methods with {@link SentinelResource} annotation.
 *
 * @author Eric Zhao
 */
@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
	// 切点是添加了 @SentinelResource 注解的类
    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    // 环绕增强
    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        // 获取受保护的方法
        Method originMethod = resolveMethod(pjp);
		// 获取 @SentinelResource 注解
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        // 获取注解上的资源名称
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();
        Entry entry = null;
        try {
            // 创建资源 Entry
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            // 执行受保护的方法
            Object result = pjp.proceed();
            return result;
        } catch (BlockException ex) {
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                return handleFallback(pjp, annotation, ex);
            }

            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }
}

简单来说,@SentinelResource注解就是一个标记,而Sentinel基于AOP思想,对被标记的方法做环绕增强,完成资源(Entry​)的创建。

Context

上一节,我们发现簇点链路中除了controller方法、service方法两个资源外,还多了一个默认的入口节点:

sentinel_spring_web_context,是一个EntranceNode类型的节点

这个节点是在初始化Context的时候由Sentinel帮我们创建的

什么是Context?

  1. Context 代表调用链路上下文,贯穿一次调用链路中的所有资源( Entry​),基于ThreadLocal
  2. Context 维持着入口节点(entranceNode​)、本次调用链路的 curNode(当前资源节点)、调用来源(origin​)等信息
  3. 后续的Slot都可以通过Context拿到DefaultNode或者ClusterNode,从而获取统计数据,完成规则判断
  4. Context初始化的过程中,会创建EntranceNode,contextName就是EntranceNode的名称

对应的API如下:

// 创建context,包含两个参数:context名称、 来源名称
ContextUtil.enter("contextName", "originName");

Context的初始化

Context又是在何时完成初始化的?

进入SentinelWebAutoConfiguration这个类:可以直接搜,也可以去Sentinel依赖的Spring.factories中找

image

WebMvcConfigurer是SpringMVC自定义配置用到的类,可以配置HandlerInterceptor

image

SentinelWebInterceptor​的声明如下:

image

发现继承了AbstractSentinelInterceptor​这个类。

image

AbstractSentinelInterceptor

HandlerInterceptor​拦截器会拦截一切进入controller的方法,执行preHandle​前置拦截方法,而Context的初始化就是在这里完成的。

我们来看看这个类的preHandle​实现:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {
    try {
        // 获取资源名称,一般是controller方法的 @RequestMapping 路径,例如/order/{orderId}
        String resourceName = getResourceName(request);
        if (StringUtil.isEmpty(resourceName)) {
            return true;
        }
        // 从request中获取请求来源,将来做 授权规则 判断时会用
        String origin = parseOrigin(request);
    
        // 获取 contextName,默认是sentinel_spring_web_context
        String contextName = getContextName(request);
        // 创建 Context
        ContextUtil.enter(contextName, origin);
        // 创建资源,名称就是当前请求的controller方法的映射路径
        Entry entry = SphU.entry(resourceName, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
        request.setAttribute(baseWebMvcConfig.getRequestAttributeName(), entry);
        return true;
    } catch (BlockException e) {
        try {
            handleBlockException(request, response, e);
        } finally {
            ContextUtil.exit();
        }
        return false;
    }
}

ContextUtil

创建Context的方法就是ContextUtil.enter(contextName, origin);

进入该方法:

public static Context enter(String name, String origin) {
    if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
        throw new ContextNameDefineException(
            "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
    }
    return trueEnter(name, origin);
}

进入trueEnter​方法:

protected static Context trueEnter(String name, String origin) {
    // 尝试获取context
    Context context = contextHolder.get();
    // 判空
    if (context == null) {
        // 如果为空,开始初始化
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // 尝试获取入口节点
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            LOCK.lock();
            try {
                node = contextNameNodeMap.get(name);
                if (node == null) {
                    // 入口节点为空,初始化入口节点 EntranceNode
                    node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                    // 添加入口节点到 ROOT
                    Constants.ROOT.addChild(node);
                    // 将入口节点放入缓存
                    Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                    newMap.putAll(contextNameNodeMap);
                    newMap.put(name, node);
                    contextNameNodeMap = newMap;
                }
            } finally {
                LOCK.unlock();
            }
        }
        // 创建Context,参数为:入口节点 和 contextName
        context = new Context(node, name);
        // 设置请求来源 origin
        context.setOrigin(origin);
        // 放入ThreadLocal
        contextHolder.set(context);
    }
    // 返回
    return context;
}

综合流程

image