记一次shardingsphere自动更新datasource引发的生产故障

背景

公司的项目大部分通过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关闭掉,避免修改不属于自己范围的内容

避免故障

清楚了原因后,避免故障的方案有两个:

  • 每次变更配置均新建sharding编排治理的配置中心,更换nacos名称
  • 魔改sharding代码,禁用动态生效功能

最终因配置变更频率低,我们采用了方案二

总结

从本次故障得出经验,在使用开源框架时必须多查阅其官网手册,基本可以避免大部分的坑;对于不确定的事情需要多做测试,多想后果,多准备预案,不要等到出现问题时自乱阵脚!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值