限流与熔断理论部分(五)——sentinel(一)

350 阅读19分钟

什么是熔断器?

限流在前面的章节已经知道了,现在谈谈熔断器

熔断器起的作用就是在特定的场景下关掉当前的通路,从而起到保护整个系统的效果

image.png

这张图需要注意的是,绿色表示接通电路或者服务健康,红色表示熔断或者服务负载

一次服务请求访问可能需要涉及到 服务A 而 服务A 又需要 服务B,C,D 三个服务器提供支持(微服务链路),如果 服务D 负载,请求延迟过高导致该请求阻塞等待服务D的响应
又请求的线程是珍贵资源,有限的,如果不将服务D熔断掉,并立即反馈失败,线程将被全部占用。
这种情况国内较雪崩效应(去你丫的雪崩效应),国外叫 cascading failures(级联故障

吐槽国内某些砖家别的不会,争KPI搞高大上的名字最牛 国内好像是2012年企鹅某高官首先提出的新名字 我当初学习的时候,就一直纠结微服务链路故障怎么就跟雪崩效应联系在一起了?雪崩效应不是加密算法的一种理想属性么?

而熔断器就是为了防止出现级联故障而存在的。。。

功能对比

市面上的断路器就三种: Sentinel、Hystrix、resilience4j
其中前文说过的,Hystrix已经进入维护,基本不考虑使用了
而resilience4j国外推荐的比较多,国内主要使用阿里的Sentinel

SentinelHystrix(维护状态)Resilience4j(Spring推荐)
开发者alibabaNetflix独立
隔离策略信号量隔离(并发线程数限流)线程池隔离/信号量隔离信号量隔离
熔断降级策略基于响应时间、异常比率、异常数 基于异常比率基于异常比率基于异常比率、响应时间
实时统计实现滑动窗口(LeapArray)滑动窗口(基于 RxJava)Ring Bit Buffer
动态规则配置支持多种数据源支持多种数据源有限支持
扩展性多个扩展点插件的形式接口的形式
基于注解的支持支持支持支持
限流基于 QPS,支持基于调用关系的限流有限的支持Rate Limiter
流量整形支持预热模式、匀速器模式、预热排队模式不支持简单的 Rate Limiter 模式
系统自适应保护支持不支持不支持
控制台提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等简单的监控查看不提供控制台,可对接其它监控系统

我们主要学习 sentinel
从:

  1. Sentinel是什么?
  2. Sentinel有什么用?
  3. 怎么用sentinel?
  4. sentinel如何实现?(底层实现)

sentinel限流框架

sentinel是什么?

Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。

Sentinel有功能?

Hystrix 主要是隔离和熔断,但是 Sentinel 侧重于

  • 多样化的流量控制
  • 熔断降级
  • 系统负载保护
  • 实时监控和控制台

记住,限流和降级都是以保护核心业务为目的的策略,除核心外的业务在一定阈值内保证可用。

什么是流量控制?

对随机到来的请求,sentinel根据系统处理能力对流量进行规整。

image.png

sentinel将java应用的任何内容认为是资源(比如:应用程序提供的服务、应用程序调用其他应用提供的服务、甚至是一段代码),然后在资源外边围绕一圈规则(流量控制规则、熔断降级规则或系统保护规则)以保护资源,并且所有规则都可以实时动态调整。

什么是sentinel的熔断降级?

sentinel的熔断降级实现方式和 Hystrix 的实现不同。
Hystrix通过线程隔离实现对资源的隔离,这样的好处在于比较彻底,缺点也很明显线程池线程过多导致线程上下文切换频繁,还需要预先设定线程池大小。
Sentinel的实现方式就有两种思路:

  1. 对每个资源设定一个阈值,如果该资源阻塞导致线程数量超过该阈值,资源就会拒绝新的请求,直到该资源的线程完成任务之后,才可以重新接受新的线程。这种方式的好处在于,不会有频繁的线程上下文切换,也不需要事先设定好线程池的大小,只要累计堆积线程的数量就行。
  2. 对资源进行定时。Sentinel 通过响应时间来快速降级不稳定的资源。当依赖的资源出现响应时间过长后,所有对该资源的访问都会被直接拒绝,直到过了指定的时间窗口之后才重新恢复。

Hystrix的线程隔离:针对不同的资源分别创建不同的线程池,不同服务调用都发生在不同的线程池中,在线程池排队、超时等阻塞情况时可以快速失败,并可以提供 fallback 机制。线程池隔离的好处是隔离度比较高,可以针对某个资源的线程池去进行处理而不影响其它资源,但是代价就是线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。

系统自适应保护

sentinel会保持外部流量和内部系统处理能力之间的平衡。在分布式系统中,如果某个资源已经负载,分布式系统就会将该次请求转发给其他资源,但其他资源正好也在边缘状态,此时就会导致两个资源都会负载,导致集群不可用。针对这个情况,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。

怎么安装sentinel控制台?

什么是Sentinel 控制台?

Sentinel 提供一个轻量级的开源控制台,它提供机器发现以及健康情况管理、监控(单机和集群),规则管理和推送的功能。

Sentinel 控制台包含如下功能:
查看机器列表以及健康情况:收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线。
监控 (单机和集群聚合):通过 Sentinel 客户端暴露的监控 API,定期拉取并且聚合应用监控信息,最终可以实现秒级的实时监控。
规则管理和推送:统一管理推送规则。
鉴权:生产环境中鉴权非常重要。这里每个开发者需要根据自己的实际情况进行定制。

注意:Sentinel 控制台目前仅支持单机部署。Sentinel 控制台项目提供 Sentinel 功能全集示例,不作为开箱即用的生产环境控制台,不提供安全可靠保障。若希望在生产环境使用请根据文档自行进行定制和改造。

启动控制台

  1. 下载 sentineljar 包 sentinel笔者使用的是1.8.5版本
  2. 启动控制台java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
  3. 访问[http://localhost:8080/](http://localhost:8080/)

其中 -Dserver.port=8080 用于指定 Sentinel 控制台端口为 8080。 从 Sentinel 1.6.0 起,Sentinel 控制台引入基本的登录功能,默认用户名和密码都是 sentinel。 用户可以通过如下参数进行配置: -Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel-Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel-Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒60m 表示 60 分钟默认为 30 分钟; 同样也可以直接在 Spring properties 文件中进行配置。 注意:部署多台控制台时,session 默认不会在各实例之间共享,这一块需要自行改造。

创建项目

cloudalibaba-sentinel-service8401

<dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--日常通用jar包配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.5</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
server:
  port: 8401
spring:
  application:
    name: cloudalibaba-sentinel-service
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    sentinel:
      transport:
        dashboard: localhost:8080
        port: 8719
management:
  endpoints:
    web:
      exposure:
        include: '*'
package com.zhazha.alibaba;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class SentinelMainApp8401 {
	
	public static void main(String[] args) throws Exception {
		SpringApplication.run(SentinelMainApp8401.class, args);
	}
	
}

package com.zhazha.alibaba.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Slf4j
public class FlowLimitController {
	
	@GetMapping("/testA")
	public String testA() {
		return "------testA";
	}
	
	@GetMapping("/testB")
	public String testB() {
		return "------testB";
	}
}

启动项目后,我们不会看到 cloudalibaba-sentinel-service这个项目出现在sentinel控制台左侧

sentinel控制台是懒加载的,如果不访问一次,控制台就不会主动去捕获信息

需要主动一次访问:http://localhost:8401/testA

image.png

sentinel控制台详情

流控规则

sentinel默认监控的是SpringMVCMapping也就是我们常说的 controller 层,sentinel将这视为资源

簇点链路显示了那些被sentinel视为资源的地址,如下图:

image.png

注意: 簇点链路监控是内存态的信息,它仅展示启动后调用过的资源。所以如果打开 sentinel 控制台后发现这里没有我们需要的资源,比如:testA,可以在浏览器上访问一次,就会出现了

我们可以为 testA 资源添加流控:
image.png

image.png

接着我们下载JMeter工具。该工具主要用来发送请求使用。

打开工具后会发现全是英文,如果阅读比较困难可以打开【option】——【choose language】选择简体中文即可

现在开始模拟多客户访问网站

  1. 创建【测试计划】。点击【文件】——【新建】,还可以修改下名称【sentinel测试计划】——ctrl+s保存,选择存储路径即可。
  2. 右键【sentinel测试计划】——【添加】——【线程】——【线程组】——image.png
  3. 创建http请求image.png
  4. 添加路径:image.png
  5. 添加结果树:image.png
  6. 直接启动
  7. 打开结果树:image.png

我们的中线程数为:20,两秒内执行完,qps计算可得:20/2=10,明显大于我们设定的qps=5

流控规则的模式

流控模式有三种:

  1. 直接:统计当前资源的请求,触发阈值时限流,比如上面我们访问 /testA资源一样
  2. 关联:统计关联资源(B)的请求,触发阈值时限流A,简单点说就是统计B限流A
  3. 链路:统计指定链路访问到本资源的请求,触发阈值,限流整个链路

关联的使用场景

它的单元是 接口,讲的是 接口与接口之间 的关联关系

image.png
用户有无数个请求访问 /textA/textB,他们之间共同竞争 image.png
现在我们要保证 /textB能够得到尽量多的资源要怎么做?

非常简单。

  1. 我们假设/textB更加重要(根据实际项目判定那个资源更加重要)
  2. 之后再对 /textA限流
  3. 然后sentinel就会统计 /textB是否超出阈值,来限流/textA,从而让出更多的数据库某表textB访问

超出表示/textB现在需要更多的资源,此时就需要让testA让出更多的请求

image.png

关联的使用场景:

  1. 两个资源有竞争关系
  2. 一个优先级高,一个优先级低
  3. 需要保证其中 一个重要的请求获得更多的资源

添加代码

	@GetMapping("query")
	public String query() {
		return "查看订单成功";
	}
	
	@GetMapping("update")
	public String update() {
		return "更新订单成功";
	}

image.png

image.png

需要注意这里是put请求不是get

image.png

访问:http://localhost:8401/query

image.png

可以看到 query请求被限流

链路的使用场景

链路的单元是一条调用链,讲的是调用链之间的关系

image.png

在上面的调用链路中,作为入口的 testAtestC 最终都会调用到goods资源

现在的问题是哪个调用链路的重要性较高,假设 testC 调用链路重要性更加重要
那么就需要在 sentinel 控制台中添加以 testA作为入口的调用链路
image.png

  1. 两个链路(或者多条)
  2. 一个链路优先级高,另一个链路优先级低
  3. 两个链路有相交的资源

还是支付和查询订单,这两个业务都需要在底层调用OrderService的查询订单信息方法。
但是我们知道 sentinel 默认监控 SpringMVC 的 Mapping
所以我们可以在OrderService方法上面添加SentinelResource注解,将方法标记为 资源

public interface OrderService {
	void queryOrder();
}

@Service
public class OrderServiceImpl implements OrderService {
	
	@SentinelResource(value = "goods")
	@Override
	public void queryOrder() {
		System.out.println("查询订单");
	}
}

@RestController
public class OrderController {
	@Resource
	private OrderService orderService;
	
	@GetMapping("query")
	public String query() {
		orderService.queryOrder();
		return "查看订单成功";
	}
	
	@GetMapping("update")
	public String update() {
		orderService.queryOrder();
		return "更新订单成功";
	}
}

接着我们访问下链接

但是我们发现还是只有一个链路:
image.png

这是有问题的,官网也说了,application.yml需要添加:

spring:
  cloud:
    sentinel:
      web-context-unify: false

再次试试

image.png
这样我们就能看到两棵不同的树

接着我们给 goods 资源添加流控

image.png

image.png

注意这里的qps是2

添加jmeter
image.png
qps=4
然后添加两个 http 请求:
image.png

访问:http://localhost:8401/query
image.png

jmeter也报错:
image.png

流控效果

流控效果是请求达到流量阈值是所采取的策略是什么?

  • 快速失败:达到阈值,新的请求将被拒绝并抛出FlowException异常(默认)
  • 预热模式(warm up):对超出阈值的请求也会抛出异常,但是这种默认的阈值是动态的,会慢慢的将阈值调高直到最大阈值为止
  • 排队等待:让所有的请求按照先后顺序排序,每个请求的间隔不能小于指定时间

预热模式

预热模式下,需要设置qps阈值预热时长
这里我们设置 qps = 10 预热时长 = 5
这样 sentinel 将在不需要预热是 qps 保持为 10 / 3 = 3
这里的除数是 sentinel 内部默认值 3
预热请求超出qps = 3 则 sentinel 会逐步预热,直到5秒后的最大值,中间会拒绝掉部分请求

这就是预热模式

threshold / coldFactor = 初始qps coldFactor默认值为 3 threshold是最大qps

设置jmeter:
image.png

image.png

起步就是 3 qps

image.png
可以看到成功的请求逐渐变高

使用场景

预热模式的使用场景一般是秒杀活动

热点参数限流

热点:统计访问数量较大的节点
参数:针对参数
限流:使用的操作

说白了,就是分别统计参数值相同的请求,判断是否超出QPS阈值。

如果不懂,还可以看下下面这张图示

image.png

sentinel对热点参数限流为 QPS = 5,然后总共发了 18 个请求,其中10个请求是非热点接口的,所以可以不看

热点请求有8个
参数 axb 的请求在一秒钟发生了5次,大于等于qps阈值5,所以将被阻塞
参数 abc 的请求在一秒内发生了3次,没有超出阈值,请求通过。

还可以使用 sentinel控制台再次说明:
image.png
如上图所示,我们设置了资源hot
参数索引为 0 表示 hot接口的第一个参数
单机阈值为5,统计窗口时长为1,表示1秒内相同参数额请求数不能超过5个

比如 hot?id=1请求在一秒的时间内发起了10次,那么请求就会被阻塞

此时前面的那一秒时间可能还发起了别的请求,比如hot?id=2发起了3次,此时并不会阻塞,虽然接口相同,但参数不同 热点参数限流针对的颗粒度是 接口+参数,接口相同,参数不同,意味着不同的请求,不会被 热点参数限流规则 统计

在springboot中试试

添加代码:

	@GetMapping("hot")
	public String hot(long id) {
		return "热点参数: " + id;
	}

然后设置 jmeter:
image.png

id=1配置的线程组是image.png

id=2配置的线程组是image.png

同时启动两个线程组,结果发现全部都是成功,没有限流效果?
限流不成功?

其原因很简单,SpringMVC的@Controller注解的资源不被热点流量限流发现
需要主动给他们添加注解@SentinelResource("hot")

注意了。热点参数限流默认对SpringMVC资源无效。

@GetMapping("hot")
@SentinelResource("hot")
public String hot(long id) {
    return "热点参数: " + id;
}

image.png
添加【热点规则】

id=1结果image.png,部分成功

id=2结果image.png,全部成功

说明 id=1id=2同时启动,不会产生任何的相互影响

参数例外项

热点限流,对该接口的热点参数进行限流,但需要有例外情况。
也就是说这个参数需要进行特殊处理,即便它是热点参数,但不能也不需要限流。

比如:某款包款热卖利润极其高的商品,我们就不能进行热点参数限流。
image.png

上图我们重新设置了 id=10的这款热点商品的例外项,将其qps上调到每秒10个请求。

添加jmeter测试:
image.png
http://localhost:8401/hot?id=10

image.png

"实时监控"汇总资源信息(集群聚合)

同时,同一个服务下的所有机器的簇点信息会被汇总,并且秒级地展示在"实时监控"下。
image.png

注意: 实时监控仅存储 5 分钟以内的数据,如果需要持久化,需要通过调用实时监控接口来定制。

注意:请确保 Sentinel 控制台所在的机器时间与自己应用的机器时间保持一致,否则会导致拉不到实时的监控数据。

规则管理及推送

本页面相关文档在 :

sentinel默认使用的是原始模式,这种模式的配置只保存在内存中,在测试环境可以用用,但是在生产环境就不能这么玩了

Sentinel 控制台同时提供简单的规则管理以及推送的功能。规则推送分为 3 种模式,包括 "原始模式"、"Pull 模式" 和"Push 模式"。
这里先简单的介绍"原始模式"。

sentinel默认是原始模式,该模式API 将规则推送至客户端并直接更新到内存中,简单,无任何依赖,但不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境

DataSource 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 DataSource 接口是更加可靠的做法。
我们推荐通过控制台设置规则后将规则推送到统一的规则中心,客户端实现 ReadableDataSource 接口端监听规则中心实时获取变更,流程如下:
image.png
DataSource 扩展常见的实现方式有:

  • 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
  • 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。

Sentinel 目前支持以下数据源扩展:

  • Pull-based: 动态文件数据源、Consul, Eureka
  • Push-based: ZooKeeper, Redis, Nacos, Apollo, etcd

拉模式

pull 模式的数据源(如本地文件、RDBMS 等)一般是可写入的。使用时需要在客户端注册数据源:将对应的读数据源注册至对应的 RuleManager,将写数据源注册至 transport 的 WritableDataSourceRegistry 中。

首先 Sentinel 控制台通过 API 将规则推送至客户端并更新到内存中,接着注册的写数据源会将新的规则保存到本地的文件中。使用 pull 模式的数据源时一般不需要对 Sentinel 控制台进行改造。这种实现方法好处是简单,坏处是无法保证监控数据的一致性。

实现拉模式的数据源最简单的方式是继承 AutoRefreshDataSource 抽象类,然后实现 readSource() 方法,在该方法里从指定数据源读取字符串格式的配置数据。比如 基于文件的数据源

隔离和降级

服务之间是相互依赖的,虽然限流可以保证服务不会因高并发产生异常,但无法保证服务不会因为其他原因而产生故障。所以需要其他手段将这些故障控制台一定的范围内,防止服务级联故障(服务雪崩)。

而这些手段大体上包括这几种:

  • 线程隔离:给每个服务专门提供一个线程池,线程池线程数量不能太高。这样出现问题 ,也仅仅只会有部分线程被阻塞。不会导致服务级联故障。
  • 熔断:判断服务的故障率,根据故障情况,对该服务进行熔断。下次请求再次访问到该服务时,就会被立即返回。
  • 限流:通过对并发访问进行限速,保证其他功能拿到更多的系统资源。

需要注意:降级是限流的一种行为(方案)

限流的方案:

  • 拒绝服务:最简单的方式,把多余的请求直接拒绝掉。做的高大上一些,可以根据一定的用户规则进行拒绝策略。
  • 服务降级:降级甚至关掉后台的某些服务。在服务必定出现高并发的情况下,比如双11。可以预先停止掉部分不重要的服务,并将部分强一致性的服务修改为最终一致性。(需要对业务有一定的了解)【弃卒保帅】
  • 特权请求: 在多租户或者对用户进行分级时,可以考虑让一些特殊的用户有限处理,其他的可以考虑干掉
  • 延迟处理:可以利用队列把请求缓存住。削峰填谷。

从上面提供的方案我们可以发现,都是对服务的调用方进行某种操作以保证服务高可用。服务的调用方也就是客户端在springcloud中一般使用feign进行调用,所以对于SpringCloud整合Sentinel,就需要对feign进行操作