3W字吃透:微服务 sentinel 限流 底层原理和实操

Posted 疯狂创客圈

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了3W字吃透:微服务 sentinel 限流 底层原理和实操相关的知识,希望对你有一定的参考价值。

文章很长,且持续更新,建议收藏起来,慢慢读!疯狂创客圈总目录 博客园版 为您奉上珍贵的学习资源 :

免费赠送 :《尼恩Java面试宝典》 持续更新+ 史上最全 + 面试必备 2000页+ 面试必备 + 大厂必备 +涨薪必备
免费赠送 :《尼恩技术圣经+高并发系列PDF》 ,帮你 实现技术自由,完成职业升级, 薪酬猛涨!加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷1)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷2)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领
免费赠送 经典图书:《Java高并发核心编程(卷3)加强版》 面试必备 + 大厂必备 +涨薪必备 加尼恩免费领

免费赠送 资源宝库: Java 必备 百度网盘资源大合集 价值>10000元 加尼恩领取


40岁老架构师尼恩的掏心窝:

现在拿到offer超级难,甚至连面试电话,一个都搞不到。

尼恩的技术社群中(50+),很多小伙伴凭借 “左手云原生+右手大数据 +SpringCloud Alibaba 微服务“三大绝活,拿到了offer,并且是非常优质的offer,据说年终奖都足足18个月 ,非常令人羡慕。

问题是:“左手云原生+右手大数据 +SpringCloud Alibaba 微服务“ 内容非常多,实操的环境非常复杂,底层原理很深

米饭要一口一口的吃,不能急。在这里,尼恩从架构师视角出发,左手云原生+右手大数据 +SpringCloud Alibaba 微服务 核心原理做一个宏观的介绍。

由于内容确实太多, 所以写多个pdf 电子书:

(1) 《 Docker 学习圣经 》PDF (V1已经完成)

(2) 《 SpringCloud Alibaba 微服务 学习圣经 》PDF (V2已经完成)

(3) 《 K8S 学习圣经 》PDF (V1已经完成)

(4) 《 flink + hbase 学习圣经 》PDF (planning ......)

以上学习圣经,并且后续会持续升级,从V1版本一直迭代发布。 就像咱们的《尼恩 Java 面试宝典》一样, 已经迭代到V62啦。

40岁老架构师尼恩的掏心窝: 通过一系列的学习圣经,带大家穿透“左手云原生+右手大数据 +SpringCloud Alibaba 微服务“ ,实现技术 自由 ,走向颠覆人生,让大家不迷路。

以上学习圣经的 基础知识是 尼恩的《高并发三部曲》,建议在看 学习圣经之前,一定把尼恩的 Java高并发三部曲过一遍,切记,切记。

版本升级说明

本文 为《SpringCloud Alibaba 学习圣经》 的 Sentinel 部分的 升级版本。

在原书的基础上,完善和修改了 5000字左右。

最新的内容,已经合入到 《SpringCloud Alibaba 学习圣经》 V2 版本PDF。

最新的、完整的PDF,可以在公众号【技术自由圈】领取,暗号:领电子书。

hystrix 服务保护

hystrix 作为老牌 SpringCloud 微服务保护的组件,很多项目仍然在使用,

另外,底层原理都是想通的,

大家可以和 sentinel 对比学习

hystrix 的详细介绍,请阅读 《 Java 高并发核心编程 卷3 加强版

Sentinel 服务保护

sentinel 是SpringCloud 阿里巴巴 的 微服务保护组件,

学习的时候,sentinel 最好与 hystrix 对比学习,

sentinel 基本概念

开发的原因,需要对吞吐量(TPS)、QPS、并发数、响应时间(RT)几个概念做下了解,查自百度百科,记录如下:

  1. 响应时间(RT)

响应时间是指系统对请求作出响应的时间。直观上看,这个指标与人对软件性能的主观感受是非常一致的,因为它完整地记录了整个计算机系统处理请求的时间。由于一个系统通常会提供许多功能,而不同功能的处理逻辑也千差万别,因而不同功能的响应时间也不尽相同,甚至同一功能在不同输入数据的情况下响应时间也不相同。所以,在讨论一个系统的响应时间时,人们通常是指该系统所有功能的平均时间或者所有功能的最大响应时间。当然,往往也需要对每个或每组功能讨论其平均响应时间和最大响应时间。

对于单机的没有并发操作的应用系统而言,人们普遍认为响应时间是一个合理且准确的性能指标。需要指出的是,响应时间的绝对值并不能直接反映软件的性能的高低,软件性能的高低实际上取决于用户对该响应时间的接受程度。对于一个游戏软件来说,响应时间小于100毫秒应该是不错的,响应时间在1秒左右可能属于勉强可以接受,如果响应时间达到3秒就完全难以接受了。而对于编译系统来说,完整编译一个较大规模软件的源代码可能需要几十分钟甚至更长时间,但这些响应时间对于用户来说都是可以接受的。

  1. 吞吐量(Throughput)

吞吐量是指系统在单位时间内处理请求的数量。对于无并发的应用系统而言,吞吐量与响应时间成严格的反比关系,实际上此时吞吐量就是响应时间的倒数。前面已经说过,对于单用户的系统,响应时间(或者系统响应时间和应用延迟时间)可以很好地度量系统的性能,但对于并发系统,通常需要用吞吐量作为性能指标。

对于一个多用户的系统,如果只有一个用户使用时系统的平均响应时间是t,当有你n个用户使用时,每个用户看到的响应时间通常并不是n×t,而往往比n×t小很多(当然,在某些特殊情况下也可能比n×t大,甚至大很多)。这是因为处理每个请求需要用到很多资源,由于每个请求的处理过程中有许多不走难以并发执行,这导致在具体的一个时间点,所占资源往往并不多。也就是说在处理单个请求时,在每个时间点都可能有许多资源被闲置,当处理多个请求时,如果资源配置合理,每个用户看到的平均响应时间并不随用户数的增加而线性增加。实际上,不同系统的平均响应时间随用户数增加而增长的速度也不大相同,这也是采用吞吐量来度量并发系统的性能的主要原因。一般而言,吞吐量是一个比较通用的指标,两个具有不同用户数和用户使用模式的系统,如果其最大吞吐量基本一致,则可以判断两个系统的处理能力基本一致。

  1. 并发用户数

