在分布式系统中,服务之间的相互调用会生成分布式流量。如何通过组件进行流量防护,并有效控制流量,是分布式系统的技术挑战之一。
什么是服务雪崩
假设我有一个微服务系统,这个系统内包含了 ABCD 四个微服务,这四个服务都是以集群模式构建的。
雪崩问题:
微服务之间相互调用,因为调用链中的一个服务故障,引起整个链路都无法访问的情况。
解决方案
服务保护技术
在SpringCloud当中支持多种服务保护技术:
Sentinel 服务容错的思路
Sentinel 是 Spring Cloud Alibaba 的一款服务容错组件,我们也经常把它叫做“防流量哨兵”。它是阿里巴巴双十一促销核心场景的保护神,内置了丰富的服务容错应用场景。它以流量作为切入点,通过各种内外防控手段达到维持服务稳定性的目的。
内部异常治理
在 Sentinel 中,我们可以采用降级和熔断的方式处理内部的异常。所谓降级,是指当服务调用发生了响应超时、服务异常等情况时,我们在服务内部可以执行一段“降级逻辑”。
而所谓熔断,是指当异常调用量达到一定的判定条件,比如在异常降级和慢调用请求的比例达到一个阈值、窗口时间内降级请求达到一定数量的情况下,微服务在一段时间内停止对目标服务发起调用,所有来访请求直接执行降级逻辑。所以,熔断是“多次服务调用异常”累积的结果。
外部流量控制
限流是流量整形流控方案的一种。在 Sentinel 中我们可以根据集群的处理能力,为每个服务设置一个限流规则,从 QPS 维度或者并发线程数的维度控制外部的访问流量。一旦访问量超过阈值,后续的请求就会被 “fast fail”,这是最为常用的一种限流手段。
Sentinel是阿里开源的项目,提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。
Sentinel 的主要特性
特性:
- 丰富的应用场景:Sentinel 承接了阿里巴巴近10年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
- 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况
- 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入Sentinel
- 完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
Sentinel 分为两个部分
注意:
Sentinel 可以简单的分为 Sentinel 核心库和 Dashboard。核心库不依赖 Dashboard,但是结合 Dashboard 可以取得最好的效果。
Sentinel谁在用
Sentinel 与 Hystrix、resilience4j 的对比
获取 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)
拉取镜像
docker pull docker.io/bladex/sentinel-dashboard
创建启动容器
docker run --name sentinel --restart=always -d -p 8878:8858 docker.io/bladex/sentinel-dashboard
查看容器
docker ps
创建工程cloud-sentinel-payment8001
POM引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
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-sentinel
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: 114.117.183.67:8848
sentinel:
transport:
# Sentinel控制台地址
dashboard: localhost:8878
# Sentinel api端口号,默认8719,
# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
logging:
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
监控应用流量的 QPS 或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
流量控制设计理念
流量控制有以下几个角度:
注意:
Sentinel 的设计理念是让您自由选择控制的角度,并进行灵活组合,从而达到想要的效果。
流控规则
参数:
- 资源名:其实就是我们请求的资源路径
- 针对来源:这个是此流控规则对应那些微服务进行流控管理,一般填写调用方的微服务名称,多个用","分割
- 阈值类型:一般有2中类型,QPS(每秒的最大请求数2)和线程数(并发线程数)
- 单机阈值:单机状态下的最大限制值
- 是否集群:根据实际情况选择
流控模式
参数:
- 直接:直接作用于当前资源,如果访问压力大于某个阈值,后续请求将被直接拦下来;
- 关联:统计与当前资源相关的另一个资源,触发阈值时,对当前资源限流
- 链路:当指定链路上的访问量大于某个阈值时,对当前资源进行限流,这里的“指定链路”是细化到 API 级别的限流维度。
流控效果
参数:
- 快速失败:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出 FlowException 。
- Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压。
- 排队等待:匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法 。这种方式主要用于处理间隔性突发的流量。
当 QPS 超过某个阈值的时候,则采取措施进行流量控制。
注意:
若使用除了直接拒绝之外的流量控制效果,则调用关系限流策略(strategy)会被忽略。
配置QPS流控规则
注意:
表示1秒钟内查询1次就ok,若超过1次,就直接快速失败,报默认错误。
测试流量控制QPS
发送请求http://localhost:8001/testA
快速发送请求
什么是关联
当与之关联的资源达到阈值后,就限流自己。如下图:当与/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会出现限流提示:
链路流控模式指的是,当从某个接口过来的资源达到限流条件时, 开启限流。它的功能有点类似于针对来源配置项,区别在于:针对来源是针对上级微服务,而链路流控是针对上级接口,也就是说它的粒度更细。
配置示例:
例如有两条请求链路:
如果只希望统计从/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 {
@Autowired
private 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-sentinel
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: 114.117.183.67:8848
sentinel:
transport:
# Sentinel控制台地址
dashboard: localhost:8878
# Sentinel api端口号,默认8719,
# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
# 关闭context整合
web-context-unify: false
重启服务,访问query和save,可以查看到sentinel的簇点链路规则中,出现了新的资源。
添加流控规则
点击goods资源后面的流控按钮,在弹出的表单中填写下面信息
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,随着时间推移,成功比例越来越高。
排队等待则是让所有请求进入一个队列中,然后按照阈值允许的时间间隔依次执行。后来的请求必须等待前面执行完成,如果请求预期的等待时间超出最大时长,则会被拒绝。
工作原理
例如: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模式,超出的请求应该会直接报错。但是我们看看队列模式的运行结果:
之前的限流是统计访问某个资源的所有请求,判断是否超过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}这个资源添加热点参数限流,规则如下:
注意事项:
热点参数限流对默认的SpringMVC资源无效,需要利用 @SentinelResource注解标记资源
@RestController
@RequestMapping("/goods")
public class GoodsController {
@SentinelResource("getGoods")
@GetMapping("/{id}")
public String getGoods(@PathVariable String id) {
return id + "号商品";
}
}
热点参数限流规则
Jmeter测试
线程隔离(舱壁模式)
注意:
- 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
- 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求。
sentinel的线程隔离
在添加限流规则时,可以选择两种阈值类型:
注意:
- QPS:每秒的请求数
- 线程数:该资源能使用用的Tomcat线程数的最大值。也就是通过限制线程数量,实现线程隔离(舱壁模式)。
流控测试
新建线程组
我们新建一个线程组,在启动的时候同时发送10个请求,如下所示:
新建HTTP请求
创建查看结果树
熔断降级是解决雪崩问题的重要手段。其思路是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即 拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问 该服务的请求。
断路器控制熔断和放行是通过状态机来完成的:
状态机包括三个状态:
- closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例。超过阈值则切换到open状态
- open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态
- half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。 请求成功:则切换到closed状态请求失败:则切换到open状态
熔断降级策略
业务的响应时长(RT)大于指定时长的请求认定为慢调用请求。在 指定时间内,如果请求数量超过设定的最小数量,慢调用比例大于设定的阈值,则触发熔断。
统计指定时间内的调用,如果调用次数超过指定请求数,并且出现异常的比例达到设定的比例阈值(或超过指定异常数),则触发熔断。
平均响应时间当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。
概述
当资源每秒异常总数占通过量的比值超过阈值之后,资源进入降级状态。异常比率的阈值范围是 [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
概念
异常数:当资源近1分钟的异常数目超过阈值之后会进行熔断。
新增接口
/*
* 测试异常数
*/
@GetMapping("/testF")
public String testF() {
int age = 10 / 0;
return "------testF 测试异常数";
}
配置异常数规则
注意: 设置异常数5。
测试
请求http://localhost:8001/testF,第一次访问绝对报错,因为除数不能为零, 我们看到error窗口
但是达到5次报错后,进入熔断后降级。
授权规则
授权规则可以对调用方的来源做控制,有白名单和黑名单两种方式。
点击左侧菜单的授权,可以看到授权规则:
注意:
我们允许请求从gateway到payment服务,不允许浏览器访问 payment,那么白名单中就要填写网关的来源名称 (origin)。
案列实现
创建网关工程cloud-gateway9090
POM文件引入依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-loadbalancerartifactId>
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: gateway
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: 114.117.183.67:8848
gateway:
routes:
- id: payment
uri: lb://payment-provider-sentinel
predicates:
- 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 {
@Override
public String
parseOrigin(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
引入系统自适应限流的主要的目的
系统规则
Sentinel的系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
系统保护规则是应用整体维度的,而不是资源维度的,并且仅对入口流量生效。入口流量指的是进入应用的流量( EntryType.IN ),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。
系统规则支持以下的模式
配置系统规则
添加规则
服务降级功能,但是只是限制后,返回不可控的结果肯定是不行的,我们还要保证调用者在调用那些被限制的服务时候,不管是不是被限制,都要让他们拿到一个合理的结果,而不是扔回去一个异常就完事了。
Sentinel提供了这样的功能,让我们可以另外定义一个方法来代替被限制或异常服务返回数据,这就是fallback和blockHandler。
@SentinelResource注解用于定义资源,并提供可选的 BlockException 异常处理(仅处理Sentinel控制台配置相关异常)和 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.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
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 {
@Autowired
private 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 "支付系统服务繁忙稍等一会~~~~";
}
}
超出流量限制的部分是否会进入到blockHandler的方法,要注意是超出流量限制的请求调用,会进入blockHandler方法。
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private 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 "限流降级处理";
}
}
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private 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 直接抛出。
Sentinel 提供对所有资源的实时监控。如果需要实时监控,客户端需引入以下依赖(以 Maven 为例):
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-transport-netty-httpartifactId>
<version>1.8.3version>
dependency>
注意: 引入上述依赖后,客户端便会主动连接 Sentinel 控制台。通过 Sentinel 控制台即可查看客户端的实时监控。
设定启动参数
只使用基本的流控功能的话,是不用额外指定启动参数的。但如果 要接入Dashboard进行实时监控,则需要在应用启动时加入JVM参 数指定控制台地址和端口。
内容如下
-Dcsp.sentinel.dashboard.server=192.168.66.101:8878
其他相关参数有:
界面展示
应用的监控指标数据已经保存到Dashboard了,这个时候,我们在控制台界面来查看监控数据:
为什么需要持久化
Sentinel的所有规则都是内存存储,重启后所有规则都会丢失。在 生产环境下,我们必须确保这些规则的持久化,避免丢失。
项目启动成功日志打印:
Sentinel支持三种规则管理模式
原始模式
Sentinel的默认模式,将规则保存在内存,重启服务会丢失。
pull模式
控制台将配置的规则推送到Sentinel客户端,而客户端会将配置规则保存在本地文件或数据库中。以后会定时去本地文件或数据库中查询,更新本地规则。
push模式
控制台将配置规则推送到远程配置中心,例如Nacos。Sentinel客户端监听Nacos,获取配置变更的推送消息,完成本地配置更新。
在开始二次开发之前,我们需要将Sentinel 的代码下载到本地。你 可以从GitHub 的Releases页面中找到1.8.3版本,在该版本下的 Assets面板中下载 Source code源文件。下载地址https://github.com/alibaba/Sentinel/releases
改造过程三个步骤
修改Nacos依赖项
首先,你需要打开 sentinel-dashboard 项⽬的 pom.xml 文件,找 到其中的依赖项 sentinel-datasource-nacos,它是连接 Nacos Config 所依赖的必要组件。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasourcenacosartifactId>
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中
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
// Nacos服务地址
properties.setProperty("serverAddr", "localhost:8848");
// NameSpace id
properties.setProperty("namespace", "0a726f6a-0290-48ff-9e49-334ba2e51551");
return ConfigFactory.createConfigService(properties);
}
FlowControllerV2接入Nacos
解释:
在代码中,我通过Qualifer标签将FlowRuleNacosProvider注入 到了ruleProvier变量中,又采用同样的方式将 FlowRuleNacosPublisher注入到了rulePublisher变量中。 FlowRuleNacosProvider和 FlowRuleNacosPublisher就是上一 步我们刚从 test目录 Copy到main目录下的两个类。
查看FlowRuleNacosPublisher源码
@Override
public 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源码
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
// 从配置中心获取规则
// app + NacosConfigUtil.FLOW_DATA_ID_POSTFIX +NacosConfigUtil.GROUP_ID
String 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> 流控规则lxxa>
li>
微服务改造
微服务端的改造非常常简单,我们不需要对代码做任何改动,只需 要添加⼀个新的依赖项,并在配置文件中添加 sentinel datasource 连接信息就可以了。
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
我们在 application.yml 配置文件中找到 spring.cloud.sentinel 节点,在这个节点下添加⼀段Nacos 数据源的配置。
spring:
cloud:
sentinel:
datasource:
# 数据源key 可以自由命名
geekbang-flow:
# 指定当前数据源是Nacos
nacos:
# 设置Nacos的连接地址,命名空间和GroupID
server-addr: localhost:8848
# Nacos 命名空间
namespace: 0a726f6a-0290-48ff-9e49-334ba2e51551
group-id: SENTINEL_GROUP
# 设置Nacos中配置⽂件的命名规则
data-id: ${spring.application.name}-flow-rules
# 必填的重要字段,指定当前规则类型是"限流"
rule-type: flow
完整:
server:
port: 8001
spring:
application:
name: payment-provider-sentinel
cloud:
nacos:
discovery:
# Nacos服务地址
server-addr: localhost:8848
sentinel:
transport:
# Sentinel控制台地址
dashboard: localhost:8080
# Sentinel api端口号,默认8719,
# 假如被占用了会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
# 关闭context整合
web-context-unify: false
datasource:
# 数据源key 可以自由命名
geekbang-flow:
# 指定当前数据源是Nacos
nacos:
# 设置Nacos的连接地址,命名空间和GroupID
server-addr: localhost:8848
# Nacos 命名空间
namespace: 0a726f6a-0290-48ff-9e49-334ba2e51551
group-id: SENTINEL_GROUP
# 设置Nacos中配置⽂件的命名规则
data-id: ${spring.application.name}-flow-rules
# 必填的重要字段,指定当前规则类型是"限流"
rule-type: flow
logging:
pattern:
#日志打印格式
console: logging.pattern.console=%d{MM/dd HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n
重点强调
验证限流规则同步效果
启动Sentinel-dashboard子项目,找主启动类启动Sentinel控制台。
请求http://localhost:8080,你可以登录 Sentinel Dashboard 服 务。这时你会看到左侧的导航栏多了⼀个“流控规则lxx”的选项。 你可以点击这个选项,并手动在当前页面右上方点击“新增流控规则”,为 requestCoupon 添加⼀条“QPS=1 快速失败”的流控规则。
最后,打开 Nacos Config 的配置列表页,你就可以看到⼀个 cloud-sentinel-payment- flow-rules 的配置文件被创建了出来。