背景介绍
传统关系型数据库在面对大数据时,面临扩展性困难问题,如果采用分库分表方案,又有不支持分布式事务,运维复杂等缺点 。采用Nosql(Not Only Sql)可以解决数据容量扩展问题,但是丢失了关系模型,复杂查询场景受限,不支持sql,事务支持不完善。
有没有一种数据库既能很方便的扩展(甚至做到业务无感知),又支持传统关系数据库的特性呢 ?本篇文章分享一下过去十年快速发展的NewSql数据库的代表之一:TiDB。学习一下TiDB是如何解决数据动态扩展、跨分片事务、数据高可用等问题。
本文将从以下几个方面介绍 TiDB :
1、TiDB产生的背景
2、TiDB的产生和架构
3、TiDB数据如何存储和可扩展
4、TiDB如何支持事务和多版本
5、TiDB如何保证高可用
6、TiDB如何支持Sql
7、TiDB如何支持OLAP
8、TiDB缺点和数据库未来趋势
一、TiDB产生的背景
传统关系型数据库的不足
无法扩展,不适合海量数据场景(要做到可扩展,能分片,必须同时在存储可扩展和跨节点事务上做出支持)
可以分库分表,但对复杂的业务,分库分表有不足,主要体现点:
-
不支持分布式事务,无法跨分片进行事务
-
运维复杂,新加机器的时候需要手工迁移(不过也有平台做了自动化),ddl的时候需要对所有的分片都操作一遍
-
不用分区key的时候查询会路由到所有分片,连接数会放大
参考:吴镝:TiDB 在今日头条的实践 提到今日头条使用传统分库分表遇到的连接数放大的问题
NoSql数据库的不足
为了支持海量数据,能够方便扩展,在业务很简单,也不需要 SQL的场景下,NoSql类型数据库诞生了,代表 Mongodb,Hbase, Cassandra
Hbase** **:使用了lsm树来作为存储结构,基于Google Bigtable 论文的开源实现,底层依赖HDFS,分布式表格。
Cassandra:跟Hbase类似,无主复制,基于亚马逊 Dynamo 论文实现,国内目前使用较少。
Mongodb: 文档型数据库,使用简单方便,最大的特点是没有schema限制。
这些也有一些不足:
-
不支持sql查询,需要用特定API
-
事务支持不完善
-
复杂性的查询受限(比如关联查询)
-
技术成本:业务需要单独去开发去理解,技术架构需要去适配
OLTP和OLAP的抉择
OLTP (On-Line Transaction Processing): 联机事务处理过程,一般按行存储,对实时性要求高,关心事务等特性,平常我们最常使用的Mysql这种。
OLAP(On-Line Analytic Processing): 联机分析处理过程,一般按列存储,因为数据分析一遍只关心一个表中几列数据,按行存储读取计算性能太差,另外按列存储可以更好的压缩。代表数据库 俄罗斯ClickHouse。
业务方在使用两种类型的数据库时,经常会遇到以下问题:
-
首先在很多业务场景中,OLTP和OLAP的界限并不那么明显。
-
一般OLAP的数据是离线异步从OLTP数据库同步过去的,需要一定的时间,在有些对实时性要求高的分析场景处理会比较麻烦,比如双11活动,需要根据最新的热点调整活动策略,当然此时可以通过flink 等流式计算中间件,但引入新的组件,又增加了复杂性和成本。
-
然后如果一份数据要同时支持两种业务场景,需要通过数据异构的方式同步到另一个存储中间间中,两份存储会有资源和运维的浪费。
于是就有了HTAP的引入:
HTAP (Hybrid Transactional and Analytical Processing): 同时支持OLTP和OLAP,这方面TiDB和OceanBase 都提供了支持,在后面会介绍他们是如何支持的。
另外需要提到一点,在一些传统的大型数据库,比如Oracle、DB2,其实也是支持一定的OLAP的,也有些类似HTAP的属性。新的HTAP数据库也会参考传统大型数据库在这方面的实践。
虽然支持HTAP的数据库可以处理OLAP任务,如果是非常重量级的OLAP任务,这类数据库可能还是不太合适
二、TiDB的产生和架构
Google 为了应对大数据量的增量更新场景,开发了Spannner(支持事务的分布式KV系统)和F1(兼容sql的分布式数据库系统),并且于2012年公布了论文,TiDB的产生也是受到谷歌论文的启发, 由国内PinCAP公司研发,类似于Google F1, 这类数据库的特点是,支持水平扩展和大规模数据,高可用,兼容sql语法, 支持跨节点事务和一致性,一般被称之为 New Sql 数据库
数据库经过多年发展,各种形式多样,适用场景不同,各种都需要了解学习,成本非常高,使用NewSql,可以降低后端开发对于数据库的学习成本:
TiDB架构
计算和存储分离,可以分别独立扩展
PD** **:负责元数据存储和协调
TiDB :负责数据的计算返回和sql解析
TiSpark :负责OLAP的语句解析和数据处理
TiKV :负责数据的存储,一致性保证,高可用保证,底层基于RocksDb(lsm树)
TiFlash:一类特殊的存储节点。和普通 TiKV 节点不一样的是,在 TiFlash 内部,数据是以列式的形式进行存储,主要的功能是为分析型的场景加速
TiDB架构示意
TiKV架构
每个Region都是一个RocksDB, 每个Region有多个备份节点,共同组成一个RaftGroup,在同一RaftGroup组里面,通过Raft协议进行数据同步和高可用保证(故障时主从切换)。
TikV的分层设计
通过分层设计能够很好的解耦,各个组件可以变成可插拔的,独立进化和替换,也方便对每个组件进行独立测试。
使用现状
美团使用现状:
美团为什么选用TiDB,截取上文中的一段话:
除此之外在金融行业有很多应用,用于替换昂贵的Oracle,Db2等大型数据库。
更多的使用情况可以查看TiDB官网
主流厂商给出的类似方案
| 数据库 | 厂商 | 备注 |
|---|---|---|
| ByteSql | 字节跳动 | 对标F1进行开发。底层依赖ByteKV,ByteKV和上面介绍的TiKV比较类似。和TiDB总体比较相似 |
| OceanBase | 蚂蚁金服 | 也是业界知名的分布式数据库之一,但和TiDB 底层设计上略有些不同 |
| F1 | 去中心化分布式数据库鼻祖,底层依赖Googel Spanner(后面Spanner也独立发展,支持了sql模块) | |
| TDSQL | 腾讯云 | 有多种存储引擎,其中一种是类似于TiDB的lsm存储引擎 |
三、数据如何存储,如何可扩展
有序kv存储结构
和传统数据库采用B+树或其他B树变种不同,TiDB底层存储引擎为lsm树(采用facebook RocksDb),lsm的优点一个是写的能力更强,而传统数据库写往往是一个瓶颈(读可以走从库或者走缓存解决),另外,从数据结构来看,当数据量过大时,相对B+树,lsm做分裂时也更容易,适合自动扩容的场景,但lsm树的结构也有缺点,主要是读取的性能会略低,这个可以从2方面进行优化:
-
硬件层面:采用固态硬盘(SSD)来代替磁盘,优化读性能。
-
架构设计层面: 一个是严格控制lsm树的容量,使其层数或容量不会过大,当容量较大时,自动进行拆分(TiDB 的单个lsm树超过96M就会自动拆分)。二是通过缓存和布隆过滤器优化读的性能。
另外lsm树读取速度不快,是平均而言的,大部分情况下,很多数据只关心最近创建的或者说越是最近创建的访问的概率越高,比如评论数据,下单数据,快递数据等等。这些情况下,最新的数据还没有下沉到Lsm树最下面几层,访问速度会快很多。
lsm架构和写入过程
内存部分解读
内存部分分为可变内存和不可变内存,数据先写入memtable, 达到一定容量后变成immutable,然后再开启一个memtable继续写入。内存的数据都是排序的,
memtable实现底层和Redis SortedSet 类似,使用了跳跃表,同红黑树和数组相比,能够支持并发去插入数据。
immutable是排队即将刷入到磁盘的部分,因为刷入磁盘和外界写入数据同时存在,为了避免阻塞,所以内存中设计了这个部分,让写入数据和刷入磁盘分开,互不影响。
另外和mysql一样,数据在写入内存前会先写入到Log并刷到磁盘中,避免数据在内存中丢失的情况(Write-Ahead Log)。
硬盘存储解读
多层级存储
每次内存数据量到一定程度,就会刷到磁盘,在内存中key是排序的,刷入磁盘数据key也是排序的,保持key有序有三个好处
-
能够支持key按范围查询。
-
便于将key按区间进行划分并按划分结果拆成多个文件,查找时能缩小范围,加快查找的速度。
-
因为有序,后面可以进行归并排序合并,相同的key会相遇,那么就可以把旧的数据删除掉。
磁盘数据分为L0层到Li 层,每一层空间都会比上层大很多倍,同时拆分了多个文件,L0层 key在多个文件可能重复,除了L0层外,每层key 都不重复。(想想为什么L0层是重复的?)
为什么要设计多层并且每层空间要比上一层大很多倍呢,这其实是一个折中的方式,我们可以考虑两种极端情况
情况一:
如果数据不进行合并,每次写入一个硬盘文件,都单独为一层,查找数据的时候,可能需要从新往旧查找成千上万的文件, IO次数太多,有非常大的读放大问题,读的性能非常差。同时因为不会合并,旧的数据没有机会删除,还会有空间放大问题。
情况二:
如果数据总是跟一个总的文件进行合并,读的问题解决了,但举个例子,拿1M文件和1G的文件进行合并是非常浪费资源的,有明显的写放大问题(因为每次合并的时候,都会将原来的数据重新排序和挪动,合并100次,就会挪动100次数据,文件非常大的时候非常占用IO和其他资源)
数据压缩
因为lsm树的数据写入后,就不会改变了,所以很方便进行压缩(每次合并后压缩),避免了一定的空间放大问题,在数据更新不频繁的场景下,占用空间相对Mysql 会大大减小。
lsm树更新和删除
lsm树是仅追加的,如果数据发生更新或者删除怎么办呢?
如果是更新:我们只需写一条最新的记录,后面在文件合并时,会将旧的删除,避免空间的占用,查找时,从上往下找,只需要找到最新一条即可。
如果是删除:可以理解另外一种更新,写入的是一个墓碑标记,后面文件合并时,也会将原来的删除,查找时,发现墓碑标记,就说明数据已被删除,不再继续向下查找了。
lsm树查找过程
单点查找
布隆过滤器
范围查找
每一层都要找到匹配的范围
然后多路归并,同一key仅保留最新的版本。范围查找性能相对来说是个缺点。
总体查找性能
如何将表转化成kv存储结构
TiDB 自动将 SQL 结构映射为 KV 结构。具体的可以参考 《三篇文章了解 TiDB 技术内幕 - 说计算》 这篇文档。简单来说,TiDB 做了两件事:
-
一行数据映射为一个 KV,Key 以 TableID构造前缀,以行 ID 为后缀
-
一条索引映射为一个 KV,Key 以 TableID+IndexID构造前缀,以索引值构造后缀
如何分区
TiKV 自动将底层数据按照 Key 的 Range 进行分片。每个 Region 是一个 Key 的范围,从 StartKey 到 EndKey 的左闭右开区间。Region 中的 Key-Value 总量超过一定值(96M),就会自动分裂。
因为对分区大小的控制比较严格,保证了读取数据时,lsm树的读取的最大延迟是可控的。
四、如何支持事务和多版本
zhuanlan.zhihu.com/p/149377959
两阶段提交的不足(XA)
首先回顾一下常见的分布式事务方式,数据库支持的XA ,它有哪些不足呢?
-
需要加锁,多次网络交互,同步阻塞,性能较差,并发能力相对没有采用分布式事务时会大大下降
-
需要引入额外的协调者
-
当协调者、参与者、网络等发生故障,锁会会占用很长时间或者数据出现不一致的情况
-
隔离性,多版本依赖单个参与者本身的能力,缺乏整体性的隔离性和多版本支持
类似mysql版mvcc实现的不足
每次事务开启前,需要记录一个ReadView, ReadView里面是活跃的事物id, 假设大容量高并发情况下,有10000个事务并行执行,每个事务的ReadView 集合大小也自然是10000个左右。那么总共就有10000*10000 = 1亿条数据。会占用上GB内存。并且计算层往往也不是单个节点的,活跃事务的统计也要跨节点进行汇总,也是一笔很大的开销。
ReadView 还会参与各种计算,比如判断某个事务id是否在ReadView中,这块数据量大了之后,消耗也比较大。
ReadView 示意
谷歌Percolator算法
Percolator 英 [ˈpɜ:kəleɪtə(r)]美 [ˈpɚkəˌletɚ]
TiDB的事务和多版本控制算法,是在谷歌Percolator基础上略加修改实现的,实际上,谷歌Percolator也是很多New sql 发展的基石。Percolator算法可以理解为一种二阶段提交的改进版本。
相对于两阶段提交,Percolator有以下特点
-
将第一个事务参与者当成全局事务协调者
-
使用一个全局的时间戳进行版本控制,并且不仅在修改数据时,在事务提交时也会记录时间戳(避免了产生类似mysql readview这种比较重的逻辑)。
为什么需要Percolator(why)
背景
增量更新困难
Percolator需要做什么(what)
目标
Percolator需要怎么做(How)
整体思路
Percolator案例快速入门
以一个经典的转账案例说起
有两个账户,数据存储和余额如下
Bob 向Joe转账7元,bob账户数据行作为第一个参与者,加primary锁
Joe账户更新,加Secondary锁
释放Bob的Primary锁(只要 primary 的行锁去掉,就表示该事务已经成功 提交)
释放Joe数据上的Secondary锁,事务完成(这一步可以做成异步的)。
Percolator架构和事务流程
架构
不改动BigTable,只是新引入一个Proxy层,负责事务的执行
事务执行流程
事务执行流程分为三个阶段
-
事务执行阶段:在proxy上完成,在内存中操作,并不真正落库
-
二阶段提交中的prepare阶段,校验锁冲突,获取锁
-
二阶段提交中的commit阶段,依次释放锁,更新数据
执行阶段详解:
在读取的时候如果有锁会尝试清理,为什么这么做呢?
因为proxy 并不是高可用的,那么就存在一部分锁没有释放的情况
如何清理的?
-
如果遇到primary锁,可能当前事务还是活跃状态,会去查询活跃事务才有的token,如果有,则是活跃事务,不会清理,如果没有,说明不是活跃事务,进行回滚处理。
-
如果遇到secondary锁,则找到他的对应primary列数据,如果锁已经提交(primary锁已经释放),则将该行数据进行提交,如果没有的话,则走上面的流程。
prepare阶段详解
perpare主要是加锁,但是如果原来的列有锁或者有新的值,都会回滚
提交阶段详解
清理锁,写入write字段
全局授时和乐观事务模型
全局授时:所有时间戳都从一个全局授权机器上取
乐观事务模型:事务执行时并不校验锁,只有提交的时候才会校验,先开始的事务不一定能提交,如下图,事务A先执行,但因为锁冲突会发生回滚。
五、存储如何高可用
为什么需要选举算法
在没有选举算法的时候,怎么去保证高可用:
当一个服务有状态的时候,我们必然要保证它的高可用性,我们可以首先通过数据同步的方式构建一个或多个备份节点,然后利用一个管理集群,维持所有节点的心跳,当主服务节点挂掉后,由管理集群选择一个合适的备份节点当选新的主节点。这种方式在可用性问题没有那么严重(因为没有选举算法时,管理集群的可用性也可能有问题)或者新增管理集群成本不高的时候也是一种选择。
但随着互联网和人工智能的迅速发展,数据量越来越大,在海量数据下,分片的需求的愈加强烈,而为了保证性能,每个分片的数据还不能过多,这就有海量的节点。另外采用分布式数据库的一个原因就是能用普通的机器代替昂贵和专业的机器,而普通的机器出故障的概率也会更高。
海量的数据节点加上更高的故障率,需要自动的,成本更低的高可用措施
我们可以通过一种选举的算法,来使集群本身就能决定当主节点挂掉后,哪个节点能成为下一个主节点,这样我们就不需要管理集群,减少了额外的成本。
数据同步一致
选举的目的是为了始终提供一个leader 对外提供数据服务,参与选举的机器都是有状态的,数据同步就很重要,所以一个选举算法除了怎么选leader,还包括了怎么进行数据同步,不同方式的数据同步可能性能差别很大,同步方案也很重要。
在选举新的leader时,也会看候选者的数据是否是最新的一份,这个也依赖于数据同步的可靠性
在数据同步方面,和mysql利用binLog同步不同,TiDB 的存储节点主从同步是依赖于Raft的数据同步协议的,能够更精准和更低的延迟。
关于Raft 的详细介绍内容比较多,这里不展开,可以看这个:bytetech.info/videos/6844…
六、如何支持SQL
TiDB SQL处理引擎架构
用户的 SQL 请求会直接或者通过 Load Balancer 发送到 TiDB Server,TiDB Server 会解析 MySQL Protocol Packet,获取请求内容,对 SQL 进行语法解析和语义分析,制定和优化查询计划,执行查询计划并获取和处理数据。数据全部存储在 TiKV 集群中,所以在这个过程中 TiDB Server 需要和 TiKV 交互,获取数据。最后 TiDB Server 需要将查询结果返回给用户
七、如何支持OLAP
支持方式的选择
TiDB是如何支持的
为什么 TiDB 能够实现 OLAP 和 OLTP 的彻底隔离,互不影响?因为 TiDB 是计算和存储分离的架构,底层的存储是多副本机制,可以把其中一些副本转换成列式存储的副本。OLAP 的请求可以直接打到列式的副本上,也就是 TiFlash 的副本来提供高性能列式的分析服务,做到了同一份数据既可以做实时的交易又做实时的分析
OceanBase混合存储
八、缺点和未来趋势
TiDB缺点
并不能100%兼容传统数据库用法,但绝大部分兼容
延迟略高一些,小事务场景原来2ms 到5ms,但比较稳定,不会随着数据量的增长而迅速扩大。
机器多一些,成本高一些
TiDB本身不属于任何云厂商,没法和像云厂商提供的数据库一样进行深度定制和底层硬件级别的优化
不过TiDB为了应对竞争进行了开源,有社区版和商业版
数据库未来趋势
这段话参考了TiDB CEO 和 OceanBase CTO 的分享
-
功能上以HTAP为主,并不断精细化
-
面向云特性和平台设计
-
智能化
-
根据Workload自动决定存储介质
-
实时跟踪热点并弹性伸缩
-
跨数据中心,跨地域做到真正高可用和根据业务的数据智能分布
-
参考文档
OceanBase 首席架构师:关系数据库到三代分布式数据库,我亲历的数据库演进史-阿里云开发者社区
OceanBase CTO 杨传辉:下一代企业级分布式数据库的一体化设计-阿里云开发者社区
Dynamo: Amazon's High Available KV Store
带你重走 TiDB TPS 提升 1000 倍的性能优化之旅