接上篇Sentinel集群限流探索,上次简单提到了集群限流的原理,然后用官方给的 demo 简单修改了一下,可以正常运行生效。

这一次需要更进一步,基于 Sentinel 实现内嵌式集群限流的高可用方案,并且包装成一个中间件 starter 提供给三方使用。

对于高可用,我们主要需要解决两个问题,这无论是使用内嵌或者独立模式都需要解决的问题,相比而言,内嵌式模式更简单一点。

  1. 集群 server 自动选举
  2. 自动故障转移
  3. Sentinel-Dashboard持久化到Apollo

集群限流

首先,考虑到大部分的服务可能都不需要集群限流这个功能,因此实现一个注解用于手动开启集群限流模式,只有开启注解的情况下,才去实例化集群限流的 Bean 和限流数据。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({EnableClusterImportSelector.class})
@Documented
public @interface SentinelCluster {
} public class EnableClusterImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
return new String[]{ClusterConfiguration.class.getName()};
}
}

这样写好之后,当扫描到有我们的 SentinelCluster 注解的时候,就会去实例化 ClusterConfiguration

@Slf4j
public class ClusterConfiguration implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private Environment environment; @Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ClusterManager.class);
beanDefinitionBuilder.addConstructorArgValue(this.environment);
registry.registerBeanDefinition("clusterManager", beanDefinitionBuilder.getBeanDefinition());
} @Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } @Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
}

在配置中去实例化用于管理集群限流的ClusterManager,这段逻辑和我们之前文章中使用到的一般无二,注册到ApolloDataSource之后自动监听Apollo的变化达到动态生效的效果。

@Slf4j
public class ClusterManager {
private Environment environment;
private String namespace;
private static final String CLUSTER_SERVER_KEY = "sentinel.cluster.server"; //服务集群配置
private static final String DEFAULT_RULE_VALUE = "[]"; //集群默认规则
private static final String FLOW_RULE_KEY = "sentinel.flow.rules"; //限流规则
private static final String DEGRADE_RULE_KEY = "sentinel.degrade.rules"; //降级规则
private static final String PARAM_FLOW_RULE_KEY = "sentinel.param.rules"; //热点限流规则
private static final String CLUSTER_CLIENT_CONFIG_KEY = "sentinel.client.config"; //客户端配置 public ClusterManager(Environment environment) {
this.environment = environment;
this.namespace = "YourNamespace";
init();
} private void init() {
initClientConfig();
initClientServerAssign();
registerRuleSupplier();
initServerTransportConfig();
initState();
} private void initClientConfig() {
ReadableDataSource<String, ClusterClientConfig> clientConfigDs = new ApolloDataSource<>(
namespace,
CLUSTER_CLIENT_CONFIG_KEY,
DEFAULT_SERVER_VALUE,
source -> JacksonUtil.from(source, ClusterClientConfig.class)
);
ClusterClientConfigManager.registerClientConfigProperty(clientConfigDs.getProperty());
} private void initClientServerAssign() {
ReadableDataSource<String, ClusterClientAssignConfig> clientAssignDs = new ApolloDataSource<>(
namespace,
CLUSTER_SERVER_KEY,
DEFAULT_SERVER_VALUE,
new ServerAssignConverter(environment)
);
ClusterClientConfigManager.registerServerAssignProperty(clientAssignDs.getProperty());
} private void registerRuleSupplier() {
ClusterFlowRuleManager.setPropertySupplier(ns -> {
ReadableDataSource<String, List<FlowRule>> ds = new ApolloDataSource<>(
namespace,
FLOW_RULE_KEY,
DEFAULT_RULE_VALUE,
source -> JacksonUtil.fromList(source, FlowRule.class));
return ds.getProperty();
});
ClusterParamFlowRuleManager.setPropertySupplier(ns -> {
ReadableDataSource<String, List<ParamFlowRule>> ds = new ApolloDataSource<>(
namespace,
PARAM_FLOW_RULE_KEY,
DEFAULT_RULE_VALUE,
source -> JacksonUtil.fromList(source, ParamFlowRule.class)
);
return ds.getProperty();
});
} private void initServerTransportConfig() {
ReadableDataSource<String, ServerTransportConfig> serverTransportDs = new ApolloDataSource<>(
namespace,
CLUSTER_SERVER_KEY,
DEFAULT_SERVER_VALUE,
new ServerTransportConverter(environment)
); ClusterServerConfigManager.registerServerTransportProperty(serverTransportDs.getProperty());
} private void initState() {
ReadableDataSource<String, Integer> clusterModeDs = new ApolloDataSource<>(
namespace,
CLUSTER_SERVER_KEY,
DEFAULT_SERVER_VALUE,
new ServerStateConverter(environment)
); ClusterStateManager.registerProperty(clusterModeDs.getProperty());
}
}

