喜马拉雅基于Apache ShardingSphere实践

背景

喜马拉雅成立之初,各个业务管理各自的数据库、缓存,个业务都要了解中间件的各种部署情况,导致业务间的合作,需要运维、开发等方面的人工介入,效率较低,扩展困难,安全风险也很高,资源利用率也不高。喜马拉雅在发展中,逐渐意识到需要在公司层面,提供统一的定制化的数据访问平台的重要性。为此,我们推出了自己的PaaS化平台,PaaS化就是对资源的使用做了统一的入口,业务只需要申请一个资源ID,就能使用数据库,达到对资源使用的全部系统化,其中对数据库的访问我们基于Apache ShardingSphere来实现,并基于Apache ShardingSphere强大功能做些优化和增强。

整体架构

我们PaaS平台建设中,负责和数据层通信的dal层中间件我们叫Arena,其中对数据库的访问我们叫Arean-Jdbc

Arena-Jdbc 层的能力基本是基于Apache ShardingSphere的能力建设,我们只是基于喜马拉雅需要的特性做了增强和优化,整体架构如下:

arena-jdbc-arch.png

Pull Frame

Consul Pull Frame 是我们对Consul 的配置自动拉起封装为统一的Pull框架,我们除了数据库,还有缓存,每种还有不同的使用方式,我们对不同的使用方式只需要实现对应的实现类和初始化,更新,切好这些接口就行,框架会统一把解析好的数据给到,具体一种场景不需要关心和Consul的交互,为后面的资源PaaS化提供了简单的接入能力。

故障容灾

  • 自动重连
    我们对故障容灾在设计时就考虑了平时通用的一些故障场景,比如数据库server 挂了,我们最做自动重链,不需要业务做重启操作。
  • 本地快照
    本地快照是为了防止Consul不可用时,业务不能启动,所以我们在拉到远程配置后,会本地存储一份,在拉配置时,如果远程失败,就用本地的配置,保证Consul挂了,不影响业务,每次拉到新的配置时,会更新本地的快照。
  • 灰度更新
    灰度更新是为了支持配置变更时找灰度的逻辑,对于数据库层面的变更,是非常危险的,如果一下就全量变更,有可能会触发线上事故,所以通过灰度变更的机制,业务可以先选择一个容器实例来变更,没有问题后,再全量变更,把风险降到最低。
  • 密码安全
    没有PaaS化之前,我们的数据库密码都是dba统一管理的,但PaaS化后,访问数据库的密码就存在配置文件中,如果明文,就太不安全,所以我们对密码统一做了加密处理,在Arean-Jdbc层统一做解密,确保密码不回泄露出去。

统一数据源

为了让业务做最低成本的改造,Arena-jdbc 需要提供一个统一的数据源,不论上层用什么框架,不影响,业务只需要替换数据源接入即可,对于数据库连接池我们默认使用HikariCP DataSource也支持个性化的业务,业务可以通过配置指定连接池。

我们基于Apache ShardingSphere的连接池封装了一个我们自己的DataSource,我们叫ArenaDataSource,通过ArenaDataSource封装了各种不同场景聚合,的使用一个ArenaDataSource支持三种使用方式:

  1. 支持原生直接连接
  2. 支持Proxy模式,也是Apache ShardingSphere的proxy
  3. 支持直接连接分库分表

业务只需要一个DataSource,即支持分库分表,也支持简单的直接连接的模式,这样的好处是业务以后要分库分表,就不再需要升级中间件了,为了彻底解决业务升级的成本,我们做了配置自动升级,就是你之前是简单直接链接使用,为了PaaS化,后来业务发展了,需要分库分表了,以及从分库分表需要多活部署了,这些都不要再升级依赖了,只需要配置动即可。

资源动态变更

资源动态变更是PaaS平台基本的能力,接入PaaS后,业务修改数据库的任何属性,都不再需要业务方代码变更,重新发布

Apache ShardingSphere也支持数据库属性的动态变更,我们基于自己的内部系统的特征,实现了基于Consul的资源变根通知。我们的资源存在Consul。

Arena-Jdbc支持对使用的资源做无损的变更,Arena-Jdbc 收到资源变更时,会先对新下发的资源做预热处理,预热后,再切换使用的数据源,切换成功后,再销毁老的数据源,业务无感知。

如果新的资源预热失败,则不会做变更处理,保证下发的资源时可用的,规避错误下发的问题。

扩容和缩容也是同理,一期数据需要运维手动迁移,迁移好了后,直接在PaaS平台下发新的配置即可,二期支持自动迁移数据和配置变更结合。

同时支持Proxy的无损上下线机制,通过PaaS平台对Proxy的变更,把需要下线的Proxy节点去掉,通知Arena-Jdbc,Arena-Jdbc会把缩容的Proxy节点去掉,做到无损下线。

读写分离