并发用户数是指系统可以同时承载的正常使用系统功能的用户的数量。与吞吐量相比,并发用户数是一个更直观但也更笼统的性能指标。实际上,并发用户数是一个非常不准确的指标,因为用户不同的使用模式会导致不同用户在单位时间发出不同数量的请求。一网站系统为例,假设用户只有注册后才能使用,但注册用户并不是每时每刻都在使用该网站,因此具体一个时刻只有部分注册用户同时在线,在线用户就在浏览网站时会花很多时间阅读网站上的信息,因而具体一个时刻只有部分在线用户同时向系统发出请求。这样,对于网站系统我们会有三个关于用户数的统计数字:注册用户数、在线用户数和同时发请求用户数。由于注册用户可能长时间不登陆网站,使用注册用户数作为性能指标会造成很大的误差。而在线用户数和同时发请求用户数都可以作为性能指标。相比而言,以在线用户作为性能指标更直观些,而以同时发请求用户数作为性能指标更准确些。

  1. QPS每秒查询率(Query Per Second)

每秒查询率QPS是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准,在因特网上,作为域名系统服务器的机器的性能经常用每秒查询率来衡量。对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。 (看来是类似于TPS,只是应用于特定场景的吞吐量)

服务雪崩效应

在微服务架构系统中通常会有多个服务,在服务调用中如果出现基础服务故障,可能会导致级联故障,即一个服务不可用,可能导致所有调用它或间接调用它的服务都不可用,进而造成整个系统不可用的情况,这种现象也被称为服务雪崩效应。

服务雪崩效应是一种因“服务提供者不可用”(原因)导致“服务调用者不可用”(结果),并将不可用逐渐放大的现象。

服务雪崩效应示意如图所示,A为服务提供者,B为A的服务调用者,C为B的服务调用者。

当服务A因为某些原因导致不可用时,会引起服务B的不可用,并将不可用放大到服务C进而导致整个系统瘫痪,这样就形成了服务雪崩效应。

出现服务雪崩效应的原因如下:

硬件故障:如服务器宕机、机房断电、光纤被挖断等。

流量激增:如异常流量、重试加大流量等。

缓存穿透:一般发生在应用重启,所有缓存失效时,以及短时间内大量缓存失效时,因大量的缓存不命中,使请求直击后端服务,造成服务提供者超负荷运行,引起服务不可用。

程序bug:如程序逻辑导致死循环或者内存泄漏等。

如何解决服务器雪崩的方法有以下这些:

  • 超时机制:在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
  • 限流机制:限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
  • 熔断机制:在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
  • 降级机制:降级是从系统功能优先级的角度考虑如何应对系统故障。 服务降级指的是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。降级其实就是为服务提供一个兜底方案,一旦服务无法正常调用,就使用兜底方案。

Sentinel 为我们提供了多种的解决服务雪崩的方法:如超时机制、限流机制、熔断机制、降级机制等等,后面会为大家进行介绍

1、什么是Sentinel:

Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
官网:https://github.com/alibaba/Sentinel/wiki

2012年,Sentinel诞生于阿里巴巴,其主要目标是流量控制。2013-2017年,Sentinel迅速发展,并成为阿里巴巴所有微服务的基本组成部分。 它已在6000多个应用程序中使用,涵盖了几乎所有核心电子商务场景。2018年,Sentinel演变为一个开源项目。2020年,Sentinel Golang发布。

Sentinel 具有以下特征:

丰富的应用场景 :Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即
突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。

完备的实时监控 :Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机
器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。

广泛的开源生态 :Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring
Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel。

完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快
速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel的生态圈

Sentinel主要特性:

关于Sentinel与Hystrix的区别见:https://yq.aliyun.com/articles/633786/

到这已经学习Sentinel的基本的使用,在很多的特性和Hystrix有很多类似的功能。以下是Sentinel和Hystrix的对比。

Sentinel 的使用

Sentinel 的使用可以分为两个部分:

  • 控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
  • 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 7 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。

在这里我们看下控制台的使用

Sentinel中的管理控制台

1)获取 Sentinel 控制台

您可以从 https://github.com/alibaba/Sentinel/releases 下载最新版本的控制台 jar 包。

您也可以从最新版本的源码自行构建 Sentinel 控制台:

  • 下载 控制台 工程
  • 使用以下命令将代码打包成一个 fat jar: mvn clean package

2)sentinel服务启动

(1)java -jar sentinl+tab键自动补全

java  -server -Xms64m -Xmx256m  -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.8.6.jar 

开机启动:启动命令可以加入到启动的 rc.local 配置文件, 之后做到开机启动

(2)启动 sentinel

/usr/bin/su  - root  -c   "nohup java  -server -Xms64m -Xmx256m  -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.8.6.jar  2>&1 &"

除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。

由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)。

关于熔断降级的介绍见:Sentinel熔断降级。

下面就使用基于注解的方式实现Sentinel的熔断降级的demo。

注意:启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。

使用如下命令启动控制台:

nohup java  -server -Xms64m -Xmx256m  -Dserver.port=8849 -Dcsp.sentinel.dashboard.server=localhost:8849 -Dproject.name=sentinel-dashboard -jar /work/sentinel-dashboard-1.8.6.jar &

其中 -Dserver.port=8849用于指定 Sentinel 控制台端口为 8849 , 这个端口可以按需指定。

从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。可以参考 鉴权模块文档 配置用户名和密码。

注:若您的应用为 Spring Boot 或 Spring Cloud 应用,您可以通过 Spring 配置文件来指定配置,详情请参考 Spring Cloud Alibaba Sentinel 文档

(1)获取 Sentinel 控制台

您可以从官方 网站中 下载最新版本的控制台 jar 包,下载地址如下:

https://github.com/alibaba/Sentinel/releases/download/1.6.3/sentinel-dashboard-1.7.1.jar

(2)启动

使用如下命令启动控制台:

其中 - Dserver.port=8888 用于指定 Sentinel 控制台端口为 8888 。

从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel 。可以参考 鉴权模块文档 配置用户名和密码。

[root@192 ~]# java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar
INFO: log base dir is: /root/logs/csp/
INFO: log name use pid is: false

  .   ____          _            __ _ _
 /\\\\ / ___\'_ __ _ _(_)_ __  __ _ \\ \\ \\ \\