这样的话,一个集群限流的基本功能已经差不多是OK了,上述步骤都比较简单,按照官方文档基本都能跑起来,接下来要实现文章开头提及到的核心的几个功能了。

自动选举&故障转移

自动选举怎么实现?简单点,不用考虑那么多,每台机器启动成功之后直接写入到 Apollo 当中,第一个写入成功的就是 Server 节点。

这个过程为了保证并发带来的问题,我们需要加锁确保只有一台机器成功写入自己的本机信息。

由于我使用 Eureka 作为注册中心,Eureka 又有CacheRefreshedEvent本地缓存刷新的事件,基于此每当本地缓存刷新,我们就去检测当前 Server 节点是否存在,然后根据实际情况去实现选举。

首先在 spring.factories 中添加我们的监听器。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.test.config.SentinelEurekaEventListener

监听器只有当开启了集群限流注解SentinelCluster之后才会生效。

@Configuration
@Slf4j
@ConditionalOnBean(annotation = SentinelCluster.class)
public class SentinelEurekaEventListener implements ApplicationListener<CacheRefreshedEvent> {
@Resource
private DiscoveryClient discoveryClient;
@Resource
private Environment environment;
@Resource
private ApolloManager apolloManager; @Override
public void onApplicationEvent(EurekaClientLocalCacheRefreshedEvent event) {
if (!leaderAlive(loadEureka(), loadApollo())) {
boolean tryLockResult = redis.lock; //redis或者其他加分布式锁
if (tryLockResult) {
try {
flush();
} catch (Exception e) {
} finally {
unlock();
}
}
}
} private boolean leaderAlive(List<ClusterGroup> eurekaList, ClusterGroup server) {
if (Objects.isNull(server)) {
return false;
}
for (ClusterGroup clusterGroup : eurekaList) {
if (clusterGroup.getMachineId().equals(server.getMachineId())) {
return true;
}
}
return false;
}
}

OK,其实看到代码已经知道我们把故障转移的逻辑也实现了,其实道理是一样的。

第一次启动的时候 Apollo 中的 server 信息是空的,所以第一台加锁写入的机器就是 server 节点,后续如果 server 宕机下线,本地注册表缓存刷新,对比 Eureka 的实例信息和 Apollo 中的 server,如果 server 不存在,那么就重新执行选举的逻辑。

需要注意的是,本地缓存刷新的时间极端情况下可能会达到几分钟级别,那么也就是说在服务下线的可能几分钟内没有重新选举出新的 server 节点整个集群限流是不可用的状态,对于业务要求非常严格的情况这个方案就不太适用了。

对于 Eureka 缓存时间同步的问题,可以参考之前的文章Eureka服务下线太慢,电话被告警打爆了

Dashboard持久化改造

到这儿为止,我们已经把高可用方案实现好了,接下来最后一步,只要通过 Sentinel 自带的控制台能够把配置写入到 Apollo 中,那么应用就自然会监听到配置的变化,达到动态生效的效果。

根据官方的描述,官方已经实现了FlowControllerV2用于集群限流,同时在测试目录下有简单的案例帮助我们快速实现控制台的持久化的逻辑。

我们只要实现DynamicRuleProvider,同时注入到Controller中使用即可,这里我们实现flowRuleApolloProvider用于提供从Apollo查询数据,flowRuleApolloPublisher用于写入限流配置到Apollo。

