乐趣区

自动化执行引擎

今天「剖析 Sharding-Sphere 系列文章」为大家带来对 Sharding-Sphere 自动化执行引擎模块的相关介绍。鉴于老板比较喜欢正经的技术文章,所以妹子我尽量用正经又不失肃穆的叙述风格,为大家带来《Sharding-Sphere 自动化执行引擎模块》的分享。

传说鱼的记忆只有 7 秒钟。前段时间刚把这个模块的代码抒写整理完,趁着我还没有失忆之前,先为大家叙述一二,愿对各位看官有所帮助。

「剖析 Sharding-Sphere 系列文章」是由 Sharding-Sphere 的核心开发成员亲自操刀向大家介绍和剖析 Sharding-Sphere 的核心模块、所使用的前沿技术、有价值的经验总结等。这一系列的文章将带您走进 Sharding-Sphere 的内核世界,获得新知、激发灵感。更希望您关注我们,共同交流切磋,一同前行。

作者介绍

潘娟,京东金融运维 DBA,主要负责京东金融生产数据库运维及数据库平台、中间件开发工作。多次参与京东金融 6.18、11.11 大促活动的护航工作。曾负责京东金融数据库自动化平台设计与开发项目,现专注于 Sharding-Sphere 分布式数据库中间件开发。乐于在数据库、自动化、分布式、中间件等相关领域进行学习和探索。

概念介绍

Q: 什么叫 ” 自动化执行引擎 ”?

A: 一条 SQL 的生命周期是:从客户端发起、经过 Sharding-Sphere 处理、再到底层数据库执行消化。而在 Sharding-Sphere 里过程则是:SQL 解析 –>SQL 优化 –>SQL 路由 –>SQL 改写 –>SQL 执行 –> 结果归并。自动化执行引擎是为了处理 SQL 执行问题的,即将路由改写后的真实 SQL 如何有控制且高效地传送到底层数据库执行。那么直接通过 JDBC 发送 SQL 至数据库执行难道行不通吗?还有其他需要考虑吗?答案是:肯定有其他考虑,否则我就不用写这篇文章了。这就体现在它的 ” 自动化 ” 上了。所谓 ” 自动化 ”,其实是为了平衡数据库连接创建与结果归并模式选择问题,为了平衡资源控制与执行效率问题。

需求场景

Q: 为何需要自动化执行引擎呢?

A: 在概念介绍部分,我们介绍了主角 - 自动化执行引擎。也谈到它的自动化是为了平衡数据库连接创建以及结果归并模式选择问题。这是它诞生的宿命,历史的选择。下面将为大家介绍这两个需要平衡的问题:

1. 数据库连接创建

作为一位混娱乐圈的 DBA 出身的 Java coder, 多少还是会从 DBA 角度考虑问题。比如从资源控制的角度看,业务方访问数据库的连接数量应当有所限制,这能够有效地防止某一业务操作过多地占用资源,从而将数据库连接的资源耗尽,以致于影响其他业务的正常访问。特别是在一个数据库实例中存在较多分表的情况下,一条不包含分片键的逻辑 SQL 将产生落在同库不同表的大量真实 SQL,如果每条真实 SQL 都占用一个独立的连接,那么一次查询肯定将会占用过多的资源。Sharding-Sphere 作为数据库中间层,如果没有控制好数据库连接数量而导致连接暴增、数据库压力过大的话,极有可能被强行背锅。

2. 结果归并模式选择

但是从执行效率的角度看,为每个分片查询维持一个独立的数据库连接,可以更加有效地利用多线程来提升执行效率。为每个数据库连接开启独立的线程,可以并行化 IO 所产生的消耗。独立的数据库连接,能够保持查询结果集的引用以及游标位置,在需要获取相应数据时移动游标即可,避免了过早将查询结果数据加载至内存。这就涉及到了结果归并模式的选择问题。通过上一篇文章《剖析 Sharding-Sphere 系列——结果归并》介绍,我们知道当前有两种结果归并的模式,分别是:

流式归并:以结果集游标下移进行结果归并的方式,称之为流式归并,它无需将结果数据全数加载至内存,可以有效地节省内存资源,进而减少垃圾回收的频次。

内存归并:以读取内存中加载的结果集进行归并的方式,进行数据对比归并。它需要将结果数据全数加载至内存。