( ( )\\___ | \'_ | \'_| | \'_ \\/ _` | \\ \\ \\ \\
 \\\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  \'  |____| .__|_| |_|_| |_\\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.0.5.RELEASE)

2020-02-08 13:07:29.316  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : Starting DashboardApplication on 192.168.180.137 with PID 114031 (/root/sentinel-dashboard-1.6.3.jar started by root in /root)
2020-02-08 13:07:29.319  INFO 114031 --- [           main] c.a.c.s.dashboard.DashboardApplication   : No active profile set, falling back to default profiles: default
2020-02-08 13:07:29.456  INFO 114031 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@59690aa4: startup date [Sat Feb 08 13:07:29 CST 2020]; root of context hierarchy
2020-02-08 13:07:33.783  INFO 114031 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8888 (http)

启动 Sentinel 控制台需要 JDK 版本为 1.8 及以上版本。

查看机器列表以及健康情况
默认情况下Sentinel 会在客户端首次调用的时候进行初始化,开始向控制台发送心跳包。也可以配置
sentinel.eager=true ,取消Sentinel控制台懒加载。
打开浏览器即可展示Sentinel的管理控制台

客户端能接入控制台

控制台启动后,客户端需要按照以下步骤接入到控制台。

父工程引入 alibaba实现的SpringCloud

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Greenwich.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.1.0.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

子工程中引入 sentinel

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

( 2)配置启动参数
在工程的application.yml中添加Sentinel 控制台配置信息

spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.180.137:8849   #sentinel控制台的请求地址

这里的 spring.cloud.sentinel.transport.dashboard 配置控制台的请求路径。

Sentinel与Hystrix的区别

Items Sentinel Hystrix remark
隔离策略 信号量隔离(并发线程数限流)(模拟信号量) 线程池隔离/信号量隔离 Sentinel不创建线程依赖tomcat或jetty容器的线程池,存在的问题就是运行容器的线程数量限制了sentinel设置值的上限可能设置不准。比如tomcat线程池为10,sentinel设置100是没有意义的,同时隔离性不好hystrix使用自己创建的线程池,隔离性会更好
熔断降级策略 基于响应时间、异常比率、异常数 基于异常比率 快速失败的本质功能
实时统计实现 滑动窗口(LeapArray) 滑动窗口(基于 RxJava)
动态规则配置 支持多种数据源 支持多种数据源
扩展性 多个扩展点 插件的形式
基于注解的支持 支持 支持
限流 基于 QPS,支持基于调用关系的限流 有限的支持(并发线程数或信号量大小) 快速失败的本质功能
流量整形 支持预热模式、匀速器模式、预热排队模式 不支持(排队) 支持排队好吧
系统自适应保护 支持(仅对linux生效) 不支持 所谓的自适应就是设置一个服务器最大允许处理量的阈值。(有比没有强,但是要知道最大负载量是多少。)
控制台 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 简单的监控查看接近实时数据 控制台是非常有竞争力的功能,因为能集中配置限制数据更方便,但是展示数据和实时性没有hystrix直观。
配置持久化 ZooKeeper, Apollo, Nacos Git/svn/本地文件 Sentinel客户端采用直接链接持久化存储,应用客户端引用了更多的依赖,同样的存储链接可能有多个配置
动态配置 支持 支持 hystrix可能需要手动触发,sentinel增加了额外的端口进行配置文件控制,应该也支持spring boot动态配置
黑白名单 支持 不支持 个人觉得这个功能用的不是很多
springcloud集成 非常高 Spring boot使用hystrix会更方便
整体优势 集中配置设置及监控+更细的控制规则 漂亮的界面+接近实时的统计结果 集中配置可能更有吸引力,但是配置值是多少以及让谁控制依然是很头疼的事情。运维控制可能不知道哪个应该优先哪个不优先,应该调整到多大。什么时候更适合使用sentinel?个人认为docker容器化部署之后sentinel可能更会发挥作用,但是会有另外的竞品出现做选型。

迁移方案
Sentinel 官方提供了详细的由Hystrix 迁移到Sentinel 的方法

2、使用 Sentinel 来进行熔断与限流

Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合
Dashboard 可以取得最好的效果。
使用 Sentinel 来进行熔断保护,主要分为几个步骤:

  1. 定义资源

    资源:可以是任何东西,一个服务,服务里的方法,甚至是一段代码。

  2. 定义规则

    规则:Sentinel 支持以下几种规则:流量控制规则、熔断降级规则、系统保护规则、来源访问控制规则
    和 热点参数规则。

  3. 检验规则是否生效

Sentinel 的所有规则都可以在内存态中动态地查询及修改,修改之后立即生效. 先把可能需要保护的资源定义好,之后再配置规则。

也可以理解为,只要有了资源,我们就可以在任何时候灵活地定义各种流量控制规则。在编码的时候,只需要考虑这个代码是否需要保护,如果需要保护,就将之定义为一个资源。

2.1 定义资源

资源是 Sentinel 的关键概念。它可以是 Java 应用程序中的任何内容,例如,由应用程序提供的服务,或由应用程序调用的其它应用提供的服务,RPC接口方法,甚至可以是一段代码。

只要通过 Sentinel API 定义的代码,就是资源,能够被 Sentinel 保护起来。大部分情况下,可以使用方法签名,URL,甚至服务名称作为资源名来标示资源。

把需要控制流量的代码用 Sentinel的关键代码 SphU.entry("资源名") 和 entry.exit() 包围起来即可。

实例代码:

Entry entry = null;
try 
    // 定义一个sentinel保护的资源,名称为test-sentinel-api
    entry = SphU.entry(resourceName);
    // 模拟执行被保护的业务逻辑耗时
    Thread.sleep(100);
    return a;
 catch (BlockException e) 
    // 如果被保护的资源被限流或者降级了,就会抛出BlockException
    log.warn("资源被限流或降级了", e);
    return "资源被限流或降级了";
 catch (InterruptedException e) 
    return "发生InterruptedException";
 finally 
    if (entry != null) 
        entry.exit();
    

    ContextUtil.exit();


在下面的例子中, 用 try-with-resources 来定义资源。参考代码如下:

public static void main(String[] args) 
    // 配置规则.
    initFlowRules();

    while (true) 
        // 1.5.0 版本开始可以直接利用 try-with-resources 特性
        try (Entry entry = SphU.entry("HelloWorld")) 
            // 被保护的逻辑
            System.out.println("hello world");
         catch (BlockException ex) 
            // 处理被流控的逻辑
            System.out.println("blocked!");
        
    

资源注解@SentinelResource

也可以使用Sentinel提供的注解@SentinelResource来定义资源,实例如下:

@SentinelResource("HelloWorld")
public void helloWorld() 
    // 资源中的逻辑
    System.out.println("hello world");

@SentinelResource 注解

注意:注解方式埋点不支持 private 方法。

@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:

  • value:资源名称,必需项(不能为空)
  • entryType:entry 类型,可选项(默认为 EntryType.OUT)
  • blockHandler / blockHandlerClass:

blockHandler 对应处理 BlockException的函数名称,可选项。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

  • fallback /fallbackClass

    fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。

  • defaultFallback

    (since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。

fallback 函数签名和位置要求:
  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
  • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
defaultFallback 函数签名要求:
  • 返回值类型必须与原函数返回值类型一致;
  • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
  • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

2.2 定义规则

规则主要有流控规则、 熔断降级规则、系统规则、权限规则、热点参数规则等:

一段硬编码的方式定义流量控制规则如下:

private void initSystemRule() 
    List<SystemRule> rules = new ArrayList<>();
    SystemRule rule = new SystemRule();
    rule.setHighestSystemLoad(10);
    rules.add(rule);
    SystemRuleManager.loadRules(rules);

加载规则:

FlowRuleManager.loadRules(List<FlowRule> rules); // 修改流控规则
DegradeRuleManager.loadRules(List<DegradeRule> rules); // 修改降级规则
SystemRuleManager.loadRules(List<SystemRule> rules); // 修改系统规则
AuthorityRuleManager.loadRules(List<AuthorityRule> rules); // 修改授权规则

3、sentinel 熔断降级

3.1 什么是熔断降级

熔断降级对调用链路中不稳定的资源进行熔断降级是保障高可用的重要措施之一。

由于调用关系的复杂性,如果调用链路中的某个资源不稳定,最终会导致请求发生堆积。Sentinel 熔断降级会在调用链路中某个资源出现不稳定状态时(例如调用超时或异常比例升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误。当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException)

3.2 熔断降级规则

熔断降级规则包含下面几个重要的属性:

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

3.3 几种降级策略

我们通常用以下几种降级策略:

  • 平均响应时间 (DEGRADE_GRADE_RT):

    当资源的平均响应时间超过阈值(DegradeRule 中的 count,以 ms 为单位)之后,资源进入准降级状态。如果接下来 1s 内持续进入 5 个请求(即 QPS >= 5),它们的 RT 都持续超过这个阈值,那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。

    注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。

  • 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):

    当资源的每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。

    异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。

  • 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):

    当资源近 1 分钟的异常数目超过阈值之后会进行熔断。

    注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

