数据库技术产生于20世纪60年代末70年代初,其主要主要研究如何存储,使用和管理数据。随着计算机硬件和软件的发展,数据库技术也不断地发展。数据库技术在理论研究和系统开发上都取得了辉煌的成就。
(1)场景引入
假设现在有一个高速发展的互联网公司,核心业务库MySQL的数据量已经近亿行,且还在不断增长中,公司对于数据资产较为重视,所有数据要求多副本保存至少5年,且除了有对历史数据进行统计分析的离线报表业务外,还有一些针对用户数据实时查询的需求,如用户历史订单实时查询。
(2)问题分析
根据以往的MySQL使用经验,MySQL单表在 5000 万行以内时,性能较好,单表超过5000万行后,数据库性能、可维护性都会极剧下降。当然这时候可以做MySQL分库分表,如使用Mycat或Sharding-jdbc。
将大表拆分成小表,单表数据量控制在 5000 万行以内,使 MySQL 性能稳定可控。将单张大表拆分成小表后,能水平扩展,通过部署到多台服务器,提升整个集群的 QPS、TPS、Latency 等数据库服务指标。
但是,此方案的缺点也非常明显:分表跨实例后,产生分布式事务管理难题,一旦数据库服务器宕机,有事务不一致风险。分表后,对 SQL 语句有一定限制,对业务方功能需求大打折扣。尤其对于实时报表统计类需求,限制非常之大。事实上,报表大多都是提供给高层领导使用的,其重要性不言而喻。分表后,需要维护的对象呈指数增长(MySQL实例数、需要执行的 SQL 变更数量等)。
(3)解决问题
基于以上核心痛点,我们需要探索新的数据库技术方案来应对业务爆发式增长所带来的挑战,为业务提供更好的数据库服务支撑。
调研市场上的各大数据库,我们可以考虑选用NewSQL技术来解决,因为NewSQL技术有如下显著特点:
而TiDB数据库 GitHub的活跃度及社区贡献者方面都可以算得上是国际化的开源项目,是NewSQL技术中的代表性产品,所以我们可以选择使用TiDB数据库!
(1)TiDB简介
官网:https://pingcap.com/index.html
TiDB 是 PingCAP 公司设计的开源分布式 HTAP (Hybrid Transactional and Analytical Processing) 数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。TiDB 的目标是为 OLTP (Online Transactional Processing) 和 OLAP (Online Analytical Processing) 场景提供一站式的解决方案。
TiDB 的设计目标是 100% 的 OLTP 场景和 80% 的 OLAP 场景,更复杂的 OLAP 分析可以通过 TiSpark 项目来完成。
TiDB 对业务没有任何侵入性,能优雅的替换传统的数据库中间件、数据库分库分表等 Sharding 方案。同时它也让开发运维人员不用关注数据库 Scale 的细节问题,专注于业务开发,极大的提升研发的生产力。
(2)OLAP与OLTP的区别
OLTP:强调支持短时间内大量并发的事务操作(增删改查)能力,每个操作涉及的数据量都很小(比如几十到几百字节)
OLAP:偏向于复杂的只读查询,读取海量数据进行分析计算,查询时间往往很长。
(1)关系型数据库(RDBMS,即SQL数据库)
(2)NoSQL非关系型的数据存储
(3)NewSQL既保持NoSQL的高可扩展和高性能,并且保持关系模型
TiDB集群主要包括三个核心组件:TiDB Server,PD Server和TiKV Server。此外,还有用于解决用户复杂OLAP需求的TiSpark组件和简化云上部署管理的TiDB Operator组件。
PD Server
Placement Driver (简称 PD) 是整个集群的管理模块,其主要工作有三个:一是存储集群的元信息(某个 Key 存储在哪个 TiKV 节点);二是对 TiKV 集群进行调度和负载均衡(如数据的迁移、Raft group leader 的迁移等);三是分配全局唯一且递增的事务 ID。
TiDB Server
TiDB Server 负责接收 SQL 请求,处理 SQL 相关的逻辑,并通过 PD 找到存储计算所需数据的 TiKV 地址,与 TiKV 交互获取数据,最终返回结果。TiDB Server 是无状态的,其本身并不存储数据,只负责计算,可以无限水平扩展,可以通过负载均衡组件(如LVS、HAProxy 或 F5)对外提供统一的接入地址。
TiKV
TiKV Server 负责存储数据,从外部看 TiKV 是一个分布式的提供事务的 Key-Value 存储引擎。存储数据的基本单位是 Region,每个 Region 负责存储一个 Key Range(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个 TiKV 节点会负责多个 Region。TiKV 使用 Raft 协议做复制,保持数据的一致性和容灾。副本以 Region 为单位进行管理,不同节点上的多个 Region 构成一个 Raft Group,互为副本。数据在多个 TiKV 之间的负载均衡由 PD 调度,这里也是以 Region 为单位进行调度。
TiSpark
TiSpark 作为 TiDB 中解决用户复杂 OLAP 需求的主要组件,将 Spark SQL 直接运行在 TiDB 存储层上,同时融合 TiKV 分布式集群的优势,并融入大数据社区生态。至此,TiDB 可以通过一套系统,同时支持 OLTP 与 OLAP,免除用户数据同步的烦恼。
TiDB Operator
TiDB Operator 提供在主流云基础设施(Kubernetes)上部署管理 TiDB 集群的能力。它结合云原生社区的容器编排最佳实践与 TiDB 的专业运维知识,集成一键部署、多集群混部、自动运维、故障自愈等能力,极大地降低了用户使用和管理 TiDB 的门槛与成本。
TiDB 具备如下众多特性,其中两大核心特性为:水平扩展与高可用
(1)高度兼容MySQL
大多数情况下,无需修改代码即可从 MySQL 轻松迁移至 TiDB,分库分表后的 MySQL 集群亦可通过 TiDB 工具进行实时迁移。
对于用户使用的时候,可以透明地从MySQL切换到TiDB 中,只是“新MySQL”的后端是存储“无限的”,不再受制于Local的磁盘容量。在运维使用时也可以将TiDB当做一个从库挂到MySQL主从架构中。
(2)分布式事务
TiDB 100% 支持标准的 ACID 事务。
(3)一站式 HTAP 解决方案
TiDB 作为典型的 OLTP 行存数据库,同时兼具强大的 OLAP 性能,配合 TiSpark,可提供一站式 HTAP 解决方案,一份存储同时处理 OLTP & OLAP,无需传统繁琐的 ETL 过程。
(4)云原生SQL数据库
TiDB 是为云而设计的数据库,支持公有云、私有云和混合云,配合 TiDB Operator 项目 可实现自动化运维,使部署、配置和维护变得十分简单。
(5)水平弹性扩展
通过简单地增加新节点即可实现 TiDB 的水平扩展,按需扩展吞吐或存储,轻松应对高并发、海量数据场景。
(6)真正的金融级高可用
相比于传统主从 (M-S) 复制方案,基于 Raft 的多数派选举协议可以提供金融级的 100% 数据强一致性保证,且在不丢失大多数副本的前提下,可以实现故障的自动恢复 (auto-failover),无需人工介入。
水平扩展
高可用
高可用是 TiDB 的另一大特点,TiDB/TiKV/PD 这三个组件都能容忍部分实例失效,不影响整个集群的可用性。下面分别说明这三个组件的可用性、单个实例失效后的后果以及如何恢复。
TiDB
TiDB 是无状态的,推荐至少部署两个实例,前端通过负载均衡组件对外提供服务。当单个实例失效时,会影响正在这个实例上进行的 Session,从应用的角度看,会出现单次请求失败的情况,重新连接后即可继续获得服务。单个实例失效后,可以重启这个实例或者部署一个新的实例。
PD
PD 是一个集群,通过 Raft 协议保持数据的一致性,单个实例失效时,如果这个实例不是 Raft 的 leader,那么服务完全不受影响;如果这个实例是 Raft 的 leader,会重新选出新的 Raft leader,自动恢复服务。PD 在选举的过程中无法对外提供服务,这个时间大约是3秒钟。推荐至少部署三个 PD 实例,单个实例失效后,重启这个实例或者添加新的实例。
TiKV
TiKV 是一个集群,通过 Raft 协议保持数据的一致性(副本数量可配置,默认保存三副本),并通过 PD 做负载均衡调度。单个节点失效时,会影响这个节点上存储的所有 Region。对于 Region 中的 Leader 节点,会中断服务,等待重新选举;对于 Region 中的 Follower 节点,不会影响服务。当某个 TiKV 节点失效,并且在一段时间内(默认 30 分钟)无法恢复,PD 会将其上的数据迁移到其他的 TiKV 节点上。
(1)存储能力-TIKV-LSM
TiKV Server通常是3+的,TiDB每份数据缺省为3副本,这一点与HDFS有些相似,但是通过Raft协议进行数据复制,TiKV Server上的数据是以Region为单位进行,由PD Server集群进行统一调度,类似HBASE的Region调度。
TiKV集群存储的数据格式是KV的,在TiDB中,并不是将数据直接存储在 HDD/SSD中,而是通过RocksDB实现了TB级别的本地化存储方案,着重提的一点是:RocksDB和HBASE一样,都是通过 LSM树作为存储方案,避免了B+树叶子节点膨胀带来的大量随机读写。从何提升了整体的吞吐量。
(2)计算能力-TiDB Server
TiDB Server本身是无状态的,意味着当计算能力成为瓶颈的时候,可以直接扩容机器,对用户是透明的。理论上TiDB Server的数量并没有上限限制。
TiDB作为新一代的NewSQL数据库,在数据库领域已经逐渐站稳脚跟,结合了Etcd/MySQL/HDFS/HBase/Spark等技术的突出特点,随着TiDB的大面积推广,会逐渐弱化 OLTP/OLAP的界限,并简化目前冗杂的ETL流程,引起新一轮的技术浪潮。
在Centos 6的版本中如果要部署,这个难度还是比较大的,而且会有很多未知的坑,根据官方的建议,是需要在Centos 7以上的版本中,否则glibc的版本问题会很快碰到。我们安装一套Centos7,采用快速的单机部署的方式来尝鲜。
(1)下载安装包
(2)解压文件
(3)启动
(4)登入:mysql -h ip -P 4000 -u root
(1)准备环境
确保你的机器上已安装:
(2)快速部署
下载 tidb-docker-compose
(3)创建并启动集群
获取最新 Docker 镜像:cd tidb-docker-compose && docker-compose pull && docker-compose up -d
注意:
(4)查看集群启动状态
(6)访问集群监控页面
(1)创建、查看和删除数据库
(2)创建、查看和删除表
先创建一个库
使用 SHOW TABLES 语句查看数据库中的所有表。例如:
使用 CREATE TABLE 语句创建表。
如果表已存在,添加 IF NOT EXISTS 可防止发生错误:
CREATE TABLE IF NOT EXISTS person (
number INT(11),
name VARCHAR(255),
birthday DATE
);
使用 SHOW CREATE 语句查看建表语句。例如:
使用 SHOW FULL COLUMNS 语句查看表的列。 例如:
使用 DROP TABLE 语句删除表。例如:
(3)创建、查看和删除索引
先创建一张表:
CREATE TABLE IF NOT EXISTS person (
number INT(11),
name VARCHAR(255),
birthday DATE
);
对于值不唯一的列,可使用 CREATE INDEX 或 ALTER TABLE 语句。例如:
使用 SHOW INDEX 语句查看表内所有索引:
使用 ALTER TABLE 或 DROP INDEX 语句来删除索引。与 CREATE INDEX 语句类似,DROP INDEX 也可以嵌入 ALTER TABLE 语句。例如:
对于值唯一的列,可以创建唯一索引。例如:
(4)增删改查数据
(5)创建、授权和删除用户
(1)功能说明
TiDB 实现了通过标准 SQL 接口读取历史数据功能,无需特殊的 client 或者 driver。当数据被更新、删除后,依然可以通过 SQL 接口将更新/删除前的数据读取出来。
另外即使在更新数据之后,表结构发生了变化,TiDB 依旧能用旧的表结构将数据读取出来。
(2)操作流程
为支持读取历史版本数据, 引入了一个新的 system variable: tidb_snapshot ,这个变量是 Session 范围有效,可以通过标准的 Set 语句修改其值。其值为文本,能够存储 TSO 和日期时间。TSO 即是全局授时的时间戳,是从 PD 端获取的; 日期时间的格式可以为: “2020-10-08 16:45:26.999”,一般来说可以只写到秒,比如”2020-10-08 16:45:26”。 当这个变量被设置时,TiDB 会用这个时间戳建立 Snapshot(没有开销,只是创建数据结构),随后所有的 Select 操作都会在这个 Snapshot 上读取数据。
注意:
TiDB 的事务是通过 PD 进行全局授时,所以存储的数据版本也是以 PD 所授时间戳作为版本号。在生成 Snapshot 时,是以 tidb_snapshot 变量的值作为版本号,如果 TiDB Server 所在机器和 PD Server 所在机器的本地时间相差较大,需要以 PD 的时间为准。
当读取历史版本操作结束后,可以结束当前 Session 或者是通过 Set 语句将 tidb_snapshot 变量的值设为 “",即可读取最新版本的数据。
(3)历史数据保留策略
TiDB 使用 MVCC 管理版本,当更新/删除数据时,不会做真正的数据删除,只会添加一个新版本数据,所以可以保留历史数据。历史数据不会全部保留,超过一定时间的历史数据会被彻底删除,以减小空间占用以及避免历史版本过多引入的性能开销。
TiDB 使用周期性运行的 GC(Garbage Collection,垃圾回收)来进行清理,关于 GC 的详细介绍参见 TiDB 垃圾回收 (GC)。
这里需要重点关注的是 tikv_gc_life_time 和 tikv_gc_safe_point 这条。tikv_gc_life_time 用于配置历史版本保留时间,可以手动修改;tikv_gc_safe_point 记录了当前的 safePoint,用户可以安全地使用大于 safePoint 的时间戳创建 snapshot 读取历史版本。safePoint 在每次 GC 开始运行时自动更新。
(4)案例演示
(1)准备数据,向集群中插入样本数据
由于docker-compose安装已经帮我们整合好tispark组件,所以我们无须再次部署。
进入集群内部:docker-compose exec tispark-master bash
进入到tispark目录:cd /opt/spark/data/tispark-sample-data
执行脚本:mysql -h tidb -P 4000 -u root < dss.ddl
(2)启动Spark shell
先退出容器内部执行命令:docker-compose exec tispark-master /opt/spark/bin/spark-shell
(3)执行Spark代码
scala> import org.apache.spark.sql.TiContext
...
scala> val ti = new TiContext(spark)
...
scala> ti.tidbMapDatabase("TPCH_001")
...
scala> spark.sql("select count(*) from lineitem").show
+--------+
|count(1)|
+--------+
| 60175|
+--------+
也可以通过 Python 或 R 来访问 Spark:
docker-compose exec tispark-master /opt/spark/bin/pyspark &&
docker-compose exec tispark-master /opt/spark/bin/sparkR
执行另一个复杂一点的 Spark SQL:
scala> spark.sql(
"""select
| l_returnflag,
| l_linestatus,
| sum(l_quantity) as sum_qty,
| sum(l_extendedprice) as sum_base_price,
| sum(l_extendedprice * (1 - l_discount)) as sum_disc_price,
| sum(l_extendedprice * (1 - l_discount) * (1 + l_tax)) as sum_charge,
| avg(l_quantity) as avg_qty,
| avg(l_extendedprice) as avg_price,
| avg(l_discount) as avg_disc,
| count(*) as count_order
|from
| lineitem
|where
| l_shipdate <= date '1998-12-01' - interval '90' day
|group by
| l_returnflag,
| l_linestatus
|order by
| l_returnflag,
| l_linestatus
""".stripMargin).show
+------------+------------+---------+--------------+--------------+
|l_returnflag|l_linestatus| sum_qty|sum_base_price|sum_disc_price|
+------------+------------+---------+--------------+--------------+
| A| F|380456.00| 532348211.65|505822441.4861|
| N| F| 8971.00| 12384801.37| 11798257.2080|
| N| O|742802.00| 1041502841.45|989737518.6346|
| R| F|381449.00| 534594445.35|507996454.4067|
+------------+------------+---------+--------------+--------------+
-----------------+---------+------------+--------+-----------+
sum_charge| avg_qty| avg_price|avg_disc|count_order|
-----------------+---------+------------+--------+-----------+
526165934.000839|25.575155|35785.709307|0.050081| 14876|
12282485.056933|25.778736|35588.509684|0.047759| 348|
1029418531.523350|25.454988|35691.129209|0.049931| 29181|
528524219.358903|25.597168|35874.006533|0.049828| 14902|
-----------------+---------+------------+--------+-----------+
(1)TiDB Lightning介绍
TiDB Lightning是一个将全量数据高速导入到TIDB集群的工具,目前支持Mydumper或CSV输出格式的数据源。你可以在以下两种场景下使用Lightning:
TiDB Lightning主要包含两个部分:
(2)下载迁移工具
wget https://download.pingcap.org/tidb-enterprise-tools-latest-linux-amd64.tar.gz
wget https://download.pingcap.org/tidb-toolkit-latest-linux-amd64.tar.gz
(3)准备mysql数据
(1)key-value
作为保存数据的系统,首先要决定的是数据的存储模型,就是数据以什么样的方式保存下来。TiKV的选择是Key-Value模型,并且提供有序遍历方法。简单来说,可以将TiDB看做一个巨大的Map,其中Key和Value都是原始的Byte数组,在这个Map中。Key按照Byte数组总的原始二进制比特位比较顺序排序。
对于TiDB的理解只需要记住两点:
(2)RocksDB
任何持久化的存储引擎,数据终归要保存在磁盘上,TiKV也不例外,但是TiKV没有直接选择向磁盘上写数据,而是把数据保存在RocksDB中,具体的数据落地由RocksDB负责。这个选择的原因是开发一个单机存储引擎工作量很大,高性能的单机存储引擎。这里可以简单的认为RocksDB是一个单机的Key-Value Map。
底层LSM树将对数据的修改增量保存在内存中,达到指定大小后批量把数据flush到磁盘中。
(3)Raft
RocksDB保证单机的高性能,但是怎么保证数据的可靠性呢,Raft是一个一致性协议,提供几个重要功能:
TiKV利用Raft来做数据复制,每个数据变更都会落地为一条Raft日志,通过Raft的日志复制功能,将数据安全可靠的同步到Group的多数节点上。
RocksDB解决数据快速的存储在磁盘上,通过Raft,我们可以将数据复制到多台机器上,以防止单机失效。数据的写入是通过Raft这一层的接口写入,而不是直接写RocksDB。通过实现Raft,我们拥有一个分
(4)Region
TiKV相当于一个巨大的有序的KV Map,为了实现存储的水平扩展,我们需要将数据分散在多台机器上。Region分散有两种方案:一种是按照Key做Hash,根据Hash值选择对应的存储节点,另一种是分Range,某一段连续的Key都保存在一个存储节点上。TiKV选择了第二种方式,将整个key-value空间分成很多段,每一段是一些列连续的key,我们将每一段叫做一个Region,并且我们会尽量保持每个Region都可以用到StartKey到EndKey这样一个左闭右开的区间来描述。
将数据划分成Region后,我们将会做两件事:
先看第一点,数据按照Key切分成很多Region,每个Region的数据只会保存在一个节点上面。我们的系统会有一个组件将Region尽可能均匀的散布在及群众所有节点上,这样一方面实现了存储容量的水平扩展,另一方面也实现了负载均衡。通过任意一个key就能查到这个key在哪个Region中,以及这个Region目前在哪个节点上。
第二点,TiKV是以Region为单位做数据的复制,也就是一个Region的数据会保存多个副本,我们将一个副本叫做一个Replica。Replica之间是通过Raft来保持数据的一致,一个Region的多个Replica会保存在不同的节点上,构成一个Raft Group。其中一个Replica回座位这个Group的Leader,其他的Replica作为Follower。所有的读和写都是通过Leader进行,再由Leader复制给Follower。
我们以Region为单位做数据的分散和复制,就有了一个分布式的具备一定容灾能力的KeyValue系统,不用担心数据存不下,或者磁盘故障丢失数据的问题。
(5)MVCC
很多数据库都会实现多版本控制(MVCC),TiKV也不例外。假设这样的场景,两个Client同事去修改一个Key,如果没有MVCC,就需要对数据上锁,在分布式场景下,可能会带来性能以及死锁的问题。TiKV的MVCC实现是通过在Key后面添加Version来实现。
注意,对于同一个Key的多个版本,我们把版本号较大的放在前面,版本号小的放在后面,这样当用户通过一个Key+Version来获取value的时候,可以将Key和Version构造出MVCC的Key,也就是Key-Version。然后可以直接Seek(Key-Version),定位到第一个大于等于这个Key-Version的位置。
(6)事务
TiKV的事务采用的是Percolator模型,并且做了大量的优化。TiKV的事务采用乐观锁,事务的执行过程中,不会检测写写冲突,只有在提交过程中,才会做冲突检测,冲突的双方中比较早完成提交的会写入成功,另一方面尝试重新执行整个事务。当业务的写入冲突不严重的情况下,这种模型性能会很好,比如随机更新表中某一行的数据,并且表很大。但是如果业务的写入冲突严重,性能就会很差,举一个极端的例子,多个客户端修改少量行,导致冲突严重,造成大量的无效重试。
(1)关系模型到 Key-Value 模型的映射
我们将关系模型简单的理解为Table和SQL语句,那么问题变为如何在KV结构上保存Table以及如何在KV结构上运行SQL语句。
SQL和KV结构之间存在巨大的区别,那么如何能够方便高效的进行映射,就成为一个很重要的问题。一个好的映射方案必须有利于对数据操作的需求。
对于一个Table来说,需要存储的数据包括三部分:
对于Row可以选择行存或者列存,这两种各有优缺点。TiDB面向的首要目标是OLTP业务,这类业务需要快速地读取、保存、修改、删除一行数据,所以采用行存是比较合适的。
对于Index,TiDB不止需要支持Primary Index,还需要支持Secondary Index。Index的作用的辅助查询,提升查询性能,以及保证某些Constraint。
查询的方式分为两种:点查和范围查询。
一个全局有序的分布式Key-value引擎。全局有序这一点重要,可以帮助我们解决不少问题。比如对于快速获取一行数据,假设我们能够构造出某一个或者几个Key,定位这一行,我们就利用TiKV提供的Seek方法快速定位到这一行数据所在的位置。在比如对于扫描全表的需求,如果能够映射为一个Key的Range,从StartKey扫描到EndKey,那么就可以简单的通过这种方式获取全表数据。操作Index数据也是类似的思路。
TiDB对每一个表分配一个TableID,每一个索引都会分配一个IndexID,每一行分配一个RowID,其中TableID在整个集群内唯一,IndexID/RowID在表内唯一,这些ID都是int64类型。
每行数据按照如下规则进行编码成Key-Value pair:
其中Key的tablePrefix/recordPrefixSep都是特定的字符串常量,用于在KV空间内区分其他数据。
对于Index数据,会按照如下规则编码成Key-Value pair:
Index数据还需要考虑Unique Index和非Unique Index两种情况,对于Unique Index,可以按照上述编码规则。但是对于非Unique Index,通过这种编码并不能构造出唯一的Key,因为同一个Index的tablePrefix{tableID}_indexPrefixSep{indexID}都一样,可能有很多行数据的ColumnsValue是一样的,所以对于非Unique Index的编码做了一点调整:
假设表中有 3 行数据:
那么首先每行数据都会映射为一个 Key-Value pair,注意这个表有一个 Int 类型的 Primary Key,所以 RowID 的值即为这个 Primary Key 的值。假设这个表的 Table ID 为 10,其 Row 的数据为:
除了 Primary Key 之外,这个表还有一个 Index,假设这个 Index 的 ID 为 1,则其数据为:
(2)原信息管理
Database/Table都有元信息,也就是其定义以及各项属性,这些信息也需要持久化,我们也将这些信息存储在TiKV中。每个Database/Table都被分配一个唯一的ID,这个ID作为唯一标识,并且在编码为Key-Value时,这个ID都会编码到Key中,再加上m_前缀。这样就可以构造出一个Key,Value中存储的是序列化后的元信息。
除此之外,还有一个专门的Key-Value存储当前Schema版本信息。TiDB使用Google F1的Online Schema变更算法,有一个后台线程不断地检查TiKV上面存储的Schema版本是否发生变化,并且保证在一定时间内一定能够获取版本的变化。
(3)SQL on KV 架构
TiKV Cluster主要作用是作为KV引擎存储数据,这里主要介绍SQL层,也就是TiDB Servers这一层,这一层的节点都是无状态的节点,本身并不存储数据,节点之间完全对等。TiDB Server这一层最重要的工作是处理用户请求,执行SQL运算逻辑。
(3)SQL 运算
理解了 SQL 到 KV 的映射方案之后,我们可以理解关系数据是如何保存的,接下来我们要理解如何使用这些数据来满足用户的查询需求,也就是一个查询语句是如何操作底层存储的数据。
能想到的最简单的方案就是通过上一节所述的映射方案,将 SQL 查询映射为对 KV 的查询,再通过 KV 接口获取对应的数据,最后执行各种计算。
比如 Select count(*) from user where name=“TiDB”; 这样一个语句,我们需要读取表中所有的数据,然后检查 Name 字段是否是 TiDB,如果是的话,则返回这一行。这样一个操作流程转换为 KV 操作流程:
构造出 Key Range:一个表中所有的 RowID 都在 [0, MaxInt64) 这个范围内,那么我们用 0 和 MaxInt64 根据 Row 的 Key 编码规则,就能构造出一个 [StartKey, EndKey) 的左闭右开区间
扫描 Key Range:根据上面构造出的 Key Range,读取 TiKV 中的数据
过滤数据:对于读到的每一行数据,计算 name=“TiDB” 这个表达式,如果为真,则向上返回这一行,否则丢弃这一行数据
计算 Count:对符合要求的每一行,累计到 Count 值上面 这个方案肯定是可以 Work 的,但是并不能 Work 的很好,原因是显而易见的:
在扫描数据的时候,每一行都要通过 KV 操作同 TiKV 中读取出来,至少有一次 RPC 开销,如果需要扫描的数据很多,那么这个开销会非常大
并不是所有的行都有用,如果不满足条件,其实可以不读取出来
符合要求的行的值并没有什么意义,实际上这里只需要有几行数据这个信息就行
(4)分布式 SQL 运算
如何避免上述缺陷也是显而易见的,首先我们需要将计算尽量靠近存储节点,以避免大量的 RPC 调用。其次,我们需要将 Filter 也下推到存储节点进行计算,这样只需要返回有效的行,避免无意义的网络传输。最后,我们可以将聚合函数、GroupBy 也下推到存储节点,进行预聚合,每个节点只需要返回一个 Count 值即可,再由 tidb-server 将 Count 值 Sum 起来。
这里有一个数据逐层返回的示意图:
(5)SQL 层架构
上面几节简要介绍了 SQL 层的一些功能,希望大家对 SQL 语句的处理有一个基本的了解。实际上 TiDB 的 SQL 层要复杂的多,模块以及层次非常多,下面这个图列出了重要的模块以及调用关系:
用户的 SQL 请求会直接或者通过 Load Balancer 发送到 tidb-server,tidb-server 会解析 MySQL Protocol Packet,获取请求内容,然后做语法解析、查询计划制定和优化、执行查询计划获取和处理数据。
数据全部存储在 TiKV 集群中,所以在这个过程中 tidb-server 需要和 tikv-server 交互,获取数据。
最后 tidb-server 需要将查询结果返回给用户。
(1)调度的基本操作
调度无非是下面三件事:
刚好 Raft 协议能够满足这三种需求,通过 AddReplica、RemoveReplica、TransferLeader 这三个命令,可以支撑上述三种基本操作。
(2)信息收集
调度依赖于整个集群信息的收集,简单来说,我们需要知道每个 TiKV 节点的状态以及每个 Region 的状态。TiKV 集群会向 PD 汇报两类消息:
每个 TiKV 节点会定期向 PD 汇报节点的整体信息
TiKV 节点(Store)与 PD 之间存在心跳包,一方面 PD 通过心跳包检测每个 Store 是否存活,以及是否有新加入的 Store;另一方面,心跳包中也会携带这个 Store 的状态信息,主要包括:
每个 Raft Group 的 Leader 会定期向 PD 汇报信息
每个 Raft Group 的 Leader 和 PD 之间存在心跳包,用于汇报这个 Region 的状态,主要包括下面几点信息:
PD 不断的通过这两类心跳消息收集整个集群的信息,再以这些信息作为决策的依据。除此之外,PD 还可以通过管理接口接受额外的信息,用来做更准确的决策。比如当某个 Store 的心跳包中断的时候,PD 并不能判断这个节点是临时失效还是永久失效,只能经过一段时间的等待(默认是 30 分钟),如果一直没有心跳包,就认为是 Store 已经下线,再决定需要将这个 Store 上面的 Region 都调度走。但是有的时候,是运维人员主动将某台机器下线,这个时候,可以通过 PD 的管理接口通知 PD 该 Store 不可用,PD 就可以马上判断需要将这个 Store 上面的 Region 都调度走。
(3)调度的策略
PD 收集了这些信息后,还需要一些策略来制定具体的调度计划。
1.一个 Region的 Replica数量正确
当 PD 通过某个 Region Leader 的心跳包发现这个 Region 的 Replica 数量不满足要求时,需要通过 Add/Remove Replica 操作调整 Replica 数量。出现这种情况的可能原因是:
2.一个 Raft Group中的多个 Replica不在同一个位置
注意第二点,『一个 Raft Group 中的多个 Replica 不在同一个位置』,这里用的是『同一个位置』而不是『同一个节点』。在一般情况下,PD 只会保证多个 Replica 不落在一个节点上,以避免单个节点失效导致多个 Replica 丢失。在实际部署中,还可能出现下面这些需求:
这些需求本质上都是某一个节点具备共同的位置属性,构成一个最小的容错单元,我们希望这个单元内部不会存在一个 Region 的多个 Replica。这个时候,可以给节点配置 lables 并且通过在 PD 上配置 location-labels 来指明哪些 lable 是位置标识,需要在 Replica 分配的时候尽量保证不会有一个 Region 的多个 Replica 所在结点有相同的位置标识。
3.副本在 Store 之间的分布均匀分配
前面说过,每个副本中存储的数据容量上限是固定的,所以我们维持每个节点上面,副本数量的均衡,会使得总体的负载更均衡。
4.Leader数量在 Store之间均匀分配
Raft 协议要读取和写入都通过 Leader 进行,所以计算的负载主要在 Leader 上面,PD 会尽可能将 Leader 在节点间分散开。
5.访问热点数量在 Store之间均匀分配
每个 Store 以及 Region Leader 在上报信息时携带了当前访问负载的信息,比如 Key 的读取/写入速度。PD 会检测出访问热点,且将其在节点之间分散开。
6.各个 Store的存储空间占用大致相等
每个 Store 启动的时候都会指定一个 Capacity 参数,表明这个 Store 的存储空间上限,PD 在做调度的时候,会考虑节点的存储空间剩余量。
7.控制调度速度,避免影响在线服务
调度操作需要耗费 CPU、内存、磁盘 IO 以及网络带宽,我们需要避免对线上服务造成太大影响。PD 会对当前正在进行的操作数量进行控制,默认的速度控制是比较保守的,如果希望加快调度(比如已经停服务升级,增加新节点,希望尽快调度),那么可以通过 pd-ctl 手动加快调度速度。
8.支持手动下线节点
当通过 pd-ctl 手动下线节点后,PD 会在一定的速率控制下,将节点上的数据调度走。当调度完成后,就会将这个节点置为下线状态。
(4)调度的实现
了解了上面这些信息后,接下来我们看一下整个调度的流程。
PD 不断的通过 Store 或者 Leader 的心跳包收集信息,获得整个集群的详细数据,并且根据这些信息以及调度策略生成调度操作序列,每次收到 Region Leader 发来的心跳包时,PD 都会检查是否有对这个 Region 待进行的操作,通过心跳包的回复消息,将需要进行的操作返回给 Region Leader,并在后面的心跳包中监测执行结果。注意这里的操作只是给 Region Leader 的建议,并不保证一定能得到执行,具体是否会执行以及什么时候执行,由 Region Leader 自己根据当前自身状态来定。