前言
随着得物 App 用户开始快速增长,业务线日趋丰富,也对底层数据库带来了较大的压力。各个业务线对于数据分片、读写分离、影子库路由等等的需求成为了刚需,所以需要一个统一的中间件来支撑这些需求,得物“彩虹桥”应运而生。
在北欧神话中,彩虹桥是连结阿斯加德( Asgard )【1】和 米德加尔特(中庭/ Midgard )的巨大彩虹桥。我们可以把它当作是“九界之间”的连接通道,也是进入阿斯加德的唯一稳定入口。而得物的彩虹桥是连接服务与数据库之间的数据库中间层处理中间件,可以说得物的每一笔订单都与它息息相关。
1. 技术选型
前期我们调研了 Mycat、ShardingSphere、kingshard、Atlas 等开源中间件,综合了适用性、优缺点、产品口碑、社区活跃度、实战案例、扩展性等多个方面,最终我们选择了 ShardingSphere 。准备在 ShardingSphere 的基础上进行二次开发、定制一套适合得物内部环境的数据库中间件。
Apache ShardingSphere 是一款开源分布式数据库生态项目,由 JDBC、Proxy 和 Sidecar(规划中) 3 款产品组成。其核心采用可插拔架构,通过组件扩展功能。对上以数据库协议及 SQL 方式提供诸多增强功能,包括数据分片、访问路由、数据安全等;对下原生支持 MySQL、PostgreSQL、SQL Server、Oracle 等多种数据存储引擎。ShardingSphere 已于2020年4月16日成为 Apache 软件基金会的顶级项目,并且在全球多个国家都有团队在使用。
目前我们主要是 ShardingSphere的Proxy 模式提供服务,后续将会在 JDBC&Proxy 混合架构继续探索。
2.彩虹桥目前的能力
其中白色模块为现阶段以及具备的能力,绿色模块为规划&正在做的功能。下面介绍一下几个重点功能。注意,以下功能都是基于 Proxy 模式。
2.1 数据分片
数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果。数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效地避免由数据量超过可承受阈值而产生的查询瓶颈。除此之外,分库还能够用于有效地分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。使用多主多从的分片方式,可以有效地避免数据单点,从而提升数据架构的可用性。
通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。数据分片的拆分方式又分为垂直分片和水平分片。
按照业务拆分的方式称为垂直分片,又称为纵向拆分,它的核心理念是专库专用。彩虹桥主要提供的分片能力是水平分片,水平分片又称为横向拆分。相对于垂直分片,它不再将数据根据业务逻辑分类,而是通过某个字段(或某几个字段),根据某种规则将数据分散至多个库或表中,每个分片仅包含数据的一部分。例如:根据主键分片,偶数主键的记录放入 0 库(或表),奇数主键的记录放入 1 库(或表),如下图所示。
水平分片从理论上突破了单机数据量处理的瓶颈,并且扩展相对自由,是数据分片的标准解决方案。
当然实际使用场景的分片规则是非常复杂的,我们提供一些内置算法比如取模、HASH 取模、自动时间段分片算法、Inline 表达式等。当内置算法无法满足要求时,还可以基于 groovy 来定制专属的分片逻辑。
2.2 读写分离
面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。通过一主多从的配置方式,可以将查询请求均匀地分散到多个数据副本,能够进一步地提升系统的处理能力。
与将数据根据分片键打散至各个数据节点的水平分片不同,读写分离则是根据 SQL 语义的分析,将读操作和写操作分别路由至主库与从库。
这里配置的方式比较简单,给目标主库绑定一个或多个从库、设置对应的负载均衡算法即可。
这里的实现方式就是通过 SQL 解析,把查询语句路由到对应的从库即可,但是在一些对主从同步延迟比较敏感的场景,可能需要强制走主库,这里我们也提供一个 API(原理就是 SQL Hint),让上游可以指定某些模块读强制走主,还有相关全局配置可以让事务内所有读请求全部走主。
2.3 影子库压测
在基于微服务的分布式应用架构下,业务需要多个服务是通过一系列的服务、中间件的调用来完成,所以单个服务的压力测试已无法代表真实场景。在测试环境中,如果重新搭建一整套与生产环境类似的压测环境,成本过高,并且往往无法模拟线上环境的复杂度以及流量。因此,业内通常选择全链路压测的方式,即在生产环境进行压测,这样所获得的测试结果能够准确地反应系统真实容量和性能水平。
全链路压测是一项复杂而庞大的工作。需要各个微服务、中间件之间配合与调整,以应对不同流量以及压测标识的透传。通常会搭建一整套压测平台以适用不同测试计划。在数据库层面需要做好数据隔离,为了保证生产数据的可靠性与完整性,需要将压测产生的数据路由到压测环境数据库,防止压测数据对生产数据库中真实数据造成污染。这就要求业务应用在执行 SQL 前,能够根据透传的压测标识,做好数据分类,将相应的 SQL 路由到与之对应的数据源。
这里配置的方式类似读写分离,也是给目标主库绑定一个影子库,当 SQL 携带了影子标就会被路由到影子库。
2.4 限流&熔断
当 DB 的压力超过自身水位线时,会导致 DB 发生故障。当我们预估出某个维度的水位线后,可以配置对于的限流规则,拒绝掉超过本身水位线以外的请求来保护 DB。让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。维度方面我们支持 DB、Table、SQL 以及 DML 类型。
彩虹桥下面连接了上百个 RDS 实例,每个 RDS 实例都有可能出现各种故障,当单个实例出现故障会影响到整个逻辑库,会迅速造成阻塞诱发雪崩效应。所以我们需要一种快速失败的机制来防止雪崩,目前我们是支持 DB 实例级别的熔断,基于获取连接& SQL 执行 2 种行为,以及执行时间跟失败比例来实现熔断,以达到在 DB 故障时快速失败的效果。
2.5 流量纠偏
在双活架构下,彩虹桥作为数据库的代理层,可以保证双活架构下流量切换过程的流量做兜底拦截,保证数据一致性。原理就是基于 SQL Hint 携带的 userId 与机房规则做匹配,拦截不属于当前机房的流量。
3. 基于 ShardingSphere 我们做了哪些改造
虽然 ShardingSphere Proxy 本身其实已经足够强大,但是针对得物内部环境,还是存在一些缺陷和功能缺失,主要分为以下几点:
-
易用性
-
分片、读写分离等规则配置文件过于复杂,对于业务开发不够友好
-
规则动态变更完全依赖配置中心,缺失完善的变更流程
-
连接池治理能力不完善
-
Hint 方式不够友好,需要业务写 RAL 语句
-
自定义分片算法需要发布
-
SQL 兼容性
-
-
稳定性
-
多集群治理功能缺失
-
逻辑库之间的隔离性缺失
-
限流熔断组件缺失
-
数据源、规则动态变更有损
-
双活架构下流量纠偏功能的缺失
-
发布有损
-
-
可观测性
-
SQL Trace 功能不完善
-
监控指标不够全面
-
SQL 洞察能力缺失
-
-
性能
- 由于多了一次网络转发,单条 SQL 的 RT 比直连会上浮 2~3ms
为了解决以上问题,我们做了以下改造与优化。
3.1 易用性提升
-
针对数据源、规则的配置、变更、审计等一系列操作,集成到管控台进行统一操作。通过图形化的方式降低了分片、读写分离等规则配置文件的复杂度,并且加上一系列校验来规避了一部分因为配置文件错误导致的低级错误。其次加上了审计功能,保障了配置动态变更的安全性和可控性。
-
管控台新增了连接池治理,基于 RDS 连接数、Proxy 节点数、挂载数据源数量等因素自动计算出一个安全合理的连接池大小。
-
新增 Client,针对 Hint 做一系列适配,比如影子标传递、双活架构用户 id 传递、trace 传递、强制路由等等。使用方只需要调用 Client 中的 API,即可在发出 SQL 阶段自动改写成 Proxy 可以识别的 Hint 增强语句。
-
管控台新增了集群治理功能:由于我们部署了多套 Proxy 集群,为了最大程度的保障故障时的爆炸半径,我们按照业务域对 Proxy 集群进行了划分,尽量保证统一业务域下面的逻辑库流量进入同一套集群。
-
新增了 Groovy 来支持自定义分片算法,在后台配置好分片逻辑后审核通过即可生效,无需 Proxy 发版
3.2 稳定性提升
3.2.1 Proxy 多集群治理
(1)背景
随着 Proxy 承载的业务域越来越多,如果所有的流量都通过负载均衡路由到Proxy 节点,当一个库出现问题时,可能会导致整个集群瘫痪,爆炸范围不可控。而且由于 DB 的连接数资源有限,这就导致 Proxy 的节点无法大规模横向扩展。
(2)解决方案
为了不同业务域之间的隔离性,我们部署了多套 Proxy 集群,并且通过管控台维护各个逻辑库与集群之间的关系。保证同一业务域下面的逻辑库流量进入同一套集群。并且在某个集群发生故障时,可把故障集群中的逻辑库迅速、无损的动态切换至备用集群,尽可能减少 Proxy 本身故障给业务带来的损失。
而针对连接数治理方面,Proxy 在初始化连接池的时候会判断一下当前逻辑库是否在当前集群,如果不在把最小连接数配置设置成最小,如果在则按照正常配置加载,并在集群切换前后做好目标集群的预热以及原集群的回收。这样可以极大程度上缓解 DB 连接池资源对 Proxy 节点的横向扩展。
(3)实现原理
上游应用引入 Rainbow(自研连接池)后,在连接池初始化之前会根据逻辑库读取当前库所在集群,并动态把 Proxy 的域名替换成其所在集群的域名。同时还会新增对集群配置的监听,这样在管控台切换集群操作后,Rainbow 会根据切换后的集群域名创建一个新的连接池,然后替换掉老的连接池,老的连接池也会进行延时优雅关闭,整个过程对上游应用无感知。
(4)架构图
3.2.2 Proxy 工作线程池隔离
(1)背景
开源版本的 Proxy,所有逻辑库共用一个线程池,当单个逻辑库发生阻塞的情况,会耗尽线程池资源,导致其他逻辑库也跟着受影响。
(2)解决方案
我们这里采用了基于线程池的隔离方案,引入了独占&共享线程池的概念,优先使用逻辑库独占线程池,当独占线程池出现排队情况再使用共享线程池,共享线程池达到一定负载后会在一个时间窗口内强制路由到独占线程池,在保障隔离性的前提下,最大化利用线程资源。
3.2.3 流控与熔断
(1)背景
开源版本的 Proxy缺少对库、表、SQL 等维度的流控,当短时间内爆发超过系统水位的流量时,很可能就会击垮 DB 导致线上故障,而且缺少针对 DB 快速失败的机制,当 DB 发生故障(比如 CPU 100%)无法快速失败的情况下,会迅速造成阻塞诱发雪崩效应。
(2)解决方案
新增了各个维度(DB、table、SQL、语句类型)的限流,各个库可以根据预估的水位线以及业务需要配置一个合理的阈值,最大程度保护 DB 。同时我们也引入 DB 实例的熔断策略,在某个实例出现故障时可快速失败。在分库场景下,可最大程度减少对其他分片的影响,进一步缩小了故障的爆炸半径,提升彩虹桥整体的稳定性。
(3)实现原理
流控跟熔断都是基于 sentinel 来实现的,在管控台配置对应的规则即可。
3.2.4 无损发布
(1)背景
在前期,Proxy 每次发布或者重启的时候,都会收到上游应用 SQL 执行失败的一些报警,主要的原因是因为上游与 Proxy 之间是长连接,这时如果有连接正在执行 SQL,那么就会被强制断开导致 SQL 执行失败,一定程度上对上游的业务造成损失。
(2)解决方案
发布系统配合自研连接池 Rainbow,在 Proxy 节点发布或重启之前,会通知 Rainbow 连接池优雅关闭应用于需要重启&发布的 Proxy 节点之间的连接,在 Proxy 流量跌 0 后再执行重启&发布。
3.3 可观测性
3.3.1 运行时指标
(1)背景
开源版本对于 Proxy 运行时的监控指标很少,导致无法观测到 Proxy 上面每个库运行的详细状态。
(2)解决方案
在 Proxy 各个执行阶段加了埋点,新增了以下指标、并绘制了对应的监控大盘。
-
库&表级别的 QPS、RT、error、慢 SQL 指标
-
Proxy-DB 连接池的各项连接数指标
-
流控熔断指标
-
线程池活跃线程数、队列大小指标
(3)效果图
3.3.2 全链路追踪
(1)背景
开源版本的 trace 只支持 Proxy 内部执行阶段的链路追踪,无法和上游串联,导致排障效率低下。
(2)解决方案
主要通过 RAL、SQL 注释 2 种方式传递 trace 信息,实现了上游到 Proxy 的全链路追踪。
(3)实现原理
我们首先想到的方案就是通过 SQL 注释方式传递 trace 信息,但是在真正投产之后却发现了一些问题,在上游使用了 prepare 模式(useServerPrepStmts=true&cachePrepStmts=true)时,Proxy 会缓存statmentId 和 SQL ,而 trace 每次都不一样,这样会导致存储缓存无限增长最终导致 OOM 。
所以通过 SQL 注释方式传递 trace 信息只适用于非 prepare 场景。于是我们又新增了一种方案就是在每次 SQL 执行之前发送一条 RAL 语句来传递 trace 信息,并在 Proxy 中缓存 channelId 与 trace 信息的对应关系,由于单个channel所有的SQL都是串行执行,加上 channelId 数量可控,不会无限膨胀。但是这种方案相对于每次 SQ L执行之前都有一次 RAL 语句的执行,对性能的影响还是比较大的。从监控上看下来每次 SQL 执行的 RT 会上浮 2-3ms(网络传输),对于一些链路较长的接口来说还是挺致命的。
(4)总结
综合下来 2 种方案其实都有比较明显的缺点,针对这个问题之前 ShardingSphere 的小伙伴来过我们得物进行过一次深度沟通,亮哥给出了一个比较可行的方案,就是通过虚拟列传输trace信息,但是需要上游对 SQL 进行改写,这可能会增加上游应用的负担,目前这块我们还没有开始做。
3.3.3 SQL 洞察
(1)背景
目前 Proxy 虽然有打印逻辑 SQL 和物理 SQL 的日志,但是由于生产请求量较大,开启日志会对 IO 有较大挑战。所以目前我们生产环境还是关闭的状态。而且这个日志也无法与上游串联,对于排障的帮助也是比较有限。
(2)解决方案
目前也只有个大概的思路,还没有完善的方案,要达到的效果就是收集所有 Proxy 执行的逻辑 SQL、物理 SQL 以及上游信息,包括 JDBCDatabaseCommunicationEngine 以外的一些 SQL(比如 TCL 等等),并通过管控台实时查询。最终的效果类似阿里云 RDS 的收费服务-SQL 洞察
3.4 bug 修复
由于历史原因,我们是基于 Apache ShardingSphere 5.0.0-alpha 上做的二次开发,在实际使用的过程中遇到了很多 bug,大部分都给官方提了 issue,并且在彩虹桥版本上做了修复,当然 ShardingSphere 社区的小伙伴也给与了很多帮助和修复的思路。
3.5 JDBC&Proxy 混合架构
(1)背景
上面 4 项内容大多数针对 Proxy 模块或上游连接池模块的改造,但是还是有一些问题是 Proxy 模式暂时无法解决的,比如前面提到的 SQL 兼容性和性能问题。还有就是如果整个 Proxy 集群全部宕机情况下我们没有一个兜底机制。所以 Proxy 模式并不适用于所有场景。在 Apache ShardingSphere 的官方文档可以看到这样一段内容:
于是我们准备在 JDBC&Proxy 混合架构上做了进一步探索。
(2)解决方案
在管控台实现对逻辑库的模式配置,并通过自研连接池 Rainbow 感知并根据不同模式启动不同类型的数据源。并且可以在后台切换模式后无损动态调整,这样对于使用者来说是完全无感知的。对 SQL 性能、兼容性要求比较高的应用可以调整为 JDBC 模式。同时当 Proxy 所有集群瘫痪时也有个兜底的方案,不至于全站崩溃。
(3)实现原理
Rainbow 连接池启动的时候会查询当前逻辑库对应的模式,如果是 Proxy 模式则直接连接 Proxy 来启动连接池,如果是 JBDC 则根据该逻辑库的数据源配置以及分片&读写分离&影子库等等规则来加载 JDBC 模式的数据源,对应的 DataSource 为 GovernanceShardingSphereDataSource。并且会监听这个模式配置,当模式发生变化后会动态无损替换当前连接池。具体的无损替换方案与前面提到的集群切换类似。同时需要解决的还有监控问题和连接池资源的管理,Proxy 切换至 JDBC 模式后指标的暴露由 Proxy 节点变成了上游节点,对应的大盘也需要做对应的融合,连接池治理这块也使用了新的计算模式做了对应的适配。
4. 目前的困惑
由于历史原因,我们是基于 Apache ShardingSphere 5.0.0-alpha 上做的二次开发,目前社区最新的版本是 5.1.2-release,从 5.0.0-alpha ~ 5.1.2-release 做了大量的优化跟 bug 修复,但是我们没有很好的办法将社区的代码合并到我们的内部代码,因为无法确定社区开源版本更改的内容,会不会对现有业务产生影响,短时间内也无法享受社区带来的红利。同时我们也在寻找一种方式将我们做的一些优化后续合并到社区中,也算是一种反哺社区。为中国开源做一份贡献~