读写分离我们完全基于Apache ShardingSphere的来实现,我们根据喜马拉雅业务的特性,对强制路由做了增强,不需要规则配置为Hint模式,只要线程上下文带有强制路由的标志,就可以路由到指定的库和表,不受分表规则的影响,我们重写了ShardingStandardRoutingEngine的Sharding时路由库和表的逻辑:

private Collection routeDataSources(final TableRule tableRule, final ShardingStrategy databaseShardingStrategy, final List databaseShardingValues) {
        //先判断是否存在Hint上下文路由标,如果有,则优先根据用户指定的规则路由库
        Collection> databaseShardings = HintManager.getDatabaseShardingValues(tableRule.getLogicTable());
        if (databaseShardings != null && databaseShardings.size() > 0) {
            List list = new ArrayList<>(4);
            for (Comparable databaseSharding : databaseShardings) {
                list.add((String) databaseSharding);
            }
            if (log.isDebugEnabled()) {
                log.debug("route dataSources, find HintManager, so hint to: {}", list);
            }
            return list;
        }
        //没有Hint路由规则,则按Sharding 规则路由
        if (databaseShardingValues.isEmpty()) {
            return tableRule.getActualDatasourceNames();
        }
        Collection result = new LinkedHashSet<>(databaseShardingStrategy.doSharding(tableRule.getActualDatasourceNames(), databaseShardingValues, properties));
        Preconditions.checkState(!result.isEmpty(), "no database route info");
        Preconditions.checkState(tableRule.getActualDatasourceNames().containsAll(result), 
                "Some routed data sources do not belong to configured data sources. routed data sources: `%s`, configured data sources: `%s`", result, tableRule.getActualDatasourceNames());
        return result;
    }

路由表也是同样的逻辑,我们重写了ShardingStandardRoutingEnginerouteTables方法,和上面一样。先从Hint 的上下文获取。这样通过上下文的方式能很好的满足业务个性化的路由规则,能和Sharding 规则共存。

Database Plus

Apache ShardingSphere 除了提供基本的分库分表,读写分离的能力外,在上层还提供了很多的插件和扩展的机制,这让我们在基于数据库提供更偏向业务的起的能力非常容易,成本非常低,这叫Database Plus

Database Plus简单的说就是你用Apache ShardingSphere的数据库中间件,不仅仅是提供了分库分表这一基本能力,通过对底层数据的封装为统一的交互标准插件模式,可以在上面实现很多业务的通用的场景的需求,比如喜马拉雅除了用到Apache ShardingSphere 基础的能力外,我们也享受了Database Plus 的威力,我们在它的基础上轻松实现了支持压测的影子库和影子表,数据加解密,机房级别容灾的同城双读,分布式唯一ID

影子库和影子表

影子库影子表我们对Apache ShardingSphere做了改动,Apache ShardingSphere 需要修改sql,我们认为对业务有改造成本,同时结合我们自己的压测平台,我们和业界一样,我们也实现了影子标记,通过全链路压测标的传递来判断是否路由到影子库/影子表,业务无需任何改造,即可使用影子库影子表来做压测,同时不需要在运行时对sql改写,提升了性能,我们重写了ShadowSQLRouter,

public class ArenaShadowSQLRouter extends ShadowSQLRouter {

    @Override
    public boolean isShadow(final SQLStatementContext sqlStatementContext, final List parameters, final ShadowRule rule) {
        if (sqlStatementContext instanceof InsertStatementContext || sqlStatementContext instanceof WhereAvailable
            || sqlStatementContext instanceof UpdateStatementContext) {
                 //这里就是判断是否有压测标,如果有,ShardingSphere则会找影子的逻辑。
                return ArenaUtilities.checkPeakRequest();
        }

        return false;

    }
}
 
 

通过spi的方式把我们自定义的ArenaShadowSQLRouter 给Apache ShardingSphere加载使用,不得不说Apache ShardingSphere的插件设计很赞,很方便自定义和扩展。

配置还是和Apache ShardingSphere的一样:

# 配置影子库规则
- !SHADOW
    # true-影子表,false-影子库(默认)
    enableShadowTable: true 
    # 源库名称(对应DataSources数据源配置中的名称),影子库才需要配,影子表不需要配置
    sourceDataSourceNames:  
      - ds0           # 源库,与影子库shadow_ds0对应
      - ds1           # 源库,与影子库shadow_ds1对应
    # 影子库名称(对应dataSources数据源配置中的名称),影子库才需要配,影子表不需要配置
    shadowDataSourceNames:  
      - shadow_ds0    # 影子库,与源库ds0对应
      - shadow_ds1    # 影子库,与源库ds1对应

enableShadowTable 我们新增了该属性,来确定是使用影子库还是影子表。

影子库:

影子库,一定要填sourceDataSourceNames和shadowDataSourceNames,enableShadowTable不用设置,或者设置为false。
sourceDataSourceNames
按顺序映射,一一对应:
ds --> shadow_ds
ds1--> shadow_ds1

