六 分布式流量防护
1 认识分布式流量防护
在分布式系统中,服务之间的相互调用会生成分布式流量。如何通过组件进行流量防护,并有效控制流量,是分布式系统的技术挑战之一。
什么是服务雪崩
假设我有一个微服务系统,这个系统内包含了 ABCD 四个微服务,这四个服务都是以集群模式构建的。
雪崩问题:
微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
解决方案
服务保护技术
在SpringCloud当中支持多种服务保护技术:
- Hystrix
- Resilience4J
- Sentinel
Sentinel 服务容错的思路
Sentinel 是 Spring Cloud Alibaba 的一款服务容错组件,我们也经常把它叫做“防流量哨兵”。它是阿里巴巴双十一促销核心场景的保护神,内置了丰富的服务容错应用场景。它以流量作为切入点,通过各种内外防控手段达到维持服务稳定性的目的。
内部异常治理
在 Sentinel 中,我们可以采用降级和熔断的方式处理内部的异常。所谓降级,是指当服务调用发生了响应超时、服务异常等情况时,我们在服务内部可以执行一段“降级逻辑”。
而所谓熔断,是指当异常调用量达到一定的判定条件,比如在异常降级和慢调用请求的比例达到一个阈值、窗口时间内降级请求达到一定数量的情况下,微服务在一段时间内停止对目标服务发起调用,所有来访请求直接执行降级逻辑。所以,熔断是“多次服务调用异常”累积的结果。
外部流量控制
限流是流量整形流控方案的一种。在 Sentinel 中我们可以根据集群的处理能力,为每个服务设置一个限流规则,从 QPS 维度或者并发线程数的维度控制外部的访问流量。一旦访问量超过阈值,后续的请求就会被 “fast fail”,这是最为常用的一种限流手段。
2 认识Sentinel
Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
Sentinel 的主要特性
特性:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分
- 控制台(Dashboard):控制台主要负责管理推送规则、监控、集群限流分配管理、机器发现等。
- 核心库(Java 客户端):不依赖任何框架/库,能够运行于 Java 7 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
注意:
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
Sentinel谁在用
Sentinel 与 Hystrix、resilience4j 的对比
3 Sentinel控制台环境安装基于Linux
获取 Sentinel 控制台
下载https://github.com/alibaba/Sentinel/releases
上传sentinel-dashboard-1.8.3.jar到虚拟机
启动 Sentinel 控制台
linux模式
java -Dserver.port=8878 \
-Dcsp.sentinel.dashboard.server=114.117.183.67:8878 \
-Dproject.name=sentinel-dashboard \
-jar ./sentinel-dashboard-1.8.3.jar
后台运行:
nohup java -server -Xms64m -Xmx256m \
-Dserver.port=8878 \
-Dcsp.sentinel.dashboard.server=114.117.183.67:8878 \
-Dproject.name=sentinel-dashboard \
-jar ./sentinel-dashboard-1.8.3.jar >> /opt/sentinel.log 2>&1 &
windows模式
java -jar ./sentinel-dashboard-1.8.3.jar --server.port=8878 --csp.sentinel.dashboard.server=localhost:8878 --project.name=sentinel-dashboard
注意:
强烈建议使用JRE8启动Sentinel控制台,高版本JDK可能存在未知BUG。
测试访问
访问http://114.117.183.67:8878/即可,登录用户名密码都是 sentinel。
备注
sentinel控制台服务器部署在云端,首先打算在本地启动微服务连接云上的sentinel
发现仅能注册进服务,却不能显示监控信息和链路信息。
查询日志后发现,云上的sentinel只能从注册中心拿到微服务,但是还是没有真正的连上本地8179端口,连接会报超时
总结:
由于sentinel需要主动连接到你的微服务才能工作,所以sentinel如果想要部署在云端,则必须让云端能够访问到你的微服务(而不是你的微服务能访问到云端,即双边的连接都必须是通的),将微服务放到云上就是保证了你的微服务能被sentinel访问到。
(本文中,我就在windows上本地localhost启动sentinel-dashboard)
4 Sentinel控制台环境安装基于Docker
拉取镜像
docker pull docker.io/bladex/sentinel-dashboard
创建启动容器
docker run --name sentinel --restart=always -d -p 8878:8858 docker.io/bladex/sentinel-dashboard
查看容器
docker ps
5 将应用接入Sentinel
创建工程cloud-sentinel-payment8001
POM引入依赖
<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><!-- Nacos 依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!-- sentinel依赖--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency>
编写主启动类
/*** 主启动类*/
@SpringBootApplication
@Slf4j
@EnableDiscoveryClient
public class PaymentSentinelMain8001 {public static void main(String[] args) {SpringApplication.run(PaymentSentinelMain8001.class,args);log.info("********* PaymentSentinelMain8001 启动成功 ********");}
}
编写YML配置文件
server:port: 8001
spring:application:name: payment-provider-sentinelcloud:nacos:discovery:# Nacos服务地址server-addr: 114.117.183.67:8848sentinel:transport:# Sentinel控制台地址dashboard: localhost:8878# Sentinel api端口号,默认8719,# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口port: 8719logging:pattern:#日志打印格式console: logging.pattern.console=%d{MM/dd HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n
编写测试controller
/*** 流控规则控制层*/
@RestController
public class FlowLimitController {/*** 测试** @return*/@GetMapping("/testA")public String testA() {return "testA";}
}
查看Sentinel控制台
注意:
Sentinel采用懒加载,发送一次请求即可。
发送请求
发送http://localhost:8878
6 流量控制概述
监控应用流量的 QPS 或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流量控制设计理念
流量控制有以下几个角度:
- 资源的调用关系:例如资源的调用链路,资源和资源之间的关系;
- 运行指标:例如 QPS、线程池、系统负载等;
- 控制的效果:例如直接限流、冷启动、排队等。
注意:
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
流控规则
参数:
- 资源名:其实就是我们请求的资源路径
- 针对来源:这个是此流控规则对应那些微服务进行流控管理,一般填写调用方的微服务名称,多个用","分割
- 阈值类型:一般有2中类型,QPS(每秒的最大请求数2)和线程数(并发线程数)
- 单机阈值:单机状态下的最大限制值
- 是否集群:根据实际情况选择
流控模式
参数:
- 直接:直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来;
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 链路:当指定链路上的访问量大于某个阈值时,对当前资源进行限流,这里的“指定链路”是细化到 API 级别的限流维度。
流控效果
参数:
- 快速失败:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowException 。
- Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压。
- 排队等待:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法 。这种方式主要用于处理间隔性突发的流量。
7 流控模式之直接模式
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。
注意:
若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。
配置QPS流控规则
注意:
表示1秒钟内查询1次就ok,若超过1次,就直接快速失败,报默认错误。
测试流量控制QPS
发送请求http://localhost:8001/testA
快速发送请求
8 流控模式之关联模式
什么是关联
当与之关联的资源达到阈值后,就限流自己。如下图:当与/testB关联的资源/testA达到阈值后,就限流/testB
新增测试接口
/*** 流控规则控制层*/
@RestController
public class FlowLimitController {/*** 测试** @return*/@GetMapping("/testA")public String testA() {return "testA";}@GetMapping("/testB")public String testB() {return "testB";}
}
配置关联规则
预期效果
由于对/testB的限流控制采用QPS 关联 ,所以直接访问不会被限流, 会发现一直刷新 /testB 不会出现限流,如图:
但是频繁访问/testA到达限流阈值时,此时访问/testB会出现限流提示:
9 流控模式之链路模式
链路流控模式指的是,当从某个接口过来的资源达到限流条件时, 开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
配置示例:
例如有两条请求链路:
- /test1 --> /common
- /test2 --> /common
如果只希望统计从/test2进入到/common的请求,则可以这样配置:
实战案例
需求:有查询订单和创建订单业务,两者都需要查询商品。针对从查询订单进入到查询商品的请求统计,并设置限流。
实现
添加查询商品方法
在支付服务中(cloud-sentinel-payment8001),添加PaymentService类并添加一个queryGoods方法:
@Service
public class PaymentService {/*** 查询商品* 默认情况下,Service中的方法是不被Sentinel监控的,* 需要我们自己通过注解来标记要监控的方法。** @return*/@SentinelResource("goods")public String queryGoods() {return "查询商品";}}
添加控制层PaymentController
/*** 支付控制层 主要练习链路*/
@RestController
public class PaymentController {@Autowiredprivate PaymentService paymentService;/*** 添加订单** @return*/@GetMapping("/save")public String saveOrder() {//添加订单时,查询商品paymentService.queryGoods();return "新增订单成功";}/*** 查询订单** @return*/@GetMapping("/query")public String queryOrder() {//查询订单时,查询商品paymentService.queryGoods();return "查询订单成功";}}
给查询商品添加资源标记
默认情况下,PaymentService中的方法是不被Sentinel监控的,需要 我们自己通过注解来标记要监控的方法。 给PaymentService的queryGoods方法添加@SentinelResource注解:
@SentinelResource("goods")public String queryGoods() {return "查询商品";}
簇点链路
链路模式中,是对不同来源的两个链路做监控。但是sentinel默认会给进入SpringMVC的所有请求设置同一个root资源,会导致链路模式失效。我们需要关闭这种对SpringMVC的资源聚合,修改 application.yml文件。
server:port: 8001
spring:application:name: payment-provider-sentinelcloud:nacos:discovery:# Nacos服务地址server-addr: 114.117.183.67:8848sentinel:transport:# Sentinel控制台地址dashboard: localhost:8878# Sentinel api端口号,默认8719,# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口port: 8719# 关闭context整合web-context-unify: false
重启服务,访问query和save,可以查看到sentinel的簇点链路规则中,出现了新的资源。
添加流控规则
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息
10 流控效果之冷启动
warm up也叫预热模式,是应对服务冷启动的一种方案。
案例实战
需求:给/payment/warmup这个资源设置限流,最大QPS为10,利用 warm up效果,预热时长为5秒。
@RestController
@RequestMapping("/payment")
public class PaymentController {/*** 测试冷启动** @return*/@GetMapping("/warmup")public String warmup() {return "warmup";}
}
Jmeter测试
查看结果树
注意:
QPS为10.刚刚启动时,大部分请求失败,成功的只有3个,说 明QPS被限定在3,随着时间推移,成功比例越来越高。
11 流控效果之排队等待
排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如:QPS阈值为5,意味着每200ms处理一个队列中的请求,超时时间2s,现在有100个请求过来,服务器最多处理 5 个,其他慢慢排队,timeout = 2S,意味着预期等待时长超过2S的请求会被拒绝并抛出异常。
假如不使用排队等待模式,现在有11个请求过来,第1秒同时接收 到10个请求,但第2秒只有1个请求,此时QPS的曲线这样的。
如果使用队列模式做流控,所有进入的请求都要排队,以固定的200ms的间隔执行,QPS会变的很平滑。
注意:
平滑的QPS曲线,对于服务器来说是更友好的。
应用场景
注意:
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
案例
需求:给/payment/queue这个资源设置限流,最大QPS为10,利用排队的流控效果,超时时长设置为5s。
@RestController
@RequestMapping("/payment")
public class PaymentController {/*** 测试排队等待** @return*/@GetMapping("/queue")public String queue() {return "queue";}
}
Jmeter测试
QPS为15,已经超过了我们设定的10。如果是之前的快速失败、 warmup模式,超出的请求应该会直接报错。但是我们看看队列模式的运行结果:
12 热点参数限流
之前的限流是统计访问某个资源的所有请求,判断是否超过QPS阈值。而热点参数限流是分别统计参数值相同的请求,判断是否超过 QPS阈值。
全局参数限流
例如,一个根据id查询商品的接口:
访问/goods/{id}的请求中,id参数值会有变化,热点参数限流会根据参数值分别统计QPS,统计结果:
当id=1的请求触发阈值被限流时,id值不为1的请求不受影响。
配置示例:
注意:
对hot这个资源的0号参数(第一个参数)做统计,每1秒相同参数值的请求数不能超过5
热点参数限流
刚才的配置中,对查询商品这个接口的所有商品一视同仁,QPS都 限定为1。而在实际开发中,可能部分商品是热点商品,例如秒杀商品,我们希望这部分商品的QPS限制与其它商品不一样,高一些。 那就需要配置热点参数限流的高级选项了:
注意:
结合上一个配置,这里的含义是对0号的long类型参数限流,每 1秒相同参数的QPS不能超过5,有两个例外: 如果参数值是100,则每1秒允许的QPS为10 如果参数值是101,则每1秒允许的QPS为15
案例需求
给/goods/{id}这个资源添加热点参数限流,规则如下:
- 默认的热点参数规则是每1秒请求量不超过2
- 给102这个参数设置例外:每1秒请求量不超过4
- 给103这个参数设置例外:每1秒请求量不超过10
注意事项:
热点参数限流对默认的SpringMVC资源无效,需要利用 @SentinelResource注解标记资源
@RestController
@RequestMapping("/goods")
public class GoodsController {@SentinelResource("getGoods")@GetMapping("/{id}")public String getGoods(@PathVariable String id) {return id + "号商品";}
}
热点参数限流规则
Jmeter测试
13 线程隔离
线程隔离(舱壁模式)
- 线程池隔离
- 信号量隔离(Sentinel默认采用)
注意:
- 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
- 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
sentinel的线程隔离
在添加限流规则时,可以选择两种阈值类型:
注意:
- QPS:每秒的请求数
- 线程数:该资源能使用用的Tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
流控测试
新建线程组
我们新建一个线程组,在启动的时候同时发送10个请求,如下所示:
新建HTTP请求
创建查看结果树
14 熔断降级
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即 拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问 该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 请求成功:则切换到closed状态请求失败:则切换到open状态
熔断降级策略
- 慢调用
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在 指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
- 异常比例、异常数
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
15 熔断降级之慢调用
平均响应时间当1s内持续进入5个请求,对应时刻的平均响应时间 (秒级)均超过阈值,那么在接下的时间窗口之内,对这个方法的调用都会自动地熔断(抛出DegradeException )。
新增接口
@GetMapping("/testC")public String testC(Integer id) {if (id == 1) {try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}}return "------------testC";}
新增RT配置
参数:
超过50ms的请求都会被认为是慢请求,当异常比例达到百分之 40,断路器打开(保险丝跳闸)微服务不可用,保险丝跳闸断电了。过5秒钟由断路器又打开状态变为半开状态放一部分请求进来。
Jemeter压力测试
创建线程组
设置HTTP请求路径
测试
未使用Jemeter情况下测试/testC接口
使用Jemeter情况下测试/testC接口(id==1,达到慢调用阈值触发熔断)。
结果访问失败
注意:
后续我停止Jmeter,没有这么大的访问量了,断路器关闭(保险 丝恢复),微服务恢复OK。
16 熔断降级之异常比例
概述
当资源每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态。异常比率的阈值范围是 [0.0,1.0]。
新增接口
/*** 测试异常比例* RT 平均响应时间** @return*/@GetMapping("testD")public String testD(Integer id) {if (id == 1) {throw new RuntimeException("故意抛出异常,触发异常比例熔断。");}return "testD";}
设置熔断规则
注意:
在5次请求中,只要异常比例超过0.4,也就是有2次以上的异常,就会触发熔断。
Jemeter压力测试
创建线程组
配置HTTP请求(id=1,抛出异常,触发异常比例阈值,开启熔断)
测试接口
发送请求localhost:8001/testD?id=2
17 熔断降级之异常数
概念
异常数:当资源近1分钟的异常数目超过阈值之后会进行熔断。
新增接口
/** 测试异常数*/@GetMapping("/testF")public String testF() {int age = 10 / 0;return "------testF 测试异常数";}
配置异常数规则
注意: 设置异常数5。
测试
请求http://localhost:8001/testF,第一次访问绝对报错,因为除数不能为零, 我们看到error窗口
但是达到5次报错后,进入熔断后降级。
18 授权规则
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
- 白名单:来源(origin)在白名单内的调用者允许访问
- 黑名单:来源(origin)在黑名单内的调用者不允许访问
点击左侧菜单的授权,可以看到授权规则:
- 资源名:就是受保护的资源,例如/payment/{query}
- 流控应用:是来源者的名单,
- 如果是勾选白名单,则名单中的来源被许可访问。
- 如果是勾选黑名单,则名单中的来源被禁止访问。
注意:
我们允许请求从gateway到payment服务,不允许浏览器访问 payment,那么白名单中就要填写网关的来源名称 (origin)。
案列实现
创建网关工程cloud-gateway9090
POM文件引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
编写主启动类
@EnableDiscoveryClient
@SpringBootApplication
@Slf4j
public class Gateway9090 {public static void main(String[] args) {SpringApplication.run(Gateway9090.class, args);log.info("********** Gateway9090 启动成功 *********");}
}
创建配置文件
server:port: 9090
spring:application:name: gatewaycloud:nacos:discovery:# Nacos服务地址server-addr: 114.117.183.67:8848gateway:routes:- id: paymenturi: lb://payment-provider-sentinelpredicates:- Path=/payment/*logging:pattern:#日志打印格式console: logging.pattern.console=%d{MM/dd HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n
测试网关服务
发送请求http://localhost:9090/payment/query
如何获取origin
Sentinel是通过RequestOriginParser这个接口的parseOrigin来获取请求的来源的。
public interface RequestOriginParser {/*** 从请求request对象中获取origin,获取方式自定义*/String parseOrigin(HttpServletRequest request);
}
注意:
这个方法的作用就是从request对象中,获取请求者的origin值并返回。默认情况下,sentinel不管请求者从哪里来,返回值永远是default,也就是说一切请求的来源都被认为是一样的值 default。因此,我们需要自定义这个接口的实现,让不同的请求,返回不同的origin。
支付微服务(cloud-sentinel-payment8001)定义一个RequestOriginParser的实现类
@Component
public class HeaderOriginParser implements RequestOriginParser {@Overridepublic StringparseOrigin(HttpServletRequest request) {// 1.获取请求头String origin = request.getHeader("origin");// 2.非空判断if (StringUtils.isEmpty(origin)) {origin = "blank";}return origin;}
}
给网关添加请求头
既然获取请求origin的方式是从reques-header中获取origin值,我们必须让所有从gateway路由到微服务的请求都带上origin头。
spring:cloud:gateway:default-filters:- AddRequestHeader=origin,gateway
注意:
这样,从gateway路由的所有请求都会带上origin头,值为 gateway。而从其它地方到达微服务的请求则没有这个头。
配置授权规则
接下来,我们添加一个授权规则,放行origin值为gateway的请求。
配置规则
测试网关请求
请求localhost:9090/payment/query
测试非网关请求
请求localhost:8001/payment/query
19 系统自适应限流
引入系统自适应限流的主要的目的
- 保证系统不被拖垮
- 在系统稳定的前提下保证系统的吞吐量。目前我们接触的限流的防护思路都是设定一个指标(阈值),例如系统的负载 load 超过某个阔值后就阻止或减少流量的继续进入,当系统负载降低到某一水平后则恢复流量的进入。通常都是被动的,其实际效果取决于阈值设置是否合理,但往往设置合理不是一件容易的事情。
系统规则
Sentinel的系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量( EntryType.IN ),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式
- Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5 。
- CPU usage:当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
- 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
- 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
- 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。
配置系统规则
添加规则
20 SentinelResource注解配置详解之只配置fallback
服务降级功能,但是只是限制后,返回不可控的结果肯定是不行的,我们还要保证调用者在调用那些被限制的服务时候,不管是不是被限制,都要让他们拿到一个合理的结果,而不是扔回去一个异常就完事了。
Sentinel提供了这样的功能,让我们可以另外定义一个方法来代替被限制或异常服务返回数据,这就是fallback和blockHandler。
- fallback:若本接口出现未知异常,则调用fallback指定的接口。
- blockHandler:若本次访问被限流或服务降级,则调用blockHandler指定的接口。
@SentinelResource注解用于定义资源,并提供可选的 BlockException 异常处理(仅处理Sentinel控制台配置相关异常)和 fallback 配置项(运行时异常以及自定义异常)。
注解属性
- value :资源名称,必需项(不能为空)
- entryType :entry 类型,可选项(默认为 EntryType.OUT )
- blockHandler / blockHandlerClass : blockHandler 对应处理 BlockException 的函数名称,可选项。blockHandler 函数访问范围需要是 public ,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException 。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
- fallback / fallbackClass :fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面 排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有fallback会生效。
- defaultFallback (since 1.6.0):默认的 fallback 函数名称
- exceptionsToIgnore (since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出
实例编写
在服务生产者(cloud-provider-payment8001)新增接口
@RestController
@RequestMapping("/payment")
public class PaymentController {@GetMapping("/findById")public String findById(String id) {return "payment findById " + id;}}
把基于open-fegin的服务消费者(cloud-consumer-openfeign-order80)接入sentinel控制台
pom中加入:
<!-- 引入降级依赖 --><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId></dependency>
配置文件中加入:
spring:cloud:sentinel:transport:# Sentinel控制台地址dashboard: localhost:8878# Sentinel api端口号,默认8719,port: 8719
编写消费者远程调用service
@Service
@FeignClient(value = "provider-payment")
public interface PaymentService {@GetMapping("/payment/findById")String findById(@RequestParam("id") String id);
}
编写消费者controller,配置fallback
此回调是针对接口出现异常了,就进入此fallback方法。
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate PaymentService paymentService;@SentinelResource(value = "testfallback",fallback = "findByIdFallBalk",fallbackClass = PaymentServiceFallback.class)@GetMapping("/findById")public String findById(String id) {if (id.equals("1")) {throw new RuntimeException("出异常了。");}return paymentService.findById(id);}
}
代码看出,只要请求http://localhost:80/order/findById?id=1 ,接口就会报异常,继而会调用fallback中的方法,走到了testFallback 方法中。
外置类
fallback 函数位置是有要求的,必须和原方法在同一个类中,但在实际需求中,我们需要放在其他类中。@SentinelResource提供了通过fallbackClass指定对应的类的Class对象,添加一个static,否则无法解析。
public class PaymentServiceFallback {/*** 降级方法** @return*/public static String findByIdFallBalk(String id, Throwable e) {return "支付系统服务繁忙稍等一会~~~~";}}
21 SentinelResource配置详解之只配置 blockHandler
超出流量限制的部分是否会进入到blockHandler的方法,要注意是超出流量限制的请求调用,会进入blockHandler方法。
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate PaymentService paymentService;@GetMapping("/index")public String index() {return paymentService.index();}@SentinelResource(value = "testblockHandler",blockHandler = "testBlockHandler",blockHandlerClass = PaymentServiceBlockHandler.class)@GetMapping("/findById")public String findById(String id) {if (id.equals("1")) {throw new RuntimeException("出异常了。");}return paymentService.findById(id);}
}
public class PaymentServiceBlockHandler {public static String testBlockHandler(String id, BlockException e) {return "限流降级处理";}
}
22 SentinelResource配置详解之fallback和 blockHandler都配置
@RestController
@RequestMapping("/order")
public class OrderController {@Autowiredprivate PaymentService paymentService;@GetMapping("/index")public String index() {return paymentService.index();}@SentinelResource(value = "testfallback",fallback = "findByIdFallBalk",fallbackClass = PaymentServiceFallback.class,blockHandler = "testBlockHandler",blockHandlerClass = PaymentServiceBlockHandler.class)@GetMapping("/findById")public String findById(String id) {if (id.equals("1")) {throw new RuntimeException("出异常了。");}return paymentService.findById(id);}
}
总结
fallback是针对方法出现异常了,则会进入fallback方法。blockhandler是针对流控设置,超出规则,则会进入blockhandler方法。
若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出BlockException时只会进入 blockHandler处理逻辑。
若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出。
23 实时监控数据
Sentinel 提供对所有资源的实时监控。如果需要实时监控,客户端需引入以下依赖(以 Maven 为例):
<!-- 在netty的基础上实现,通过http协议传输数据 --><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-transport-netty-http</artifactId><version>1.8.3</version></dependency>
注意: 引入上述依赖后,客户端便会主动连接 Sentinel 控制台。通过 Sentinel 控制台即可查看客户端的实时监控。
设定启动参数
只使用基本的流控功能的话,是不用额外指定启动参数的。但如果 要接入Dashboard进行实时监控,则需要在应用启动时加入JVM参 数指定控制台地址和端口。
内容如下
-Dcsp.sentinel.dashboard.server=192.168.66.101:8878
其他相关参数有:
界面展示
应用的监控指标数据已经保存到Dashboard了,这个时候,我们在控制台界面来查看监控数据:
24 Sentinel为什么需要持久化
为什么需要持久化
Sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在 生产环境下,我们必须确保这些规则的持久化,避免丢失。
项目启动成功日志打印:
Sentinel支持三种规则管理模式
原始模式
Sentinel的默认模式,将规则保存在内存,重启服务会丢失。
pull模式
控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
push模式
控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
25 Sentinel组件二次开发
在开始二次开发之前,我们需要将Sentinel 的代码下载到本地。你 可以从GitHub 的Releases页面中找到1.8.3版本,在该版本下的 Assets面板中下载 Source code源文件。下载地址https://github.com/alibaba/Sentinel/releases
改造过程三个步骤
- 修改 Nacos 依赖项的应用范围,将其打入jar 包中;
- 后端程序对接 Nacos,将 Sentinel 限流规则同步到 Nacos;
- 开放单独的前端限流规则配置页面。
修改Nacos依赖项
首先,你需要打开 sentinel-dashboard 项⽬的 pom.xml 文件,找 到其中的依赖项 sentinel-datasource-nacos,它是连接 Nacos Config 所依赖的必要组件。
<dependency> <groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasourcenacos</artifactId>
<!-- 将scope注释掉 改为编译器打包-->
<!-- <scope>test</scope>-->
</dependency>
后端程序对接Nacos
首先,你需要打开 sentinel-dashboard 项目下的 src/test/java目 录。定位到 com.alibaba.csp.sentinel.dashboard.rule.nacos目录 下。
解释:
NacosConfig:初始化 Nacos Config 的连接;
NacosConfigUtil:约定了 Nacos 配置⽂件所属的 Group 和⽂件命名后缀等常量字段; FlowRuleNacosProvider:从 Nacos Config 上获取限流规则;
FlowRuleNacosPublisher:将限流规则发布到 Nacos Config。
为了让这些类在 Sentinel 运行期可以发挥作用,你需要在 src/main/java 下创建同样的包路径,然后将这四个文件从 test 路径拷贝到 main 路径下。
实现Nacos数据同步
在NacosConfig中
@Beanpublic ConfigService nacosConfigService() throws Exception {Properties properties = new Properties();// Nacos服务地址properties.setProperty("serverAddr", "localhost:8848");// NameSpace idproperties.setProperty("namespace", "0a726f6a-0290-48ff-9e49-334ba2e51551");return ConfigFactory.createConfigService(properties);}
FlowControllerV2接入Nacos
解释:
在代码中,我通过Qualifer标签将FlowRuleNacosProvider注入 到了ruleProvier变量中,又采用同样的方式将 FlowRuleNacosPublisher注入到了rulePublisher变量中。 FlowRuleNacosProvider和 FlowRuleNacosPublisher就是上一 步我们刚从 test目录 Copy到main目录下的两个类。
查看FlowRuleNacosPublisher源码
@Overridepublic void publish(String app, List<FlowRuleEntity> rules) throws Exception {AssertUtil.notEmpty(app, "app name cannot be empty");if (rules == null) {return;}//将规则发布到Nacos配置中心configService.publishConfig(app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,NacosConfigUtil.GROUP_ID, converter.convert(rules));}
NacosConfigUtil.FLOW_DATA_ID_POSTFIX
public final class NacosConfigUtil {// 这个是Sentinel 注册的配置项所在的分组public static final String GROUP_ID = "SENTINEL_GROUP";// 流量整形规则的后缀public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
查看FlowRuleNacosProvider源码
@Overridepublic List<FlowRuleEntity> getRules(String appName) throws Exception {// 从配置中心获取规则// app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX +NacosConfigUtil.GROUP_IDString rules = configService.getConfig(appName + NacosConfigUtil.FLOW_DATA_ID_POSTFIX,NacosConfigUtil.GROUP_ID, 3000);if (StringUtil.isEmpty(rules)) {return new ArrayList<>();}return converter.convert(rules);}
解析:
我们就完成了对后端程序的改造,将 Sentinel 限流规则同步到 了 Nacos。接下来我们需要对前端页面稍加修改,开放⼀个独立的页面,用来维护那些被同步到 Nacos 上的限流规则。
前端页面改造
首先,我们打开 sentinel-dashboard 模块下的 webapp 目录,该目录存放了 Sentinel 控制台的前端页面资源。我们需要改造的文件是 sidebar.html,这个 html文件定义了控制台的左侧导航栏。
新增内容
<li ui-sref-active="active"><a ui-sref="dashboard.flow({app:entry.app})"><i class="glyphicon glyphiconfilter"></i> 流控规则lxx</a>
</li>
微服务改造
微服务端的改造非常常简单,我们不需要对代码做任何改动,只需 要添加⼀个新的依赖项,并在配置文件中添加 sentinel datasource 连接信息就可以了。
<!-- 引入nacos数据源 --><dependency><groupId>com.alibaba.csp</groupId><artifactId>sentinel-datasource-nacos</artifactId></dependency>
我们在 application.yml 配置文件中找到 spring.cloud.sentinel 节点,在这个节点下添加⼀段Nacos 数据源的配置。
spring:cloud:sentinel:datasource:# 数据源key 可以自由命名geekbang-flow:# 指定当前数据源是Nacosnacos:# 设置Nacos的连接地址,命名空间和GroupIDserver-addr: localhost:8848# Nacos 命名空间namespace: 0a726f6a-0290-48ff-9e49-334ba2e51551group-id: SENTINEL_GROUP# 设置Nacos中配置⽂件的命名规则data-id: ${spring.application.name}-flow-rules# 必填的重要字段,指定当前规则类型是"限流"rule-type: flow
完整:
server:port: 8001
spring:application:name: payment-provider-sentinelcloud:nacos:discovery:# Nacos服务地址server-addr: localhost:8848sentinel:transport:# Sentinel控制台地址dashboard: localhost:8080# Sentinel api端口号,默认8719,# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口port: 8719# 关闭context整合web-context-unify: falsedatasource:# 数据源key 可以自由命名geekbang-flow:# 指定当前数据源是Nacosnacos:# 设置Nacos的连接地址,命名空间和GroupIDserver-addr: localhost:8848# Nacos 命名空间namespace: 0a726f6a-0290-48ff-9e49-334ba2e51551group-id: SENTINEL_GROUP# 设置Nacos中配置⽂件的命名规则data-id: ${spring.application.name}-flow-rules# 必填的重要字段,指定当前规则类型是"限流"rule-type: flowlogging:pattern:#日志打印格式console: logging.pattern.console=%d{MM/dd HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n
重点强调
- 我们在微服务端的 sentinal 数据源中配置的 namespace 和 groupID,⼀定要和 Sentinal Dashoboard 二次改造中的中的配置相同,否则将无法正常同步限流规则。
- dataId 的文件命名规则,需要和 Sentinel 二次改造中的FlowRuleNacosPublisher 类保持⼀致。
验证限流规则同步效果
启动Sentinel-dashboard子项目,找主启动类启动Sentinel控制台。
请求http://localhost:8080,你可以登录 Sentinel Dashboard 服 务。这时你会看到左侧的导航栏多了⼀个“流控规则lxx”的选项。 你可以点击这个选项,并手动在当前页面右上方点击“新增流控规则”,为 requestCoupon 添加⼀条“QPS=1 快速失败”的流控规则。
最后,打开 Nacos Config 的配置列表页,你就可以看到⼀个 cloud-sentinel-payment- flow-rules 的配置文件被创建了出来。