嘉宾介绍
姚维,现PingCAP TiDB内核专家,曾就职于360基础架构部门、UC。
为什么我会加入PingCAP呢?
在360的时候,我负责Atlas的Sharding(切片技术)的实现。在这个过程中,我发现中间件这个数据库方案存在了诸多限制。比如说数据迁移的场景,当槽位需要在原有设定值的基础上再增加,整个操作过程需要人工介入而且极其复杂,同时也避免不了系统停机。比如说分布式事务,这在跨Sharding的节点上几乎不可能支持的。
为什么要做一个新的数据库?
关于数据库发展的历史,如图一所示在早期,大家主要是使用单机数据库,如Mysql等。这些数据库的性能完全可以满足当时业务的需求。但是自2005年开始,也就是互联网浪潮到来的时候,这些早期的单机数据库就慢慢开始力不从心了。当时Google发表了几篇论文,谈论了其内部使用的Bigtable Mapreduce。然后就有了Redis、HBase 等非关系型数据库,这些数据库实际上已经足以满足当时业务的需求。
图 1
直到最近的五六年,我们发现尽管各种NoSQL产品大行其道,但是Mysql依然是不可或缺的。即使是Google,在某一些不能丢数据的场景中,对一些数据的处理依然需要用到ACID,需要用到跨行事务。因此Google在12年的时候发表了一篇名为《Google Spanner》的论文(注:Spanner是在Bigtable之上,用2PC实现了分布式事务)。然后基于Spanner,Google的团队做了F1(注:F1实际上是一个SQL层,支持SQL的语法),F1用了一些中间状态来屏蔽了单机Mysql可能碰到的阻塞的场景。
在PingCAP成立之前,当时还在豌豆荚的PingCAP的创始人,在接触数据库的中间件方案过程中也是深受其害。因此在受到Google上述的两篇论文的启发后,就成立了PingCAP,开始TiDB的研发。
TiDB的架构
图 2
TiDB的架构如图2所示。TiDB主要分为三个部分:负责对接SQL语句的SQL Layer、负责底层存储的TiKV和负责元数据管理的Placement Driver(PD,下文以此简称)。
其中TiDB和PD是用Go实现的,TiKV是用Rust实现的。它们的具体功能在后文会提到。
TiKV的概述
图 3
首先是Region的概念,它是指一段连续key的key-value对的集合。所有的数据都是以Region为一个基本单位来组织调度的。
Region是高度分层的。最底层的是RocksDB,这一层负责键值对的存储。之上是Raft状态机,每一个Region都是是通过Raft协议来保证数据的外部一致性的(我们的设计保证了即使在写数据的过程中宕机也不会丢数据),足以支持金融级别的场景。然后是数据库中比较常用的MVCC层,它可以实现隔离级别等很多功能。再之上,就是事务,这里的事务是用2PC的变种的方式实现的。(可能大家觉得2PC做分布式事务会出现阻塞的情况。但是我们也知道,2PC只会在键发生冲突时阻塞从而引起时延。但是这是一种极端情况,实际的业务场景,比如用户ID,只要我们将key均匀分布,这种冲突情况是几乎很少发生甚至可以完全避免的)。
现在一个Region的大小是96MB,当Region快要填满时,Region会发生分裂,整个分裂的过程都是由PD来调度,完全不需要人为干预。
PD的概述
PD主要负责元数据的管理。PD的主要作用:1、管理TiKV(一段范围key的数据),通过PD,可以获取每一段数据具体的位置;2、F1中提到的授时,即用于保证分布式数据库的外部一致性。
图 4
TiKV中的Multi-Raft
如图五所示,每一个Region都是一组Raft状态机。
假设节点一中出现Region的阻塞,这种设计能够保证其它节点并不会因此而受影响发生阻塞。
图 5
TiKV的水平扩容
TiKV的扩容,唯一需要人工操作的就是把机器加入到这个集群即可。剩下的完全会由PD来实现整个扩容的流程,同时PD还保证Region会均匀地分布在各个节点上。
图 6
实际上,在新节点刚刚加入集群时,存在一种潜在的可能发生阻塞的情况,即在该节点还没有任何数据加入时就需要进行"Raft"中的投票环节。我们给出的解决方案是,在该节点还没有获取到数据时,不让其参与投票的过程,然后直到数据迁移完成后再让其加入投票的过程。
SQL Layer
整个这一层都是用Go语言开发的。
在我看来,任何的键值对系统,只要是和SQL有关,那么其复杂度会以指数级上升。因为数据库中最难的就是查询优化器。大家有没有想过,为什么Mysql问题很多,但是这么多年却依旧没有人对其进行改进和优化。实际上就是因为这一块的实现难度太大了。
图 7
SQL本身实际上就是一串字符串,所以我们首先会有一个解析器。语法解析器(Parser)的实现本身不难,但是却极其繁琐。这主要是因为Mysql比较奇葩的语法,Mysql在SQL标准的基础上增加了很多自己的语法,因此这一模块的实现耗时极长,有一两个月之久。
但由于Parser只做语法规则的检查,SQL语句本身的逻辑却没法保证,因此在Parser后紧跟了Validate模块来检查SQL语句的逻辑意义。
在Validate模块后是一个类型推断。之所以有这个模块,主要是因为同一个SQL语句在不同的上下文环境有不同的语义,因此我们不得不根据上下文来进行类型推断。
接下来是优化器,这个模块我们有一个专门的团队来负责。优化器主要分为逻辑优化器和物理优化器。逻辑优化器主要是基于关系代数,比如子查询去关联等。然后是和底层的物理存储层打交道的物理优化器,索引等就是在这一块实现的。
后面还有一个分布式执行器,它主要是利用MPP模型来实现的,这也是分布式数据库比单机数据库要快的主要原因。
为什么选用go?
如图七 图八所示,主要是基于分布式数据库所带来的挑战以及Go语言在这一块所拥有的优势等考量,因而我们采用Go语言来实现TiDB。
首先是分布式数据库的实现本身确实极其复杂;TiDB底层通讯涉及大量RPC过程,如果是用诸如c++语言来实现的话,不是说不能,而是太过复杂了,Go语言和c++相比,它封装了底层的很多细节,而且Go语言本身也易学容易上手,生成效率高,更何况我们整个团队都是以Go语言为主;同时Go语言也足以满足性能方面的需求,尤其是在高并发、数据量大和OLTP查询等场景,甚至有时用户输入的不合理复杂的OLAP查询也不是问题;外部一致性,即Spanner中提到的全局一致性的保证;SQL带来的复杂度;易于Bug的发现,以及Profile等工具以及够用的GC性能。
图 8
图 9
Go在TiDB中的作用
图 10
由图九所示,TiDB中Golang的语言实现占得比例相当大,以及包括谢大在内的138个开源贡献者。
Go的调优
实际上主要是涉及内存方面的。
图 11
对象重用
图 12
OLTP和OLAP的融合
图 13
一些大型公司在做大数据分析时,基本是在晚上把数据从数据库通过ETL导入到Hive、ODPS等,也就是说要到第二天才能完成对这部分数据的查询。
因此我们团队考虑,是否可以直接在OLTP上跑一个大数据查询的引擎,这样就可以避免上述提到的这个过程,而且数据本身也可以比较实时地出现在报表中。TiSpark项目就是来实现这个功能的。
图 14
TiSpark是用的Spark的引擎,然后用Scala语言写了一个CoProcessor来实现Spark相关的一些操作。
这样子的话,和提供分布式事务功能的TiDB不同,TiSpark主要是用于需要分析和跑得快的场景。
有疑问加站长微信联系(非本文作者)