参考资料:
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服务原理与实战》
《B站 尚硅谷 SpringCloud 框架开发教程 周阳》
《Sentinel GitHub 官网》
《Sentinel 官网》
Sentinel 是面向分布式服务架构的轻量级流量控制组件,主要以流量为切入点,从限流、流量整形、服务降级、系统负载保护等多个维度来帮助我们保障微服务的稳定性;
功能 | 说明 |
---|---|
实时监控 | 实时监控每个资源名(接口、请求路径)的通过 QPS、拒绝 QPS 和响应时间; |
簇点链路 | 通过树状视图和列表视图展示接口调用的关系以及通过 QPS、拒绝 QPS、并发数、平均 RT、分钟通过和拒绝等信息; |
流控规则 | 又称:流量控制(flow control)。其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性; |
熔断规则 | 对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩; |
热点规则 | 又称:热点参数限流规则。热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制; |
系统规则 | 系统保护规则是从应用级别的入口流量进行控制,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性; |
授权规则 | 根据调用来源来判断该次请求是否允许放行; |
集群流控 | 集群流控可以解决流量不均匀导致总体限流效果不佳的问题; |
机器列表 | 收集 Sentinel 客户端发送的心跳包,用于判断机器是否在线; |
Sentinel 的核心分为三部分:工作流程、数据结构和限流算法;
调用链路是 Sentinel 的工作主流程,由各个 Slot 插槽组成,将不同的 Slot 按照顺序串在一起(责任链模式),从而将不同的功能(限流、降级、系统保护)组合在一起;
Sentinel 中各个 Slot 承担了不同的职责,如:LogSlot 负责记录日志、StatisticSlot 负责统计指标数据、FlowSlot 负责限流等。这是一种职责分离的设计,每个模块更聚焦于实现某个功能;
在 Sentinel 中,所有的资源都对应一个资源名称(resourceName),每次访问该资源都会创建一个 Entry 对象,在创建 Entry 的同时,会创建一系列功能槽(Slot Chain),这些槽会组成一个责任链,每个槽负责不同的职责;
Slot 插槽 | 说明 |
---|---|
NodeSelectorSlot | 负责收集资源的调用路径,以树状结构存储调用栈,用于根据调用路径来限流降级; |
ClusterBuilderSlot | 负责创建以资源名维度统计的 ClusterNode ,以及创建每个 ClusterNode 下按调用来源 origin 划分的 StatisticNode; |
LogSlot | 在出现限流、熔断、系统保护时负责记录日志; |
AuthoritySlot | 权限控制,支持黑名单和白名单两种策略; |
SystemSlot | 控制总的入口流量,限制条件依次是总 QPS、总线程数、RT 阈值、操作系统当前 load1、操作系统当前 CPU 利用率; |
FlowSlot | 根据限流规则和各个Node中的统计数据进行限流判断; |
DegradeSlot | 根据熔断规则和各个Node中的统计数据进行服务降级; |
StatisticSlot | 统计不同维度的请求数、通过数、限流数、线程数等 runtime 信息,这些数据存储在 DefaultNode、OriginNode 和 ClusterNode 中; |
Sentinel 与 Nacos 类似,有两种安装方式:使用已经编译好的安装包和源码部署;由于要对 Sentinel 源码进行分析,这里推荐源码部署;
这里选择的版本是 1.8.3;
netstat -ano
或者指定查看 8080 端口占用情况 netstat -aon|findstr 8080
;tasklist|findstr {上面查到的进程 PID}
;taskkill /f /t /im {上面查到的应用程序}
;java -jar sentinel-dashboard-1.8.3.jar
;java -Dserver.port=7777 -Dcsp.sentinel.dashboard.server=localhost:7777 -jar sentinel-dashboard-1.8.3.jar
;
sentinel
;
mvn install
将项目安装到本地;sentinel-dashboard
模块下的主程序类 DashboardApplication 运行即可;
- <dependency>
- <groupId>com.alibaba.cloudgroupId>
- <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
- dependency>
- spring:
- application:
- name: nacos-config-client #必须,构成 Nacos 配置管理 Data ID 字段的一部分
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848 #Nacos 服务注册中心地址
- config:
- server-addr: localhost:8848 #Nacos 作为配置中心地址
- file-extension: yaml #指定 yaml 格式的配置
- #以下新增
- sentinel:
- transport:
- dashboard: localhost:28080 #配置Sentinel dashboard地址
- port: 8719 #默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
- @RestController
- public class ConfigClientController {
- @GetMapping("/easytest")
- public String testMothod(){
- return "test";
- }
- }
下图表示 1 秒钟内查询 1 次就是 OK,若超过次数 1,就直接-快速失败,报默认错误;
流控规则控制页面.png
Blocked by Sentinel (flow limiting)
;下图表示当我们的请求响应超过 1000ms ,并且该统计的请求比例超过 50% 时(统计的请求数量需要大于 5),触发熔断;
经过熔断时长 5s 后进入探测恢复状态,若下一个请求响应时间小于 1000ms,则熔断结束;反之再次熔断;
1. 简单示例
@SentinelResource
的用法详情请见本篇第 4 点;
- @GetMapping("/testHotKey")
- @SentinelResource(value = "testHotKey",blockHandler = "dealHandlerTestHotKey")
- public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
- @RequestParam(value = "p2",required = false) String p2){
- return "testHotKey";
- }
- public String dealHandlerTestHotKey(String p1, String p2, BlockException exception) {
- return "dealHandler_testHotKey";
- }
下图表示第一个参数(索引为0)有值的话(对应上述代码的 p1),1 秒的 QPS 为 1,超过就限流,限流后调用 dealHandler_testHotKey 支持方法;
资源名 resource:唯一名称,默认请求路径;
限流模式 grade:限流模式只支持 QPS 模式;
参数索引 paramIdx:必填,对应 SphU.entry(xxx, args) 中的参数索引位置;
单机阈值 count:限流阈值,必填;
统计窗口时长 durationInSec:单位为秒,1.6.0 版本开始支持;
是否集群 clusterMode:是否是集群参数流控规则;
集群流控相关配置 clusterConfig;
参数例外项 paramFlowItemList:可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型;
流控效果 controlBehavior:流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持;
测试访问:
http://localhost:18082/testHotKey?p1=22
:触发限流;http://localhost:18082/testHotKey?p1=22&p2=33
:触发限流;http://localhost:18082/testHotKey?p2=33
:不触发限流;2. 参数例外项配置
当 p1 的值等于 5 时,它的阈值可以达到 200;
maxQps * minRt
估算得出。设定参考值一般是 CPU cores * 2.5
;
,
分隔,如 appA,appB;AUTHORITY_WHITE
为白名单模式,AUTHORITY_BLACK
为黑名单模式,默认为白名单模式;
- public @interface SentinelResource {
- //资源名称,必需项(不能为空)
- String value() default "";
-
- //entry 类型,有 IN 和 OUT 两个选项,(默认为 EntryType.OUT)
- EntryType entryType() default EntryType.OUT;
-
- int resourceType() default 0;
-
- //对应处理 BlockException 的函数名称,可选项
- String blockHandler() default "";
-
- /**
- * blockHandler 函数默认需要和原方法在同一个类中,如果希望使用其他类的函数,则需要指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析
- Class>[] blockHandlerClass() default {};
-
- /**
- * fallback 函数默认需要和原方法在同一个类中;
- * 若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象;
- * 对应的函数必需为 static 函数,否则无法解析;
- **/
- String fallback() default "";
-
- /**
- * 默认的 fallback 函数名称,可选项,通常用于通用的 fallback逻辑(即可以用于很多服务或方法);
- * 默认 fallback 函数可以针对所有类型的异常(除了exceptionsToIgnore里面排除掉的异常类型)进行处理;
- * 若同时配置了 fallback 和 defaultFallback,则只有 fallback会生效;
- * defaultFallback 函数签名要求:返回值类型必须与原函数返回值类型一致;方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常;
- * defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析;
- **/
- String defaultFallback() default "";
-
- /**
- * fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑;
- * fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理;
- * fallback 函数签名和位置要求:返回值类型必须与原函数返回值类型一致;方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常
- **/
- Class>[] fallbackClass() default {};
-
- //用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出
- Class extends Throwable>[] exceptionsToTrace() default {Throwable.class};
-
- Class extends Throwable>[] exceptionsToIgnore() default {};
- }
在 handler 包下新建 CustomerBlockHandler 类;
- public class CustomerBlockHandler{
- public static CommonResult handlerException(BlockException exception){
- return new CommonResult(4444,"按客戶自定义,global handlerException----1");
- }
- public static CommonResult handlerException2(BlockException exception){
- return new CommonResult(4444,"按客戶自定义,global handlerException----2");
- }
- }
- @GetMapping("/rateLimit/customerBlockHandler")
- @SentinelResource(value = "customerBlockHandler",
- blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handleException2")
- public CommonResult customerBlockHandler(){
- return new CommonResult(200,"按客户自定义限流处理逻辑");
- }
try-catch-finally
方式进行处理;
- @GetMapping("/testHotKey")
- @SentinelResource(value = "testHotKey",blockHandler = "dealHandlerTestHotKey")
- public String testHotKey(@RequestParam(value = "p1",required = false) String p1,
- @RequestParam(value = "p2",required = false) String p2){
- return "testHotKey";
- }
- public String dealHandlerTestHotKey(String p1, String p2, BlockException exception) {
- return "dealHandler_testHotKey";
- }
- public class FlowRuleInitFunc implements InitFunc{
- @Override
- public void init() throws Exception{
- List
rules=new ArrayList<>(); - FlowRule rule=new FlowRule();
- rule.setcount(1);
- rule.setResource("testHotKey");
- rule.setGrade(RuleConstant.FLOW_GRADE_QPS);
- rule.setLimitApp("default");
- rules.add(rule);
- FlowRuleManager.loadRules(rules);
- }
META-INF/services/com.alibaba.csp.sentinelinit.InitFunc
文件,文件内容就是自定义扩展点 FlowRuleInitFunc 的全路径;
- <dependency>
- <groupId>com.alibaba.cloudgroupId>
- <artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
- dependency>
- <dependency>
- <groupId>com.alibaba.cspgroupId>
- <artifactId>sentinel-datasource-nacosartifactId>
- dependency>
- spring:
- application:
- name: nacos-config-client
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848
- config:
- server-addr: localhost:8848
- file-extension: yaml
- sentinel:
- transport:
- dashboard: localhost:28080
- port: 8719
- # 以下新增
- datasource:
- ds1:
- nacos:
- server-addr: localhost:8848 #将规则保存进 Nacos 配置中心
- dataId: cloudalibaba-sentinel-service
- groupId: DEFAULT_GROUP
- data-type: json #指定配置项的内容格式,可选:JSON、XML。如果需要自定义,则可以将值配置为 custom,并配置 converter-class 指向 converte r类;
- rule-type: flow #数据源中规则的类型,可选:flow、degrade、param-flow、gw-flow
/easytest
接口添加流控规则;
/easytest
后才能看见配置规则;
- <dependency>
- <groupId>com.alibaba.cspgroupId>
- <artifactId>sentinel-datasource-nacosartifactId>
-
- <dependency>
src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html
;里的 dashboard.flowV1 为 dashboard.flow;
- <li ui-sref-active="active" ng-if="!entry.isGateway">
-
- <a ui-sref="dashboard.flow({app: entry.app})">
- <i class="glyphicon glyphicon-filter">i> 流控规则a>
- li>
application.yml
里添加 Nacos 服务器的配置信息:
sentinel.nacos.serverAddr=localhost:8848
;sentinel.nacos.namespace=
sentinel.nacos.group-id=DEFAULT_GROUP
;com/alibaba/csp/sentinel/dashboard/rule/nacos/
;src/test/java/com/alibaba/csp/sentinel/dashboard/rule/nacos
;
NacosPropertiesConfiguration.java
类,用来加载外部化配置;
- @ConfigurationProperties(prefix="sentinel.nacos")
- public class NacosPropertiesConfiguration {
- private String serverAddr;
- private String dataId;
- private String groupId = "DEFAULT_GROUP";
- private String namespace;
- //这里省略 get/set 方法
- }
- @EnableConfigurationProperties(NacosPropertiesConfiguration.class)
- @Configuration
- public class NacosConfiguration {
-
- //Converter 转换器,将 FlowRuleEntity 转化成 FlowRule,以及反向转化
- @Bean
- public Converter<List<FlowRuleEntity>, String> flowRuleEntityEncoder(){
- return JSON::toJSONString;
- }
-
- @Bean
- public Converter<String, List<FlowRuleEntity>> flowRuleEntityDecoder(){
- return s -> JSON.parseArray(s, FlowRuleEntity.class);
- }
-
- //注入 Nacos 服务 ConfigService
- @Bean
- public ConfigService nacosConfigService(NacosPropertiesConfiguration nacosPropertiesConfiguration) throws NacosException {
- Properties properties = new Properties();
- properties.put(PropertyKeyConst.SERVER_ADDR, nacosPropertiesConfiguration.getServerAddr());
- properties.put(PropertyKeyConst.NAMESPACE, nacosPropertiesConfiguration.getNamespace());
- return ConfigFactory.createConfigService(properties);
- }
- }
-
- public class NacosConstants {
- public static final String DATA_ID_POSTFIX = "-sentinel-flow";
- public static final String GROUP_ID = "DEFAULT_GROUP";
- }
- @Component("flowRuleNacosProvider")
- public class FlowRuleNacosProvider implements DynamicRuleProvider
> {
-
- private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosProvider.class);
-
- @Autowired
- private NacosPropertiesConfiguration nacosConfigProperties;
-
- @Autowired
- private ConfigService configService;
-
- @Autowired
- private Converter
> converter; -
- @Override
- public List
getRules(String appName) throws Exception { - String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
- //通过ConfigServic.getConfig方法从Nacos Config Server中读取指定配置信息,并通过converter转化为FlowRule规则
- String rules = configService.getConfig(dataID, nacosConfigProperties.getGroupId(), 3000);
- if (StringUtil.isEmpty(rules)) {
- return new ArrayList<>();
- }
- return converter.convert(rules);
- }
- }
- @Component("flowRuleNacosPublisher")
- public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
-
- @Autowired
- private NacosPropertiesConfiguration nacosPropertiesConfiguration;
-
- @Autowired
- private ConfigService configService;
-
- @Autowired
- private Converter<List<FlowRuleEntity>, String> converter;
-
- @Override
- public void publish(String appName, List<FlowRuleEntity> rules) throws Exception {
- AssertUtil.notEmpty(appName, "appName cannot be empty");
- if (rules == null) {
- return;
- }
- String dataID = new StringBuilder(appName).append(NacosConstants.DATA_ID_POSTFIX).toString();
- configService.publishConfig(dataID, nacosPropertiesConfiguration.getGroupId(), converter.convert(rules));
- }
- }
- spring:
- application:
- name: nacos-config-client
- cloud:
- nacos:
- discovery:
- server-addr: localhost:8848
- config:
- server-addr: localhost:8848
- file-extension: yaml
- sentinel:
- transport:
- dashboard: localhost:28080
- port: 8719
- datasource:
- ds1:
- nacos:
- server-addr: localhost:8848
- # 修改下面这条
- dataId: ${spring.appliction.name}-sentinel-flow
- groupId: DEFAULT_GROUP
- data-type: json
- rule-type: flow
Blocked by Sentinel (flow 1imiting)
;
- @Service public class CustomurlBlockHandler implements Ur1BlockHandler{
- @Override
- public void blocked(HttpServletRequest httpServletRequest, HttpServletResponse httpservletResponse, BlockException e)throws IOException{
- httpServletResponse.setHeader("Content-Type","application/json;charset=UTF-8");
- String message="{\"code\":999,\"msg\":\"访问人数过多\"}";
- httpServletResponse.getWriter().write(message);
- }
spring.cloud.sentinel.servlet.block-page-{url}
;/clean/{id}
带有一个参数 id,这个参数有很多种属性 {id可取整数},有多少个请求 Sentinel 默认统计多个,而我们期望是一个;/clean/{id}
这个 URL,我们可以统一归集到 /clean/*
资源下:
- @Service
- public class CustomerUrlCleaner implements UrlCleaner{
- @Override
- public String clean(String originurl){
- if(Stringutils.isEmpty(originur1)){
- return originUrl;
- }
- if(originUr1.startswith("/clean/")){
- return"/clean/*";
- }
- return originUrl;
- }