在微服务架构中,我们将业务拆分成一个个的服务,服务与服务之间可以相互调用,但是由于网络原因或者自身的原因,服务并不能保证服务的100%可用,如果单个服务出现问题,调用这个服务就会出现网络延迟,此时若有大量的网络涌入,会形成任务堆积,最终导致服务瘫痪。
在传统SpringCloud的微服务架构下,我们最常使用的是hystrix,《SpringCloud05—服务容错保护:Spring Cloud Hystrix
》后来随着Fegin的引入,我们又开始使用Fegin的容错和降级服务《SpringCloud06—声明式服务调用:Spring Cloud Feign
》,但是我们今天带来一个新的组件Sentinel,Sentinel 是阿里巴巴开源的一款断路器实现,本身在阿里内部已经被大规模采用,非常稳定。在讲解Sentinel之前,我们先看看常见的容错组件之间的差异
Sentinel | Hystrix | |
---|---|---|
隔离策略 | 信号量隔离(并发线程数限流) | 线程池隔离/信号量隔离 |
熔断降级策略 | 基于响应时间、异常比率、异常数 | 基于异常比率 |
实时统计实现 | 滑动窗口(LeapArray) | 滑动窗口(基于 RxJava) |
动态规则配置 | 支持多种数据源 | 支持多种数据源 |
扩展性 | 多个扩展点 | 插件的形式 |
基于注解的支持 | 支持 | 支持 |
限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 |
流量整形 | 支持预热模式、匀速器模式、预热排队模式 | 不支持 |
系统自适应保护 | 支持 | 不支持 |
控制台 | 提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等 | 简单的监控查看 |
我们先来通过一个很简单的程序来看看服务雪崩
首先我们先准备一个压力测试小工具:jmeter
点击此处下载
使用方法也很简单:
1.修改配置,并启动软件
进入bin目录,修改jmeter.properties文件中的语言支持为language=zh_CN,然后点击jmeter.bat,启动软件。
6.修改服务的并发配置
# 配置tomcat的最大并发数,默认200
server.tomcat.threads.max=10
@RequestMapping("/order/message")
public Map<String, Object> getMessage() {
Map<String, Object> map = new HashMap<>();
map.put("code", 20000);
map.put("msg", "高并发下的测试问题");
return map;
}
此时我们会发现由于order方法囤积了大量请求, 导致message方法的访问出现了问题,这就是服务雪崩的雏形。
在分布式系统中,由于网络原因或自身的原因,服务一般无法保证 100% 可用。如果一个服务出现了问题,调用这个服务就会出现线程阻塞的情况,此时若有大量的请求涌入,就会出现多条线程阻塞等待,进而导致服务瘫痪。
由于服务与服务之间的依赖性,故障会传播,会对整个微服务系统造成灾难性的严重后果,这就是服务故障的 “雪崩效应” 。
服务雪崩产生的流程如下所示:
雪崩发生的原因多种多样,有不合理的容量设计,或者是高并发下某一个方法响应变慢,亦或是某台机器的资源耗尽。我们无法完全杜绝雪崩源头的发生,只有做好足够的容错,保证在一个服务发生问题,不会影响到其它服务的正常运行。也就是"雪落而不雪崩"。
它是指将系统按照一定的原则划分为若干个服务模块,各个模块之间相对独立,无强依赖。当有故障发生时,能将问题和影响隔离在某个模块内部,而不扩散风险,不波及其它模块,不影响整体的系统服务。常见的隔离方式有:线程池隔离和信号量隔离
在上游服务调用下游服务的时候,设置一个最大响应时间,如果超过这个时间,下游未作出反应,就断开请求,释放掉线程。
限流就是限制系统的输入和输出流量已达到保护系统的目的。为了保证系统的稳固运行,一旦达到的需要限制的阈值,就需要限制流量并采取少量措施以完成限制流量的目的。
《Redis限流——滑动窗口限流》
在互联网系统中,当下游服务因访问压力过大而响应变慢或失败,上游服务为了保护系统整体的可用性,可以暂时切断对下游服务的调用。这种牺牲局部,保全整体的措施就叫做熔断。
服务熔断一般有三种状态:
降级其实就是为服务提供一个托底方案,一旦服务无法正常调用,就使用托底方案
Sentinel (分布式系统的流量防卫兵) 是阿里开源的一套用于服务容错的综合性解决方案。
它以流量为切入点, 从流量控制、熔断降级、系统负载保护等多个维度来保护服务的稳定性。
Sentinel的特征主要有以下几点:
Sentinel 分为两个部分:
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.4.13version>
<relativePath/>
parent>
<properties>
<java.version>11java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
<version>2021.1version>
dependency>
dependencies>
project>
Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。
进入cmd,使用指令启动
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.3.jar
参数解释:
# 配置sentinel
#跟控制台交流的端口,随意指定一个未使用的端口即可
spring.cloud.sentinel.transport.port=9999
# 指定控制台服务的地址
spring.cloud.sentinel.transport.dashboard=localhost:8090
注意:如果没有的话可以尝试先随便访问一个接口,如本项目上面演示服务雪崩的接口:http://localhost:8080/order/message,然后在刷新Sentinel控制台页面
Sentinel的控制台其实就是一个SpringBoot编写的程序。我们需要将我们的微服务程序注册到控制台上,即在微服务中指定控制台的地址, 并且还要开启一个跟控制台传递数据的端口, 控制台也可以通过此端口调用微服务中的监控程序获取微服务的各种信息。
接下来我们通过sentinel实现一个接口的限流
在浏览器端快速刷新http://localhost:8080/order/message页面,我们将会看到以下页面:
Sentinel的主要功能就是容错,主要体现为下面这三个:
流量控制在网络传输中是一个常用的概念,它用于调整网络包的数据。任意时间到来的请求往往是随机不可控的,而系统的处理能力是有限的。我们需要根据系统的处理能力对流量进行控制。Sentinel 作为一个调配器,可以根据需要把随机的请求调整成合适的形状。
当检测到调用链路中某个资源出现不稳定的表现,例如请求响应时间长或异常比例升高的时候,则对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联故障。
Sentinel 对这个问题采取了两种手段:
Sentinel 和 Hystrix 的区别
两者的原则是一致的, 都是当一个资源出现问题时, 让其快速失败, 不要波及到其它服务但是在限制的手段上, 确采取了完全不一样的方法:
- Hystrix 采用的是线程池隔离的方式, 优点是做到了资源之间的隔离, 缺点是增加了线程切换的成本。
- Sentinel 采用的是通过并发线程的数量和响应时间来对资源做限制。
Sentinel 同时提供系统维度的自适应保护能力。当系统负载较高的时候,如果还持续让请求进入可能会导致系统崩溃,无法响应。在集群环境下,会把本应这台机器承载的流量转发到其它的机器上去。如果这个时候其它的机器也处在一个边缘状态的时候,Sentinel 提供了对应的保护机制,让系统的入口流量和系统的负载达到一个平衡,保证系统在能力范围之内处理最多的请求。
我们了解这么多只是对其工作原理有个基本了解,那么我们开发者需要干嘛呢,我们只需要在Sentinel的资源上配置各种各样的规则,来实现各种容错的功能。
流量控制,其原理是监控应用流量的QPS(每秒查询率) 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。
我们先创建一个接口
@RequestMapping("/order/message1")
public Map<String, Object> getMessage_1() {
Map<String, Object> map = new HashMap<>();
map.put("code", 20000);
map.put("msg", "流控规则测试");
return map;
}
上图的配置则表示接口message1一旦每秒的请求达到三次以上就会进行限流
同时我们还可以看到有并发线程数,这个也很好理解,就是指每次访问的线程数量不能超过单击阈值,一旦超过就会禁止访问。
再往下接着我们能见到其显示的流控模式,流控模式有三种,我大致说一下这三种都分别有什么作用 :
直接模式我们之前都见过效果,我们看看关联和链路模式
关联流控模式指的是,当指定接口关联的接口达到限流条件时,开启对指定接口开启限流
我们在创建一个接口 message1_asssociate
@RequestMapping("/order/message1_asssociate")
public Map<String, Object> message1_asssociate() {
Map<String, Object> map = new HashMap<>();
map.put("code", 20000);
map.put("msg", "getMessage_1的关联接口");
return map;
}
我们继续使用压力测试工具jmeter对接口message1_asssociate进行测试
他们分别都有什么样的含义呢?
降级规则就是设置当满足什么条件的时候,对服务进行降级。
目前Sentinel提供了三个衡量条件:
我们以上图的设置为例对这三个策略进行解释:
注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
我们具体以一个实际代码作为演示:
int i = 0;
@RequestMapping("/order/message3")
public Map<String, Object> message3() {
Map<String, Object> map = new HashMap<>();
i++;
if (i % 3 == 0) {
throw new RuntimeException("异常比例");
}
map.put("code", 20000);
map.put("msg", "异常比例");
return map;
}
在上面的设置中 ,我们可能观察到流控规则和降级规则显示的内容是一样的,我们如何进行区分呢?
热点参数流控规则是一种更细粒度的流控规则, 它允许将规则具体到参数上。
接下来我们分别尝试连续快速访问:
http://localhost:8080/order/message4?name=ninesun
http://localhost:8080/order/message4?age=12
可以看到访问第一个元素的链接已经被熔断
很多时候,我们需要根据调用来源来判断该次请求是否允许放行,这时候可以使用 Sentinel 的来源访问控制的功能。来源访问控制根据资源的请求来源(origin)限制资源是否通过:
@RequestMapping("/order/message5")
public Map<String, Object> message5() {
Map<String, Object> map = new HashMap<>();
map.put("code", 20000);
map.put("msg", "授权规则");
return map;
}
自定义来源处理规则
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.RequestOriginParser;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class RequestOriginParserDefinition implements RequestOriginParser {
@Override
public String parseOrigin(HttpServletRequest httpServletRequest) {
String serviceName = httpServletRequest.getParameter("serviceName");
return serviceName;
}
}
这个配置的意思是只有serviceName=android不能访问(黑名单)
我们进行测试
访问:http://localhost:8080/order/message5?serviceName=android
我们可以看到这个链接已经无法访问
import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.fastjson.JSON;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@Component
public class ExceptionHandlerPage implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, BlockException e) throws Exception {
httpServletResponse.setContentType("application/json;charset=utf-8");
Map<String, Object> resultMap = new HashMap<>();
resultMap.put("code", 500);
if (e instanceof FlowException) {
resultMap.put("message", "接口被限流了");
resultMap.put("data", -1);
} else if (e instanceof DegradeException) {
resultMap.put("message", "接口被降级了");
resultMap.put("data", -2);
}
httpServletResponse.getWriter().write(JSON.toJSONString(resultMap));
}
}
我们回到之前的测试接口,再次测试就会发现已经有比较友好的提示了
我们在不停的重启项目之后会发现我们之前配置的规则都会莫名其妙没了,这是因为我们的规则保存在内存里,这就导致每次重启使得之前的配置就消失了,所以我们需要将规则持久化
本地文件数据源会定时轮询文件的变更,读取规则。这样我们既可以在应用本地直接修改文件来更新规则,也可以通过 Sentinel 控制台推送规则。
添加持久化配置
import com.alibaba.csp.sentinel.command.handler.ModifyParamFlowRulesCommandHandler;
import com.alibaba.csp.sentinel.datasource.*;
import com.alibaba.csp.sentinel.init.InitFunc;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRuleManager;
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;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRuleManager;
import com.alibaba.csp.sentinel.slots.system.SystemRule;
import com.alibaba.csp.sentinel.slots.system.SystemRuleManager;
import com.alibaba.csp.sentinel.transport.util.WritableDataSourceRegistry;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import java.io.File;
import java.io.IOException;
import java.util.List;
public class DataSourceInitFunc implements InitFunc {
@Override
public void init() throws Exception {
// 持久化在本地的目录,如果你对这个路径不喜欢,可修改为你喜欢的路径
String ruleDir = System.getProperty("user.home") + "\\sentinel\\order\\rules";
String flowRulePath = ruleDir + "\\flow-rule.json";
String degradeRulePath = ruleDir + "\\degrade-rule.json";
String systemRulePath = ruleDir + "\\system-rule.json";
String authorityRulePath = ruleDir + "\\authority-rule.json";
String paramFlowRulePath = ruleDir + "\\param-flow-rule.json";
this.mkdirIfNotExits(ruleDir);
this.createFileIfNotExits(flowRulePath);
this.createFileIfNotExits(degradeRulePath);
this.createFileIfNotExits(systemRulePath);
this.createFileIfNotExits(authorityRulePath);
this.createFileIfNotExits(paramFlowRulePath);
// 流控规则
ReadableDataSource<String, List<FlowRule>> flowRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
flowRuleListParser
);
// 将可读数据源注册至FlowRuleManager
// 这样当规则文件发生变化时,就会更新规则到内存
FlowRuleManager.register2Property(flowRuleRDS.getProperty());
WritableDataSource<List<FlowRule>> flowRuleWDS = new FileWritableDataSource<>(
flowRulePath,
this::encodeJson
);
// 将可写数据源注册至transport模块的WritableDataSourceRegistry中
// 这样收到控制台推送的规则时,Sentinel会先更新到内存,然后将规则写入到文件中
WritableDataSourceRegistry.registerFlowDataSource(flowRuleWDS);
// 降级规则
ReadableDataSource<String, List<DegradeRule>> degradeRuleRDS = new FileRefreshableDataSource<>(
degradeRulePath,
degradeRuleListParser
);
DegradeRuleManager.register2Property(degradeRuleRDS.getProperty());
WritableDataSource<List<DegradeRule>> degradeRuleWDS = new FileWritableDataSource<>(
degradeRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerDegradeDataSource(degradeRuleWDS);
// 系统规则
ReadableDataSource<String, List<SystemRule>> systemRuleRDS = new FileRefreshableDataSource<>(
systemRulePath,
systemRuleListParser
);
SystemRuleManager.register2Property(systemRuleRDS.getProperty());
WritableDataSource<List<SystemRule>> systemRuleWDS = new FileWritableDataSource<>(
systemRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerSystemDataSource(systemRuleWDS);
// 授权规则
ReadableDataSource<String, List<AuthorityRule>> authorityRuleRDS = new FileRefreshableDataSource<>(
flowRulePath,
authorityRuleListParser
);
AuthorityRuleManager.register2Property(authorityRuleRDS.getProperty());
WritableDataSource<List<AuthorityRule>> authorityRuleWDS = new FileWritableDataSource<>(
authorityRulePath,
this::encodeJson
);
WritableDataSourceRegistry.registerAuthorityDataSource(authorityRuleWDS);
// 热点参数规则
ReadableDataSource<String, List<ParamFlowRule>> paramFlowRuleRDS = new FileRefreshableDataSource<>(
paramFlowRulePath,
paramFlowRuleListParser
);
ParamFlowRuleManager.register2Property(paramFlowRuleRDS.getProperty());
WritableDataSource<List<ParamFlowRule>> paramFlowRuleWDS = new FileWritableDataSource<>(
paramFlowRulePath,
this::encodeJson
);
ModifyParamFlowRulesCommandHandler.setWritableDataSource(paramFlowRuleWDS);
}
private Converter<String, List<FlowRule>> flowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<FlowRule>>() {
}
);
private Converter<String, List<DegradeRule>> degradeRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<DegradeRule>>() {
}
);
private Converter<String, List<SystemRule>> systemRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<SystemRule>>() {
}
);
private Converter<String, List<AuthorityRule>> authorityRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<AuthorityRule>>() {
}
);
private Converter<String, List<ParamFlowRule>> paramFlowRuleListParser = source -> JSON.parseObject(
source,
new TypeReference<List<ParamFlowRule>>() {
}
);
private void mkdirIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.mkdirs();
}
}
private void createFileIfNotExits(String filePath) throws IOException {
File file = new File(filePath);
if (!file.exists()) {
file.createNewFile();
}
}
private <T> String encodeJson(T t) {
return JSON.toJSONString(t);
}
}
com.example.sentinel.config.DataSourceInitFunc
在application.properties文件中添加以下配置
# 取消控制台懒加载
spring.cloud.sentinel.eager=true
# 配置sentinel日志
## 默认值${home}/logs/csp/
spring.cloud.sentinel.log.dir=./logs
# 日志带上线程id
spring.cloud.sentinel.log.switch-pid=true
同时即使我们重启项目也可以看到我们的规则仍然保留着
当然我们还有更高级的《sentinel规则持久化到nacos》