@RestController
@RequestMapping(value = "/v2/flow")
public class FlowControllerV2 {
private final Logger logger = LoggerFactory.getLogger(FlowControllerV2.class); @Autowired
private InMemoryRuleRepositoryAdapter<FlowRuleEntity> repository; @Autowired
@Qualifier("flowRuleApolloProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
@Qualifier("flowRuleApolloPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher; }

实现方式很简单,provider 通过 Apollo 的 open-api 从 namespace 中读取配置,publisher 则是通过 open-api 写入规则。

@Component("flowRuleApolloProvider")
public class FlowRuleApolloProvider implements DynamicRuleProvider<List<FlowRuleEntity>> { @Autowired
private ApolloManager apolloManager;
@Autowired
private Converter<String, List<FlowRuleEntity>> converter; @Override
public List<FlowRuleEntity> getRules(String appName) {
String rules = apolloManager.loadNamespaceRuleList(appName, ApolloManager.FLOW_RULES_KEY); if (StringUtil.isEmpty(rules)) {
return new ArrayList<>();
}
return converter.convert(rules);
}
} @Component("flowRuleApolloPublisher")
public class FlowRuleApolloPublisher implements DynamicRulePublisher<List<FlowRuleEntity>> { @Autowired
private ApolloManager apolloManager;
@Autowired
private Converter<List<FlowRuleEntity>, String> converter; @Override
public void publish(String app, List<FlowRuleEntity> rules) {
AssertUtil.notEmpty(app, "app name cannot be empty");
if (rules == null) {
return;
}
apolloManager.writeAndPublish(app, ApolloManager.FLOW_RULES_KEY, converter.convert(rules));
}
}

ApolloManager实现了通过open-api查询和写入配置的能力,使用需要自行配置 Apollo Portal 地址和 token,这里不赘述,可以自行查看 Apollo 的官方文档。

@Component
public class ApolloManager {
private static final String APOLLO_USERNAME = "apollo";
public static final String FLOW_RULES_KEY = "sentinel.flow.rules";
public static final String DEGRADE_RULES_KEY = "sentinel.degrade.rules";
public static final String PARAM_FLOW_RULES_KEY = "sentinel.param.rules";
public static final String APP_NAME = "YourAppName"; @Value("${apollo.portal.url}")
private String portalUrl;
@Value("${apollo.portal.token}")
private String portalToken;
private String apolloEnv;
private String apolloCluster = "default";
private ApolloOpenApiClient client; @PostConstruct
public void init() {
this.client = ApolloOpenApiClient.newBuilder()
.withPortalUrl(portalUrl)
.withToken(portalToken)
.build();
this.apolloEnv = "default";
} public String loadNamespaceRuleList(String appName, String ruleKey) {
OpenNamespaceDTO openNamespaceDTO = client.getNamespace(APP_NAME, apolloEnv, apolloCluster, "default");
return openNamespaceDTO
.getItems()
.stream()
.filter(p -> p.getKey().equals(ruleKey))
.map(OpenItemDTO::getValue)
.findFirst()
.orElse("");
} public void writeAndPublish(String appName, String ruleKey, String value) {
OpenItemDTO openItemDTO = new OpenItemDTO();
openItemDTO.setKey(ruleKey);
openItemDTO.setValue(value);
openItemDTO.setComment("Add Sentinel Config");
openItemDTO.setDataChangeCreatedBy(APOLLO_USERNAME);
openItemDTO.setDataChangeLastModifiedBy(APOLLO_USERNAME);
client.createOrUpdateItem(APP_NAME, apolloEnv, apolloCluster, "default", openItemDTO); NamespaceReleaseDTO namespaceReleaseDTO = new NamespaceReleaseDTO();
namespaceReleaseDTO.setEmergencyPublish(true);
namespaceReleaseDTO.setReleasedBy(APOLLO_USERNAME);
namespaceReleaseDTO.setReleaseTitle("Add Sentinel Config Release");
client.publishNamespace(APP_NAME, apolloEnv, apolloCluster, "default", namespaceReleaseDTO);
} }

对于其他规则,比如降级、热点限流都可以参考此方式去修改,当然控制台要做的修改肯定不是这一点点,比如集群的flowId默认使用的单机自增,这个肯定需要修改,还有页面的传参、查询路由的修改等等,比较繁琐,就不在此赘述了,总归也就是工作量的问题。

好了,本期内容就这些,我是艾小仙,我们下期见。

从-99打造Sentinel高可用集群限流中间件的更多相关文章

  1. 打造kubernetes 高可用集群(nginx+keepalived)

    一.添加master 部署高可用k8s架构 1.拷贝/opt/kubernetes目录到新的master上(注意如果新机上部署了etcd要排除掉) scp -r /opt/kubernetes/ ro ...

  2. sentinel监控redis高可用集群(一)

    一.首先配置redis的主从同步集群. 1.主库的配置文件不用修改,从库的配置文件只需增加一行,说明主库的IP端口.如果需要验证的,也要加多一行,认证密码. slaveof 192.168.20.26 ...

  3. (六) Docker 部署 Redis 高可用集群 (sentinel 哨兵模式)

    参考并感谢 官方文档 https://hub.docker.com/_/redis GitHub https://github.com/antirez/redis happyJared https:/ ...

  4. 【转】harbor仓库高可用集群部署说明

    之前介绍Harbor私有仓库的安装和使用,这里重点说下Harbor高可用集群方案的部署,目前主要有两种主流的Harbor高可用集群方案:1)双主复制:2)多harbor实例共享后端存储. 一.Harb ...