3.4 熔断降级代码实现

可以通过调用 DegradeRuleManager.loadRules() 方法来用硬编码的方式定义流量控制规则。

@PostConstruct
public void initSentinelRule()

    //熔断规则: 5s内调用接口出现异常次数超过5的时候, 进行熔断
    List<DegradeRule> degradeRules = new ArrayList<>();
    DegradeRule rule = new DegradeRule();
    rule.setResource("queryGoodsInfo");
    rule.setCount(5);

    rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);//熔断规则
    rule.setTimeWindow(5);
    degradeRules.add(rule);
    DegradeRuleManager.loadRules(degradeRules);

具体源码,请参见疯狂创客圈crazy-springcloud 源码工程

3.5 控制台降级规则

配置

参数

Field 说明 默认值
resource 资源名,即限流规则的作用对象
count 阈值
grade 降级模式,根据 RT 降级还是根据异常比例降级 RT
timeWindow 降级的时间,单位为 s

3.6 与Hystrix的熔断对比:

Hystrix常用的线程池隔离会造成线程上下切换的overhead比较大;Hystrix使用的信号量隔离对某个资源调用的并发数进行控制,效果不错,但是无法对慢调用进行自动降级;

Sentinel通过并发线程数的流量控制提供信号量隔离的功能;此外,Sentinel支持的熔断降级维度更多,可对多种指标进行流控、熔断,且提供了实时监控和控制面板,功能更为强大。

4、Sentinel 流控(限流)

流量控制(Flow Control),原理是监控应用流量的QPS或并发线程数等指标,当达到指定阈值时对流量进行控制,避免系统被瞬时的流量高峰冲垮,保障应用高可用性。

通过流控规则来指定允许该资源通过的请求次数,例如下面的代码定义了资源 HelloWorld 每秒最多只能通过 20 个请求。 参考的规则定义如下:

private static void initFlowRules()
    List<FlowRule> rules = new ArrayList<>();
    FlowRule rule = new FlowRule();
    rule.setResource("HelloWorld");
    rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
    // Set limit QPS to 20.
    rule.setCount(20);
    rules.add(rule);
    FlowRuleManager.loadRules(rules);

一条限流规则主要由下面几个因素组成,我们可以组合这些元素来实现不同的限流效果:

  • resource:资源名,即限流规则的作用对象
  • count: 限流阈值
  • grade: 限流阈值类型(QPS 或并发线程数)
  • limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
  • strategy: 调用关系限流策略
  • controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)

基本的参数

资源名:唯一名称,默认请求路径

针对来源:Sentinel可以针对调用者进行限流,填写微服务名,默认为default(不区分来源)

阈值类型/单机阈值:

  1. QPS:每秒请求数,当前调用该api的QPS到达阈值的时候进行限流
  2. 线程数:当调用该api的线程数到达阈值的时候,进行限流

是否集群:是否为集群

流控的几种strategy

  1. 直接:当api大达到限流条件时,直接限流
  2. 关联:当关联的资源到达阈值,就限流自己
  3. 链路:只记录指定路上的流量,指定资源从入口资源进来的流量,如果达到阈值,就进行限流,api级别的限流

4.1 直接失败模式

使用API进行资源定义

/**
 * 限流实现方式一: 抛出异常的方式定义资源
 *
 * @param orderId
 * @return
 */
