文章来源于沈辉
背景
喜马拉雅成立之初,各个业务管理各自的数据库、缓存,各个业务都要理解中间件的各种部署状况,导致业务间的单干,须要运维、开发等方面的人工染指,效率较低,扩大艰难,平安危险也很高,资源利用率也不高。喜马拉雅在倒退中,逐步意识到须要在公司层面,提供对立的定制化的数据拜访平台的重要性。为此,咱们推出了本人的 PaaS 化平台,PaaS 化就是对资源的应用做了对立的入口,业务只须要申请一个资源 ID,就能应用数据库,达到对资源应用的全副系统化,其中对数据库的拜访咱们基于 Apache ShardingSphere 来实现,并基于 Apache ShardingSphere 弱小性能做些优化和加强。
整体架构
咱们 PaaS 平台建设中,负责和数据层通信的 dal 层中间件咱们叫 Arena,其中对数据库的拜访咱们叫 Arena-Jdbc。
Arena-Jdbc 层的能力根本是基于 Apache ShardingSphere 的能力建设,咱们只是基于喜马拉雅须要的个性做了加强和优化,整体架构如下:
Pull Frame
Consul Pull Frame 是咱们对 Consul 的配置主动拉起封装为对立的 Pull 框架,咱们除了数据库,还有缓存,每种还有不同的应用形式,咱们对不同的应用形式只须要实现对应的实现类和初始化,更新,切好这些接口就行,框架会对立把解析好的数据给到,具体一种场景不须要关怀和 Consul 的交互,为前面的资源 PaaS 化提供了简略的接入能力。
故障容灾
· 主动重连
咱们对故障容灾在设计时就思考了平时通用的一些故障场景,比方数据库 server 挂了,咱们做主动重链,不须要业务做重启操作。
· 本地快照
本地快照是为了避免 Consul 不可用时,业务不能启动,所以咱们在拉到近程配置后,会本地存储一份,在拉配置时,如果近程失败,就用本地的配置,保障 Consul 挂了,不影响业务,每次拉到新的配置时,会更新本地的快照。
· 灰度更新
灰度更新是为了反对配置变更时找灰度的逻辑,对于数据库层面的变更,是十分危险的,如果一下就全量变更,有可能会触发线上事变,所以通过灰度变更的机制,业务能够先抉择一个容器实例来变更,没有问题后,再全量变更,把危险降到最低。
· 明码平安
没有 PaaS 化之前,咱们的数据库明码都是 DBA 对立治理的,但 PaaS 化后,拜访数据库的明码就存在配置文件中,如果明文,就太不平安,所以咱们对明码对立做了加密解决,在 Arena-Jdbc 层对立做解密,确保明码不会泄露进来。
对立数据源
为了让业务做最低老本的革新,Arena-Jdbc 须要提供一个对立的数据源,不管下层用什么框架,不影响业务只须要替换数据源接入即可,对于数据库连接池咱们默认应用 HikariCP DataSource 也反对个性化的业务,业务能够通过配置指定连接池。
咱们基于 Apache ShardingSphere 的连接池封装了一个咱们本人的 DataSource,咱们叫 ArenaDataSource,通过 ArenaDataSource 封装了各种不同场景聚合的应用一个 ArenaDataSource 反对三种应用形式:
- 反对原生间接连贯
- 反对 Proxy 模式,也是 Apache ShardingSphere 的 Proxy
- 反对间接连贯分库分表
业务只须要一个 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<String> routeDataSources(final TableRule tableRule, final ShardingStrategy databaseShardingStrategy, final List<ShardingConditionValue> databaseShardingValues) {
// 先判断是否存在 Hint 上下文路由标,如果有, 则优先依据用户指定的规定路由库
Collection<Comparable<?>> databaseShardings = HintManager.getDatabaseShardingValues(tableRule.getLogicTable());
if (databaseShardings != null && databaseShardings.size() > 0) {List<String> 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<String> 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;
}
路由表也是同样的逻辑,咱们重写了 ShardingStandardRoutingEngine 的 routeTables 办法,和下面一样,先从 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<Object> 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 的读写拆散性能,读和写能够配置独立的数据源,咱们只须要在下面做了一层封装,在切换时候动静变更对应的数据源即可,为了切换时不影响业务的流量,咱们是先预热新的数据源,再销毁老的数据源。
架构图如下:
另外咱们也对双写做了钻研和摸索,关键在于数据库的双向同步,基于阿里开源的 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,做报警等后续解决。
其余
咱们除了基于 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 越来越好。
如果大家对 Apache ShardingSphere 有任何疑难或倡议,欢送在 GitHub issue 列表提出,或可返回中文社区交换探讨。
GitHub issue:https://github.com/apache/sha…
奉献指南:https://shardingsphere.apache…
中文社区:https://community.sphere-ex.com/
欢送点击链接,理解更多内容:
Apache ShardingSphere 官网:https://shardingsphere.apache…
Apache ShardingSphere GitHub 地址:https://github.com/apache/sha…
SphereEx 官网:https://www.sphere-ex.com