本章主要内容
本章我们主要讲述如何使用Sentinel的熔断降级能力,包括熔断策略的设置、熔断后处理逻辑、以及在Sentinel控制台中观测熔断效果。
什么是熔断降级
首先我们需要认识到微服务架构的一大特点就是服务被拆分地非常细,所以一个可供前端使用的、完整的接口,调用经常会需要涉及到多个微服务,比如说一个获取订单详情的接口,开放给前端的就是单单一个接口,但是这个接口里面可能调用了订单数据、然后同时还调用了商品的接口、用户的接口,最后把数据聚合后返回给前端。
假设其中有某个接口突然坏掉了,比如说因为各种原因(比如缓存失效,数据库崩溃等)导致响应时间过长,那么所有需要调用到此接口的其他服务将全部“卡死”在这儿了,随之而来的就是整个系统逐渐把所有网络连接挂起,服务器连接数暴增,最后系统崩溃了,通常这种情况叫做雪崩。
而熔断降级就是解决此类问题的,它可以以响应时间、异常比例或次数等多个纬度来定制熔断规则,即达到设定的条件后,马上把该服务熔断,让其他调用者马上知道这个服务暂时不可用,并执行自己的应对措施,从而保证系统的整体稳定可用。因为这样子的整体稳定是一种“降级”的稳定,所以我们一般把它称为熔断降级。
在SpringCloud中使用Sentinel的熔断功能
我们在之前章节中已经熟悉了SpringCloud GateWay中使用Sentinel流控功能,由于目前的版本Sentinel并没有为网关去实现熔断功能,所以在本章中我主要是在每个微服务中去使用熔断。
首先,在工程中添加 由 SpringCloud 集成的 Sentinel
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
完整pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zjf.csdn</groupId>
<artifactId>combat-provider</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>zjf微服务实战</description>
<name>combat-provider</name>
<url>https://blog.csdn.net/u011177064</url>
<properties>
<!-- Dependency Versions -->
<spring.boot.version>2.1.0.RELEASE</spring.boot.version>
<spring.cloud.version>Greenwich.SR2</spring.cloud.version>
<spring.cloud.alibaba.version>2.1.0.RELEASE</spring.cloud.alibaba.version>
</properties>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</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>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<id>spring</id>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/release</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/libs-snapshot-local</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-releases</id>
<name>Spring Releases</name>
<url>https://repo.spring.io/libs-release-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>
</project>
编写 熔断功能 的接口,使用 @SentinelResource 注解,代替 Sentinel 编程式风格,更为简洁。
关于注解的详细使用可参考 https://github.com/alibaba/Sentinel/wiki/注解支持
SentinelApi.java
package com.zjf.combat.api.sentinel;
import java.util.ArrayList;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.RuleConstant;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeRuleManager;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRuleManager;
@RestController
@RequestMapping(value = "/sentinel")
public class SentinelApi {
/**
* 在构造方法里初始化了 限流、熔断 规则。(我们暂主要只测试熔断)
* 写在这里只是为了方便测试,实际开发中,规则的初始化可以放在统一的地方,而不是写在每个控制器里
*/
public SentinelApi() {
//initFlowRules();
initDegradeRules() ;
}
/**
* 熔断测试接口
* @SentinelResource 中的 value 即为熔断/限流规则中设置的 Resource 名
blockHandler与blockHandlerClass 结合起来是定义
熔断后处理逻辑由ExceptionUtil.handleException(前面可加自己的参数,BlockException e)控制
* @return
*/
@GetMapping(value = "/test")
@SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
public String test() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "sentinel pass";
}
/**
* 初始化限流规则
*/
private static void initFlowRules() {
/*
* resource:资源名,即限流规则的作用对象
count: 限流阈值
grade: 限流阈值类型(QPS 或并发线程数)
limitApp: 流控针对的调用来源,若为 default 则不区分调用来源
strategy: 调用关系限流策略
controlBehavior: 流量控制效果(直接拒绝、Warm Up、匀速排队)
*/
List<FlowRule> rules = new ArrayList<>();
FlowRule rule = new FlowRule();
rule.setResource("test");
rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
rule.setCount(10);
rules.add(rule);
FlowRuleManager.loadRules(rules);
}
/**
* 初始化熔断规则
*/
private static void initDegradeRules() {
List<DegradeRule> rules = new ArrayList<DegradeRule>();
DegradeRule rule = new DegradeRule();
rule.setResource("test");
/*
* 【策略1】
* 平均响应时间 (DEGRADE_GRADE_RT) 触发: 1s 内持续进入超过 5 个请求
* 平均响应时间超过 count (10ms) 则熔断 timewindow(10s)
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setCount(10);
rule.setTimeWindow(10);
【策略2】
异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO) 触发:每秒请求量 >= 5
每秒异常总数占通过量的比值超过阈值count([0.0, 1.0],代表 0% - 100%) 之后 则熔断 timewindow(10s)
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO);
rule.setCount(0.1);
rule.setTimeWindow(10);
【策略3】
异常数 (DEGRADE_GRADE_EXCEPTION_COUNT)
当资源近 1 分钟的异常数超过阈值 (count) 之后会进行熔断
注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。
rule.setGrade(RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT);
rule.setCount(5);
rule.setTimeWindow(90);
*/
rule.setGrade(RuleConstant.DEGRADE_GRADE_RT);
rule.setCount(10);
rule.setTimeWindow(10);
rules.add(rule);
DegradeRuleManager.loadRules(rules);
}
}
ExceptionUtil.java
package com.zjf.combat.api.sentinel;
import com.alibaba.csp.sentinel.slots.block.BlockException;
public class ExceptionUtil {
public static String handleException(BlockException e) {
return "sentinel block "+e;
}
}
我们接下来要测试这样一个场景:
如上面代码,熔断的策略选用 响应超时熔断,平均响应时间超过10毫秒则熔断10秒。
而我们在 “test" 这个接口中休眠了500毫秒,所以预期效果应该是开始调用后,系统就会发生熔断,10秒内所有请求均失败,然后短暂恢复(有请求开始被通过),马上又进入熔断。
熔断效果测试
启动Sentinel控制台
首先我们需要启动 Sentinel 控制台,可以参考 接入Sentinel 控制台可视化观测流控效果
然后我们当前的工程启动时需加上 JVM参数指向 Sentinel控制台的IP和端口
JVM 参数 :-Dcsp.sentinel.dashboard.server=127.0.0.1:9001
对接口发起并发请求
我们的测试接口完整路径为:http://localhost:9996/sentinel/test
以下代码实现QPS为20的并发请求
package com.zjf.combat.api.sentinel;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;
public class TestSentinel {
public static void main(String[] args) throws InterruptedException {
while(true) {
for (int i = 0; i < 20; i++) {
final int i1=i;
new Thread(() ->{
System.out.println((System.currentTimeMillis()/1000)+"---"+i1+"---"+
sendGetRequest("http://localhost:9996/sentinel/test"));
}).start();
}
Thread.sleep(1000);
}
}
public static String sendGetRequest(String getUrl) {
StringBuffer sb = new StringBuffer();
InputStreamReader isr = null;
BufferedReader br = null;
try {
URL url = new URL(getUrl);
URLConnection urlConnection = url.openConnection();
urlConnection.setAllowUserInteraction(false);
isr = new InputStreamReader(url.openStream());
br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
br.close();
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return sb.toString();
}
}
在控制台中观测熔断效果
可以发现,图中每隔10秒,所有的请求都被拒绝了,然后短暂的恢复,如此往复,与预期相符。
另外,在调用端的日志中可以发现,我们配置的熔断后处理逻辑也已经生效。