@ApiOperation(value = "纯代码限流")
@GetMapping("/getOrder")
@ResponseBody
public String getOrder(@RequestParam(value = "orderId", required = false)String orderId)


    Entry entry = null;
    // 资源名
    String resourceName = "getOrder";
    try
    
        // entry可以理解成入口登记
        entry = SphU.entry(resourceName);
        // 被保护的逻辑, 这里为订单查询接口
        return "正常的业务逻辑 OrderInfo :" + orderId;
     catch (BlockException blockException)
    
        // 接口被限流的时候, 会进入到这里
        log.warn("---getOrder1接口被限流了---, exception: ", blockException);
        return "接口限流, 返回空";
     finally
    
        // SphU.entry(xxx) 需要与 entry.exit() 成对出现,否则会导致调用链记录异常
        if (entry != null)
        
            entry.exit();
        
    

代码限流规则

//限流规则 QPS mode,
List<FlowRule> rules = new ArrayList<FlowRule>();
FlowRule rule1 = new FlowRule();
rule1.setResource("getOrder");
// QPS控制在2以内
rule1.setCount(2);
// QPS限流
rule1.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule1.setLimitApp("default");
rules.add(rule1);
FlowRuleManager.loadRules(rules);

网页限流规则配置

选择QPS,直接,快速失败,单机阈值为2。

配置

参数

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 或线程数模式 QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 判断的根据是资源自身,还是根据其它关联资源 (refResource),还是根据链路入口 根据资源本身
controlBehavior 流控效果(直接拒绝 / 排队等待 / 慢启动模式) 直接拒绝

测试

频繁刷新请求,1秒访问2次请求,正常,超过设置的阈值,将报默认的错误。

再次的1秒访问2次请求,访问正常。超过2次,访问异常

4.2 关联模式

调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关系。Sentinel 通过 NodeSelectorSlot 建立不同资源间的调用的关系,并且通过 ClusterBuilderSlot 记录每个资源的实时统计信息。

当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。

比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢.

举例来说,read_dbwrite_db 这两个资源分别代表数据库读写,我们可以给 read_db 设置限流规则来达到写优先的目的。具体的方法:

设置 `strategy` 为 `RuleConstant.STRATEGY_RELATE` 
设置 `refResource` 为 `write_db`。
这样当写库操作过于频繁时,读数据的请求会被限流。

还有一个例子,电商的 下订单 和 支付两个操作,需要优先保障 支付, 可以根据 支付接口的 流量阈值,来对订单接口进行限制,从而保护支付的目的。

使用注解进行资源定义

添加2个请求

@SentinelResource(value = "test1", blockHandler = "exceptionHandler")
@GetMapping("/test1")
public String test1()

    log.info(Thread.currentThread().getName() + "\\t" + "...test1");
    return "-------hello baby,i am test1";



// Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致.
public String exceptionHandler(BlockException ex)

    // Do some log here.
    ex.printStackTrace();
    log.info(Thread.currentThread().getName() + "\\t" + "...exceptionHandler");
    return String.format("error: test1  is not OK");


@SentinelResource(value = "test1_ref")
@GetMapping("/test1_ref")
public String test1_ref()

    log.info(Thread.currentThread().getName() + "\\t" + "...test1_related");
    return "-------hello baby,i am test1_ref";

代码配置关联限流规则

// 关联模式流控  QPS控制在1以内
String refResource = "test1_ref";
FlowRule rRule = new FlowRule("test1")
    .setCount(1)  // QPS控制在1以内
    .setStrategy(RuleConstant.STRATEGY_RELATE)
    .setRefResource(refResource);

rules.add(rRule);
FlowRuleManager.loadRules(rules);

网页限流规则配置

测试

选择QPS,单机阈值为1,选择关联,关联资源为/test_ref,这里用Jmeter模拟高并发,请求/test_ref。

在大批量线程高并发访问/test_ref,导致/test失效了

链路类型的关联也类似,就不再演示了。多个请求调用同一微服务。

4.3 Warm up(预热)模式

当流量突然增大的时候,我们常常会希望系统从空闲状态到繁忙状态的切换的时间长一些。即如果系统在此之前长期处于空闲的状态,我们希望处理请求的数量是缓步的增多,经过预期的时间以后,到达系统处理请求个数的最大值。Warm Up(冷启动,预热)模式就是为了实现这个目的的。

默认 coldFactor 为 3,即请求 QPS 从 threshold / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。

使用注解定义资源

@SentinelResource(value = "testWarmUP", blockHandler = "exceptionHandlerOfWarmUp")
@GetMapping("/testWarmUP")
public String testWarmUP()

    log.info(Thread.currentThread().getName() + "\\t" + "...test1");
    return "-------hello baby,i am testWarmUP";

代码限流规则

FlowRule warmUPRule = new FlowRule();
warmUPRule.setResource("testWarmUP");
warmUPRule.setCount(20);
warmUPRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
warmUPRule.setLimitApp("default");
warmUPRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP);
warmUPRule.setWarmUpPeriodSec(10);

网页限流规则配置

先在单机阈值10/3,3的时候,预热10秒后,慢慢将阈值升至20。刚开始刷/testWarmUP,会出现默认错误,预热时间到了后,阈值增加,没超过阈值刷新,请求正常。

通常冷启动的过程系统允许通过的 QPS 曲线如下图所示:

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

通过jmeter进行测试

4.4 排队等待模式

匀速排队(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER)方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。阈值必须设置为QPS。

这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。

某瞬时来了大流量的请求, 而如果此时要处理所有请求,很可能会导致系统负载过高,影响稳定性。但其实可能后面几秒之内都没有消息投递,若直接把多余的消息丢掉则没有充分利用系统处理消息的能力。Sentinel的Rate Limiter模式能在某一段时间间隔内以匀速方式处理这样的请求, 充分利用系统的处理能力, 也就是削峰填谷, 保证资源的稳定性.

Sentinel会以固定的间隔时间让请求通过, 访问资源。当请求到来的时候,如果当前请求距离上个通过的请求通过的时间间隔不小于预设值,则让当前请求通过;否则,计算当前请求的预期通过时间,如果该请求的预期通过时间小于规则预设的 timeout 时间,则该请求会等待直到预设时间到来通过;反之,则马上抛出阻塞异常。

