共计 12617 个字符,预计需要花费 32 分钟才能阅读完成。
原文出处:阿里云 RDS- 数据库内核组
HybridDB for MySQL(原名 petadata)是面向在线事务(OLTP)和在线分析(OLAP)混合场景的关系型数据库。HybridDB 采用一份数据存储来进行 OLTP 和 OLAP 处理,解决了以往需要把一份数据多次复制来分别进行业务交易和数据分析的问题,极大地降低了数据存储的成本,缩短了数据分析的延迟,使得实时分析决策称为可能。
HybridDB for MySQL 兼容 MySQL 的语法及函数,并且增加了对 Oracle 常用分析函数的支持,100% 完全兼容 TPC- H 和 TPC-DS 测试标准,从而降低了用户的开发、迁移和维护成本。
TokuDB 是 TokuTek 公司(已被 Percona 收购)研发的新引擎,支持事务 /MVCC,有着出色的数据压缩功能,支持异步写入数据功能。
TokuDB 索引结构采用 fractal tree 数据结构,是 buffer tree 的变种,写入性能优异,适合写多读少的场景。除此之外,TokuDB 还支持在线加减字段,在线创建索引,锁表时间很短。
Percona Server 和 Mariadb 支持 TokuDB 作为大数据场景下的引擎,目前官方 MySQL 还不支持 TokuDB。ApsaraDB for MySQL 从 2015 年 4 月开始支持 TokuDB,在大数据或者高并发写入场景下推荐使用。
TokuDB 优势
数据压缩
TokuDB 最显著的优势就是数据压缩,支持多种压缩算法,用户可按照实际的资源消耗修改压缩算法,生产环境下推荐使用 zstd,实测的压缩比是 4:1。
目前 HybridDB for MySQL 支持 6 中压缩算法:
- lzma: 压缩比最高,资源消耗高
- zlib:Percona 默认压缩算法,最流行,压缩比和资源消耗适中
- quicklz:速度快,压缩比最低
- snappy:google 研发的,压缩比较低,速度快
- zstd:压缩比接近 zlib,速度快
- uncompressed:不压缩,速度最快
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 语句被转化成 FT_INSERT/FT_DELETE,此类 msg 只应用到 leaf 节点。
在线加索引 / 在线加字段被转化成广播 msg,此类 msg 会被应用到每个数据块的每个数据项。
实际上,fractal tree 是 buffer tree 的变种,在索引块内缓存更新操作,把随机请求转化成顺序请求,缩短 server 线程上下文的访问路径,缩短 RT。所以,TokuDB 在高并发大数据量场景下,可以提供稳定高效的写入性能。
除此之外,TokuDB 实现了 bulk fetch 优化,range query 性能也是不错的。
在线增加索引
TokuDB 支持在线加索引不阻塞更新语句 (insert, update, delete) 的执行。可以通过变量 tokudb_create_index_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,记录着要写入的 <key,value> 和事务信息用于跟踪。
Server 上下文的写路径很短,只要把写请求对应的 msg 追加到 roo 数据块的 msg buffer 即可,这是 LSM 数据结构的核心思想,把随机写转换成顺序写,LevelDB 和 RocksDB 也是采用类似实现。
由于大家都在 root 数据块缓存 msg,必然造成 root 块成为热点,也就是性能瓶颈。
为了解决这个问题,TokuDB 提出 promotion 概念,从 root 数据块开始至多往下看 2 层。如果当前块数据块是中间块并且 msg buffer 是空的,就跳过这层,把 msg 缓存到下一层中间块。
下面我们举例说明 write 过程。
假设,insert 之 qiafractal tree 状态如下图所示:
- insert 300
root 数据块上 300 对应的 msg buffer 为空,需要进行 inject promotion,也就是说会把 msg 存储到下面的子树上。下一级数据块上 300 对应的 msg buffer 非空(msg:291),不会继续 promotion,msg 被存储到当前的 msg buffer。
- insert 100
root 数据块上 100 对应的 msg buffer 为空,需要进行 inject promotion,也就是说会把 msg 存储到下面的子树上。下一级数据块上 100 对应的 msg buffer 也为空,需要继续 promotion。再下一级数据块上 100 对应的 msg buffer 非空(msg:84),不会继续 promotion,msg 被存储到当前的 msg buffer。
- insert 211
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] 的数据
- 以 5 作为 search key 从 root 到 leaf 搜索 >= 5 的数据,每个数据块内部做 binary search,最终定位到第一个 leaf 块。读数据之前,判断第一个 leaf 块所包含的 [5,9] 区间存在需要 apply 的 msg(上图中是 6,7,8),需要先做 msg apply 然后读取数据(5,6,7,8,9);
- 第一个 leaf 块读取完毕,以 9 作为 search key 从 root 到 leaf 搜索 >9 的数据,每个数据块内部做 binary search,最终定位到第二个 leaf 块。读数据之前,判断第二个 leaf 块所包含的 [10,16] 区间存在需要 apply 的 msg(上图中是 15),需要先做 msg apply 然后读取数据(10,12,15,16);
- 第二个 leaf 块读取完毕,以 16 作为 search key 从 root 到 leaf 搜索 >16 的数据,每个数据块内部做 binary search,最终定位到第三个 leaf 块。第三个数据块所包含的 [17,18] 区间不存在需要 apply 的 msg,直接读取数据(17,18)。
优化 range query
为了减少 merge 代价,TokuDB 提供 bulk fetch 功能:每个 basement node 大小 64K(这个是数据压缩解压缩的单位)只要做一次 merge 操作;并且 TokuDB 的 cursor 支持批量读,一个 batch 内读取若干行数据缓存在内存,之后每个 handler::index_next 先去缓存里取下一行数据,只有当缓存数据全部被消费过之后发起下一个 batch 读,再之后 handler::index_next 操作还是先去缓存里取下一行数据。
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 中所有数据块遍历一遍,对每个数据块打上 checkpoint_pending 标记,这个过程是拿着 client 端 exclusive 锁的,所有 INSERT/DELETE 操作会被阻塞。标记 checkpoint_pending 过程结束后,释放 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 数据的任务扔给线程池处理。
Cachetable
所有缓存在内存的数据块按照首次访问(cachemiss)时间顺序组织成 clock_list。TokuDB 没有维护 LRU list,而是使用 clock_list 和 count(可理解成 age)来模拟数据块使用频率。
Evictor,checkpoint 和 cleaner 线程(参见异步合并小结)都是扫描 clock_list,每个线程维护自己的 head 记录着下次扫描开始位置。
如上图所示,hash 中黑色连线表示 bucket 链表,蓝色连线表示 clock_list。Evictor,checkpoint 和 cleaner 的 header 分别是 m_clock_head,m_checkpoint_head 和 m_cleaner_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 记录一个更新事件,主要包含:
- 长度 1
- log command(标识操作类型)
- lsn
- timestamp
- 事务 id
- crc
- db
- key
- val
- 长度 2
其中,db,key 和 val 不是必须的,比如 checkpoint 就没有这些信息。
长度 1 和长度 2 一定是相等的,记两个长度是为了方便前向(backward)和后向(forward)扫描。
Recory 过程首先前向扫描,寻找最后一个有效的 checkpoint;从那个 checkpoint 开始后向扫描回放 redo log,直至最后一个 commit 事务。然后把所有活跃事务 abort 掉,最后做一个 checkpoint 把数据修改同步到磁盘上。
TokuDB 的 undo 日志是记录在一个单独的文件上,undo 日志也是逻辑的,记录的是更新的逆操作。独立的 undo 日志,避免老数据造成数据空间膨胀问题。
事务和 MVCC
相对 RocksDB,TokuDB 最显著的优势就是支持完整事务,支持 MVCC。
TokuDB 还支持事务嵌套,可以用来实现 savepoint 功能,把一个大事务分割成一组小事务,小事务失败只要重试它自己就好了,不用回滚整个事务。
ISOLATION LEVEL
TokuDB 支持隔离级别:READ UNCOMMITTED, READ COMMITTED (default), REPEATABLE READ, SERIALIZABLE。SERIALIZABLE 是通过行级锁实现的;READ COMMITTED (default), 和 REPEATABLE READ 是通过 snapshot 实现。
TokuDB 支持多版本,多版本数据是记录在页数据块上的。每个 leaf 数据块上的 <key,value> 二元组,key 是索引的 key 值(其实是拼了 pk 的),value 是 MVCC 数据。这与 oracle 和 InnoDB 不同,oracle 的多版本是通过 undo segment 计算构造出来的。InnoDB MVCC 实现原理与 oracle 近似。
事务的可见性
每个写事务开始时都会获得一个事务 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 活跃事务列表的事务。
GC
随着事务提交 snapshot 结束,老版本数据不在被访问需要清理,这就引入了 GC 的问题。
为了判断写事务的更新是否被其他事务访问,TokuDB 的事务 mgr 维护了 reference_xids 数组,记录事务提交时刻,系统中处于活跃状态 snapshot 个数,作用相当于 reference_count。
以上描述了 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 条件最匹配的聚合索引。
与单机版 ApsaraDB for MySQL 对比
与阿里云 OLTP+OLAP 混合方案对比
性能报告
高并发业务
压测配置:
- 4 节点,每节点 8 -core,32G,12000 iops,ssd 盘
高吞吐业务
压测配置:
- 8 节点,每节点 16-core,48G,12000 iops,ssd 盘