前段时间大致用了一下sentinel。但是阿里云的sentinel开源并没有实现持久化,针对这个问题我采用了apollo去实现sentinel的持久化。下面就记录一下实现sentinel持久化的过程。
Sentinel 是什么?随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 是面向分布式服务架构的流量控制组件,主要以流量为切入点,从限流、流量整形、熔断降级、系统负载保护、热点防护等多个维度来帮助开发者保障微服务的稳定性。
github项目地址
如何使用sentinel?对于初学者来说,使用sentinel只需要限流功能就可以了,所以在本篇文章中,仅针对SpringCloud项目的限流功能进行阐述。
开启sentinel控制台可以先去官网下载源代码,然后在本地打开sentinel控制台,账号密码都是sentinel,端口是8001(都可以在application配置里面改)
打开后如图所示,因为没有任何服务连接sentinel控制台,所以现在是一片空白。
开启一个项目连接sentinel控制台开启一个SpringCloud项目,写一个最简单的controller
添加sentinel依赖
org.springframework.cloud spring-cloud-starter-alibaba-sentinel 0.9.0.RELEASE
添加application配置
# port settings
server:
port: 8081
spring:
application:
name: sentinel-web-demo
cloud:
sentinel:
transport:
dashboard: localhost:8001 # sentinel客户端服务的地址
eager: true
这时候再登录sentinel就可以看到名为sentinel-web-demo的服务注册上去了
sentinel实现限流如图所示,在流控规则下新增一个流控规则,资源名中填写请求路径,然后设置阈值就可以了。
成功时正常显示,失败时则会输出提示信息。
是的,完成了这步我们就实现了最简单的sentinel控流。
sentinel的持久化很快我们就能发现存在这样的问题,一旦服务器或者客户端重启了,sentinel的控流规则就失效了。这对于一个上线的项目来说无疑是致命的,万一机器出现故障,那么我们大量的sentinel控流规则都会消失。这是因为开源的sentinel控流规则是存储在内存中的,我们需要通过一些方法来实现sentinel的持久化,在这里就只简单说一下使用apollo实现sentinel持久化。
感兴趣的同学可以看一下官方的文档
| 推送模式 | 说明 | 优点 | 缺点 |
|---|---|---|---|
| 原始模式 | API 将规则推送至客户端并直接更新到内存中,扩展写数据源(WritableDataSource) | 简单,无任何依赖 | 不保证一致性;规则保存在内存中,重启即消失。严重不建议用于生产环境 |
| Pull 模式 | 扩展写数据源(WritableDataSource), 客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件 等 | 简单,无任何依赖;规则持久化 | 不保证一致性;实时性不保证,拉取过于频繁也可能会有性能问题。 |
| Push 模式 | 扩展读数据源(ReadableDataSource),规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。生产环境下一般采用 push 模式的数据源。 | 规则持久化;一致性;快速 | 引入第三方依赖 |
pom.xml文件先把Apollo包的test作用域去掉,顺便log4j更新成 2.15(防止恶意代码注入)
log4j log4j 1.2.15 com.ctrip.framework.apollo apollo-openapi 1.2.0
写一个Apollo的config,Converter
public class ApolloConfig {
@Value("${apollo.meta}")
private String portalUrl;
@Value("${apollo.token}")
private String token;
@Bean
public Converter, String> flowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> flowRuleEntityDecoder() {
return s -> JSON.parseArray(s, FlowRuleEntity.class);
}
@Bean
public Converter, String> degradeRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> degradeRuleEntityDecoder() {
return s -> JSON.parseArray(s, DegradeRuleEntity.class);
}
@Bean
public Converter, String> authorityRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> authorityRuleEntityDecoder() {
return s -> JSON.parseArray(s, AuthorityRuleEntity.class);
}
@Bean
public Converter, String> systemRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> systemRuleEntityDecoder() {
return s -> JSON.parseArray(s, SystemRuleEntity.class);
}
@Bean
public Converter, String> paramFlowRuleEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> paramFlowRuleEntityDecoder() {
return s -> JSON.parseArray(s, ParamFlowRuleEntity.class);
}
@Bean
public Converter, String> clusterGroupEntityEncoder() {
return JSON::toJSONString;
}
@Bean
public Converter> clusterGroupEntityDecoder() {
return s -> JSON.parseArray(s, ClusterAppAssignMap.class);
}
@Bean
public ApolloOpenApiClient apolloOpenApiClient() {
ApolloOpenApiClient client = ApolloOpenApiClient.newBuilder()
.withPortalUrl(portalUrl)
.withToken(token)
.build();
return client;
}
}
建一个Apollo的Util类
public final class ApolloConfigUtil {
public static final String FLOW_DATA_ID_POSTFIX = "sentinel-flow-rules";
public static final String DEGRADE_DATA_ID_POSTFIX = "sentinel-degrade-rules";
public static final String PARAM_FLOW_DATA_ID_POSTFIX = "sentinel-param-flow-rules";
public static final String SYSTEM_DATA_ID_POSTFIX = "sentinel-system-rules";
public static final String AUTHORITY_DATA_ID_POSTFIX = "sentinel-authority-rules";
public static final String CLUSTER_GROUP_DATA_ID_POSTFIX = "sentinel-cluster-group-rules";
public static final String NAMESPACE_NAME = "application";
private ApolloConfigUtil() {
}
public static String getClusterGroupDataId(String appName) {
return String.format("%s_%s",appName,CLUSTER_GROUP_DATA_ID_POSTFIX);
}
public static String getFlowDataId(String appName) {
return String.format("%s_%s",appName,FLOW_DATA_ID_POSTFIX);
}
public static String getDegradeDataId(String appName) {
return String.format("%s_%s",appName,DEGRADE_DATA_ID_POSTFIX);
}
public static String getParamFlowDataId(String appName) {
return String.format("%s_%s",appName,PARAM_FLOW_DATA_ID_POSTFIX);
}
public static String getSystemDataId(String appName) {
return String.format("%s_%s",appName,SYSTEM_DATA_ID_POSTFIX);
}
public static String getAuthorityDataId(String appName) {
return String.format("%s_%s",appName,AUTHORITY_DATA_ID_POSTFIX);
}
public static String getNamespaceName(String appName) {
return String.format("%s_%s",appName,NAMESPACE_NAME);
}
}
接下来写一个控流规则的Provider和Publisher
@Component("flowRuleApolloProvider")
public class FlowRuleApolloProvider implements DynamicRuleProvider> {
@Autowired
private ApolloOpenApiClient apolloOpenApiClient;
@Autowired
private Converter> converter;
@Value("${app.id}")
private String appId;
@Value("${spring.profiles.active}")
private String env;
@Value("${apollo.clusterName}")
private String clusterName;
@Value("${apollo.namespaceName}")
private String namespaceName;
@Override
public List getRules(String appName){
String flowDataId = ApolloConfigUtil.getFlowDataId(appName);
OpenNamespaceDTO openNamespaceDTO = apolloOpenApiClient.getNamespace(appId, env, clusterName, namespaceName);
String rules = openNamespaceDTO
.getItems()
.stream()
.filter(p -> p.getKey().equals(flowDataId))
.map(OpenItemDTO::getValue)
.findFirst()
.orElse("");
if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
}
@Component("flowRuleApolloPublisher")
public class FlowRuleApolloPublisher implements DynamicRulePublisher> {
@Autowired
private ApolloOpenApiClient apolloOpenApiClient;
@Autowired
private Converter, String> converter;
@Value("${app.id}")
private String appId;
@Value("${spring.profiles.active}")
private String env;
@Value("${apollo.user}")
private String user;
@Value("${apollo.clusterName}")
private String clusterName;
@Value("${apollo.namespaceName}")
private String namespaceName;
@Override
public void publish(String app, List rules){
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
filterField(rules);
// Increase the configuration
String flowDataId = ApolloConfigUtil.getFlowDataId(app);
OpenItemDTO openItemDTO = new OpenItemDTO();
openItemDTO.setKey(flowDataId);
openItemDTO.setValue(converter.convert(rules));
openItemDTO.setComment("Program auto-join");
openItemDTO.setDataChangeCreatedBy(user);
apolloOpenApiClient.createOrUpdateItem(appId, env, clusterName, namespaceName, openItemDTO);
// Release configuration
NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
namespaceReleaseDTO.setEmergencyPublish(true);
namespaceReleaseDTO.setReleaseComment("Modify or add configurations");
namespaceReleaseDTO.setReleasedBy(user);
namespaceReleaseDTO.setReleaseTitle("Modify or add configurations");
apolloOpenApiClient.publishNamespace(appId, env, clusterName, namespaceName, namespaceReleaseDTO);
// System.out.println("publish");
}
private void filterField(List rules) {
// 对不必要的信息进行过滤
for (FlowRuleEntity rule : rules) {
rule.setGmtCreate(null);
rule.setGmtModified(null);
// rule.setIp(null);git
// rule.setPort(null);
}
}
}
然后就是改造Controller代码,在这里我就只针对FlowControllerV1做修改。
在这之前我们先把刚刚写的provider和publisher注入进Controller
@Autowired
@Qualifier("flowRuleApolloProvider")
private DynamicRuleProvider> ruleProvider;
@Autowired
@Qualifier("flowRuleApolloPublisher")
private DynamicRulePublisher> rulePublisher;
首先是列出所有规则的接口,我们可以直接从apollo中获取规则,这里用的是ruleProvider.getRules(app);
@GetMapping("/rules")
@AuthAction(PrivilegeType.READ_RULE)
public Result> apiQueryMachineRules(@RequestParam String app,
@RequestParam String ip,
@RequestParam 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 = ruleProvider.getRules(app);
repository.saveAll(rules);
if (rules != null && !rules.isEmpty()) {
for (FlowRuleEntity entity : rules) {
entity.setApp(app);
if (entity.getClusterConfig() != null && entity.getClusterConfig().getFlowId() != null) {
entity.setId(entity.getClusterConfig().getFlowId());
}
}
}
return Result.ofSuccess(rules);
} catch (Throwable throwable) {
logger.error("Error when querying flow rules", throwable);
return Result.ofThrowable(-1, throwable);
}
}
然后是修改publishRules,因为我们在每次修改完规则后都会调用这个函数
private CompletableFuturepublishRules(String app, String ip, Integer port) { List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); rulePublisher.publish(app, rules); return sentinelApiClient.setFlowRuleOfMachineAsync(app, ip, port, rules); }
还有一个地方值得注意,如果我们重启后,再新增规则的时候,id会从1开始计算,那这样就有覆盖以前规则的风险。我才用的方法是:新增加一个cnt变量,每次的时候自增cnt变量,从apollo获取cnt时,取的是id的最大值。
entity.setId(repository.nextID());
为InMemoryRuleRepositoryAdapter<>类新增cnt变量,增加一个NextID()函数,用作cnt的自增
private static long cnt = 0;
public Long nextID(){
return ++cnt ;
}
在获取Apollo规则的时候,将cnt换为最大值。
@Override
public List saveAll(List rules) {
// TODO: check here.
allRules.clear();
machineRules.clear();
appRules.clear();
if (rules == null) {
return null;
}
List savedRules = new ArrayList<>(rules.size());
for (T rule : rules) {
savedRules.add(save(rule));
if(rule instanceof FlowRuleEntity) {
cnt=cnt
回到FlowControllerV1中,将接口“rule”的代码entity.setID改为entity.setId(repository.nextID())。
这样我们就实现了sentinel控制台规则的持久化啦!