影子库/影子表是最后一个路由规则,如果发现有影子库/影子表,则根据实际的库找到对应的影子库/影子表,执行sql

同城多活

基于喜马拉雅的业务特性,读多写少,我们只实现了对读业务的双机房部署,写业务还是路由到主机房。

为了增强容灾能力,喜马拉雅搭建了双机房,同时承载业务流量,当一个机房故障时,可以把流量快速切换到另一个机房,我们在dal层设计上支持双写,这
里充分利用了Apache ShardingSphere的读写分离功能,读和写可以配置独立的数据源,我们只需要在上面做了一层封装,在切换时候动态变更对应的数据源即可,为了切换时不影响业务的流量,我们是先预热新的数据源,再销毁老的数据源。
架构图如下:

b276cfc1-a4e0-4f13-9377-22cdd4775523.png

另外我们也对双写做了研究和探索,关键在于数据库的双向同步,基于阿里开源的otter做了改造,支持基于gtid模式同步,不依赖打标,打标回有性能开销,在一些业务做了试用。

分布式唯一ID

分库分表后,唯一id是必须要满足的需求,Apache ShardingSphere默认提供了snowfake和uuid 算法,但不是很时候db的场景,db需要保证顺序和格式,所以我们基于Apache ShardingSphere提供的接口,也实现自己的唯一id生成策略:

数据分片后,不同Mysql实例生成全局唯一主键是非常棘手的问题。Arena-Jdbc实现了Apache ShardingSphere的分布式主键生成器接口,通过集成喜马拉雅内部的全局唯一id生成服务,提供了适用于喜马拉雅内部的自增主键生成算法-BoushId主键生成策略。

监控和报警:

做一个数据库中间件,监控是必不可少的部分,就像我们的眼睛,没有监控就是瞎抓,以及对异常情况的报警也是非常重要的部分,只有完善的监控和报警才能算是一个完整的产品,得益于Apache ShardingSphere在设计时就提供了钩子,我们能非常小的成本就能实现对sql层面的监控和报警。

Arena-Jdbc客户端通过钩子回调,从多维度数据来分析使用数据库的运行情况,以30s为一次统计周期,每个周期统计的数据包括:Mysql总请求量,新增、删除、修改和查询的请求量,失败的请求量和慢请求量,影子库的流量,以及统计响应时间的TP百分比,还有连接池的等待时间、建连时间、连接数等信息。这些指标会发送给专门的收集指标服务,并持久化到时序数据库,PaaS平台可以从时序数据库中查询数据,展示给各个业务,对于异常sql和慢sql,做报警等后续处理。

image.png

其他

我们除了基于Apache ShardingSphere实现上述关键特性外,我们还对Apache ShardingSphere做了一些优化和改进,以更适合喜马拉雅的业务。

  • 优化分片规则,启动时,如果分片的真实表不存在的情况则报错,将配置错误前置

  • 有的业务方有几百,甚至几千的分表,这种情况下,由于Apache ShardingSphere中的联邦查询需要依次扫表,启动速度很慢,达到了分钟级别。针对这种情况,我们新增了props配置项,不再初始化联邦查询,打打加快了启动速度,并且再使用中,也没有用联邦查询

  • 优化了Apache ShardingSphere,执行sql异常不报错误的情况

  • 由于有的业务方,对重要的表采用了大写的表名和列名,我们去掉Apache ShardingSphere中,对配置中的大写的表名列名强制小写的情况,允许大写的表名和列名

  • 新增了props配置项,可以调节Apache ShardingSphere的编译缓存的大小

  • 优化Apache ShardingSphere复合分片算法,精确匹配分片字段
    在ComplexShardingStrategyConfiguration中,添加shardingColumnList字段:
    修复Apache ShardingSphere,批量insert,不返回主键的问题,这个问题在mybatis-plus中比较常见

  • 不分片的表,支持使用默认的主键id生成策略

总结

基于Apache ShardingSphere实现的数据库中间件Arena-Jdbc,经过半年的时间,已经覆盖了喜马拉雅的70%的核心业务,目前没有发现任何问题,表现的非常稳定,通过和我们的PaaS平台结合,业务也非常愿意接入,另外我们使用Apache ShardingSphere时,社区还没有发布stable的版本,所以我们在使用过程中也遇到了些问题,基本上我们都解决了,有的社区也有对应的解决方案,得益于社区非常活跃,我们以后也希望把我们做的一些feature能回馈到社区,为Apache ShardingSphere的发展做出一点点小贡献。

另外非常感谢亮哥亲自来喜马拉雅对Apache ShardingSphere的技术内幕和规则做了一次全面的分享,非常关心我们在使用Apache ShardingSphere过程中遇到的问题,在现场对小伙伴提的问题都一一作了 深入的解答,非常感谢亮哥,祝Apache ShardingSphere越来越好。

你可能感兴趣的:(喜马拉雅基于Apache ShardingSphere实践)