相信只要是智商在线的朋友,一定会选择流式归并来处理结果集。可是,如果无法保证每个分片查询持有一个独立数据库连接的话,那么就需要在复用该数据库连接、获取下一张分表的查询结果集之前,将当前的查询结果集全数加载至内存。因此,即使可以采用流式归并,在此场景下也不得不退化为内存归并。

一方面是对数据库连接资源的控制保护,一方面是采用更优的归并模式达到内存资源节省的目的,如何处理好两者之间的关系,是 Sharding-Sphere 执行引擎需求解决的问题。具体来说,如果一条 SQL 在经过 Sharding-Sphere 的分片后,需要操作某数据库实例下的 200 张表,那么,是选择创建 200 个连接并行执行,还是选择创建一个连接串行执行呢?效率与资源控制又应该如何抉择呢?

进化论

针对上述的场景,Sharding-Sphere 在 3.0.0.M4 之前提供了一种解决思路,即提出了连接模式(Connection Mode)的概念,并划分了两种模式:内存限制模式(MEMORY_STRICTLY)和连接限制模式(CONNECTION_STRICTLY)这两种类型。

  • 内存限制模式。使用此模式的前提是数据库对其一次操作所耗费的连接数量不做限制。如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。并且在 SQL 满足条件情况下,优先选择流式归并,以防止出现内存溢出或避免频繁垃圾回收情况。
  • 连接限制模式。使用此模式的前提是数据库严格控制对其一次操作所耗费的连接数量。如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,那么只会创建唯一的数据库连接,并对其 200 张表串行处理。如果分片在不同的数据库,仍然是多线程处理不同库,但每个库的每次操作仍然只创建一个唯一的数据库连接。这样即可以防止对一次请求对数据库连接占用过多所带来的问题。该模式始终选择内存归并。

内存限制模式适用于 OLAP 操作,可以通过放宽对数据库连接的限制提升系统吞吐量;连接限制模式适用于 OLTP 操作,OLTP 通常带有分片键,会路由到单一的分片,因此严格控制数据库连接,以保证在线系统数据库资源能够被更多的应用所使用,是明智的选择。

而 Sharding-Sphere 最终使用何种模式的决定权就交由用户。Sharding-Sphere 提供对连接模式的配置,让开发者依据自己业务的实际场景需求选择使用内存限制模式或连接限制模式。

可是,将两难的选择的决定权甩锅给用户,使得用户必须要了解这两种模式的利弊,并依据业务场景需求进行选择。这显然增加了用户对 Sharding-Sphere 的学习和使用的成本,这并不是一种最优的解决方案。

此外,这种一分为二的处理方案,将两种模式的切换交由静态的初始化配置,缺乏灵活应性。在实际的使用场景中,面对不同 SQL 以及占位符参数,每次的路由结果是不同的。这就意味着某些操作可能需要使用内存归并,而某些操作则可能选择流式归并更优,它们不应该由用户在 Sharding-Sphere 启动之前配置好,而更应该根据 SQL 和占位符参数的场景,来动态的决定连接模式。

像 Sharding-Sphere 这样,总是站在用户角度考虑问题并且不断优化精进的七道杠青年是一定要进行相关优化调整的,于是自动化执行引擎就进化出来了。

为了降低用户的使用成本以及连接模式动态化这两个问题,Sharding-Sphere 提炼出自动化执行引擎的思路,在其内部消化了连接模式的概念。用户无需了解所谓的内存限制模式和连接限制模式是什么,而是交由执行引擎根据当前场景自动选择最优的执行方案。

同时,自动化执行引擎将连接模式的选择粒度细化至每一次 SQL 的操作。针对每次 SQL 请求,自动化执行引擎都将根据其路由结果,进行实时的演算和权衡,并自主地采用恰当的连接模式执行,以达到资源控制和效率的最优平衡。针对自动化的执行引擎,用户只需配置 maxConnectionSizePerQuery 即可,该参数表示一次查询时每个数据库所允许使用的最大连接数,剩余的处理逻辑将由自动化执行引擎为您负责。

实现解析

整个自动化执行引擎的执行流程如下图所示。

在路由改写完成后,我们会得到路由结果 (SQLRouteResult),这个结果集主要包含了 SQL、SQL 的参数集、数据库等信息。其数据结构如下图所示:

执行引擎的执行过程分为准备、执行两个阶段。

  • 准备阶段

顾名思义,此阶段用于准备执行的数据。它分为结果集分组和执行单元创建两个步骤。

a. 结果集分组

