Date: 2021.04
目录
===== 1. Introduction
===== 2. RAFT-BASED HTAP
===== 3. ARCHITECTURE
===== 4. MULTI-RAFT STORAGE
===== 4.1 Row-based Storage (TiKV)
===== 4.1.1 Optimization between Leaders and Followers
===== 4.1.2 Accelerating Read Requests from Clients
===== 4.1.3 Managing Massive Regions
===== 4.1.4 Dynamic Region Split and Merge
===== 4.2 Column-based Storage (TiFlash)
===== 4.2.1 Log Replayer
===== 4.2.2 Schema Synchronization
===== 4.2.3 Columnar Delta Tree
===== 4.2.4 Read Process
===== 5. HTAP ENGINES
===== 5.1 Transactional Processing
===== 5.2 Analytical Processing
===== 5.2.1 Query Optimization in SQL Engine
===== 5.2.2 TiSpark
===== 5.3 Isolation and Coordination
===== 6. EXPERIMENTS
===== 7. RELATED WORK
===== 9. REFERENCES
[该图来自网络]
These systems follow the “one size does not fit all” paradigm [37], using different data models and technologies for the different purposes of OLAP and OLTP.
这些系统遵循着 "one size does not fit all" 的模式,针对OLTP&OLAP的不同目的 而使用 不同的数据模型&技术
多个系统来完成TP+AP的能力维护成本高、实时分析最新的数据十分重要(时效性)。
HTAP系统首先需要像 NewSQL 一样支持扩展性、可用性、事务能力,同时还需要在数据新鲜度(freshness)和隔离(isolation)的前提下保证吞吐&延迟。
数据新鲜度(freshness):分析查询可以读到多新的数据。// ETL的方式无法做到实时、streaming可以降低同步延时,但是仍然很难保证数据一致性。
隔离性(isolation):保证 OLTP & OLAP 的性能。// in-memory db 可以提供freshness(同机读取),但是很难同时对TP&AP查询提供高性能,因为数据同步&工作负载的原因。而且如果in-memory db部署在单机就无法提供高可用&扩展性
所以为了保证隔离的性能,OLTP & OLAP 需要运行在不同的物理硬件上。那么问题的难点就在于解决在单个系统中解决副本间数据时效性以及一致性(维护一致性副本需要高可用[29])的问题。系统高可用已经被 Paxos[20] & Raft[29] 论证实现。那么扩展这些一致性算法来提供HTAP workload就称为了可能。
我们提出了 Raft-based HTAP database: TiDB。通过在 Raft 中引入 learner (列存),来从 leader 异步复制日志 构建新的副本解决OLAP的问题。
该论文贡献如下:
基于复制状态机来完成数据实时同步,以保证不同的server应对不同的HTAP负载。
Raft协议更加易于理解&实现,所以我们基于Raft来进行扩展。
以 Learner 角色加入的原因:标准Raft协议中,follower可能会成为leader角色,增加follower没法做到资源隔离;增加follower会影响性能(因为增大了majority)。引入 Learner 角色 不会参与到 leader 选举中,也不会增加 quorum,数据复制是异步且实时的。
行存可以利用 索引(index) 有效的进行事务查询,列存可以利用 数据压缩(data compression)& 向量化处理(vectorized processing)。数据同步到learner时进行行列转换,节点独立部署也可以做到资源隔离。
支持 HTAP 数据库,我们还面临一些工程挑战:
TiDB兼容MySQL协议,支持MySQL客户端。TiDB有3个核心组件:distributed storage layer、Placement Driver、computation engine layer
distributed storage layer:包含一个行存(TiKV)和一个列存(TiFlash)。数据在 TiKV 中按照有序 key-value 结构存储。每个数据元组(Tuple)都映射为一个 key-value 结构。key由 tableID & rowID 组成(tableID和rowID都是一个int,rowID来自于primary key的列),value 就是实际的行数据。
[该图来自网络]
编码形式:Key -- {table{tableID}_record{rowID}} ; Value -- {col0, col1, col2, col3}
使用 Range 的策略进行partition(便于range查询),每个分片称为 Region。每个Region有多个副本,并使用 Raft 协议来保证一致性(每个region一个Raft协议)。
Placement Driver负责
computation engine layer:
TiDB符合HTAP数据库要求:
相同的形状表示相同的role
存储层由 TiKV & TiFlash 组成。TiKV 中将 table 数据拆分成多个region存储,每个region都使用 Raft协议 来维持一致性。数据复制到TiFlash时,多个region的数据可能会被merge成一个partition,以加速scan。存储层使用多个raft group管理数据,所以我们称其为 multi-Raft storage。
[该图来自网络]
Leader --> Follower 之间的数据同步流程【基础Raft协议】:
上述基础的Raft协议保证了数据的一致性以及高可用,但是没法提供很好的性能,因为上述操作是串行的,而且可能有大量的IO负载
优化点1:第二步(写本地log)和第三步(发送给follower)可以并行执行,因为他们之间没有依赖
优化点2:leader批量发送log给follower,并无需等待follower返回。如果中间有失败的log,这里会retrans
优化点3:leader在其他线程中异步apply log,这里没有不一致风险
优化后数据同步流程:
TiKV提供 linearizable 的读取语义:当在 时间t 进行读取时,获得的结果一定不会是时间t之前的版本。
这个需求可以基于 Raft 实现:对每个 read 请求下发一个log entry,并等待该 log entry commit才返回结果。然而这种方式对IO、同步时效性都有要求,所以这里我们需要规避过程中的日志同步
Raft 保证当数据成功写入以后,leader返回响应结果都无须日志交互。然而在数据读取过程中,如果发生 leader election,就无法保证从leader读取了,也就不能保证返回了最新的数据
比如 t时刻 从leader读取,此时的apply index 为 t-2,而 t-1时刻 发生了 leader选举并产生了一条日志,而当前节点并未感知这些操作,那么返回的结果就不是最新的
为了保证从leader进行读取,TiKV进行了如下优化:
read index:当leader收到 read请求后,会记录当前的提交index为read index。随后发送心跳确认自己的leader地位,如果自己是leader,那么等待本地的apply index >= read index 再返回给 client。
lease read:该机制可以降低read index的网络负载,leader & follower约定一个lease period,在此期间内,follower不发起竞选
follower read:为了减轻 leader 的读取压力,可以从follower上读取。follower收到read请求后,向leader询问最新的 read index,并等待本地的 apply index >= read index才返回给client
Servers&数据量大小是动态变化的,这容易导致server之间的region不均衡。
PD 负责调度region的副本数量及其location,一个原则是 region最少有3个副本在不同的TiKV上。PD会通过心跳收集不同server上的信息,并监控其workload,将hot region进行迁移
而管理大量的region需要发送 heartbeat并管理元信息,这会增加一些网络以及存储负载。这里会根据负载动态调整心跳频率
hot&large的region需要进行split以均衡负载,small&cold region需要merge以减少网络&CPU负载。PD会动态的进行region的split&merge
Split操作将region拆分成几个连续的region,最右侧的region接管原Raft Group,其他region使用新的Raft Group:
Merge region是split的反向操作,merge操作需要通过2阶段操作的方式完成:停止一个region的服务,由另外一个reigon接管。这里没法使用简单的log replication来完成region的merge
TiFlash以learner的角色加入到Raft group中,仅接收Raft log并将其转换为行存,而不参与Raft的日志提交&选举,所以几乎不对TiKV造成影响。
可以通过 'ALTER TABLE x SET TiFLASH REPLICA n;' 来修改列存的副本数(n),默认为1.
TiFlash也会做partition,每个partition会包含TiKV中的几个Region,方便做range scan。
TiFlash初始化:1. leader 发送 snapshot 给 learner;2. learner 监听 Raft log。learner接收Raft log依次进行 replaying the log, transforming the data format, updating the refered value in local storage
raw log: {transaction id} {operation type} {transaction status} {operation data} , transaction status: [transaction status][@start_ts][#commit ts]
column data: operation types, commit timestamps, keys, two column data
为了保证 linearizable的语义,TiFlash FIFO地进行日志重放:
为了保证log能转换为列存,learner需要感知schema,而这些schema存储在TiKV上。为了降低schema同步频率,learner节点维护一个schema cache
schema syncer负责从TiKV同步schema到本地cache。有2种同步机制:
设计了一个薪的列存引擎:DeltaTree,来保证高效的读写列数据。
在DeltaTree中,delta updates & stable data是分开存储的。在stable space中,partition的数据像chunk一样存储。在delta space中,数据按照TiKV生成的顺序存储。
这个存储结构比较类似 Parquet [4] ,不同的是,TiFlash将row group及其元信息分开存储,以便于并行更新文件。TiFlash 使用 LZ4 [2] 来将数据压缩存储到磁盘上
新写入的deltas都是原子的增删操作,他们被cache在内存并持久化到磁盘。这些数据是按序存储的,所以等同于实现了 Write-ahead log (WAL)
这些delta存储在大量的小文件汇总,所以引入读取的时候引入额外的IO负载(读放大)。为了降低影响,定期将这些小文件compact。内存中cache的数据可以加速读取最新数据的响应时间(facilitates reading the latest data),并采用LRU的方式淘汰
当进行数据读取的时候,需要读取所有的delta file&以及对应的stable file,因为无法确定相关delta文件分布,所以这里会有读放大(read amplification)的问题
此外,许多delta文件可能存储无用的数据,这里有空间放大(space amplification)的问题,浪费存储空间并减慢与stable层merge的速度.
所以我们定期将delta文件与stable层进行合并。
而由于key在delta层是无序的,所以无论是进行delta层的merge,还是进行delta层与stable层的compact,亦或是读取数据的时候需要遍历delta&stable层,代价都是昂贵的。因此这里针对delta层构建了一个 B+Tree,B+Tree中的数据按照key×tamp进行排序,这样可以便于进行对keys进行更新 或是 对单个key进行查询(point select)
这里做了一个简短的时间对比 DeltaTree 与 LSM-Tree [28] 的性能。基于sysbench [6] 压测,LSM-Tree使用universal compaction而不是level compaction(ClickHouse也是使用这种方式)
可以看到,DeltaTree的读性能是LSM-Tree的2倍(在测试的几组数据量&transactional load下都是这样),这是因为DeltaTree每次读操作都是需要读取一次delta层(因为有B+Tree索引),而LSM-Tree需要访问多次。然而 DeltaTree的写放大是LSM Tree的4倍。(// TODO 为啥会有写放大?)
Summary下 DeltaTree,主体上还是一个2层LSM-Tree结构(Stable层&Delta层),Stable层理解为是实际的数据存储,Delta层理解为是WAL log,所以等于是定期的快照+增量log的形式。
那么在这样的结构下一些问题(根因上还是因为Delta层是不断append的log造成的):
比如无法确定key在Delta层归属的文件而引入的读放大问题,为了解决这个问题,构建了一层B+Tree索引,拥有了这个索引其实不太需要进行Delta层的merge
比如Delta文件可能存储无用数据而导致写放大的问题,为了解决这个问题,需要不断的进行compact(可能是Delta层内部的compact,可能是Delta&Stable的compact)
为了满足snapshot isolation,learner read 和 follower read相似,都是通过read index的方式来完成。当 learner节点收到read请求后,向leader节点发送read index来获取可以满足timestamp的最新数据,leader将相关日志发送给learner,learner replay日志后相应请求
提供SQL Engine 来支持评估事务&分析查询:
HTAP查询可以在独立的物理资源上运行,并且能同时从行列进行查询来
TiDB提供 snapshot-isolation(SI) & repeatable read(RR)级别的事务支持:
我们使用 multi-version concurrency control(MVCC)来实现事务支持,同时避免了 读写锁 以及 写写冲突
Transaction由3个组件来协同完成:
基于 Percolator 模型实现乐观锁(optimistic lock)&悲观锁(pessimistic lock)事务:选择一个key作为primary key,并使用它来表示事务状态。基于2PC来实施事务
乐观事务执行流程:
乐观锁事务&悲观锁事务的主要区别就是 何时来获取锁?
悲观锁事务在每次DML的时候都会获取ts(for_update_ts)并lock相关的key,基于该ts进行本次请求的读取(RR),发生冲突也只会进行局部重试。
对于悲观锁事务,用户可以选择RC隔离级别,以减少冲突获取更好的性能。区别在于:RR事务中,如果读请求访问的key被其他transaction锁定,必须返回conflict;而RC事务会忽略这种锁而进行读取
TiDB实现分布式事务而无需中心化的锁管理,具备更好的扩展性。
Timestamp从PD获取,每个Timestamp包含physical time&logical time 2部分,pt精确到ms,lt占用18bit(支持2^18个时间戳)。理论上 PD可以支持每毫秒 2^18个时间戳,实际上也可以达到每秒100w的时间戳生成。
该section描述对 OLAP请求的优化,包含optimizer、indexes、pushing down computation
[该图来自网络]
TiDB实现2个阶段的查询优化:
索引可以加速数据扫描的代价,创建和删除索引都是在后台异步完成.
每个region都负责存储其管理的数据对应的index,index按照Key-Value的方式存储在TiKV上。
[该图来自网络]
unique key编码形式:Key -- {table{tableID}_index{indexID}_indexedColValue} ; Value -- {rowID}
非unique key编码形式:Key -- {table{tableID}_index{indexID}_indexedColValue_rowID} ; Value -- {null}
Index Intersection - 多个候选索引的选择方式:基于多个索引返回的结果进行merge,来返回精确结果
物理计划使用 pulling iterator [17] 模型执行,算子下推到 TiKV的coprocessor,coprocessor支持逻辑运算、算数运算、其他常用计算,一些情况也可以执行aggregate&TopN
TiSpark集成 TiDB:1. 从TiKV读取元数据;2. 从PD读取ts,以获取一致性数据;3. 支持算子下推;3. 自定义从TiKV or TiFlash 读取数据
TiSpark与常用connector不同:
AP&TP查询混合使用会导致较大的延迟,在 [24, 34] 中被验证。为了规避这个问题,我们调度事务查询&分析查询到不同的engine上,并且将TiKV&TiFlash部署在独立的机器上
TiKV&TiFlash上的数据是一致的,所以我们的查询优化器可以从更大的物理计划中选择,也可以从TiKV&TiFlash中都读取数据。所以这里有3种扫描场景:扫描行存、扫描列存、扫描索引。
三种扫描方式(行存、列存、索引)的评估方式是不同的:行存&列存按照primary key排序,索引提供多种排序方式。
tuple/column/index 的平均大小:S ;tuple/region 的数量:N;scan&seek代价:f
隔离性标准:
PD生成时间戳,不成为瓶颈。6server获取时间戳,每秒可以获取60w+
复制延迟:HTAP负载下可以保持1s以内的复制延迟,延迟程度取决于数据量大小
构建HTAP系统的常见方法:
TiDB是从头构建的,并且在架构、数据组织、计算引擎、一致性保证与其他系统都有所不同
从现有数据库演进 -- evolving from an existing database,成熟的数据库提供HTAP服务基于现有产品,并focus在加速AP查询上。他们使用不同的方式来实现数据一致性&高可用(TiDB使用Raft来实现数据一致性&高可用)。
扩展开源系统 -- Transforming an open-source system,Spark是一个开源的分析框架,其需要一个TP模块来实现HTAP,很多系统按照这种方式来完成
从头构建 -- building from scratch,许多新的HTAP系统研究了 HTAP 的不同方面:使用内存计算来提升性能、优化存储&可用性。但是他们不能像TiDB一样同时支持高可用、一致性、扩展性、数据新鲜度&隔离性
Theory
Algorithm / Data struct
Research
Product
Tools