考虑做一个存储系统,首先要考虑的就是数据的存储模型。TiKV
选择的是Key-Value
模型,并且这个模型提供有序遍历的方法。简单来讲,TiKV
就是一个巨大的Map
,其中Key
和Value
都是原始的Byte
数组,在这个Map
中,Key
按照Byte
数组二进制比特位比较排列。
-
TiKV
是一个巨大的Map
,存储的是Key-Value Pair
。 -
TiKV Map
中的Key-Value pair
按照Key
的二进制顺序有序。可以Seek
到某一个Key
的位置,然后不断的调用Next
方法,以递增的顺序获取比这个Key
大的Key-Value
。
实现上,TiKV
没有直接向磁盘上写数据,而是把数据写入RocksDB
中,具体的数据落地由RocksDB
负责。可以简单的认为,TiKV
是一个分布式Key-Value Map
,RocksDB
是一个单机的Key-Value Map
。
有了这个存储模型,接下来就需要考虑如何在KV
模型上保存关系型数据以及如何在KV
上运行SQL
语句。
一、SQL
到KV
模型的存储
创建一个表结构:
CREATE TABLE User {
ID int,
Age int,
Name varchar(20),
Gender varchar(20),
PRIMARY KEY (ID),
UNIQUE index idxName (Name), //唯一索引
index idxGender (Gender)
};
接着插入数据:
image.png
那么到底需要存储哪些数据,做哪些映射?为了方便理解,我们拿SQL
到B+
树模型(MySQL默认数据引擎InnoDB数据存储模型)来做说明。
1.表数据以及主索引存储
InnoDB
中,表数据本身就是按B+
树组织的一个索引结构,B+
树叶节点的data
域保存了完整的数据记录。这个索引的key
是数据表的主键,因此InnoDB
表数据本身就是主索引。表数据以及主索引实际存储如下图:
TiDB
也采用了这个方式,即表数据本身也就是主索引。data
域即value:[col1,col2,col3,col4]
保存完整的数据记录,这里的难点是key
的设计。
TiKV
使用了RocksDB
的Column Families(CF)
特性,默认 RocksDB 实例将 KV 数据存储在内部的 default、write 和 lock 3 个 CF 内:
-
default CF
存储的是真正的数据,与其对应的参数位于[rocksdb.defaultcf]
项中; -
write CF
存储的是数据的版本信息(MVCC)
以及索引相关的数据,相关的参数位于[rocksdb.writecf]
项中; -
lock CF
存储的是锁信息,系统使用默认参数。
表数据文件数据存储在rocksdb.defaultcf
中,索引存储在rocksdb.writecf
。所以KEY中需要包含识别Database/Table
的tableID
,识别的行的rowID(Primary Key)
,识别索引的indexID
。每行数据按照规则进行编码成KV pair
:
Key: tablePrefix{tableID}_recordPrefixSep{rowID}
Value: [col1,col2,col3,col4]
注:tablePrefix/recordPrefixSep
都是特定的字符串常量,用于区分其他数据。
var(
tablePrefix = []byte{"_t"}
recordPrefixSep = []byte("_r")
indexPrefixSep = []byte("_i")
)
假设TiDB
分配的tableId
为10,表数据以及主索引实际存储如下:
t10_r15--->[34,Bob,Female]
t10_r18--->[77,Alice,Male]
t10_r20--->[5,Jim,Female]
t10_r30--->[91,Eric,Female]
t10_r49--->[22,Tom,Male]
t10_r50--->[89,Rose,Male]
2.辅助索引存储
InnoDB
中除了主索引以外还有辅助索引。InnoDB
的辅助索引data
域存储相应记录主键的值,而不是完整的数据记录。主索引key
是唯一的,而辅助索引的key
可以重复。但是B+
树要求键的值必须唯一,所以这里把辅助键的值和主键的值合并起来作为在B+
树中的真正键值,保证了唯一性。辅助索引存储如下图:
TiDB
与InnoDB
辅助索引的设计基本保持一致,但是区分了重复辅助索引和唯一辅助索引,并且为每个辅助索引分配了一个indexID
。
唯一辅助索引:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue
Value: rowID
假设TiDB
分配的tableID
为10,indexID
为1。Unique
索引indexName
实际存储为:
t10_r1_Bob--->15
t10_r1_Alice--->18
......
重复辅助索引:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue_rowID
Value: null
假设TiDB
分配的tableId
为10,indexID
为2。索引idxGender
实际存储为:
t10_r2_Fmale_15--->null
t10_r2_Male_18--->null
......
3.表结构存储
每个Database/Table
都被分配了一个唯一的ID
,这个ID
作为唯一标识,并且在编码为Key-Value
时,这个ID
会编码到Key
中,加上m
前缀。这样可以构造出一个Key
,Value
中存储的是序列化后的元信息。一对KV pair
就可以表示一个表结构信息:
Key: tableMetaPrefix{tableID}
value:[TableName,cloName,cloName,colName,colName]
假设TiDB分配的tableId为10,实际表结构存储为:
m10--->[User,ID,Age,Name,Gender]
二 、SQL
到kv
模型的查询映射
前面提到,将TiKV
看做一个巨大的有序的KV Map
,那么为了实现存储的水平扩展,需要将数据分散在多台机器上。对于一个KV
系统,将数据分散在多台机器上有两种比较典型的方案:一种是按照Key
做Hash
,根据Hash
值选择对应的存储节点;另一种是分Range
,某一段连续的Key
都保存在一个存储节点上。TiKV
选择了第二种方式,将整个Key-Value
空间分成很多段,每一段是一系列连续的Key
,我们将每一段叫做一个Region
,并且会尽量保持每个Region
中保存的数据不超过一定的大小(这个大小可以配置,目前默认是64mb
)。每一个Region
都可以用StartKey
到EndKey
这样一个左闭右开区间来描述。也就是说上面的数据在实际存储中,按Region
划分,是存储在不同的TiKV
的Node
中。如下图所示:
当对这个表执行一条SQL语句Select count(*) from User where id > 0 and id < 90
时,数据不再分布在一台物理机上,而是分布在3个物理机上。分布式SQL
操作如下:
- 构造
Key Range
区间(主键ID
划分)。区间:[0,20),[20,40),[40,60)… - 获取
key Range
所在Tikv
节点。 - 优化执行计划。即将
where
条件与count(*)
,一起推到相应的Tikv
节点。 - 每个
Tikv
节点过滤数据,计算count(*)
。 -
TiDB Server
将每个Tikv
节点计算count(*)
,合并起来计算。
image.png
以上就是一个分布式的SQL
计算,很明显它是不同于MySQL
的SQL
查询。不管是NewSQL
数据库还是传统数据库,都会对 optimizer
进行优化。上面对count(*)
的优化设计,一方面增加了计算并行度,同时也减少了网络交互。TiDB
在优化方面做了很多工作,比如常量折叠、常量传播、JOIN
选择等,并且有一个框架去统计下层数据信息,在此基础上继续做优化。具体优化可以参考MPP and SMP in TiDB。
三、TiDB SQL
层框架
上面主要介绍了SQL
到KV
存储映射以及SQL
分布式计算相关功能,让我们从SQL
的角度去了解数据是如何存储,SQL
是如何计算执行的。这些都是在SQL
层框架中实现的。实际TiDB
的SQL
层非常复杂,涉及到很多优化器分布原理,分布框架执行的细节,是TiDB
中比较复杂的模块。下图列出了重要的模块以及调用关系(部分来图来自网络):
用户的SQL
请求会直接或者通过Load Balancer
发送到tidb-server
,tidb-server
会解析MySQL Protocol Packet
,获取请求内容,然后做语法解析、查询计划制定和优化、执行查询计划获取和处理数据。数据全部存储在TiKV
集群中,所以在这个过程中tidb-server
需要和tikv-server
交互,获取数据。最后tidb-server
需要将查询结果返回给用户。
网友评论