1.概述:
1.1 标准SQL、noSQL、newSQL的区别:
SQL(Structured Query Language):数据库,指传统的关系型数据库。缺点是面对大量的数据时,他的性能会随着数据库的增大而急剧下降。主要代表:SQL Server、Oracle、MySQL、PostgreSQL。
NoSQL(Not Only SQL):泛指非关系型数据库。以放宽ACID原则为代价,保证最终一致性原则。主要代表:MongoDB、Redis、CouchDB。
NewSQL:对各种新的可扩展/高性能数据库的简称,也是关系型数据库,汲取了SQL和NewSQL的优点,希望将ACID和可扩展性以及高性能结合。主要代表:Clustrix、GenieDB和国内PingCap开源的TiDB。
1.2行存储和列存储
行存储指的每一行的数据使用连续的存储空间,比如传统的关系型数据库MySQL、DB2等都采用行存储。列存储指的每一列的数据使用连续的存储空间,比如新兴的数据库HBase。
行存储适合的使用场景:
- 适合以行为单位的增删改查;
- 适合频繁的更新和删除;
- 适合返回所有列的查询操作;
列存储适合的使用场景:
- 每一列天然就是索引,无需新建索引,适合少数列的查询聚合分析等场景,能够减少磁盘IO;
- 适合压缩,比如列中有很多重复元素,压缩能极大的节省空间;
- 不适合实时的更新和删除操作;
2.TiDB介绍及基本原理
2.1 TIDB简介
- 一键水平扩容或者缩容
得益于 TiDB 存储计算分离的架构的设计,可按需对计算、存储分别进行在线扩容或者缩容,扩容或者缩容过程中对应用运维人员透明。 - 金融级高可用
数据采用多副本存储,数据副本通过 Multi-Raft 协议同步事务日志,多数派写入成功事务才能提交,确保数据强一致性且少数副本发生故障时不影响数据的可用性。可按需配置副本地理位置、副本数量等策略满足不同容灾级别的要求。 - 实时 HTAP,HTAP数据库是OLTP和OLAP数据库的融合;
提供行存储引擎 TiKV、列存储引擎 TiFlash 两款存储引擎,TiFlash 通过 Multi-Raft Learner 协议实时从 TiKV 复制数据,确保行存储引擎 TiKV 和列存储引擎 TiFlash 之间的数据强一致。TiKV、TiFlash 可按需部署在不同的机器,解决 HTAP 资源隔离的问题。 - 云原生的分布式数据库
专为云而设计的分布式数据库,通过 TiDB Operator 可在公有云、私有云、混合云中实现部署工具化、自动化。 - 兼容 MySQL 5.7 协议和 MySQL 生态
兼容 MySQL 5.7 协议、MySQL 常用的功能、MySQL 生态,应用无需或者修改少量代码即可从 MySQL 迁移到 TiDB。提供丰富的数据迁移工具帮助应用便捷完成数据迁移。
2.2 TIDB架构
-
TiDB Server:SQL 层,对外暴露 MySQL 协议的连接 endpoint,负责接受客户端的连接,执行 SQL 解析和优化,最终生成分布式执行计划。TiDB 层本身是无状态的,实践中可以启动多个 TiDB 实例,通过负载均衡组件(如 LVS、HAProxy 或 F5)对外提供统一的接入地址,客户端的连接可以均匀地分摊在多个 TiDB 实例上以达到负载均衡的效果。TiDB Server 本身并不存储数据,只是解析 SQL,将实际的数据读取请求转发给底层的存储节点 TiKV(或 TiFlash)。
-
PD (Placement Driver) Server:是 TiDB 集群的管理模块,同时也负责集群数据的实时调度,包括TiKV和TiFlash集群的调度。基于Raft协议,收集集群节点的信息进行调度。
第一类:作为一个分布式高可用存储系统,必须满足的需求,包括几种- 副本数量不能多也不能少
- 副本需要根据拓扑结构分布在不同属性的机器上
- 节点宕机或异常能够自动合理快速地进行容灾
第二类:作为一个良好的分布式系统,需要考虑的地方包括
- 维持整个集群的 Leader 分布均匀
- 维持每个节点的储存容量均匀
- 维持访问热点分布均匀
- 控制负载均衡的速度,避免影响在线服务
- 管理节点状态,包括手动上线/下线节点
-
存储节点
-
TiKV Server:负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 的 API 在 KV 键值对层面提供对分布式事务的原生支持,默认提供了 SI (Snapshot Isolation) 的隔离级别,这也是 TiDB 在 SQL 层面支持分布式事务的核心。TiDB 的 SQL 层做完 SQL 解析后,会将 SQL 的执行计划转换为对 TiKV API 的实际调用。所以,数据都存储在 TiKV 中。另外,TiKV 中的数据都会自动维护多副本(默认为三副本),天然支持高可用和自动故障转移。
-
TiFlash:TiFlash 是一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,TiSpark流式处理,主要的功能是为分析型的场景加速。
-
2.2.1 TiDB存储
Key-Value Pairs(键值对)
作为保存数据的系统,首先要决定的是数据的存储模型,也就是数据以什么样的形式保存下来。TiKV 的选择是 Key-Value 模型,并且提供有序遍历方法。
TiKV 数据存储的两个关键点:
- 这是一个巨大的 Map(可以类比一下 C++ 的 std::map),也就是存储的是 Key-Value Pairs(键值对)
- 这个 Map 中的 Key-Value pair按照Key的二进制顺序有序,也就是可以 Seek 到某一个 Key 的位置,然后不断地调用 Next 方法以递增的顺序获取比这个 Key 大的 Key-Value。
表数据与 Key-Value 的映射关系
在关系型数据库中,一个表可能有很多列。要将一行中各列数据映射成一个 (Key, Value) 键值对,需要考虑如何构造 Key。首先,OLTP 场景下有大量针对单行或者多行的增、删、改、查等操作,要求数据库具备快速读取一行数据的能力。因此,对应的 Key 最好有一个唯一 ID(显示或隐式的 ID),以方便快速定位。其次,很多 OLAP 型查询需要进行全表扫描。如果能够将一个表中所有行的 Key 编码到一个区间内,就可以通过范围查询高效完成全表扫描的任务。
基于上述考虑,TiDB 中的表数据与 Key-Value 的映射关系作了如下设计:
- 为了保证同一个表的数据放在一起,方便查找,TiDB 会为每个表分配一个表 ID,用 TableID 表示。表 ID 是一个整数,在整个集群内唯一。
- TiDB 会为表中每行数据分配一个行 ID,用 RowID 表示。行 ID 也是一个整数,在表内唯一。对于行 ID,TiDB 做了一个小优化,如果某个表有整数型的主键,TiDB 会使用主键的值当做这一行数据的行 ID。
每行数据按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_recordPrefixSep{RowID} Value: [col1, col2, col3, col4]
其中 tablePrefix 和 recordPrefixSep 都是特定的字符串常量,用于在 Key 空间内区分其他数据。
索引数据和 Key-Value 的映射关系
TiDB 同时支持主键和二级索引(包括唯一索引和非唯一索引)。与表数据映射方案类似,TiDB 为表中每个索引分配了一个索引 ID,用 IndexID 表示。
对于主键和唯一索引,需要根据键值快速定位到对应的 RowID,因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValue Value: RowID
对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID。因此,按照如下规则编码成 (Key, Value) 键值对:
Key: tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID} Value: null
2.3 TiKV存储引擎
TiKV 是一个分布式事务型的键值数据库,提供了满足 ACID 约束的分布式事务接口,并且通过Raft 协议保证了多副本数据一致性以及高可用。TiKV 作为 TiDB 的存储层,为用户写入 TiDB 的数据提供了持久化以及读写服务,同时还存储了 TiDB 的统计信息数据。
将数据按照 key 的范围划分成大致相等的切片(下文统称为 Region),每一个切片会有多个副本(通常是 3 个),其中一个副本是 Leader,提供读写服务。TiKV 通过 PD 对这些 Region 以及副本进行调度,以保证数据和读写负载都均匀地分散在各个 TiKV 上,这样的设计保证了整个集群资源的充分利用并且可以随着机器数量的增加水平扩展。每个TiKV上所有的region都在同一个RocksDB上,以便不同的写入可以合并在一次I/O种。
2.3.1 RocksDB
RocksDB由 Facebook 基于 LevelDB 开发的一款提供键值存储与读写功能的 LSM-tree 架构引擎。RocksDB大概由以下几部分组成。主要解决写多读少的问题,牺牲读性能来加速写性能;
-
WAL(write ahead log):写前日志,类似于MySQL的redoLog,WAL日志可以用于恢复memtable的数据;当memtable中的数据落盘到sst文件后,才会删除对应的WAL日志。
-
memtable:memtable是一个内存数据结构,它保存了落盘到sst文件前的数据;读取的时候也是先查询memtable;一旦一个memtable被写满,它会变为immutable memtable;
-
immutable memtable:immutable memtable将异步落盘的sst文件中,之后该memtable将会被删除。
-
blockcache:rocksdb中纯内存存储结构,存储SST文件被经常访问的热点数据,以提高读性能;
-
sst(sorted string table): 是一段排序好的表文件,它是实际持久化的数据文件,里面的数据按照Key进行排序能方便对其进行二分查找,每个immutable memtable都会落盘为一个ss table文件;sst是分层结构,每一层的sst的数量达到N之后会触发compact操作,合并到下一个层形成更大的sst。
LSM树会将所有的数据插入、修改、删除等操作记录(注意是操作记录)保存在内存之中,当此类操作达到一定的数据量后,再批量地顺序写入到磁盘当中。不断地将Immutable MemTable flush到持久化存储即可,而不用去修改之前的SSTable中的key,保证了顺序写。问题是会出现冗余写放大,不同层的sst之间会出现重复数据,同一层内不会出现重复数据。查询sst时,逐层查询,每一层sst有一个布隆过滤器,查询之前先判断本层有没有需要查询的key值。
TiKV中的RocksDB
每个 TiKV 实例中有两个 RocksDB 实例,一个用于存储 Raft 日志(通常被称为 raftdb),另一个用于存储用户数据以及 MVCC 信息(通常被称为 kvdb)。
ColumnFamily(列族, CF)
Rocksdb中引入了ColumnFamily(列族, CF)的概念,所有的读写操作都需要先指定列族。每个列族对应了一套lsm-tree;kvdb 中有四个列族:raft、lock、default 和 write:
- raft 列:用于存储各个 Region 的元信息。仅占极少量空间,用户可以不必关注。
- lock 列:用于存储悲观事务的悲观锁以及分布式事务的一阶段 Prewrite 锁。当用户的事务提交之后,lock cf 中对应的数据会很快删除掉,因此大部分情况下 lock cf 中的数据也很少(少于 1GB)。如果 lock cf 中的数据大量增加,说明有大量事务等待提交,系统出现了 bug 或者故障。
- write 列:用于存储用户真实的写入数据以及 MVCC 信息(该数据所属事务的开始时间以及提交时间)。当用户写入了一行数据时,如果该行数据长度小于 255 字节,那么会被存储 write 列中,否则的话该行数据会被存入到 default 列中。由于 TiDB 的非 unique 索引存储的 value 为空,unique 索引存储的 value 为主键索引,因此二级索引只会占用 writecf 的空间。
- default 列:用于存储超过 255 字节长度的数据。
3.查询调优:
sql 性能调优:https://docs.pingcap.com/zh/tidb/stable/dev-guide-optimize-sql
Explain执行计划解析;https://docs.pingcap.com/zh/tidb/stable/explain-overview
TiKV算子简介:
算子是为返回查询结果而执行的特定步骤。真正执行扫表(读盘或者读 TiKV Block Cache)操作的算子有如下几类:
- TableFullScan:全表扫描。
- TableRangeScan:带有范围的表数据扫描。
- TableRowIDScan:根据上层传递下来的 RowID 扫描表数据。时常在索引读操作后检索符合条件的行。
- IndexFullScan:另一种“全表扫描”,扫的是索引数据,不是表数据。
- IndexRangeScan:带有范围的索引数据扫描操作。
TiDB 会汇聚 TiKV/TiFlash 上扫描的数据或者计算结果,这种“数据汇聚”算子目前有如下几类:
- TableReader:将 TiKV 上底层扫表算子 TableFullScan 或 TableRangeScan 得到的数据进行汇总。
- IndexReader:将 TiKV 上底层扫表算子 IndexFullScan 或 IndexRangeScan 得到的数据进行汇总。
- IndexLookUp:先汇总 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。
- IndexMerge:和 IndexLookupReader 类似,可以看做是它的扩展,可以同时读取多个索引的数据,有多个 Build 端,一个 Probe 端。执行过程也很类似,先汇总所有 Build 端 TiKV 扫描上来的 RowID,再去 Probe 端上根据这些 RowID 精确地读取 TiKV 上的数据。Build 端是 IndexFullScan 或 IndexRangeScan 类型的算子,Probe 端是 TableRowIDScan 类型的算子。
4.参考文献:
1.TiDB官网文档:https://docs.pingcap.com/zh/
2.https://www.cnblogs.com/klb561/p/12064126.html
3.https://zhuanlan.zhihu.com/p/378814476
4.https://zhuanlan.zhihu.com/p/181498475