SpringCloud Alibaba 改造Sentinel Dashboard将熔断规则持久化到Nacos
Sentinel Dashboard集成Nacos目录:
SpringCloud Alibaba 改造Sentinel Dashboard将流控规则持久化到Nacos
SpringCloud Alibaba 改造Sentinel Dashboard将熔断规则持久化到Nacos 本文
在《SpringCloud Alibaba 改造Sentinel Dashboard将流控规则持久化到Nacos》介绍了如何修改Sentinel Dashboard的源代码,使得通过Sentinel Dashboard维护的流控规则自动持久化到Nacos上,应用程序通过订阅Nacos上的配置实现流量控制。
本文接着介绍如何修改源码实现熔断规则的持久化。
二. Sentinel Dashboard集成Nacos实现熔断规则持久化
同前文介绍的集成Nacos实现流控规则持久化类似,为了实现熔断规则持久化,其大致实现步骤仍然是:
- 创建新的实现类,实现DynamicRuleProvider接口和DynamicRulePublisher接口。
- 修改对应的Controller,注入新的实现类。
2.1 准备工作
为了减少代码,更加规范写法,统一流控规则和熔断规则的持久化的写法,首先对之前流控规则的代码予以部分修改,新的包结构如下:
以下是代码明细:
NacosConfiguration.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos; import java.util.Properties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.config.ConfigFactory; import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.exception.NacosException; /** * Nacos配置类 * @author gang.wang * 2021年10月31日 */ @EnableConfigurationProperties(NacosPropertiesConfiguration.class) @Configuration public class NacosConfiguration { @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); } }
NacosConstants.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos; import java.util.ArrayList; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; import com.alibaba.csp.sentinel.util.StringUtil; import com.alibaba.fastjson.JSON; import com.alibaba.nacos.api.config.ConfigService; import com.alibaba.nacos.api.exception.NacosException; /** * Nacos常量类 * @author gang.wang * 2021年11月8日 */ public class NacosConstants { private static Logger logger = LoggerFactory.getLogger(NacosConstants.class); public static final String GROUP_ID = "DEFAULT_GROUP"; /** * 流控规则后缀 */ public static final String FLOW_DATA_ID_POSTFIX = "-sentinel-flow"; /** * 熔断规则后缀 */ public static final String DEGRADE_DATA_ID_POSTFIX = "-sentinel-degrade"; /** * 从Nacos server中查询响应规则,并将其反序列化成对应Rule实体 * * @param configService nacos config service * @param groupId 组ID * @param dataId Nacos DataId * @param clazz 类 * @param泛型 * @return 规则对象列表 * @throws NacosException 异常 */ public staticList getRuleEntitiesFromNacos(ConfigService configService, String groupId, String dataId, Class clazz) throws NacosException { String rules = configService.getConfig(dataId, groupId, 3000); logger.info("Pull Rule from Nacos Config : {}", rules); if (StringUtil.isEmpty(rules)) { return new ArrayList<>(); } return JSON.parseArray(rules, clazz); } /** * 将规则序列化成为JSON信息,并发布到Nacos上 * @param * @param configService * @param groupId * @param dataId * @param ruleEntities * @return * @throws NacosException */ @Bean public staticBoolean setRuleEntitiesFromNacos(ConfigService configService, String groupId, String dataId, List ruleEntities) throws NacosException { String ruleEntitiesStr = JSON.toJSONString(ruleEntities); logger.info("Push Rule to Nacos Config : {}", ruleEntitiesStr); return configService.publishConfig(dataId, groupId, ruleEntitiesStr); } }
NacosPropertiesConfiguration.java的内容依然保持不变:
package com.alibaba.csp.sentinel.dashboard.rule.nacos; import org.springframework.boot.context.properties.ConfigurationProperties; /** * 加载Nacos配置 * @author gang.wang * 2021年10月31日 */ @ConfigurationProperties(prefix="sentinel.nacos") public class NacosPropertiesConfiguration { /** * Nacos服务地址 */ private String serverAddr; private String dataId; private String groupId = "DEFAULT_GROUP"; private String namespace; public String getServerAddr() { return serverAddr; } public void setServerAddr(String serverAddr) { this.serverAddr = serverAddr; } public String getDataId() { return dataId; } public void setDataId(String dataId) { this.dataId = dataId; } public String getGroupId() { return groupId; } public void setGroupId(String groupId) { this.groupId = groupId; } public String getNamespace() { return namespace; } public void setNamespace(String namespace) { this.namespace = namespace; } }
FlowRuleNacosProvider.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConstants; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosPropertiesConfiguration; import com.alibaba.nacos.api.config.ConfigService; /** * 实现从Nacos配置中心获取流控规则 * @author gang.wang * 2021年11月8日 */ @Service public class FlowRuleNacosProvider implements DynamicRuleProvider> { @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public List
getRules(String appName) throws Exception { //定义dataId 应用名+固定后缀 String dataId = new StringBuilder(appName).append(NacosConstants.FLOW_DATA_ID_POSTFIX).toString(); List list = NacosConstants.getRuleEntitiesFromNacos(configService, nacosConfigProperties.getGroupId(), dataId, FlowRuleEntity.class); return list; } }
FlowRuleNacosPublisher.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos.flow; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConstants; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosPropertiesConfiguration; import com.alibaba.nacos.api.config.ConfigService; /** * 将通过Sentinel Dashboard上维护的流控规则数据持久化到Nacos中 * @author gang.wang * 2021年11月8日 */ @Service public class FlowRuleNacosPublisher implements DynamicRulePublisher> { private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosPublisher.class); @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public void publish(String appName, List
rules) throws Exception { if(StringUtils.isBlank(appName)) { logger.error("传入的AppName为Null"); return ; } if(null == rules) { logger.error("传入的流控规则数据为null"); return ; } String dataId = new StringBuilder(appName).append(NacosConstants.FLOW_DATA_ID_POSTFIX).toString(); NacosConstants.setRuleEntitiesFromNacos(configService, nacosConfigProperties.getGroupId(), dataId, rules); } }
2.2 熔断规则持久化部分代码
2.2.1 分别创建类实现DynamicRuleProvider接口和DynamicRulePublisher接口
实现熔断规则持久化的代码和之前流控规则的类似,都需要创建两个类分别实现DynamicRuleProvider接口和DynamicRulePublisher接口,只不过接口的泛型不同,熔断规则的为:List
在com.alibaba.csp.sentinel.dashboard.rule.nacos包下创建degrade包。创建类:DegradeRuleNacosProvider.java和DegradeRuleNacosPublisher.java,分别实现<从Nacos拉取规则并展示在Dashboard上>和<将通过Dashboard维护的熔断规则持久化到Nacos上>这两个功能。
二者代码实现也同流控规则中的代码类似。
DegradeRuleNacosProvider.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos.degrade; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConstants; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosPropertiesConfiguration; import com.alibaba.nacos.api.config.ConfigService; /** * 实现从Nacos配置中心获取熔断规则 * @author gang.wang * 2021年11月15日 */ @Service public class DegradeRuleNacosProvider implements DynamicRuleProvider> { @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public List
getRules(String appName) throws Exception { //定义dataId 应用名+固定后缀 String dataId = new StringBuilder(appName).append(NacosConstants.DEGRADE_DATA_ID_POSTFIX).toString(); List list = NacosConstants.getRuleEntitiesFromNacos(configService, nacosConfigProperties.getGroupId(), dataId, DegradeRuleEntity.class); return list; } }
DegradeRuleNacosPublisher.java
package com.alibaba.csp.sentinel.dashboard.rule.nacos.degrade; import java.util.List; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConstants; import com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosPropertiesConfiguration; import com.alibaba.csp.sentinel.dashboard.rule.nacos.flow.FlowRuleNacosPublisher; import com.alibaba.nacos.api.config.ConfigService; /** * 实现将熔断规则持久化到Nacos中 * @author gang.wang * 2021年11月15日 */ @Service public class DegradeRuleNacosPublisher implements DynamicRulePublisher> { private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosPublisher.class); @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public void publish(String appName, List
rules) throws Exception { if(StringUtils.isBlank(appName)) { logger.error("传入的AppName为Null"); return ; } if(null == rules) { logger.error("传入的熔断规则数据为null"); return ; } String dataId = new StringBuilder(appName).append(NacosConstants.DEGRADE_DATA_ID_POSTFIX).toString(); NacosConstants.setRuleEntitiesFromNacos(configService, nacosConfigProperties.getGroupId(), dataId, rules); } }
2.2.2 修改com.alibaba.csp.sentinel.dashboard.controller.DegradeController,注入新的实现类
此Controller中只需要修改两个方法即可,分别是:
- com.alibaba.csp.sentinel.dashboard.controller.DegradeController.apiQueryMachineRules(String, String, Integer)
- com.alibaba.csp.sentinel.dashboard.controller.DegradeController.publishRules(String, String, Integer)
修改后的代码如下:
/* * Copyright 1999-2018 Alibaba Group Holding Ltd. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.alibaba.csp.sentinel.dashboard.controller; import java.util.Date; import java.util.List; import com.alibaba.csp.sentinel.dashboard.auth.AuthAction; import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType; import com.alibaba.csp.sentinel.dashboard.repository.rule.RuleRepository; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider; import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher; import com.alibaba.csp.sentinel.slots.block.RuleConstant; import com.alibaba.csp.sentinel.slots.block.degrade.circuitbreaker.CircuitBreakerStrategy; import com.alibaba.csp.sentinel.util.StringUtil; import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.DegradeRuleEntity; import com.alibaba.csp.sentinel.dashboard.domain.Result; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * Controller regarding APIs of degrade rules. Refactored since 1.8.0. * * @author Carpenter Lee * @author Eric Zhao */ @RestController @RequestMapping("/degrade") public class DegradeController { private final Logger logger = LoggerFactory.getLogger(DegradeController.class); /** * 将规则加载到Sentinel Dashboard的内存中 */ @Autowired private RuleRepositoryrepository; @Autowired @Qualifier("degradeRuleNacosProvider") private DynamicRuleProvider > ruleProvider; @Autowired @Qualifier("degradeRuleNacosPublisher") private DynamicRulePublisher
> rulePublisher; @Autowired private SentinelApiClient sentinelApiClient; @GetMapping("/rules.json") @AuthAction(PrivilegeType.READ_RULE) public Result
> apiQueryMachineRules(String app, String ip, Integer port) { if (StringUtil.isEmpty(app)) { return Result.ofFail(-1, "app can't be null or empty"); } if (StringUtil.isEmpty(ip)) { return Result.ofFail(-1, "ip can't be null or empty"); } if (port == null) { return Result.ofFail(-1, "port can't be null"); } try { //注释掉原有代码 //List
rules = sentinelApiClient.fetchDegradeRuleOfMachine(app, ip, port); //修改为从Nacos中加载熔断规则 Listrules = ruleProvider.getRules(app); rules = repository.saveAll(rules); return Result.ofSuccess(rules); } catch (Throwable throwable) { logger.error("queryApps error:", throwable); return Result.ofThrowable(-1, throwable); } } @PostMapping("/rule") @AuthAction(PrivilegeType.WRITE_RULE) public Result apiAddRule(@RequestBody DegradeRuleEntity entity) { Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; } Date date = new Date(); entity.setGmtCreate(date); entity.setGmtModified(date); try { entity = repository.save(entity); } catch (Throwable t) { logger.error("Failed to add new degrade rule, app={}, ip={}", entity.getApp(), entity.getIp(), t); return Result.ofThrowable(-1, t); } if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { logger.warn("Publish degrade rules failed, app={}", entity.getApp()); } return Result.ofSuccess(entity); } @PutMapping("/rule/{id}") @AuthAction(PrivilegeType.WRITE_RULE) public Result apiUpdateRule(@PathVariable("id") Long id, @RequestBody DegradeRuleEntity entity) { if (id == null || id <= 0) { return Result.ofFail(-1, "id can't be null or negative"); } DegradeRuleEntity oldEntity = repository.findById(id); if (oldEntity == null) { return Result.ofFail(-1, "Degrade rule does not exist, id=" + id); } entity.setApp(oldEntity.getApp()); entity.setIp(oldEntity.getIp()); entity.setPort(oldEntity.getPort()); entity.setId(oldEntity.getId()); Result checkResult = checkEntityInternal(entity); if (checkResult != null) { return checkResult; } entity.setGmtCreate(oldEntity.getGmtCreate()); entity.setGmtModified(new Date()); try { entity = repository.save(entity); } catch (Throwable t) { logger.error("Failed to save degrade rule, id={}, rule={}", id, entity, t); return Result.ofThrowable(-1, t); } if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) { logger.warn("Publish degrade rules failed, app={}", entity.getApp()); } return Result.ofSuccess(entity); } @DeleteMapping("/rule/{id}") @AuthAction(PrivilegeType.DELETE_RULE) public Result delete(@PathVariable("id") Long id) { if (id == null) { return Result.ofFail(-1, "id can't be null"); } DegradeRuleEntity oldEntity = repository.findById(id); if (oldEntity == null) { return Result.ofSuccess(null); } try { repository.delete(id); } catch (Throwable throwable) { logger.error("Failed to delete degrade rule, id={}", id, throwable); return Result.ofThrowable(-1, throwable); } if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { logger.warn("Publish degrade rules failed, app={}", oldEntity.getApp()); } return Result.ofSuccess(id); } private boolean publishRules(String app, String ip, Integer port) { List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); //注释掉原有代码 //return sentinelApiClient.setDegradeRuleOfMachine(app, ip, port, rules); try { rulePublisher.publish(app, rules); return true; } catch(Exception ex) { logger.error("Publish degrade rules failed", ex); return false; } } private Result checkEntityInternal(DegradeRuleEntity entity) { if (StringUtil.isBlank(entity.getApp())) { return Result.ofFail(-1, "app can't be blank"); } if (StringUtil.isBlank(entity.getIp())) { return Result.ofFail(-1, "ip can't be null or empty"); } if (entity.getPort() == null || entity.getPort() <= 0) { return Result.ofFail(-1, "invalid port: " + entity.getPort()); } if (StringUtil.isBlank(entity.getLimitApp())) { return Result.ofFail(-1, "limitApp can't be null or empty"); } if (StringUtil.isBlank(entity.getResource())) { return Result.ofFail(-1, "resource can't be null or empty"); } Double threshold = entity.getCount(); if (threshold == null || threshold < 0) { return Result.ofFail(-1, "invalid threshold: " + threshold); } Integer recoveryTimeoutSec = entity.getTimeWindow(); if (recoveryTimeoutSec == null || recoveryTimeoutSec <= 0) { return Result.ofFail(-1, "recoveryTimeout should be positive"); } Integer strategy = entity.getGrade(); if (strategy == null) { return Result.ofFail(-1, "circuit breaker strategy cannot be null"); } if (strategy < CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType() || strategy > RuleConstant.DEGRADE_GRADE_EXCEPTION_COUNT) { return Result.ofFail(-1, "Invalid circuit breaker strategy: " + strategy); } if (entity.getMinRequestAmount() == null || entity.getMinRequestAmount() <= 0) { return Result.ofFail(-1, "Invalid minRequestAmount"); } if (entity.getStatIntervalMs() == null || entity.getStatIntervalMs() <= 0) { return Result.ofFail(-1, "Invalid statInterval"); } if (strategy == RuleConstant.DEGRADE_GRADE_RT) { Double slowRatio = entity.getSlowRatioThreshold(); if (slowRatio == null) { return Result.ofFail(-1, "SlowRatioThreshold is required for slow request ratio strategy"); } else if (slowRatio < 0 || slowRatio > 1) { return Result.ofFail(-1, "SlowRatioThreshold should be in range: [0.0, 1.0]"); } } else if (strategy == RuleConstant.DEGRADE_GRADE_EXCEPTION_RATIO) { if (threshold > 1) { return Result.ofFail(-1, "Ratio threshold should be in range: [0.0, 1.0]"); } } return null; } }
2.3 测试通过Sentinel Dashboard维护的熔断规则是否可以持久化到Nacos中
访问Sentinel Dashboard,维护一条熔断规则,示例如下:
为了后面验证简单,这里我们定义的熔断规则的含义是:此接口在1000ms内当至少有3次访问,并至少有2次异常,则此接口熔断10秒。
刷新列表可以看到刚刚维护的信息:
访问Nacos,查看是否正常存储:
由上可见,通过Sentinel Dashboard维护的熔断规则已经正常持久化到Nacos中了。
2.4 应用程序接入
应用程序需要订阅Nacos上的对应DataId,当熔断规则有变化时,Nacos会自动推送到已经接入的应用程序上。其具体接入的方式同之前介绍的流控规则类似。
首先在application.yml中添加对应的配置。application.yml中与Sentinel相关的配置如下:
spring:
sentinel:
transport:
dashboard: 127.0.0.1:8080
datasource:
flow:
nacos:
server-addr: 127.0.0.1:8848
namespace: 37c7c263-bdf1-41db-9f34-bf10948be752
data-id: ${spring.application.name}-sentinel-flow
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
degrade:
nacos:
server-addr: 127.0.0.1:8848
namespace: 37c7c263-bdf1-41db-9f34-bf10948be752
data-id: ${spring.application.name}-sentinel-degrade
group-id: DEFAULT_GROUP
data-type: json
rule-type: degrade
为了配合上面定义的基于异常数的熔断规则,我们修改之前定义的接口,抛出任意异常即可。
@SentinelResource(value = "hello", blockHandler = "blockHandlerHello") @GetMapping("/say") public String hello() { int a = 10; int b = 0; int c = a / b; return "hello, Gary!"; }
启动应用程序,并利用Postman并发访问刚刚我们定义了熔断规则的接口。
可以看到前两次接口访问报500错误,从第三次开始http code就开始等于200,responseBody的值为当前请求已被限流。所以可见我们定义的熔断规则生效了。
至此熔断规则持久化到Nacos的功能就已完成了!