该步骤是实现内化连接模式概念的关键。执行引擎根据 maxConnectionSizePerQuery 配置项,结合当前路由结果,自动选择恰当的连接模式。具体步骤如下:

  • 将 SQL 的路由结果按照数据库的名称进行分组。
  • 通过下图的公式获得每个数据库实例在 maxConnectionSizePerQuery 的允许范围内,每个数据库连接需要执行的 SQL 路由结果组,并演算出本次请求最优的连接模式。

在 maxConnectionSizePerQuery 允许的范围内,当一个连接需要执行的请求数量大于 1 时,意味着当前的数据库连接无法持有相应的数据结果集,则必须采用内存归并;反之,当一个连接需要执行的请求数量等于 1 时,意味着当前的数据库连接可以持有相应的数据结果集,则可以采用流式归并。

每一次的连接模式的选择,是针对每一个物理数据库的。也就是说,在同一次查询中,如果路由至一个以上的数据库,每个数据库的连接模式不一定一样,它们可能是混合存在的形态。

b. 执行单元创建

该步骤通过上一步骤获得的路由分组结果创建用于执行的单元。执行单元是指为每个路由分组结果创建相应的数据库连接。

当数据库被限制了连接资源数量且线上业务出现大量并发操作时,如果不妥善处理并发获取数据库连接的问题,则很有可能会发送死锁。在多个请求相互等待对方释放数据库连接资源时,就会产生饥饿等待,造成交叉死锁。

举个栗子,假设一次查询需要在某一数据库上获取 2 个数据库连接,用于路由至一库的 2 个分表查询。有可能出现查询 A 已获取到该数据库的 1 个数据库连接,并等待获取另一个数据库连接;而查询 B 则也已经获得了该数据库上的 1 个数据库连接,并同样等待另一个数据库连接的获取。如果数据库连接池的允许最大连接数是 2,那么这 2 个查询请求将永远孤独地等待着彼此,图绘版的解释可能会更便于大家理解:

为了避免死锁的出现,Sharding-Sphere 在获取数据库连接时进行了同步处理。它在创建执行单元时,以原子性的方式一次性获取本次 SQL 请求所需的全部数据库连接,杜绝了每次查询请求获取到部分资源的可能。这种加锁做法确实可以解决死锁问题,只是,同时会带来一定程度并发性能的损失。为了展示我们不一样!有啥不一样呢?

我们针对此问题还进行了以下两方面优化:

1.    避免锁定一次性只需获取一个数据库连接的操作。因为每次仅需要获取一个连接,就不会发生两个请求相互等待的场景,无需锁定。对于大部分 OLTP 的操作,都是使用分片键路由至唯一的数据节点,此时无需担心交叉死锁问题,也无需考虑加锁问题,从而减少对并发效率的影响。除了路由至单分片的情况,读写分离也属于此范畴之内的场景。

2.    仅针对内存限制模式进行链接资源的锁定。在使用连接限制模式时,数据库连接资源在所有查询结果集装载至内存之后被释放掉,因此不必考虑死锁等待、加锁处理的问题。

  • 执行阶段

该阶段用于真正的执行 SQL,它分为分组执行和归并结果集生成两个步骤。

a. 分组执行

该步骤将准备执行阶段生成的执行单元分组下发至底层并发执行引擎,并针对执行过程中的每个关键步骤发送事件。如:执行开始事件、执行成功事件以及执行失败事件。执行引擎仅关注事件的发送,它并不关心事件的订阅者。Sharding-Sphere 的其他模块,如:分布式事务、调用链路追踪等,会订阅感兴趣的事件,并进行相应的处理。

b. 归并结果集生成

Sharding-Sphere 通过在执行准备阶段的获取的连接模式,生成内存归并结果集或流式归并结果集,并将其传递至结果归并引擎,以进行下一步的工作。内存归并结果集或流式归并结果集的核心区别是:流式归并结果集会通过游标方式获取结果集的数据,而内存归并结果集则是从内存里获取数据。这也是内存归并和流式归并的数据基础。

通过上述所有步骤就完成了自动化执行引擎的执行流程。其核心目的是自动化平衡数据库连接创建以及结果归并模式选择问题,实现细粒度地平衡把控每一次查询的资源控制与执行效率,从而减少用户的使用学习成本和业务场景变化的担忧。

看官一杯茶的时间,是妹子我反复修改数次的结果。妹子无法被打赏,只愿正在阅读的你能有所收获,这也是我们 coding、writing 的意义所在。以后还会有「剖析 Sharding-Sphere 系列文章」其他文章与大家见面,敬请关注~

退出移动版