原文出处:阿里云RDS-数据库内核组node
HybridDB for MySQL(原名petadata)是面向在线事务(OLTP)和在线分析(OLAP)混合场景的关系型数据库。HybridDB采用一份数据存储来进行OLTP和OLAP处理,解决了以往须要把一份数据屡次复制来分别进行业务交易和数据分析的问题,极大地下降了数据存储的成本,缩短了数据分析的延迟,使得实时分析决策称为可能。mysql
HybridDB for MySQL兼容MySQL的语法及函数,而且增长了对Oracle经常使用分析函数的支持,100%彻底兼容TPC-H和TPC-DS测试标准,从而下降了用户的开发、迁移和维护成本。算法
TokuDB是TokuTek公司(已被 Percona收购)研发的新引擎,支持事务/MVCC,有着出色的数据压缩功能,支持异步写入数据功能。sql
TokuDB索引结构采用fractal tree数据结构,是buffer tree的变种,写入性能优异,适合写多读少的场景。除此以外,TokuDB还支持在线加减字段,在线建立索引,锁表时间很短。数据库
Percona Server和Mariadb支持TokuDB做为大数据场景下的引擎,目前官方MySQL还不支持TokuDB。ApsaraDB for MySQL从2015年4月开始支持TokuDB,在大数据或者高并发写入场景下推荐使用。数组
TokuDB最显著的优点就是数据压缩,支持多种压缩算法,用户可按照实际的资源消耗修改压缩算法,生产环境下推荐使用zstd,实测的压缩比是4:1。缓存
目前HybridDB for MySQL支持6中压缩算法:安全
Percona建议6核如下场景使用默认压缩算法zlib,6核以上可使用压缩率更高的压缩算法,大数据场景下推荐使用zstd压缩算法,压缩比高,压缩和解压速度快,也比较稳定。数据结构
用户能够在建表时使用ROW_FORMAT子句指定压缩算法,也可用使用ALTER TABLE修改压缩算法。ALTER TABLE执行后新数据使用新的压缩算法,老数据还是老的压缩格式。并发
mysql> CREATE TABLE t_test (column_a INT NOT NULL PRIMARY KEY, column_b INT NOT NULL) ENGINE=TokuDB ROW_FORMAT=tokudb_zstd;
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_b` int(11) NOT NULL,
PRIMARY KEY (`column_a`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=TOKUDB_ZSTD
mysql> ALTER TABLE t_test ROW_FORMAT=tokudb_snappy;
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_b` int(11) NOT NULL,
PRIMARY KEY (`column_a`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=TOKUDB_SNAPPY复制代码
TokuDB采用块级压缩,每一个块大小是4M,这是压缩前的大小;假设压缩比是4:1,压缩后大小是1M左右。比较tricky地方是:TokuDB压缩单位是partition,大小是64K。相比innodb16K的块大小来讲要大很多,更有利压缩算法寻找重复串。
上面提到,修改压缩算法后新老压缩格式的数据能够同时存在。如何识别呢?
每一个数据块在压缩数据前预留一个字节存储压缩算法。从磁盘读数据后,会根据那个字节的内容调用相应的解压缩算法。
另外,TokuDB还支持并行压缩,数据块包含的多个partition能够利用线程池并行进行压缩和序列化工做,极大加速了数据写盘速度,这个功能在数据批量导入(import)状况下开启。
TokuDB还支持在轻微阻塞DML状况下,增长或删除表中的字段或者扩展字段长度。
执行在线增减字段时表会锁一小段时间,通常是秒级锁表。锁表时间短得益于fractal tree的实现。TokuDB会把这些操做放到后台去作,具体实现是:往root块推送一个广播msg,经过逐层apply这个广播msg实现增减字段的操做。
须要注意的:
mysql> ALTER TABLE t_test ADD COLUMN column_c int(11) NOT NULL;
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_b` int(11) NOT NULL,
`column_c` int(11) NOT NULL,
PRIMARY KEY (`column_a`),
KEY `ind_1` (`column_b`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=TOKUDB_SNAPPY
mysql> ALTER TABLE t_test DROP COLUMN column_b;
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_c` int(11) NOT NULL,
PRIMARY KEY (`column_a`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1复制代码
TokuDB索引采用fractal tree结构,索引修改工做由后台线程异步完成。TokuDB会把每一个索引更新转化成一个msg,在server层上下文只把msg加到root(或者某个internal)块msg buffer中即可返回;msg应用到leaf块的工做是由后台线程完成的,此后台线程被称做cleaner,负责逐级apply msg直至leaf块
DML语句被转化成FTINSERT/FTDELETE,此类msg只应用到leaf节点。
在线加索引/在线加字段被转化成广播msg,此类msg会被应用到每一个数据块的每一个数据项。
实际上,fractal tree是buffer tree的变种,在索引块内缓存更新操做,把随机请求转化成顺序请求,缩短server线程上下文的访问路径,缩短RT。因此,TokuDB在高并发大数据量场景下,能够提供稳定高效的写入性能。
除此以外,TokuDB实现了bulk fetch优化,range query性能也是不错的。
TokuDB支持在线加索引不阻塞更新语句 (insert, update, delete) 的执行。能够经过变量 tokudbcreateindex_online 来控制是否开启该特性, 不过遗憾的是目前只能经过 CREATE INDEX 语法实如今线建立;若是用ALTER TABLE建立索引仍是会锁表的。
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_b` int(11) NOT NULL,
PRIMARY KEY (`column_a`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=TOKUDB_SNAPPY
mysql> SET GLOBAL tokudb_create_index_online=ON;
mysql> CREATE INDEX ind_1 ON t_test(column_b);
mysql> SHOW CREATE TABLE t_test\G
Table: t_test
Create Table: CREATE TABLE `t_test` (
`column_a` int(11) NOT NULL,
`column_b` int(11) NOT NULL,
PRIMARY KEY (`column_a`),
KEY `ind_1` (`column_b`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1 ROW_FORMAT=TOKUDB_SNAPPY复制代码
若是不考虑unique constraint检查,TokuDB写是异步完成的。每一个写请求被转化成FT_insert类型的msg,记录着要写入的
Server上下文的写路径很短,只要把写请求对应的msg追加到roo数据块的msg buffer便可,这是LSM数据结构的核心思想,把随机写转换成顺序写,LevelDB和RocksDB也是采用相似实现。
因为你们都在root数据块缓存msg,必然形成root块成为热点,也就是性能瓶颈。
为了解决这个问题,TokuDB提出promotion概念,从root数据块开始至多往下看2层。若是当前块数据块是中间块而且msg buffer是空的,就跳过这层,把msg缓存到下一层中间块。
下面咱们举例说明write过程。
假设,insert之qiafractal tree状态以下图所示:
root数据块上300对应的msg buffer为空,须要进行inject promotion,也就是说会把msg存储到下面的子树上。下一级数据块上300对应的msg buffer非空(msg:291),不会继续promotion,msg被存储到当前的msg buffer。
root数据块上100对应的msg buffer为空,须要进行inject promotion,也就是说会把msg存储到下面的子树上。下一级数据块上100对应的msg buffer也为空,须要继续promotion。再下一级数据块上100对应的msg buffer非空(msg:84),不会继续promotion,msg被存储到当前的msg buffer。
root数据块上211对应的msg buffer为空,须要进行inject promotion,也就是说会把msg存储到下面的子树上。下一级数据块上211对应的msg buffer也为空,须要继续promotion。再下一级数据块上211对应的msg buffer也为空,可是不会继续promotion,msg被存储到当前的msg buffer。这是由于promotion至多向下看2层,这么作是为了不dirty的数据块数量太多,减小checkpoint刷脏的压力。
TokuDB提供行级锁处理并发读写数据。
全部的INSERT、DELETE或者SELECT FOR UPDATE语句在修改索引数据结构fractal tree以前,须要先拿记录(也就是key)对应的行锁,获取锁以后再去更新索引。与InnoDB行锁实现不一样,InnoDB是锁记录数据结构的一个bit。
因而可知,TokuDB行锁实现致使一些性能问题,不适合大量并发更新的场景。
为了缓解行锁等待问题,TokuDB提供了行锁timeout参数(缺省是4秒),等待超时会返回失败。这种处理有助于减小deadlock发生。
因为中间数据块(internal block)会缓存更新操做的msg,读数据时须要先把上层msg buffer中的msg apply到叶子数据块(leaf block)上,而后再去leaf上把数据读上来。
3,4,5,6,7,8,9是中间数据块,10,11,12,13,14,15,16,17是叶子数据块;
上图中,每一个中间数据块的fanout是2,表示至多有2个下一级数据块;中间节点的msg buffer用来缓存下一级数据块的msg,橘黄色表示有数据,黄绿色表示msg buffer是空的。
若是须要读block11的数据,须要先把数据块3和数据块6中的msg apply到叶子数据块11,而后去11上读数据。
Msg apply的过程也叫合并(merge),全部基于LSM原理的k-v引擎(比方LevelDB,RocksDB)读数据时都要先作merge,而后去相应的数据块上读数据。
如上图所示,绿色是中间数据块,紫色是叶数据块;中间数据块旁边的黄色矩形是msg buffer。
如要要query区间[5-18]的数据
为了减小merge代价,TokuDB提供bulk fetch功能:每一个basement node大小64K(这个是数据压缩解压缩的单位)只要作一次merge操做;而且TokuDB的cursor支持批量读,一个batch内读取若干行数据缓存在内存,以后每一个handler::indexnext先去缓存里取下一行数据,只有当缓存数据所有被消费过以后发起下一个batch读,再以后handler::indexnext操做仍是先去缓存里取下一行数据。
Batch读过程由cursor的callback驱动,直接把数据存到TokuDB handler的buffer中,不只减小了merge次数,也减小了handler::index_next调用栈深度。
TokuDB支持后台异步合并msg,把中间数据块中缓存的msg逐层向下刷,直至leaf数据块。
这过程是由周期运行的cleaner线程完成的,cleaner线程每秒被唤醒一次。每次执行扫描必定数目的数据块,寻找缓存msg最多的中间数据块;扫描结束后,把msg buffer中的msg刷到(merge)下一层数据块中。
前面提到,大部分写数据并不会把msg直接写到leaf,而是把msg缓存到root或者某一级中间数据块上。虽然promotion缓解了root块热点问题,局部热点问题依然存在。
假设某一个时间段大量并发更新某范围的索引数据,msg buffer短期内堆积大量msg;因为cleaner线程是单线程顺序扫描,极可能来不及处理热点数据块,致使热点数据msg堆积,而且数据块读写锁争抢现象愈来愈严重。
为了解决这个问题,TokuDB引入了专门的线程池来帮助cleaner线程快速处理热点块。大体处理是:若是msg buffer缓存了过多的msg,写数据上下文就会唤醒线程池中的线程帮助cleaner快速合并当前数据块。
为了加速数据处理过程,TokuDB在内存缓存数据块,全部数据块组织成一个hash表,能够经过hash计算快速定位,这个hash表被称做cachetable。InnoDB也有相似缓存机制,叫作buffer pool(简记bp)。
内存中数据块被修改后不会当即写回磁盘,而是被标记成dirty状态。Cachetable满会触发evict操做,选择一个victim数据块释放内存。若是victim是dirty的,须要先把数据写回。Evict操做是由后台线程evictor处理的,缺省1秒钟运行一次,也可能因为缓存满由server上下文触发。
TokuDB采用激进的缓存策略,尽可能把数据保留在内存中。除了evictor线程之外,还有一个按期刷脏的checkpoint线程,缺省60每秒运行一次把内存中全部脏数据回刷到磁盘上。Checkpoint结束后,清理redo log文件。
TokuDB采用sharp checkpoint策略,checkpoint开始时刻把cachetable中全部数据块遍历一遍,对每一个数据块打上checkpointpending标记,这个过程是拿着client端exclusive锁的,全部INSERT/DELETE操做会被阻塞。标记checkpointpending过程结束后,释放exclusive锁,server的更新请求能够继续执行。
随后checkpoint线程会对每一个标记checkpoint_pending的脏页进行回写。为了减小I/O期间数据块读写锁冲突,先把数据clone一份,而后对cloned数据进行回写;clone过程是持有读写锁的write锁,clone结束后释放读写锁,数据块能够继续提供读写服务。Cloned数据块写回时,持有读写I/O的mutex锁,保证on-going的I/O至多只有一个。
更新数据块发现是checkpoint_pending而且dirty,那么须要先把老数据写盘。因为checkpoint是单线程,可能来不及处理这个数据块。为此,TokuDB提供一个专门的线程池,server上下文只要把数据clone一份,而后把回写cloned数据的任务扔给线程池处理。
全部缓存在内存的数据块按照首次访问(cachemiss)时间顺序组织成clocklist。TokuDB没有维护LRU list,而是使用clocklist和count(可理解成age)来模拟数据块使用频率。
Evictor,checkpoint和cleaner线程(参见异步合并小结)都是扫描clock_list,每一个线程维护本身的head记录着下次扫描开始位置。
如上图所示,hash中黑色连线表示bucket链表,蓝色连线表示clocklist。Evictor,checkpoint和cleaner的header分别是mclockhead,mcheckpointhead和mcleaner_head。
数据块被访问,count递增(最大值15);每次evictor线程扫到数据块count递减,减到0整个数据块会被evict出去。
TokuDB块size比较大,缺省是4M;因此按照块这个维度去作evict不是特别合理,有些partition数据比较热须要在内存多呆一会,冷的partition能够尽早释放。
为此,TokuDB还提供partial evict功能,数据块被扫描时,若是count>0而且是clean的,就把冷partition释放掉。Partial evict对中间数据块(包含key分布信息)作了特殊处理,把partition转成压缩格式减小内存使用,后续访问须要先解压缩再使用。Partial evict对leaf数据块的处理是:把partition释放,后续访问须要调用pf_callback从磁盘读数据,读上来的数据也是先解压缩的。
这里说的写优先是指并发读写数据块时,写操做优先级高,跟行级锁无关。
假设用户要读区间[210, 256],须要从root->leaf每层作binary search,在search以前要把数据块读到内存而且加readlock。
如上图所示,root(height 3)和root子数据块(height 2)尝试读锁(try_readlock)成功,可是在root的第二级子数据块(height 1)尝试读锁失败,这个query会把root和root子数据块(height 2)读锁释放掉,退回到root从新尝试读锁。
TokuDB采用WAL(Write Ahead Log),每一个INSERT/DELETE/CREATE INDEX/DROP INDEX操做以前会记redo log和undo log,用于崩溃恢复和事务回滚。
TokuDB的redo log是逻辑log,每一个log entry记录一个更新事件,主要包含:
其中,db,key和val不是必须的,好比checkpoint就没有这些信息。
长度1和长度2必定是相等的,记两个长度是为了方便前向(backward)和后向(forward)扫描。
Recory过程首先前向扫描,寻找最后一个有效的checkpoint;从那个checkpoint开始后向扫描回放redo log,直至最后一个commit事务。而后把全部活跃事务abort掉,最后作一个checkpoint把数据修改同步到磁盘上。
TokuDB的undo日志是记录在一个单独的文件上,undo日志也是逻辑的,记录的是更新的逆操做。独立的undo日志,避免老数据形成数据空间膨胀问题。
相对RocksDB,TokuDB最显著的优点就是支持完整事务,支持MVCC。
TokuDB还支持事务嵌套,能够用来实现savepoint功能,把一个大事务分割成一组小事务,小事务失败只要重试它本身就行了,不用回滚整个事务。
TokuDB支持隔离级别:READ UNCOMMITTED, READ COMMITTED (default), REPEATABLE READ, SERIALIZABLE。SERIALIZABLE是经过行级锁实现的;READ COMMITTED (default),和REPEATABLE READ是经过snapshot实现。
TokuDB支持多版本,多版本数据是记录在页数据块上的。每一个leaf数据块上的
每一个写事务开始时都会得到一个事务id(TokuDB记作txnid,InnoDB记作trxid)。其实,事务id是一个全局递增的整数。全部的写事务都会被加入到事务mgr的活跃事务列表里面。
所谓活跃事务就是处于执行中的事务,对于RC以上隔离界别,活跃事务都是不可见的。前面提到过,SERIALIZABLE是经过行级锁实现的,没必要考虑可见性。
通常来讲,RC可见性是语句级别的,RR可见性是事务级别的。这在TokuDB中是如何实现的呢?
每一个语句执行开始都会建立一个子事务。若是是RC、RR隔离级别,还会建立snapshot。Snapshot也有活跃事务列表,RC隔离级别是复制事务mgr在语句事务开始时刻的活跃事务列表,RR隔离级别是复制事务mgr在server层事务开始时刻的活跃事务列表。
Snapshot可见性就是事务id比snapshot的事务id更小,意味着更早开始执行;可是不在snapshot活跃事务列表的事务。
随着事务提交snapshot结束,老版本数据不在被访问须要清理,这就引入了GC的问题。
为了判断写事务的更新是否被其余事务访问,TokuDB的事务mgr维护了referencexids数组,记录事务提交时刻,系统中处于活跃状态snapshot个数,做用至关于referencecount。
以上描述了TokuDB如何跟踪写事务的引用者。那么GC是什么时候执行的呢?
能够调用OPTIMIZE TABLE显式触发,也能够在后续访问索引key时隐式触发。
以上介绍了TokuDB引擎内核原理,下面咱们从HybridDB for MySQL产品的角度谈一下业务场景和性能。
HybridDB for MySQL设计目标是提供低成本大容量分布式数据库服务,一体式处理OLTP和OLAP混合业务场景,提供存储和计算能力;而存储和计算节点在物理上是分离的,用户能够根据业务特色定制存储计算节点的配比,也能够单独购买存储和计算节点。
HybridDB for MySQL数据只存储一份,减小数据交换成本,同时也下降了存储成本;全部功能集成在一个实例之中,提供统一的用户接口,一致的数据视图和全局统一的SQL兼容性。
HybridDB for MySQL支持数据库分区,总体容量和性能随分区数目增加而线性增加;用户可先购买一个基本配置,随业务发展后续能够购买更多的节点进行扩容。HybridDB for MySQL提供在线的扩容和缩容能力,水平扩展/收缩存储和计算节点拓扑结构;在扩展过程当中,不影响业务对外提供服务,优化数据分布算法,减小从新分布数据量;采用流式迁移,备份数据不落地。
除此以外,HybridDB for MySQL还支持高可用,复用链路高可用技术,采用一主多备方式实现三副本。HybridDB for MySQL复用ApsaraDB for MySQL已有技术框架,部署、升级、链路管理、资源管理、备份、安全、监控和日志复用已有功能模块,技术风险低,验证周期短,能够说是站在巨人肩膀上的创新。
HybridDB for MySQL使用软硬件总体方案解决大容量低成本问题。
软件方面,HybridDB for MySQL是分布式数据库,摆脱单机硬件资源限制,提供横向扩展能力,容量和性能随节点数目增长而线性增长。存储节点MySQL实例选择使用TokuDB引擎,支持块级压缩,压缩算法以表单位进行配置。用户可根据业务自身特色选择使用压缩效果好的压缩算法好比lzma,也能够选择quicklz这种压缩速度快资源消耗低的压缩算法,也能够选择像zstd这种压缩效果和压缩速度比较均衡的压缩算法。若是选用zstd压缩算法,线上实测的压缩比是3~4。
硬件方面,HybridDB for MySQL采用分层存储解决方案,大量冷数据存储在SATA盘上,少许温数据存储在ssd上,热数据存储在数据库引擎的内存缓存中(TokuDB cachetable)。SATA盘和ssd上数据之间的映射关系经过bcache驱动模块来管理,bcache能够配置成WriteBack模式(写路径数据写ssd后即返回,ssd中更新数据由bcache负责同步到SATA盘上),可加速数据库checkpoint写盘速度;也能够配置成WriteThrough模式(写路径数据同时写到ssd和SATA上,二者都ack写才算完成)。
TokuDB采用fractal tree(中文译做分型树)数据结构,优化写路径,大部分二级索引的写操做是异步的,写被缓存到中间数据块即返回。写操做同步到叶数据块能够经过后台cleaner线程异步完成,也可能由后续的读操做同步完成(读合并)。Fractal tree在前面的内核原理部分有详尽描述,这里就不赘述了。
细心的朋友可能会发现,咱们在异步写前加了个前缀:大部分二级索引。那么大部分是指那些状况呢?这里大部分是指不须要作quickness检查的索引,写请求直接扔给fractal tree的msg buffer便可返回。若是二级索引包含unique索引,必须先作惟一性检查保证不存在重复键值。不然,异步合并(或者读合并)没法通知惟一性检查失败,也没法回滚其余索引的更新。Pk字段也有相似的惟一性语义,写以前会去查询pk键值是否已存在,顺便作了root到leaf数据块的预读和读合并。因此,每条新增数据执行INSERT INTO的过程不彻底是异步写。
ApsaraDB for MySQL对于日志场景作了优化,利用INSERT IGNORE语句保证pk键值惟一性,而且经过把二级索引键值1-1映射到pk键值空间的方法保证二级索引惟一性,将写操做转换成全异步写,大大下降了写延迟。因为省掉惟一性检查的读过程,引擎在内存中缓存的数据量大大减小,缓存写请求的数据块受读干扰被释放的可能性大大下降,进而写路径上发生cachetable miss的可能性下降,写性能更加稳定。
HybridDB for MySQL同时提供单分区事务和分布式事务支持,支持跨表、跨引擎、跨数据库、跨MySQL实例,跨存储节点的事务。HybridDB for MySQL使用两阶段提交协议支持分布式事务,提交阶段proxy做为协调者将分布式事务状态记录到事务元数据库;分区事务恢复时,proxy从事务元数据库取得分布式事务状态,并做为协调者从新发起失败分区的事务。
HybridDB for MySQL还能够经过判断WHERE条件是否包含分区键的等值条件,决定是单分区事务仍是分布式事务。若是是单分区事务,直接发送给分区MySQL实例处理。
HybridDB for MySQL经过将存储分区无缝迁移到更多(或更少的)MySQL分区实例上实现弹性数据扩展(收缩)的功能,分区迁移完成以后proxy层更新路由信息,把请求切到新分区上,老分区上的数据会自动清理。Proxy切换路由信息时会保持链接,不影响用户业务。
数据迁移是经过全量备份+增量备份方式实现,全量备份不落地直接流式上传到oss。增量备份经过binlog方式同步,HybridDB for MySQL没必要自行实现binlog解析模块,而是利用ApsaraDB for MySQL优化过的复制逻辑完成增量同步,经过并行复制提高性能,而且保证数据一致性。
TokuDB支持一个表上建立多个聚合索引,以空间代价换取查询性能,减小回pk取数据。阿里云ApsaraDB for MySQL在优化器上对TokuDB聚合索引作了额外支持,在cost接近时能够优先选择聚合索引;存在多个cost接近的聚合索引,能够优先选择与WHERE条件最匹配的聚合索引。
压测配置:
压测配置: