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 <T> 泛型 * @return 規則對象列表 * @throws NacosException 異常 */ public static <T> List<T> getRuleEntitiesFromNacos(ConfigService configService, String groupId, String dataId, Class<T> 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 <T> * @param configService * @param groupId * @param dataId * @param ruleEntities * @return * @throws NacosException */ @Bean public static <T> Boolean setRuleEntitiesFromNacos(ConfigService configService, String groupId, String dataId, List<T> 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<List<FlowRuleEntity>> { @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public List<FlowRuleEntity> getRules(String appName) throws Exception { //定義dataId 應用名+固定后綴 String dataId = new StringBuilder(appName).append(NacosConstants.FLOW_DATA_ID_POSTFIX).toString(); List<FlowRuleEntity> 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<List<FlowRuleEntity>> { private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosPublisher.class); @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public void publish(String appName, List<FlowRuleEntity> 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<FlowRuleEntity>
在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<List<DegradeRuleEntity>> { @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public List<DegradeRuleEntity> getRules(String appName) throws Exception { //定義dataId 應用名+固定后綴 String dataId = new StringBuilder(appName).append(NacosConstants.DEGRADE_DATA_ID_POSTFIX).toString(); List<DegradeRuleEntity> 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<List<DegradeRuleEntity>> { private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosPublisher.class); @Autowired private NacosPropertiesConfiguration nacosConfigProperties; @Autowired private ConfigService configService; @Override public void publish(String appName, List<DegradeRuleEntity> 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 RuleRepository<DegradeRuleEntity, Long> repository; @Autowired @Qualifier("degradeRuleNacosProvider") private DynamicRuleProvider<List<DegradeRuleEntity>> ruleProvider; @Autowired @Qualifier("degradeRuleNacosPublisher") private DynamicRulePublisher<List<DegradeRuleEntity>> rulePublisher; @Autowired private SentinelApiClient sentinelApiClient; @GetMapping("/rules.json") @AuthAction(PrivilegeType.READ_RULE) public Result<List<DegradeRuleEntity>> 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<DegradeRuleEntity> rules = sentinelApiClient.fetchDegradeRuleOfMachine(app, ip, port); //修改為從Nacos中加載熔斷規則 List<DegradeRuleEntity> rules = 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<DegradeRuleEntity> apiAddRule(@RequestBody DegradeRuleEntity entity) { Result<DegradeRuleEntity> 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<DegradeRuleEntity> 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<DegradeRuleEntity> 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<Long> 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<DegradeRuleEntity> 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 <R> Result<R> 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的功能就已完成了!