赞
踩
公司的项目大部分通过shardingsphere编排治理的配置中心管理数据分片规则和数据源配置,参见:ShardingSphere编排治理。最近有个需求需要做数据迁移,为了确保新数据源没问题采用了双数据源动态切换的灰度方案,但上线当天却因为shardingsphere的自动更新出现了生产故障。。。
shardingsphere版本:4.1.1
因业务合并,原有数据源A的数据需要迁移到数据源B,大概思路如下:
为了稳定性需要支持动态切换以及按租户切换:
首先注册一个动态数据源,根据特定的key访问特定数据源
- // 数据源配置类
- @Configuration
- public class DataSourceConfig {
-
- /**
- * 原编排治理的配置中心的名称
- */
- @Value("${spring.shardingsphere.orchestration-name}")
- private String springShardingsphereOrchestrationName;
- /**
- * 新编排治理的配置中心的名称
- */
- @Value("${spring.shardingsphere.orchestration-name.new}")
- private String newSpringShardingsphereOrchestrationName;
-
- private static final CenterRepositoryConfigurationYamlSwapper CONFIGURATION_YAML_SWAPPER = new CenterRepositoryConfigurationYamlSwapper();
-
- /**
- * 创建数据源
- * @param conf
- * @param name
- * @return
- */
- @SneakyThrows
- public static DataSource createDataSource(YamlCenterRepositoryConfiguration conf, String name) {
- Map<String, CenterConfiguration> instanceConfigurationMap = Maps.newHashMapWithExpectedSize(1);
- instanceConfigurationMap.put(name, CONFIGURATION_YAML_SWAPPER.swap(conf));
- OrchestrationConfiguration orchestrationConfiguration = new OrchestrationConfiguration(instanceConfigurationMap);
- return OrchestrationShardingDataSourceFactory.createDataSource(orchestrationConfiguration);
- }
-
- /**
- * 注册动态数据源
- * @param properties
- * @return
- */
- @Bean
- public DataSource dataSource(SpringBootRootConfigurationProperties properties) {
- log.info("=========初始化灰度数据源开始===========");
- // 初始化新旧数据源
- YamlCenterRepositoryConfiguration conf = properties.getOrchestration().get(this.springShardingsphereOrchestrationName);
- final DataSource oldDataSource = createDataSource(conf, this.springShardingsphereOrchestrationName);
- final DataSource newDataSource = createDataSource(conf, this.newSpringShardingsphereOrchestrationName);
-
- // 初始化动态数据源
- DynamicDataSource dataSource = new DynamicDataSource();
- Map<Object, Object> dataSourceMap = Maps.newHashMap();
- dataSourceMap.put("OLD_SOURCE", oldDataSource);
- dataSourceMap.put("NEW_SOURCE", newDataSource);
- dataSource.setTargetDataSources(dataSourceMap);
- dataSource.setDefaultTargetDataSource(oldDataSource);
- log.info("=========初始化灰度数据源完成===========");
- return dataSource;
- }
- }
自定义的动态数据源类继承AbstractRoutingDataSource,重写获取数据源的方法
- public class DynamicDataSource extends AbstractRoutingDataSource {
-
- @Override
- protected Object determineCurrentLookupKey() {
- String dataSourceKey = DataSourceContextHolder.getDataSourceKey();
- log.info("[determineCurrentLookupKey] dataSourceKey:{}", dataSourceKey);
- return dataSourceKey;
- }
- }
而特定的key则利用ThreadLocal实现动态切换数据源,为了支持异步线程也能继承数据源使用了阿里开源的TransmittableThreadLocal
- public class DataSourceContextHolder {
-
- private static final ThreadLocal<String> DATASOURCE_KEY_HOLDER = new TransmittableThreadLocal<>();
-
- public static void setDataSourceKey(String dataSourceKey) {
- DATASOURCE_KEY_HOLDER.set(dataSourceKey);
- }
-
- public static String getDataSourceKey() {
- return DATASOURCE_KEY_HOLDER.get();
- }
-
- public static void clearDataSourceKey() {
- DATASOURCE_KEY_HOLDER.remove();
- }
- }
通过AOP无侵入增加灰度切换,方便随时变更灰度范围
- @Aspect
- @Component
- @Slf4j
- public class DataSourceGrayHttpAspect {
-
- @Pointcut("execution(* com.web.*.*(..))")
- public void dataSourcePointcut() {
- }
-
- @Before("dataSourcePointcut()")
- public void beforeMethod(JoinPoint joinPoint) {
- Long appId = getAppId();
- // 根据租户计算使用的数据源
- DataSourceContextHolder.setDataSourceKey("NEW_SOURCE");
- }
-
- @After("dataSourcePointcut()")
- public void afterMethod() {
- DataSourceContextHolder.clearDataSourceKey();
- }
- }
这里有个关键点(记住,后面会提!):本来AOP切到具体表的Mapper中,方便控制哪些表参加灰度,但由于获取动态数据源(determineCurrentLookupKey)是在事务开始前获取,若代码里使用了显式事务(如@Transactional ),则在切面之前就确定了数据源,之后的计算已没意义
一切准备就绪后,正式进入正题
由于上面提到的关键点,第一次发版后发现需要改到在Controller层就计算数据源,因此进行第二次发版,此时服务已经同时连接两个数据源,但新数据源并没有任何流量
在发版之前需要在新的shardingsphere编排治理配置中心增加表配置,想着新数据源没有流量就打算直接修改,结果刚刚改完还没开始发布,告警群突然出现大量告警
原来是新配置写错规则了!但当时一脸懵B,还没发布呀 ,怎么就生效了呢?为了及时止血赶紧回滚配置,结果回滚后出现更多的告警。。。
已经痛不欲生的我只好赶紧重启服务,故障才终于解决!
从以上经过可以看出,出现故障的直接原因有两个:
到底配置是怎么实时生效,数据源又为什么会被关闭了呢?带着问题我翻查了sharding官网,结果发现编排治理的配置中心确实是支持动态生效的,参见:ShardingSphere 配置中心说明,那他又是如何实现呢?果然让我找到一个listener调用了dataSource的renew方法
- // org.apache.shardingsphere.shardingjdbc.orchestration.internal.datasource.OrchestrationEncryptDataSource
-
- public class OrchestrationEncryptDataSource extends AbstractOrchestrationDataSource {
-
- /**
- * Renew encrypt rule.
- *
- * @param encryptRuleChangedEvent encrypt configuration changed event
- */
- @Subscribe
- @SneakyThrows
- public final synchronized void renew(final EncryptRuleChangedEvent encryptRuleChangedEvent) {
- dataSource = new EncryptDataSource(
- dataSource.getDataSource(), new EncryptRule(encryptRuleChangedEvent.getEncryptRuleConfiguration()), dataSource.getRuntimeContext().getProperties().getProps());
- }
- }
通过本次debug,刚修改了sharding配置后,sharding会重建所有应用内的dataSource
因此可以解释出为何会配置实时生效,而出现数据源被关闭的原因是新数据源的配置覆盖了原数据源,导致需要访问原数据源时就提示被关闭
由于特殊需求我们自定义了dataSource,其中包含多个sharding dataSource的实例,当配置变更后sharding是无法区分应该重建哪个实例,只能全部重建(本来使用了sharding编排治理的配置中心就可以自定义对应的数据源,可能他们也不知道有这样的场景);但个人认为如果不是使用默认创建的dataSource实例就应该把实时生效的listener关闭掉,避免修改不属于自己范围的内容
清楚了原因后,避免故障的方案有两个:
最终因配置变更频率低,我们采用了方案二
从本次故障得出经验,在使用开源框架时必须多查阅其官网手册,基本可以避免大部分的坑;对于不确定的事情需要多做测试,多想后果,多准备预案,不要等到出现问题时自乱阵脚!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。