一、概述
Spider是为MySQL/MariaDB开发的一个特殊引擎,具有内嵌分片功能。MariaDB从10.0.4开始支持Spider。作为MariaDB的一个新的主要特性。Spider的主要功能是将数据分散到多个后端节点,它的作用类似于一个代理。
Spider有三个作用:作为分库分表的中间件;
支持像访问本地数据表一样访问远端表(进行普通查询、跨表Join和DML等);
业务和数据库间的代理(Proxy)。
MySQL和Spider的架构图:
Spider是由日本的MySQL开发者Kentoku Shiba创建和开发的存储引擎,腾讯数据库团队贡献了非常多Patch。据说腾讯游戏、腾讯支付等有大规模使用。
二、表链接
Spider的表链接的技术参考ISO/IEC 9075-9:2008 SQL/MED标准。利用Spider的这个特性,你可以像操作本地MariaDB实例的表一样来操作分布在多个MariaDB实例上的表。
Spider表是个虚拟表,本身不存储数据。当创建一个Spider存储引擎的表时,该表指向单个或多个后端MariaDB实例上对应的表。后端实例上的表可以是任何存储引擎的表。Spider的系统表spider_tables记录了各个数据分片的实例位置、连接、状态等信息(如下图中TABLE和PART部分)。该系统表可以便利Spider跨节点的join操作:访问数据所在的机器,然后把数据拉取到本地进行join操作;如果进行join操作字段不是分片字段,那么需要广播SQL语句将数据拉取到Spider节点进行join操作。
在执行CREATE TABLE命令创建Spider引擎的表时,通过添加COMMENT或CONNECTION语法来指定后端实例的地址等信息。多个实例时,通过PARTITION语法来指定。
在Spider节点,表字段定义可以忽略。Spider第一次访问表的时候,如果发现没有表字段定义,会从后端节点拉取相关元数据,然后缓存在本地。
2.1 水平分表
水平分表应该是最常用的模式,类似于现有MySQL中间件的分库分表,或者手动分库分表。可以将一张表拆分为多个分区,每个分区保存一部分数据。如下所示:
Spider支持MySQL/MariaDB所有类型的分区表,有Range、Hash、List、Key分区。下面演示的基于Range分区。
例如,在2个节点上创建如下表:
CREATE TABLE user_info (
id int(10) unsigned NOT NULL AUTO_INCREMENT,
k int(10) unsigned NOT NULL DEFAULT '0',
c char(120) NOT NULL DEFAULT '',
pad char(60) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY k (k)
) ENGINE=innodb;
然后在spider节点上创建如下表来链接2个节点的表:
CREATE TABLE user_info(
id int(lO) unsigned NOT NULL AUTO_INCREMENT,
k int(10) unsigned NOT NULL DEFAULT,
c char(120) NOT NULL DEFAULT '',
pad char(60) NOT NULL DEFAULT,
PRIMARY KEY (id),
KEY k (k)
) ENGINE=spider COMMENT='wrapper "mysql", table "user_info"'
PARTITION BY range columns (id)
(
PARTITION ptl values less than (1000) COMMENT = 'srv "ndl"',
PARTITION pt2 values less than (2000) COMMENT = 'srv "nd2"'
);
2.2 垂直分表
垂直分表可以使用在多个不同的场景,包括将多个数据库实例的表映射到同个数据库实例上。这既可以实现跨表Join,又可以起到数据垂直拆分的效果。如下图所示:
业务将用户的不同类型信息user_info、user_msg、user_detail和user_log分别保存到不同实例上实现垂直扩展,业务可以直接访问这些表进行DML操作,相互间没有干扰,这样可以解除单台服务器的存储和计算资源瓶颈。但毕竟它们都属于用户信息,有多表查询的需求,所以可以将这些表映射到独立的Spider实例上,业务通过Spider实例进行普通查询操作,包括跨表的各种Join,还可以在Spider实例上进行各种统计和分析操作。当然,业务也可以通过Spider实例统一对这些表进行DML操作,通过部署多个Spider实例实现访问的负载均衡。
例如,先在两个后端节点分别创建如下的一个表:
CREATE TABLE user_info(
id int,
name varchar(128),
sex tinyint
) ENGINE=innodb;
CREATE TABLE user_msg(
id int,
city varchar(128),
country varchar(128)
) ENGINE=innodb;
然后在spider节点上链表两个后端节点:
CREATE TABLE user_info(
id int,
name varchar(128),
sex tinyint
) ENGINE=SPIDER COMMENT 'srv "node2"';
CREATE TABLE user_msg(
id int,
city varchar(128),
country varchar(128)
) ENGINE=SPIDER COMMENT 'srv "nodel"';
三、事务
Spider分别针对单机事务与XA事务实现了相应的操作事务的方法。下图列出了两者部分实现的方法。
上述方法的主要实现是向后端节点发送消息,有些阶段同时需要执行记录系统表的行为。Spider依赖后端数据节点保证事务的持久性以及隔离性。它只负责开启事务,以及在适当的时机发送提交或者回滚事务的命令。如果单机事务涉及多个数据节点,Spider需要将相应的连接保存在队列中。在事务提交或者回滚的时候,逐个发送相应的命令。
3.1 分布式事务DTP/XA模型
Spider分布式XA事务的实现参照了分布式事务DTP/XA模型(如下图)。在这个模型中,有三个角色:RM(Resource Manager,资源管理器)
TM(Transaction Manager, 事务管理器)
AP(Application, 应用程序)
AP通过RM API来操作和管理资源,通过TM接口开启/终止/结束事务。RM与TM之间需要实现XA接口。XA接口定义了两阶段提交的必要步骤,以及RM与TM之间需要进行的交互。
其两阶段提交步骤如下:
XA START 'trx-id'; //开启XA事务
do actual work; //实际的查询执行语句
XA END 'trx-id'; //XA事务结束
XA PREPARE 'trx-id'; //预提交
XA COMMIT 'trx-id'; //提交
DTP/XA模型图示:
3.2 Spider事务的实现
Spider扮演的是TM角色,而后端的数据节点扮演的是RM的角色。
在Spider中,XA事务分别有四种状态,如下图,对应于NOT YET,PREPAED,ROLLBACK以及COMMITTED。Spider在开始PREPARE阶段之际会在系统表spider_xa中标记该XA事务的状态为NOT YET。在所有数据节点都接收到PREPARE消息以后,该XA事务的状态进入到PREPARED阶段。假如在PREPARE阶段,某一个数据节点发生故障,那么Spider会回滚该事务。相应地,事务的状态变成ROLLBACK。
最后,如果所有参与事务的节点都返回PREPARE OK,该事务进入提交阶段。
执行XA事务,Spider与两个后端节点的详细交互步骤:
从上图可以看到Spider向后端节点发送XA START命令时会设置会话级别的事务特性,同时将XA事务ID发送到后端节点。因为XA事务ID由三部分组成,Spider会将这三个部分的解析出来,然后拼接成对应的字符串发送到后端节点。为了节省网络开销,Spider将XA END与XA PREPARE命令合并起来一起发送。也就是在这个阶段初始,Spider在系统表里面记录事务的状态。如果所有的RM都返回OK,那么Spider进入PREPARED状态,准备提交事务。否则,事务进入到回滚状态。
四、读写流程
为了更清楚地了解Spider的读写流程,我们有必要研究一下数据库系统的查询执行模型,以及MySQL的插拔式引擎如何跟这个模型对接的。
数据库系统基本都采用迭代器模型处理查询,也叫volcano查询执行引擎(发明这个词的学者大概是因为查询执行计划树看起来像一座火山,如下图)。执行计划树的上层节点通过get_next方法驱动子节点获取一条元组,子节点递归调用。在叶子节点也就是基本表将数据返回。
这个模型的一个好处就是实现起来很优雅,同时数据流与控制流结合在一起方便程序的调试。这个模型的缺点是函数的大量调用使得进程/线程上下文切换频繁,程序的局部性受到损害。因此,后来针对OLAP场景,采用了向量查询执行模型来减少进程上下文的切换以及保证保证高速缓存的命中率。
再次以下图为例子,图中的SQL语句的功能是查询一个部门的平均薪资。假如在职工表EMP的员工ID字段Dno上存在索引,MySQL在Server层针对该查询语句生成的查询计划如下:顺序扫描部门表,通过索引访问职工表,然后在两表join操作之后进行投影操作。下一个阶段为分组排序操作。上层的操作算子(例如join),驱动子节点调用get_next方法(表扫描方法)获取一条元组。底层操作算子(表访问方法,handler接口定义)将数据返回。至此,我们可以总结一下MySQL体系的工作原理:查询执行计划由MySQL Server层生成,存储引擎受执行计划驱动而访问表。MySQL的handler已经定义好表的访问方法,实现了这些访问方法的存储引擎就可以作为MySQL的插件式引擎而存在。
查询计划树示例:
下面我们对Spider的读写流程结合Server层代码进行分析。
4.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也提供了并行扫描数据节点的选项)。如下两图给出了Spider对于上述两种表访问方法的实现机制。
索引扫描实现:
全表扫描:
4.2 INSERT操作
MySQL的handler类对于INSERT操作提供的接口函数的名字是write_row。存储引擎想要支持INSERT操作就必须实现write_row方法。Spider对于write_row方法的实现是简单地根据查询解析的信息拼接一条INSERT语句,发往后端节点处理。如果是批量插入操作则需要与MySQL Server层配合,将INSERT语句批量发到后端节点。
下图结合一条批量插入的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行)。
4.3 DELETE实现
Spider想要支持DELETE操作必须实现MySQL handler类提供的ha_delete_row方法。与INSERT操作不同,DELETE操作需要生成一条SELECT语句将查询涉及的分区键拉到Spider节点。这是因为MySQL Server层的“once-a-tuple”的查询执行模型(实际上基本所有的关系数据库系统都采用该模型)会驱动Spider逐个拼接DELETE语句,然后发往后端节点。这时候,Spider需要知道对应的DELETE语句该往哪个后端节点发送。为了减少网络开销,Spider提供了批量发送DELETE语句的功能。
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操作(删除原来分区的数据,将新的数据插入到不同的分区)。
4.4 其他说明
在Spider表上进行的DML和查询操作均下发到后端数据节点。在HA(高可用,后面会介绍)模式下,Spider能够进行负责均衡,将查询请求分摊到不同的数据几点上。对于DML操作,不管是HA模式还是分表模式,Spider节点均对数据节点进行并发地数据增删改。涉及到多个数据节点的DML操作,Spider基于两阶段事务实现数据的一致性。
对于DDL操作情况有些不一样。对于Spider表进行drop table操作不会影响后端数据节点,也就是说数据节点对应的表不会被删除。类似地,对Spider节点进行alter table加索引或删索引等操作也不会影响后端数据。但若执行truncate table,则后端数据节点的表也会被清空。
五、性能优化
为了提高性能,还支持多种功能,包括使用DirectSQL直接操作后端数据节点,各种下推(push down)优化等。
5.1 DirectSQL
Spider作为MySQL的一个可插拔引擎,实现了handler类定义的相应的存取方法。Spider本身并不存放数据,而是类似一个代理的功能将访问请求路由到后端的数据节点。Spider提供了两种途径访问后端节点存储的数据。
MySQL体系下的Spider:
如上图所示,Spider可以遵循MySQL传统的查询处理流程来访问数据,也开发了自有的一套来加速数据访问。在传统的查询处理方式下,SQL查询请求经过查询解析、查询重写、查询优化等步骤。按照生成的查询执行计划,Spider从后端节点拉取数据,交给MySQL服务器处理。Spider在这种查询处理框架之下的一个缺点是不能很好地利用后端节点可并行化特性,同时需要对SQL查询进行两次解析,带来的性能损耗问题比较严重。
有测试表明,传统处理方式性能损耗约50%左右。基于这个原因,为了加速聚集、统计等查询,Spider开发团队提供了DirectSQL方式执行查询。DirectSQL的原理类似于Map Reduce方案,将查询直接下发到后端节点,无需在MySQL服务器层进行解析(Map阶段);后端节点将结果返回给Spider,由Spider合并结果集(Reduce阶段)。这个方式很好地利用后端节点可并行处理查询的特点,消除重复解析SQL语句的行为。
Spider提供了2个DirectSQL,分别是SPIDER_DIRECT_SQL和SPIDER_BG_DIRECT_SQL。其用法如下:
SPIDER_DIRECT_SQL('sql', 'tmp_table_list', 'parameters')
DirectSQL它会直接在parameters参数指定的远端节点执行sql参数指定的SQL,并将结果保存在tmp_table_list指定的临时表上,parameters一般指定为在mysql.servers中定义的一到多个后端数据节点server。类似于MapReduce机制,直接将SQL并行发送给指定的server(Map过程),将结果集返回给Spider节点,由其经过处理后返回给客户端(Reduce阶段)。操作如下所示:
SELECT spider_direct_sql(
'SELECT * FROM user_info as s WHERE s.id IN(1,2,3)',
'res', -- 临时表名(需要手动创建?) 'srv "nd1"'
);
5.2 下推(Push Down)
5.2.1 聚合下推
将min、max、avg、count和sum等常用的聚合操作直接下推到数据节点,再聚合数据节点返回的结果。
5.2.2 Update/Delete下推
如果是批量更新或删除,Spider将整个Update/Delete操作下推到数据节点,而不是在Spider节点解析Values后逐条下发到数据节点。
5.2.3 Join下推
与聚合操作、DML操作类似,Join操作也可以直接下推到数据节点,Spider仅做结果集处理。除此之外,Spider还支持引擎条件(Engine Condition)下推、索引hints下推等,这些下推操作即减少了Spider节点和数据节点重复的SQL解析优化,又减少了返回给Spider的结果集大小,可以节省系统的资源消耗,并提高实例的性能。
六、高可用
Spider支持分区加HA(High Availability)的部署方式,实现数据水平扩展的同时实现数据高可靠和高可用。部署模式参考如下:
在上图中,部署3个Spider节点用于服务高可用和负载均衡,分布表TABLE1有3个分区,Spider下面有3个数据节点,每个数据节点保存2个分区的数据。每个分区的数据均放置在2个数据节点上。多个数据节点的同一个分区也是通过两阶段提交来保证数据一致。
下面部署方式示例:
CREATE SERVER nd3_1 FOREIGN DATA WRAPPER mysql OPTIONS (HOST '127.0.0.1', DATABASE 'db1' , USER 'user1' , PASSWORD 'pass1' , PORT 10003);
CREATE SERVER nd2_1 FOREIGN DATA WRAPPER mysql OPTIONS (HOST '127.0.0.1', DATABASE 'db1' , USER 'user1' , PASSWORD 'pass1' , PORT 10002);
CREATE SERVER nd1_1 FOREIGN DATA WRAPPER mysql OPTIONS (HOST '127.0.0.1', DATABASE 'db1' , USER 'user1' , PASSWORD 'pass1' , PORT 10001);
CREATE TABLE user_info(
id int(10) unsigned NOT NULL AUTO_INCREMENT,
k int(10) unsigned NOT NULL DEFAULT '0',
c char(120) NOT NULL DEFAULT '',
pad char(60) NOT NULL DEFAULT '',
PRIMARY KEY (id),
KEY k (k)
)ENGINE=spider COMMENT='wrapper "mysql", table "user_info"'
PARTITION BY range columns (id)
(
PARTITION ptl values less than (1000) COMMENT = 'srv "nodel nd2_1",mkd "2",msi "5054", link一status "0 0"',
PARTITION pt2 values less than (1000) COMMENT = 'srv "node2 nd3_1",mkd "2",msi "5054", link一status "0 0"',
PARTITION pt3 values less than (1000) COMMENT = 'srv "node3 nd1_1",mkd "2",msi "5054", link一status "0 0"'
)';
七、其他
7.1 性能
下图是MariaDB官方文档中提供的性能指标。
上图为sysbench只读场景下,MariaDB(InnoDB)、Spider和其他Proxy套件的性能对比,可以发现在低并发场景下Spider处于弱势,但并发数达到128以上,除了Spider和HaProxy,MariaDB和MySQL-Proxy性能均有急剧下降。
上图为读写操作在不同Spider表数据冗余下的性能对比,可以发现副本多了对读帮助较大,对写影响较小。
7.2 优缺点
Spider最大的优势是在数据库进程内部实现了分库分表和跨节点的表复杂查询,这样容易做到跟MySQL协议的100%兼容。依托于成熟而灵活的MySQL插件式存储引擎框架,可以进一步堆叠/扩展更多高级功能,比如后端节点的探活和自动切换,多个Spider节点的负载均衡等。可以说,基于引擎插件层进行MySQL功能特性扩展是个非常不错的方向。
Spider的缺点在于用户数还是太少,功能集未经过大规模验证,成熟度不高。此外文档偏少,功能也还不够丰富。
Spider的优势:
a、对业务透明,业务程序可以不用修改或做非常少的修改。业务连接到Spider节点,具体访问哪个数据节点由Spider处理,无需放在业务逻辑中;
b、方便水平扩展,能解决单个服务器的计算和存储资源瓶颈问题。扩展能力可以是无穷的;
c、支持多种后端数据节点类型,比如后端节点可以是MariaDB,也可以是MySQL,甚至是Oracle(MariaDB目前暂不支持);
d、拆分方式灵活。可以根据需求实现垂直拆分和水平拆分功能,水平拆分支持基于分区表,可支持哈希、范围、列表等算法;
e、完全兼容MySQL协议,由于Spider位于存储引擎层,基于分区表实现数据拆分,所以MySQL的所有SQL在Spider均可以进行适配。这是Spider相比传统的分库分表中间件非常大的优势;
f、可透明使用后端数据节点的多种功能特性。比如全文索引,地理位置索引等。
Spider的劣势:
a、Spider的表不支持查询缓存;
b、Spider实例一致性物理备份困难。需要到每个数据节点单独进行物理备份,整个实例的一致性物理备份不好做;
c、Spider本身是单点。可通过MySQL复制来实现高可用,但需为业务提供VIP实现Spider节点切换后的服务可用性;
d、性能不如单MariaDB/MySQL实例。这是由于用户请求多跨一层网络,性能上会有一些损耗。尤其是在跨分区、跨表查询时。
e、线上使用案例较少,功能稳定性还未得到全面验证。比如笔者在某些部署模式下使用XA事务会报错,在执行DirectSQL时会导致mysqld crash。
八、总结
Spider的最大亮点是为MySQL的使用者提供分库分表的中间件解决方案,同时在SQL语法上兼容MySQL。这得益于Spider作为MySQL的插拔式引擎而存在。Spider是一个proxy,其本身并没有存储数据,因此上层的读写表请求需要转换成SQL语句,重新路由到后端的数据节点。相比其它的中间件解决方案,Spider的查询解析次数都是两次,并没有过多开销。此外,Spider还针对聚集、排序等操作提供了MAP REDUCE的解决方案。
Spider部署非常简单,使用方便。可以使用在三类业务场景:一是业务存在多个数据库实例,某些实例有访问其他实例上数据的需求(比如做Join);二是分库分表场景,比如业务存在超大表,或者业务的数据量一直在增加导致单数据库实例无法容纳,或者业务的并发请求非常多,单实例无法处理;三是作为Proxy,这个场景非常多,比如使用Spider实例作为堡垒机,外部业务均访问Spider节点,不直接访问后端数据库服务,提高网络安全性。
总之,从兼容性、性能上衡量,Spider是MySQL分库分表一个不错的选项。
九、参考