使用Sentinel的这种策略, 简单点说, 就是使用一个时间段(比如20s的时间)处理某一瞬时产生的大量请求, 起到一个削峰填谷的作用, 从而充分利用系统的处理能力, 下图能很形象的展示这种场景: X轴代表时间, Y轴代表系统处理的请求.

示例

模拟2个用户同时并发的访问资源,发出100个请求,

如果设置QPS阈值为1, 拒绝策略修改为Rate Limiter匀速RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER方式, 还需要设置setMaxQueueingTimeMs(20 * 1000)表示每一请求最长等待时间, 这里等待时间大一点, 以保证让所有请求都能正常通过;

假设这里设置的排队等待时间过小的话, 导致排队等待的请求超时而抛出异常BlockException, 最终结果可能是这100个并发请求中只有一个请求或几个才能正常通过, 所以使用这种模式得根据访问资源的耗时时间决定排队等待时间. 按照目前这种设置, QPS阈值为10的话, 每一个请求相当于是以匀速100ms左右通过.

使用注解定义资源

@SentinelResource(value = "testLineUp",
                  blockHandler = "exceptionHandlerOftestLineUp")
@GetMapping("/testLineUp")
public String testLineUp()

    log.info(Thread.currentThread().getName() + "\\t" + "...test1");
    return "-------hello baby,i am testLineUp";

代码限流规则

FlowRule lineUpRule = new FlowRule();
lineUpRule.setResource("testLineUp");
lineUpRule.setCount(10);
lineUpRule.setGrade(RuleConstant.FLOW_GRADE_QPS);
lineUpRule.setLimitApp("default");
lineUpRule.setMaxQueueingTimeMs(20 * 1000);
// CONTROL_BEHAVIOR_DEFAULT means requests more than threshold will be rejected immediately.
// CONTROL_BEHAVIOR_DEFAULT将超过阈值的流量立即拒绝掉.
lineUpRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER);
rules.add(lineUpRule);

网页限流规则配置

通过jmeter进行测试

4.5 热点规则 (ParamFlowRule)

何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:

  • 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制
  • 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。 使用该规则需要引入依赖:

热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

属性 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

自定义资源

@GetMapping("/byHotKey")
@SentinelResource(value = "byHotKey",
                  blockHandler = "userAccessError")
public String test4(@RequestParam(value = "userId", required = false) String userId,
                    @RequestParam(value = "goodId", required = false) int goodId)

    log.info(Thread.currentThread().getName() + "\\t" + "...byHotKey");
    return "-----------by HotKey: UserId";

限流规则代码:

可以通过 ParamFlowRuleManager 的 loadRules 方法更新热点参数规则,下面是官方实例:

ParamFlowRule rule = new ParamFlowRule(resourceName)
    .setParamIdx(0)
    .setCount(5);
// 针对 int 类型的参数 PARAM_B,单独设置限流 QPS 阈值为 10,而不是全局的阈值 5.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(PARAM_B))
    .setClassType(int.class.getName())
    .setCount(10);
rule.setParamFlowItemList(Collections.singletonList(item));

ParamFlowRuleManager.loadRules(Collections.singletonList(rule));

具体的限流代码如下:

ParamFlowRule pRule = new ParamFlowRule("byHotKey")
    .setParamIdx(1)
    .setCount(1);
// 针对 参数值1000,单独设置限流 QPS 阈值为 5,而不是全局的阈值 1.
ParamFlowItem item = new ParamFlowItem().setObject(String.valueOf(1000))
    .setClassType(int.class.getName())
    .setCount(5);
pRule.setParamFlowItemList(Collections.singletonList(item));

ParamFlowRuleManager.loadRules(Collections.singletonList(pRule));

网页限流规则配置

5、Sentinel 系统保护

系统保护的目的

在开始之前,我们先了解一下系统保护的目的:

  • 保证系统不被拖垮
  • 在系统稳定的前提下,保持系统的吞吐量

长期以来,系统保护的思路是根据硬指标,即系统的负载 (load1) 来做系统过载保护。当系统负载高于某个阈值,就禁止或者减少流量的进入;当 load 开始好转,则恢复流量的进入。这个思路给我们带来了不可避免的两个问题:

  • load 是一个“结果”,如果根据 load 的情况来调节流量的通过率,那么就始终有延迟性。也就意味着通过率的任何调整,都会过一段时间才能看到效果。当前通过率是使 load 恶化的一个动作,那么也至少要过 1 秒之后才能观测到;同理,如果当前通过率调整是让 load 好转的一个动作,也需要 1 秒之后才能继续调整,这样就浪费了系统的处理能力。所以我们看到的曲线,总是会有抖动。
  • 恢复慢。想象一下这样的一个场景(真实),出现了这样一个问题,下游应用不可靠,导致应用 RT 很高,从而 load 到了一个很高的点。过了一段时间之后下游应用恢复了,应用 RT 也相应减少。这个时候,其实应该大幅度增大流量的通过率;但是由于这个时候 load 仍然很高,通过率的恢复仍然不高。

系统保护的目标是 在系统不被拖垮的情况下,提高系统的吞吐率,而不是 load 一定要到低于某个阈值。如果我们还是按照固有的思维,超过特定的 load 就禁止流量进入,系统 load 恢复就放开流量,这样做的结果是无论我们怎么调参数,调比例,都是按照果来调节因,都无法取得良好的效果。

Sentinel 在系统自适应保护的做法是,用 load1 作为启动自适应保护的因子,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。

系统保护规则的应用

系统规则支持以下的模式:

  • 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 达到阈值即触发系统保护。

系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

系统规则的参数说明:

  • highestSystemLoad 最大的 load1,参考值 -1 (不生效)
  • avgRt 所有入口流量的平均响应时间 -1 (不生效)
  • maxThread 入口流量的最大并发数 -1 (不生效)
  • qps 所有入口资源的 QPS -1 (不生效)

硬编码的方式定义流量控制规则如下:

List<SystemRule> srules = new ArrayList<>();
SystemRule srule = new SystemRule();
srule.setAvgRt(3000);
srules.add(srule);
SystemRuleManager.loadRules(srules);

网页限流规则配置

6、黑白名单规则

很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源

5.Sentinel源码分析—Sentinel如何实现自适应限流?

