执行引擎负责将路由和改写完成之后的真实 SQL 安全且高效发送到底层数据源执行。

img

这篇文章,我们聊聊执行引擎的设计原理。

首先,我们需要理解:

执行引擎并不是简单地将 SQL 通过 JDBC 直接发送至数据源执行,也并非直接将执行请求放入线程池去并发执行。

它更关注平衡数据源连接创建以及内存占用所产生的消耗,以及最大限度地合理利用并发等问题。 执行引擎的目标是自动化的平衡资源控制与执行效率。

1. 连接模式

从资源控制的角度看,业务方访问数据库的连接数量应当有所限制。 它能够有效地防止某一业务操作过多的占用资源,从而将数据库连接的资源耗尽,以致于影响其他业务的正常访问。

特别是在一个数据库实例中存在较多分表的情况下,一条不包含分片键的逻辑 SQL 将产生落在同库不同表的大量真实SQL,如果每条真实SQL都占用一个独立的连接,那么一次查询无疑将会占用过多的资源。

从执行效率的角度看,为每个分片查询维持一个独立的数据库连接,可以更加有效的利用多线程来提升执行效率。

  1. 流式归并

为每个分片维持一个独立的数据库连接,还能够避免过早的将查询结果数据加载至内存。 独立的数据库连接,能够持有查询结果集游标位置的引用,在需要获取相应数据时移动游标即可。

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

  1. 内存归并

当无法保证每个分片查询持有一个独立数据库连接时,则需要在复用该数据库连接获取下一张分表的查询结果集之前,将当前的查询结果集全数加载至内存。 因此,即使可以采用流式归并,在此场景下也将退化为内存归并。

一方面是对数据库连接资源的控制保护,一方面是采用更优的归并模式达到对中间件内存资源的节省,如何处理好两者之间的关系,是 ShardingSphere 执行引擎需求解决的问题。

具体来说,如果一条 SQL 在经过 ShardingSphere 的分片后,需要操作某数据库实例下的 200 张表。

那么,是选择创建 200 个连接并行执行,还是选择创建一个连接串行执行呢?效率与资源控制又应该如何抉择呢?

针对上述场景,ShardingSphere 提供了一种解决思路。 它提出了连接模式(Connection Mode)的概念,将其划分为内存限制模式(MEMORY_STRICTLY)和连接限制模式(CONNECTION_STRICTLY)这两种类型。

1、内存限制模式

使用此模式的前提是,ShardingSphere 对一次操作所耗费的数据库连接数量不做限制。

如果实际执行的 SQL 需要对某数据库实例中的 200 张表做操作,则对每张表创建一个新的数据库连接,并通过多线程的方式并发处理,以达成执行效率最大化。 并且在 SQL 满足条件情况下,优先选择流式归并,以防止出现内存溢出或避免频繁垃圾回收情况。

2、连接限制模式

使用此模式的前提是,ShardingSphere 严格控制对一次操作所耗费的数据库连接数量。

如果实际执行的SQL需要对某数据库实例中的200张表做操作,那么只会创建唯一的数据库连接,并对其 200 张表串行处理。

如果一次操作中的分片散落在不同的数据库,仍然采用多线程处理对不同库的操作,但每个库的每次操作仍然只创建一个唯一的数据库连接。 这样即可以防止对一次请求对数据库连接占用过多所带来的问题。该模式始终选择内存归并。

内存限制模式适用于 OLAP 操作,可以通过放宽对数据库连接的限制提升系统吞吐量;

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

2. 自动选择连接模式

ShardingSphere 最初将使用何种模式的决定权交由用户配置,让开发者依据自己业务的实际场景需求选择使用内存限制模式或连接限制模式。

这种解决方案将两难的选择的决定权交由用户,使得用户必须要了解这两种模式的利弊,并依据业务场景需求进行选择。 这无疑增加了用户对 ShardingSphere 的学习和使用的成本,并非最优方案。

在实际的使用场景中,面对不同 SQL 以及占位符参数,每次的路由结果是不同的。

这就意味着某些操作可能需要使用内存归并,而某些操作则可能选择流式归并更优,具体采用哪种方式不应该由用户在ShardingSphere启动之前配置好,而是应该根据SQL和占位符参数的场景,来动态的决定连接模式

为了降低用户的使用成本以及连接模式动态化这两个问题,ShardingSphere 提炼出自动化执行引擎的思路,在其内部消化了连接模式概念。

用户无需了解所谓的内存限制模式和连接限制模式是什么,而是交由执行引擎根据当前场景自动选择最优的执行方案

针对每次 SQL 请求,自动化执行引擎都将根据其路由结果,进行实时的演算和权衡,并自主地采用恰当的连接模式执行,以达到资源控制和效率的最优平衡。

用户只需配置 maxConnectionSizePerQuery 即可,该参数表示一次查询时每个数据库所允许使用的最大连接数。执行引擎会根据 maxConnectionSizePerQuery 配置项,结合当前路由结果,选择恰当的连接模式。

具体步骤如下 :

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

img

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

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

如图,maxConnectionSizePerQuery 默认值为 1 ,在同一次查询中,需要路由到两个数据库分片 ,分片 1 和分片 2 的连接模式不一样,混合存在。

img

3. 执行阶段

执行阶段用于真正的执行 SQL ,它分为分组执行生成两个步骤。

1、将 SQL 的路由结果按照数据源的名称进行分组

img

上图中,shardingsphere 在执行 SQL 前按照数据源的名称做了分组。

分组内容包括:

  1. 分片数据源名称(图中分片 ds0 , ds1 );
  2. 每个分片上需要执行的改写 SQL 语句 ;
  3. 存储资源(数据库物理连接 Connection 对象、底层 JDBC Statement 对象等)。

2、将不同的分组执行上下文提交到线程池执行

笔者将代码做了简化,分为两个流程:

  1. 将每个分组上下提交到线程池

img

  1. 将执行结果封装到 List 对象

img

当我们调用 Future 的 get 方法 ,当前执行线程会阻塞 ,等所有的分组任务执行完成后,将结果封装到 List 对象里。

4. 总结

img

执行引擎并不是简单地将 SQL 通过 JDBC 直接发送至数据源执行,也并非直接将执行请求放入线程池去并发执行。

1、选择连接模式

将 SQL 的路由结果按照数据源的名称进行分组,判断每个连接需要执行的 SQL 数量是否等于 1 ,若等于 1 ,则选择内存限制模式,否则选择连接限制模式

我们可以简单的认为,这么做的目的是让每个数据库分片在执行当前 SQL 时,仅仅分配最多 maxConnectionSizePerQuery 个连接。

2、将 SQL 的路由结果按照数据源的名称进行分组

分组内容包括:

  1. 分片数据源名称(图中分片 ds0 , ds1 );
  2. 每个分片上需要执行的改写 SQL 语句 ;
  3. 存储资源(数据库物理连接 Connection 对象、底层 JDBC Statement 对象等)。

3、多线程执行分组上下文

这里使用了 多线程 + Future 的编程技巧,多线程执行各个分组的 SQL 语句,并最终聚合在 List 对象中。

编程范式如下图:

img


本站由 卡卡龙 使用 Stellar 1.29.1主题创建

本站访问量 次. 本文阅读量 次.