  5. LVS+Keepalived实现高可用集群

    LVS+Keepalived实现高可用集群来源: ChinaUnix博客 日期: 2009.07.21 14:49 (共有条评论) 我要评论 操作系统平台:CentOS5.2软件:LVS+keepal ...

  6. linux高可用集群(HA)原理详解(转载)

    一.什么是高可用集群 高可用集群就是当某一个节点或服务器发生故障时,另一个 节点能够自动且立即向外提供服务,即将有故障节点上的资源转移到另一个节点上去,这样另一个节点有了资源既可以向外提供服务.高可用 ...

  7. activemq+Zookeper高可用集群方案配置

    在高并发.对稳定性要求极高的系统中,高可用的是必不可少的,当然ActiveMQ也有自己的集群方案.从ActiveMQ 5.9开始,ActiveMQ的集群实现方式取消了传统的Master-Slave方式 ...

  8. Centos7.5基于MySQL5.7的 InnoDB Cluster 多节点高可用集群环境部署记录

    一.   MySQL InnoDB Cluster 介绍MySQL的高可用架构无论是社区还是官方,一直在技术上进行探索,这么多年提出了多种解决方案,比如MMM, MHA, NDB Cluster, G ...

  9. LVS-Keepalived高可用集群(NAT)

    LEA-6-LVS-NAT+Keepalived高可用集群-------client-----------------主LVS-----------------从LVS---------------- ...

随机推荐

  1. 斯坦福NLP课程 | 第15讲 - NLP文本生成任务

    作者:韩信子@ShowMeAI,路遥@ShowMeAI,奇异果@ShowMeAI 教程地址:http://www.showmeai.tech/tutorials/36 本文地址:http://www. ...

  2. 降维、特征提取与流形学习--非负矩阵分解(NMF)

    非负矩阵分解(NMF)是一种无监督学习算法,目的在于提取有用的特征(可以识别出组合成数据的原始分量),也可以用于降维,通常不用于对数据进行重建或者编码. NMF将每个数据点写成一些分量的加权求和(与P ...

  3. Tarjan入门

    Tarjan系列!我愿称Tarjan为爆搜之王! 1.Tarjan求LCA 利用并查集在一遍DFS中可以完成所所有询问.是一种离线算法. 遍历到一个点时,我们先将并查集初始化,再遍历完一个子树之后,将 ...

  4. CF1681F Unique Occurrences

    题意:一棵树,问每条路径上只出现一次的值的个数的和. 思路: 显然想到考虑边贡献.每条边权下放到下面的哪个点.\(up_i\)为上面第一个点权等于它的点.我们需要一个子树内点权等于它的点(如果满足祖孙 ...

  5. 用树莓派USB摄像头做个监控

    [前言] 看着阴暗的角落里吃灰噎到嗓子眼的树莓派,一起陪伴的时光历历在目,往事逐渐涌上心头,每每触及此处,内心总会升腾起阵阵怜悯之情... 我这有两个设备,一个是积灰已久的树莓派,另一个是积灰已久的U ...

  6. 聊聊消息中间件(1),AMQP那些事儿

    开篇 说到消息队列,相信大家并不陌生.大家在日常的工作中其实都有用过.相信大部分的研发在使用消息队列的过程中也仅仅是停留在用上面,里面的知识点掌握得并不是很系统,有部分强大的功能可能由于本身公司的业务 ...

  7. SSMS设置为深色模式

    更新记录 2022年4月16日:本文迁移自Panda666原博客,原发布时间:2022年2月8日. 2022年4月16日:SSMS很好用,但现在我更多使用DataGrip了. 2022年6月11日:S ...

  8. 浅析DispatchProxy动态代理AOP

    浅析DispatchProxy动态代理AOP(代码源码) 最近学习了一段时间Java,了解到Java实现动态代理AOP主要分为两种方式JDK.CGLIB,我之前使用NET实现AOP切面编程,会用Fil ...

  9. 2 万字 + 30 张图 | 细聊 MySQL undo log、redo log、binlog 有什么用?

    作者:小林coding 计算机八股文网站:https://xiaolincoding.com/ 大家好,我是小林. 从这篇「执行一条 SQL 查询语句,期间发生了什么?」中,我们知道了一条查询语句经历 ...

  10. Vue最新防抖方案

    函数防抖(debounce):当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时.举个栗子,持续触发scroll事件时,并 ...