Sentinel源码解析系列:
1.Sentinel源码分析—FlowRuleManager加载规则做了什么?

2. Sentinel源码分析—Sentinel是如何进行流量统计的?

3. Sentinel源码分析— QPS流量控制是如何实现的?

4.Sentinel源码分析— Sentinel是如何做到降级的?


这篇文章主要学习一下Sentinel如何实现自适应限流的。
为什么要做自适应限流,官方给了两个理由:

  1. 保证系统不被拖垮
  2. 在系统稳定的前提下,保持系统的吞吐量

我再贴一下官方的原理:
技术图片

  1. 能够保证水管里的水量,能够让水顺畅的流动,则不会增加排队的请求;也就是说,这个时候的系统负载不会进一步恶化。
  2. 当保持入口的流量是水管出来的流量的最大的值的时候,可以最大利用水管的处理能力。
    更加具体的原理解释可以看官方:系统自适应限流

所以看起来好像很厉害的样子,所以我们来看看具体实现吧。

例子:

  1. 设置系统自适应规则
List<SystemRule> rules = new ArrayList<SystemRule>();
SystemRule rule = new SystemRule();
//限制最大负载
rule.setHighestSystemLoad(3.0);
// cpu负载60%
rule.setHighestCpuUsage(0.6);
// 设置平均响应时间 10 ms
rule.setAvgRt(10);
// 设置qps is 20
rule.setQps(20);
// 设置最大线程数 10
rule.setMaxThread(10);

rules.add(rule);
SystemRuleManager.loadRules(Collections.singletonList(rule));
  1. 设置限流
Entry entry = null;
try 
    entry = SphU.entry("methodA", EntryType.IN); 
    //dosomething
 catch (BlockException e1) 
    block.incrementAndGet();
    //dosomething
 catch (Exception e2) 
    // biz exception
 finally  
    if (entry != null) 
        entry.exit();
    

注意:系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量(EntryType.IN),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。

我们先讲一下SystemRuleManager这个类在初始化的时候做了什么吧。

SystemRuleManager

private static SystemStatusListener statusListener = null;
@SuppressWarnings("PMD.ThreadPoolCreationRule")
private final static ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1,
    new NamedThreadFactory("sentinel-system-status-record-task", true));

static 
    checkSystemStatus.set(false);
    statusListener = new SystemStatusListener();
    scheduler.scheduleAtFixedRate(statusListener, 5, 1, TimeUnit.SECONDS);
    currentProperty.addListener(listener);

SystemRuleManager初始化的时候会调用静态代码块,然后用scheduler线程池定时调用SystemStatusListener类的run方法。我们进入到SystemStatusListener类里看一下:

SystemStatusListener#run

public void run() 
    try 
        OperatingSystemMXBean osBean = ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);
        currentLoad = osBean.getSystemLoadAverage();
       
        currentCpuUsage = osBean.getSystemCpuLoad();

        StringBuilder sb = new StringBuilder();
        if (currentLoad > SystemRuleManager.getHighestSystemLoad()) 
            sb.append("load:").append(currentLoad).append(";");
            sb.append("cpu:").append(currentCpuUsage).append(";");
            sb.append("qps:").append(Constants.ENTRY_NODE.passQps()).append(";");
            sb.append("rt:").append(Constants.ENTRY_NODE.avgRt()).append(";");
            sb.append("thread:").append(Constants.ENTRY_NODE.curThreadNum()).append(";");
            sb.append("success:").append(Constants.ENTRY_NODE.successQps()).append(";");
            sb.append("minRt:").append(Constants.ENTRY_NODE.minRt()).append(";");
            sb.append("maxSuccess:").append(Constants.ENTRY_NODE.maxSuccessQps()).append(";");
            RecordLog.info(sb.toString());
        

     catch (Throwable e) 
        RecordLog.info("could not get system error ", e);
    

这个方法用来做两件事:

  1. 定时收集全局资源情况,并打印日志
  2. 给全局变量currentLoad和currentCpuUsage赋值,用来做限流使用。

然后看一下SystemRuleManager.loadRules方法。SystemRuleManager和其他的规则管理是一样的,当调用loadRules方法的时候会调用内部的listener并触发它的configUpdate方法。
在SystemRuleManager中实现类了一个SystemPropertyListener,最终SystemRuleManager.loadRules方法会调用到SystemPropertyListener的configUpdate中。

SystemPropertyListener#configUpdate

public void configUpdate(List<SystemRule> rules) 
    restoreSetting();
    // systemRules = rules;
    if (rules != null && rules.size() >= 1) 
        for (SystemRule rule : rules) 
            loadSystemConf(rule);
        
     else 
        checkSystemStatus.set(false);
    

    RecordLog.info(String.format("[SystemRuleManager] Current system check status: %s, "
            + "highestSystemLoad: %e, "
            + "highestCpuUsage: %e, "
            + "maxRt: %d, "
            + "maxThread: %d, "
            + "maxQps: %e",
        checkSystemStatus.get(),
        highestSystemLoad,
        highestCpuUsage,
        maxRt,
        maxThread,
        qps));

这个方法很简单,首先是调用restoreSetting,用来重置rule的属性,然后遍历rule调用loadSystemConf对规则进行设置:

SystemRuleManager#loadSystemConf

public static void loadSystemConf(SystemRule rule) 
    boolean checkStatus = false;
    // Check if it's valid.

    if (rule.getHighestSystemLoad() >= 0) 
        highestSystemLoad = Math.min(highestSystemLoad, rule.getHighestSystemLoad());
        highestSystemLoadIsSet = true;
        checkStatus = true;
    

    if (rule.getHighestCpuUsage() >= 0) 
        highestCpuUsage = Math.min(highestCpuUsage, rule.getHighestCpuUsage());
        highestCpuUsageIsSet = true;
        checkStatus = true;
    

    if (rule.getAvgRt() >= 0) 
        maxRt = Math.min(maxRt, rule.getAvgRt());
        maxRtIsSet = true;
        checkStatus = true;
    
    if (rule.getMaxThread() >= 0) 
        maxThread = Math.min(maxThread, rule.getMaxThread());
        maxThreadIsSet = true;
        checkStatus = true;
    

    if (rule.getQps() >= 0) 
        qps = Math.min(qps, rule.getQps());
        qpsIsSet = true;
        checkStatus = true;
    

    checkSystemStatus.set(checkStatus);

