本文主要分成四个部分来介绍Spider:
表链接:利用Spider,多个后端节点的表看起来就像存在于单一实例上一样。
事务:Spider实现了XA事务/单机事务接口,支持XA事务,以便在多个数据节点之间同步或者更新数据。
插拔式引擎:Spider作为MySQL/MariaDB的一个插拔式引擎,实现handler类定义的表访问方法。
读写流程:受MySQL Server层驱动,执行访问数据的动作。
Spider的表链接的技术参考ISO/IEC 9075-9:2008 SQL/MED标准。利用Spider的这个特性,你可以像操作本地MariaDB实例的表一样来操作远程MariaDB实例上的表,也可以像操作本地MariaDB实例的表一样来操作分布在多个MariaDB实例上的表。
当创建一个Spider存储引擎的表时,该表指向远程服务器上对应的一张表或者多个实例上的表,就像UNIX/Linux中的软链接一样。远程服务器上的表可以是任何存储引擎的表。在执行CREATE TABLE命令创建Spider引擎的表时,需要添加COMMENT或CONNECTION语法来指定远程服务器的地址等信息。例如,在远程服务器(该服务器是数据节点,假设IP为192.168.0.1)上创建了如下一张表:
CREATE TABLE s(id INT NOT NULL AUTO_INCREMENT, code VARHCAR(10), PRIMARY KEY(id));
在Spider节点创建一张表指向该表:
CREATE TABLE s(id INT NOT NULL AUTO_INCREMENT, code VARHCAR(10), PRIMARY KEY(id)) ENGINE=SPIDER COMMENT ‘host “192.168.0.1”,user “user1”, password “pwd1”, port “3307”’
在Spider节点,表字段定义可以忽略。Spider第一次访问表的时候,如果发现没有表字段定义,会从后端节点拉取相关元数据,然后缓存在本地。
Spider的系统表spider_tables记录了各个数据分片的位置信息,类似于编程语言中指针作用。该系统表可以便利Spider跨节点的join操作:访问数据所在的机器,然后把数据拉取到本地进行join操作;如果进行join操作字段不是分片字段,那么需要广播SQL语句将数据拉取到Spider节点进行join操作。
Spider_tables类似图1所示。
图1. Spider表链接
Spider分别针对单机事务与XA事务实现了相应的操作事务的方法。图2列出了部分实现的方法。
图2. Spider部分实现的事务接口
上述方法的主要实现是向后端节点发送消息,有些阶段同时需要执行记录系统表的行为。Spider依赖后端数据节点保证事务的持久性以及隔离性。它只负责开启事务,以及在适当的时机发送提交或者回滚事务的命令。如果单机事务涉及多个数据节点,Spider需要将相应的连接保存在队列中。在事务提交或者回滚的时候,逐个发送相应的命令。
Spider参照分布式事务DTP/XA模型实现了分布式XA事务(见图3)。在这个模型中,存在RM(Resource Manager,资源管理器)、TM(Transaction Manager, 事务管理器)以及AP(Application, 应用程序)三种角色。AP通过RM API来操作和管理资源,通过TM接口开启/终止/结束事务。RM与TM之间需要实现XA接口。XA接口定义了两阶段提交的必要步骤,以及RM与TM之间需要进行的交互。Spider扮演的是TM角色,而后端的数据节点扮演的是RM的角色。
图3. 分布式DTP/XA模型
为了使用分布式XA事务,业界定义的XA命令如下:
XA START 'trx-id'; //开启XA事务
do actual work; //实际的查询执行语句
XA END 'trx-id'; //XA事务结束
XA PREPARE 'trx-id'; //预提交
XA COMMIT 'trx-id'; //提交
Spider会在系统表spider_xa中记录XA事务的状态,同时在另外一张系统表spider_xa_members中记录参与该XA事务的节点,以便进行操作。
在Spider中,XA事务分别有四种状态,如图4所示,对应于NOT YET,PREPAED,ROLLBACK以及COMMITTED。Spider在开始PREPARE阶段之际会在系统表spider_xa中标记该XA事务的状态为NOT YET。在所有数据节点都接收到PREPARE消息以后,该XA事务的状态进入到PREPARED阶段。假如在PREPARE阶段,某一个数据节点发生故障,那么Spider会回滚该事务。相应地,事务的状态变成ROLLBACK。
最后,如果所有参与事务的节点都返回PREPARE OK,该事务进入提交阶段。图5给出了对应上述命令的每一个步骤,Spider向后端节点发送的消息。
图4. Spider XA事务状态转换
图5. 执行XA事务,Spider与两个后端节点的交互
从图5可以看到Spider向后端节点发送XA START命令时会设置会话级别的事务特性,同时将XA事务ID发送到后端节点。因为XA事务ID由三部分组成,Spider会将这三个部分的解析出来,然后拼接成对应的字符串发送到后端节点。为了节省网络开销,Spider将XA END与XA PREPARE命令合并起来一起发送。也就是在这个阶段初始,Spider在系统表里面记录事务的状态。如果所有的RM都返回OK,那么Spider进入PREPARED状态,准备提交事务。否则,事务进入到回滚状态。
MySQL最强大的功能之一,以及区别于其它关系型数据库系统的一个主要的特色是不同的表能够采用不同的存储引擎。每一个存储引擎都有其优缺点,用户能够根据自己的需要定制MySQL的存储引擎。存储引擎能够控制在哪里以及如何存放、获取数据。它代表了下面物理层提供的抽象逻辑接口,也是数据库执行实际I/O操作的地方。这是一个组件体系结构。在这个结构中,handler类定义了存储引擎提供的接口和功能。因为所有的存储引擎从基类handler继承而来,所以它们能够提供相同的功能。
总的来说,handler类和handlerton结构在整个体系结构中扮演了中间层的角色。你所编写的存储引擎只有满足了handler的要求后,才能顺利插入到运行的MySQL服务器中。所有的网络连接、安全认证、解析和优化由MySQL服务器本身完成,与存储引擎无关。
Spider作为MySQL的一个可插拔引擎,实现了handler类定义的相应的存取方法。Spider本身并不存放数据,而是类似一个代理的功能将访问请求路由到后端的数据节点。Spider提供了两种途径访问后端节点存储的数据。如图6所示,Spider可以遵循MySQL传统的查询处理流程来访问数据,也开发了自有的一套来加速数据访问。在传统的查询处理方式下,SQL查询请求经过查询解析、查询重写、查询优化等步骤。按照生成的查询执行计划,Spider从后端节点拉取数据,交给MySQL服务器处理。Spider在这种查询处理框架之下的一个缺点是不能很好地利用后端节点可并行化特性,同时需要对SQL查询进行两次解析,带来的性能损耗问题比较严重。
在我们的测试中,性能损耗约50%左右。基于这个原因,为了加速聚集、统计等查询,Spider开发团队提供了DirectSQL方式执行查询。DirectSQL的原理类似于Map Reduce方案,将查询直接下发到后端节点,无需在MySQL服务器层进行解析(Map阶段);后端节点将结果返回给Spider,由Spider合并结果集。(Reduce阶段)。这个方式很好地利用后端节点可并行处理查询的特点,消除重复解析SQL语句的行为。
图6. MySQL体系下的Spider
上面已经谈到,Spider本身并不存储数据,因此需要将数据访问请求转换成其它方式,例如Handler、Handler Socket以及SQL方式。前面两种访问方式更像是一种NoSQL的数据访问方式,允许查询绕过SQL layer层。Spider允许后端的数据节点可以是不同的数据库系统,通过2PC保证事务提交的原子性。
为了更清楚地了解Spider的读写流程,我们有必要研究一下数据库系统的查询执行模型,以及MySQL的插拔式引擎如何跟这个模型对接的。
数据库系统基本都采用迭代器模型处理查询,也叫volcano查询执行引擎(发明这个词的学者大概是因为查询执行计划树看起来像一座火山,如图7)。执行计划树的上层节点通过get_next方法驱动子节点获取一条元组,子节点递归调用。在叶子节点也就是基本表将数据返回。
这个模型的一个好处就是实现起来很优雅,同时数据流与控制流结合在一起方便程序的调试。这个模型的缺点是函数的大量调用使得进程/线程上下文切换频繁,程序的局部性受到损害。因此,后来针对OLAP场景,采用了向量查询执行模型来减少进程上下文的切换以及保证保证高速缓存的命中率。
再次以图7为例子,图中的SQL语句的功能是查询一个部门的平均薪资。假如在职工表EMP的员工ID字段Dno上存在索引,MySQL在Server层针对该查询语句生成的查询计划如下:顺序扫描部门表,通过索引访问职工表,然后在两表join操作之后进行投影操作。下一个阶段为分组排序操作。上层的操作算子(例如join),驱动子节点调用get_next方法(表扫描方法)获取一条元组。底层操作算子(表访问方法,handler接口定义)将数据返回。至此,我们可以总结一下MySQL体系的工作原理:查询执行计划由MySQL Server层生成,存储引擎受执行计划驱动而访问表。MySQL的handler已经定义好表的访问方法,实现了这些访问方法的存储引擎就可以作为MySQL的插件式引擎而存在。
下面我们对Spider的读写流程结合Server层代码进行分析。
图7. 查询计划树示例
1、SELECT操作
上面提到Spider的作用类似一个proxy,本身并不存储数据。因此Spider处理SELECT语句(UPDATE与DELETE类似)首先需要根据查询解析的信息生成一个SELECT语句,发送到查询涉及的后端节点,将数据从远端拉到本地,然后进行处理。函数spider_db_append_select_columns根据查询涉及的读集以及写集获取相应的字段,构造一个SQL语句从后端节点拉取数据到本地。如果涉及多个分片,spider将从不同实例获取过来的结果集存放在不同的结果集spider_db_result中。类spider_db_fetch提供了fetch_next, current_row等方法供上层方法调用。Server层调用get_next方法驱动引擎层获取下一条数据。
对于表访问方法,MySQL实现了索引扫描(ha_index_read)与随机访问(ha_rnd_next)的方法。对于切分为多个分片的DB,索引扫描需要借助优先队列。索引扫描需要区分是否是第一次调用该方法。如果是第一次调用该方法,需要遍历所有的分片读取一条记录,然后插入到优先队列。对应到Spider,如果第一次调用访问远端实例表的方法,需要生成SELECT语句,将远端实例的数据拉到本地存放。在使用索引扫描的情况,MySQL 为每个分片保留一个key buffer以及record buffer。server 利用队列头部的m_top_entry 获得访问的分片ID。接着,调用get_next方法获取相应的元组,将返回的数据存放在record buffer,并插入到优先队列。函数最后将元组从优先队列返回。
为缓解内存等资源的压力,Spider实现全表扫描的方法是逐个分片串行扫描(为了加速,spider也提供了并行扫描数据节点的选项)。图8给出了Spider对于上述两种表访问方法的实现机制。
图8-1. 索引扫描实现
图8-2. 全表扫描
2、INSERT操作
MySQL的handler类对于INSERT操作提供的接口函数的名字是write_row。存储引擎想要支持INSERT操作就必须实现write_row方法。Spider对于write_row方法的实现是简单地根据查询解析的信息拼接一条INSERT语句,发往后端节点处理。如果是批量插入操作则需要与MySQL Server层配合,将INSERT语句批量发到后端节点。
图9结合一条批量插入的INSERT语句给出MySQL中INSERT操作的具体实现。
mysql_insert调用write_row执行具体的插入操作(第8行)。这是存储引擎必须实现的方法。对应于spider,spider根据查询涉及到的列(field)拼成一条INSERT语句(如果是分片数据库,VALUSE中的列必须包含分区键,分区键是自增列的情况除外)。图9中的QUERY将用户ID(ID)和用户名(Name)插入到user表,其中ID是分区键。mysql_insert根据VALUES包含的元组数目,判断是否需要进行批量插入操作。该例子的QUERY的VALUES包含4条元组,所有需要进行批量插入操作。MySQL循环调用write_row方法触发spider生成INSERT语句。Spider的write_row方法实现中会根据分区键将INSERT语句进行分组(第5行~第9行)。图9给出的实例只有两个数据分片,所以SQL语句被分成两组。处理完VALUES以后,Spider的INSERT语句也拼接完成。
ha_end_bulk_insert方法通知Spider完成VALUES处理。此时,Spider将INSERT发送到后端节点进行处理(第11行)。
图9. Spider中INSERT操作的实现
3、DELETE实现
Spider想要支持DELETE操作必须实现MySQL handler类提供的ha_delete_row方法。与INSERT操作不同,DELETE操作需要生成一条SELECT语句将查询涉及的分区键拉到Spider节点。这是因为MySQL Server层的“once-a-tuple”的查询执行模型(实际上基本所有的关系数据库系统都采用该模型)会驱动Spider逐个拼接DELETE语句,然后发往后端节点。这时候,Spider需要知道对应的DELETE语句该往哪个后端节点发送。为了减少网络开销,Spider提供了批量发送DELETE语句的功能。
图10. DELETE实现
图10给出了Spiderpider中delete的实现。MySQL Server层首先确定表的访问方法:采用索引扫描或者全部扫描(第5行)?DELETE方法需要执行一次查找操作,调用get_next方法(info.read_record)获取一条元组(第10行)。Spider需要判断是否第一次调用get_next方法。如果是的话,则需要生成SELECT语句,将数据节点的数据拉到本地。否则,Spider直接从本地返回数据给上层调用者。接下来,Server层调用ha_delete_row方法将数据删除。这是存储引擎需要具体实现的方法。由于Spider本身并不存储数据的缘故,其实现delete操作主要思想是利用从后端节点拉取过来的数据(分区键,过滤条件等),拼接成一条DELETE语句。然后,发送该请求到数据节点。Spider为了优化网络开销,提供了批量发送DELETE语句的选项。
UPDATE操作的实现类似DELETE,都需要Spider生成SELECT语句从后端节点拉取数据。只不过,UPDATE在更新区分键的时候,可能需要多一次DELETE操作(删除原来分区的数据,将新的数据插入到不同的分区)。
Spider的最大亮点是为MySQL的使用者提供分库分表的中间件解决方案,同时在SQL语法上兼容MySQL。这得益于Spider作为MySQL的插拔式引擎而存在。Spider是一个proxy,其本身并没有存储数据,因此上层的读写表请求需要转换成SQL语句,重新路由到后端的数据节点。相比其它的中间件解决方案,Spider的查询解析次数都是两次,并没有过多开销。此外,Spider还针对聚集、排序等操作提供了MAP REDUCE的解决方案。
总之,从兼容性、性能上衡量,Spider是MySQL分库分表一个不错的选项。
本文转自腾讯课堂Coding学院订阅号,经作者和平台同意授权转载。