背景
通过官方在生产环境中使用 Sentinel文档的说明,可以通过三种模式实现规则的推送。
默认情况下是基于原始模式。
这种做法的好处是简单,无依赖;坏处是应用重启规则就会消失,仅用于简单测试,不能用于生产环境。
官方也给出了推模式和拉模式来优化原始模式的不足。
-
拉模式
*推模式
方案
我这里通过推模式来实现规则动态发布。
- 通过对Sentinel Dashboard的改造,实现配置的规则持久化到Nacos中。
- Nacos将规则变更消息推送到监听变更消息的应用端。
方案中关于Nacos推送到应用端的实现在官方文档中已经说明如何配置了。下面就是实现如何将Sentinel Dashboard的规则配置持久化到Nacos中。
改造思路
Sentinel Dashboard工程是一个标准的MVC框架。
Dashboard界面对规则的修改所调用的API就是调用工程中的controller路径下的接口。
以FlowControllerV1为例,规则的添加由apiAddFlowRule方法响应。
控制层中的:
- getRules方法实现从持久层获取规则(原始实现中是通过HTTP调用,从应用端获取)
- publishRules方法实现规则更新发布到持久层中(原始实现中通过HTTP调用,直接更新到应用端)。
那只需要将原始实现中是通过HTTP调用的API改为通过Nacos的客户端组件读写。
改造
下面基于1.5.2版本的Sentinel Dashboard进行优化。
尽量将优化的新增类放在不同路径下,以便新版本的改造。
引入nacos客户端组件包
<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
封装nacos配置
基于springboot 初始化nacos连接信息。
NacosConfigConstant
public final class NacosConfigConstant {
public static final String GROUP_ID = "SENTINEL_GROUP";
public static final String FLOW_DATA_ID_POSTFIX = "-flow-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "-param-flow-rules";
public static final String DEGRADE_DATA_ID_POSTFIX = "-degrade-rules";
public static final String SYSTEM_DATA_ID_POSTFIX = "-system-rules";
public static final String AUTHORITY_DATA_ID_POSTFIX = "-authority-rules";
}
NacosConfigProperties
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class NacosConfigProperties {
@Value("${houyi.nacos.server.ip}")
private String ip;
@Value("${houyi.nacos.server.port}")
private String port;
@Value("${houyi.nacos.server.namespace}")
private String namespace;
@Value("${houyi.nacos.server.group-id}")
private String groupId;
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getPort() {
return port;
}
public void setPort(String port) {
this.port = port;
}
public String getNamespace() {
return namespace;
}
public void setNamespace(String namespace) {
this.namespace = namespace;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getServerAddr() {
return this.getIp()+":"+this.getPort();
}
@Override
public String toString() {
return "NacosConfigProperties [ip=" + ip + ", port=" + port + ", namespace="
+ namespace + ", groupId=" + groupId + "]";
}
}
NacosConfig
import java.util.Properties;
import org.springframework.beans.factory.annotation.Autowired;
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;
@Configuration
public class NacosConfig {
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Bean
public ConfigService nacosConfigService() throws Exception {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, nacosConfigProperties.getServerAddr());
if(nacosConfigProperties.getNamespace() != null && !"".equals(nacosConfigProperties.getNamespace()))
properties.put(PropertyKeyConst.NAMESPACE, nacosConfigProperties.getNamespace());
return ConfigFactory.createConfigService(properties);
}
}
添加启动配置
houyi.nacos.server.ip=10.7.91.65
houyi.nacos.server.port=8848
houyi.nacos.server.namespace=0af72e2d-a853-4519-9a5c-ecb5d417f766
houyi.nacos.server.group-id=SENTINEL_GROUP
基于Nacos读写规则实现
在工程中已经为不同规则读写规则提供了统一的接口。
我们只需要基于该接口,针对不同的规则类型实现就可以了。
下面以FlowRule为例
FlowRuleNacosProvider
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.util.RuleUtils;
import com.alibaba.csp.sentinel.slots.block.flow.FlowRule;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.nacos.api.config.ConfigService;
import com.houyi.sentinel.dashboard.config.NacosConfigProperties;
import com.houyi.sentinel.dashboard.config.NacosConfigConstant;
@Component("flowRuleNacosProvider")
public class FlowRuleNacosProvider implements DynamicRuleProvider<List<FlowRuleEntity>> {
private static Logger logger = LoggerFactory.getLogger(FlowRuleNacosProvider.class);
@Autowired
private ConfigService configService;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Override
public List<FlowRuleEntity> getRules(String appName) throws Exception {
String rulesStr = configService.getConfig(appName + NacosConfigConstant.FLOW_DATA_ID_POSTFIX,
nacosConfigProperties.getGroupId(), 3000);
logger.info("nacosConfigProperties{}:",nacosConfigProperties);
logger.info("从Nacos中获取到限流规则信息{}",rulesStr);
if (StringUtil.isEmpty(rulesStr)) {
return new ArrayList<>();
}
List<FlowRule> rules = RuleUtils.parseFlowRule(rulesStr);
if (rules != null) {
return rules.stream().map(rule -> FlowRuleEntity.fromFlowRule(appName, nacosConfigProperties.getIp(), Integer.valueOf(nacosConfigProperties.getPort()), rule))
.collect(Collectors.toList());
} else {
return new ArrayList<>();
}
}
}
FlowRuleNacosPublisher
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.util.AssertUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import com.houyi.sentinel.dashboard.config.NacosConfigProperties;
import com.houyi.sentinel.dashboard.config.NacosConfigConstant;
@Component("flowRuleNacosPublisher")
public class FlowRuleNacosPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> {
@Autowired
private ConfigService configService;
@Autowired
private NacosConfigProperties nacosConfigProperties;
@Override
public void publish(String app, List<FlowRuleEntity> rules) throws Exception {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
configService.publishConfig(app + NacosConfigConstant.FLOW_DATA_ID_POSTFIX,
nacosConfigProperties.getGroupId(),
JSON.toJSONString(rules.stream().map(FlowRuleEntity::toFlowRule).collect(Collectors.toList())));
}
}
改造Controller
找到对应的Controller,将原始HTTP调用改为对应的Provider及Publisher
下面以FlowControllerV1为例:
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
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.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.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.csp.sentinel.dashboard.auth.AuthService;
import com.alibaba.csp.sentinel.dashboard.auth.AuthService.AuthUser;
import com.alibaba.csp.sentinel.dashboard.auth.AuthService.PrivilegeType;
import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.FlowRuleEntity;
import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo;
import com.alibaba.csp.sentinel.dashboard.domain.Result;
import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRuleProvider;
import com.alibaba.csp.sentinel.dashboard.rule.DynamicRulePublisher;
import com.alibaba.csp.sentinel.util.StringUtil;
import com.alibaba.fastjson.JSON;
/**
* Flow rule controller.
*
* @author leyou
* @author Eric Zhao
*/
@RestController
@RequestMapping(value = "/v1/flow")
public class FlowControllerV1 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV1.class);
@Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository;
@Autowired
private AuthService<HttpServletRequest> authService;
@Autowired
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> provider;
@Autowired
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> publisher;
@GetMapping("/rules")
public Result<List<FlowRuleEntity>> apiQueryMachineRules(HttpServletRequest request,
@RequestParam String app,
@RequestParam String ip,
@RequestParam Integer port) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(app, PrivilegeType.READ_RULE);
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<FlowRuleEntity> rules = provider.getRules(app);
rules = repository.saveAll(rules);
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying flow rules", throwable);
return Result.ofThrowable(-1, throwable);
}
}
... ...
@PostMapping("/rule")
public Result<FlowRuleEntity> apiAddFlowRule(HttpServletRequest request, @RequestBody FlowRuleEntity entity) {
AuthUser authUser = authService.getAuthUser(request);
authUser.authTarget(entity.getApp(), PrivilegeType.WRITE_RULE);
Result<FlowRuleEntity> checkResult = checkEntityInternal(entity);
if (checkResult != null) {
return checkResult;
}
entity.setId(null);
Date date = new Date();
entity.setGmtCreate(date);
entity.setGmtModified(date);
entity.setLimitApp(entity.getLimitApp().trim());
entity.setResource(entity.getResource().trim());
try {
entity = repository.save(entity);
} catch (Throwable throwable) {
logger.error("Failed to add flow rule", throwable);
return Result.ofThrowable(-1, throwable);
}
if (!publishRules(entity.getApp(), entity.getIp(), entity.getPort())) {
logger.error("Publish flow rules failed after rule add");
}
return Result.ofSuccess(entity);
}
... ...
private boolean publishRules(String app, String ip, Integer port) {
List<FlowRuleEntity> rules = repository.findAllByMachine(MachineInfo.of(app, ip, port));
try {
publisher.publish(app, rules);
logger.info("添加限流规则成功{}",JSON.toJSONString(rules.stream().map(FlowRuleEntity::toFlowRule).collect(Collectors.toList())));
return true;
} catch (Exception e) {
logger.info("添加限流规则失败{}",JSON.toJSONString(rules.stream().map(FlowRuleEntity::toFlowRule).collect(Collectors.toList())));
e.printStackTrace();
return false;
}
}
}