这些属性都是在限流控制中会用到的属性,无论设置哪个属性都会设置checkStatus=true表示开启系统自适应限流。

在设置好限流规则后会进入到SphU.entry方法中,通过创建slot链调用到SystemSlot,这里是系统自适应限流的地方。

SystemSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable 
      //检查一下是否符合限流条件,符合则进行限流
    SystemRuleManager.checkSystem(resourceWrapper);
    fireEntry(context, resourceWrapper, node, count, prioritized, args);

SystemRuleManager#checkSystem

public static void checkSystem(ResourceWrapper resourceWrapper) throws BlockException 
    // Ensure the checking switch is on.
    if (!checkSystemStatus.get()) 
        return;
    
    //如果不是入口流量,那么直接返回
    // for inbound traffic only
    if (resourceWrapper.getType() != EntryType.IN) 
        return;
    

    // total qps
    double currentQps = Constants.ENTRY_NODE == null ? 0.0 : Constants.ENTRY_NODE.successQps();
    if (currentQps > qps) 
        throw new SystemBlockException(resourceWrapper.getName(), "qps");
    

    // total thread
    int currentThread = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.curThreadNum();
    if (currentThread > maxThread) 
        throw new SystemBlockException(resourceWrapper.getName(), "thread");
    

    double rt = Constants.ENTRY_NODE == null ? 0 : Constants.ENTRY_NODE.avgRt();
    if (rt > maxRt) 
        throw new SystemBlockException(resourceWrapper.getName(), "rt");
    

    // load. BBR algorithm.
    if (highestSystemLoadIsSet && getCurrentSystemAvgLoad() > highestSystemLoad) 
        if (!checkBbr(currentThread)) 
            throw new SystemBlockException(resourceWrapper.getName(), "load");
        
    

    // cpu usage
    if (highestCpuUsageIsSet && getCurrentCpuUsage() > highestCpuUsage) 
        if (!checkBbr(currentThread)) 
            throw new SystemBlockException(resourceWrapper.getName(), "cpu");
        
    

这个方法首先会校验一下checkSystemStatus状态和EntryType是不是IN,如果不是则直接返回。
然后对Constants.ENTRY_NODE进行操作。这个对象是一个final static 修饰的变量,代表是全局对象。

public final static ClusterNode ENTRY_NODE = new ClusterNode();

所以这里的限流操作都是对全局其作用的,而不是对资源起作用。ClusterNode还是继承自StatisticNode,所以最后都是调用StatisticNode的successQps、curThreadNum、avgRt,这几个方法我的前几篇文章都已经讲过了,感兴趣的可以自己去翻一下,这里就不过多涉及了。

在下面调用getCurrentSystemAvgLoad方法和getCurrentCpuUsage方法调用到SystemStatusListener设置的全局变量currentLoad和currentCpuUsage。这两个参数是SystemRuleManager的定时任务定时收集的,忘了的同学回到上面讲解SystemRuleManager的地方看一下。

在做load判断和cpu usage判断的时候会还会调用checkBbr方法来判断:

private static boolean checkBbr(int currentThread) 
    if (currentThread > 1 &&
        currentThread > Constants.ENTRY_NODE.maxSuccessQps() * Constants.ENTRY_NODE.minRt() / 1000) 
        return false;
    
    return true;

也就是说:当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 maxQps * minRt 计算得出。

StatisticNode#maxSuccessQps

public double maxSuccessQps() 
    return rollingCounterInSecond.maxSuccess() * rollingCounterInSecond.getSampleCount();

maxSuccessQps方法是用窗口内的最大成功调用数和窗口数量相乘rollingCounterInSecond的窗口1秒的窗口数量是2,最大成功调用数如下得出:
ArrayMetric#maxSuccess

public long maxSuccess() 
    data.currentWindow();
    long success = 0;

    List<MetricBucket> list = data.values();
    for (MetricBucket window : list) 
        if (window.success() > success) 
            success = window.success();
        
    
    return Math.max(success, 1);

最大成功调用数是通过整个遍历整个窗口,获取所有窗口里面最大的调用数。所以这样的最大的并发量是一个预估值,不是真实值。

看到这里我们再来看一下Constants.ENTRY_NODE的信息是怎么被收集的。
我在分析StatisticSlot这个类的时候有一段代码我当时也没看懂有什么用,现在就迎刃而解了:
StatisticSlot#entry

public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                  boolean prioritized, Object... args) throws Throwable 
    try  
            ....
        if (resourceWrapper.getType() == EntryType.IN) 
            // Add count for global inbound entry node for global statistics.
            Constants.ENTRY_NODE.increaseThreadNum();
            Constants.ENTRY_NODE.addPassRequest(count);
        
            ....
     catch (PriorityWaitException ex) 
            ....
        if (resourceWrapper.getType() == EntryType.IN) 
            // Add count for global inbound entry node for global statistics.
            Constants.ENTRY_NODE.increaseThreadNum();
        
         ....
     catch (BlockException e) 
            ....
        if (resourceWrapper.getType() == EntryType.IN) 
            // Add count for global inbound entry node for global statistics.
            Constants.ENTRY_NODE.increaseBlockQps(count);
        
            .... 
        throw e;
     catch (Throwable e) 
         ....
        if (resourceWrapper.getType() == EntryType.IN) 
            Constants.ENTRY_NODE.increaseExceptionQps(count);
        
        throw e;
    

在StatisticSlot的entry方法里有很多对于type的判断,如果是EntryType.IN,那么就调用Constants.ENTRY_NODE的静态方法进行数据的收集。

所以看到这里我们可以知道,在前面有很多看不懂的代码其实只要慢慢琢磨,打个标记,那么在后面的解析的过程中还是能够慢慢看懂的。

共勉~~

以上是关于3W字吃透:微服务 sentinel 限流 底层原理和实操的主要内容,如果未能解决你的问题,请参考以下文章

微服务架构 | 5.2 基于 Sentinel 的服务限流及熔断

微服务架构 Sentinel 的服务限流及熔断

微服务限流Sentinel讲解

谈谈微服务中Sentinel的熔断限流到底有多重要?

微服务限流容错降级Sentinel实战

微服务限流容错降级Sentinel实战