TiDB 的 HTAP 之路:过去,现在和将来

  • 2019 年 11 月 22 日
  • 笔记

TiDB 有很多故事

每个故事都可以有多个视角,这是一个从 AP 视角讲 HTAP 故事的分享,当然还有技术讨论。

我们开始做 TiDB 时只是想做 TP 数据库,替换 MySQL 分库分表方便用户使用,但是后续用户使用中发现,除了 TP 功能之外,还有很多分析场景也期望数据库来解决。今天主要讲一下 TiDB 在 HTAP 这个领域过去的探索、现在状态和将来的计划。

从 TiDB 的上古时代说起

最开始做的低版本就是想实现水平扩展:数据存储进来将数据分片,加节点能够解决存储空间不够的问题,支持 MySQL 的语法、协议和行为,方便用户将数据存储进去。比如在多个节点中单个节点失效时,保证整个集群还是能够提供服务,做一个实现高可用、分布式的数据库。

上图是 TiDB 的架构,TiKV 是将数据存储进去,PD 是其调度节点,数据从 DistSQL 写进去。从外面就是 MySQL 服务,调用 MySQL 相关协议进行数据同步下一个 TiDB,这样只要加节点就能实现无限存储,前期测试扩展到几十 T 甚至上百 T 都是可以实现的。早期更多的是类似于一个存储系统,计算能力较弱。相对于 MySQL,MySQL 是小号 T 恤,而 TiDB 是大号 T 恤,其他功能尽可能接近。

用户反馈体验很好,先前需要分库分表,现在不需要分库分表,当业务场景很多时,推动一次业务变更非常困难,拥有一个简单扩展的数据库能解决很多问题。但是在早期对一个数据库而言,尤其是存储交易数据的数据库而言,用户会对其安全性不放心。因此可以存储一些边缘场景,如存日志数据,当其数据丢失可以从 Hadoop 中恢复。但用户反馈数据库挺好,可以做一些数据分析,而前期我们的想法是想做一个承载交易数据的数据库,而三四个用户前期都是做分析的。如游戏客户将游戏投放到广告页面,分析广告投放的效果或者分析流水型的数据分析,当时做了一个并行的 hash join,而 MySQL 没有 hash join,运行还挺快的。

后来从 TP 领域贴近用户场景中,解决有些现有数据库无法解决的问题,如将上百个分片的 MySQL 数据同步,做一些规则变换允许用户进行裁剪和过滤,随着用户增多,越来越多的数据同步过来,对数据库的承载能力也是一个考验。

在进行数据同步过程中需要进行计算,在 TiDB alpha 阶段计算很简单,包括优化器、执行引擎都很粗糙。我们在此基础下进行优化,1.0 beta 版本的时候做了一个分布式计算框架,让 TiKV 能够承载一部分计算开销,在 TiDB 进行 SQL 优化,选择一部分算子,尽可能多的将算子推到 TiKV 中,将数据从本地导出,在本地做 filter、聚合、limit 等流式算子,然后将数据返回到 TiDB Server 中做一个最终的聚合,同时我们做了很多自己的 join,可以在很多传感器中调用 MySQL,运行效率比较快,能够允许用户在数据量大的时候增加节点,提高计算并发解决问题。

我们分析了用户用我们产品的初衷,在产品早期让用户相信产品是稳定的是很难的事情,除了使用还有数据库运维、日常处理等都需要学习。如游戏运营人员分析游戏日志时分了很多 MySQL 库,写很多 SQL 操作,多个数据库将数据取出来放到 Excel 中,然后通过 Excel 公式进行最后的数据聚合,这样做非常麻烦,现在使用 TiDB 可以直接聚合,非常简单方便。能够实现无障碍跨分片操作,不用额外操作,同时还有实时性,以前是很多 MySQL 数据通过 ETL 拉出来放到 Hadoop 中,再用 Hive 计算,虽然计算能力强,但无论是 ETL 实时性也好,还是维护流水线的复杂度,都很难实时。使用 TiDB 后没有这些操作后可以专注于业务,更加方便,当数据丢失还有备库,这种方式叫中台,可以实现海量存储汇聚。

Everyone Happy Now ?

这样实现后还是出现了一些问题,在简单的 TP 场景下,点查联系、低频率写入对优化器负担较小,对执行引擎也没什么压力。但是用户在 AP 场景中越用越深,数据量越来越多,一些场景中,会遇到问题,比如一些 Query 跑得慢,还有就是会遇到 OOM。因为计算是在 TiKV 上经过聚合、filter 后还需要在 TiKV server 中单点聚合,可能会遇到 CPU 瓶颈,TiKV 内存小,使用也不精细,还有就是没有自动算子选择,还是手动设置,对用户来说很不方便;还有就是只能通过 MySQL 协议和外界交互,没有和其他数据库平台打通,还是需要 ETL 将数据从 TiDB 中通过 MySQL 协议读取写到其他地方或者通过 Spark、JDBC 连接数据库。

不匹配的算力

随着用户越来越多,场景也越来越多,TiDB 的存储扩展能力是非常强的,能存储很多数据,但是早期版本中,计算能力相对存储能力偏弱。这方面不光包括复杂的计算如何去处理,也包括整个计算的负载怎么去分配;存储的调度相对简单,但是计算的调度很难分配,这样导致一些业务出现问题。

我们的数据最终都要汇聚到一个 TiDB 上做聚合、join 等操作,每个 TiDB 间无法交换数据,当两个大表做 hash join 时,如果使用内存过多会出现 OOM 的问题,如果通过分布式 join 就能很好解决这个问题。TiKV 间也不能交换数据,只是相对于多个 Map 加一个 Reduce 的模式;还有就是 join 和 Distinct 无法推到 TiKV 上做,在 TiKV 只能进行流式计算。

TiSpark

那么解决方案可以是自己去做执行优化算子,借鉴 Spark、Hive 等专业分析引擎,运用所有节点集群,做一个真正的 MPP 执行引擎;或者借用一些现有的成熟的分析系统,更好地运用存储进来的数据。我们的选择是借助 TiSpark,在 Spark 中加入 TiDB driver,Spark 在优化器中有部分扩展点,可以将一部分查询优化截取出来,告诉优化器哪些可以做优化哪些算子不能优化,最后返回所需要的数据格式,将 table scan、 inner scan、聚合等简单操作通过 driver 让 TiKV 做初步数据预处理,再将数据返回 Spark 做更重的 join。这样不仅有了一个很好的计算平台,也有了一个良好的计算生态。TiSpark 具有完整继承 Apache Spark 生态圈,无缝衔接大数据生态圈、成熟的分布式计算平台等优点。

应用 TiSpark 比较重的用户是易果生鲜,先前应用的是 SQL Server、MySQL,想做数据分析,维护的是 ETL 流水线。应用 TiSpark 后基本能做到实时分析,将多个数据源的数据汇总到一个 TiDB 集群中与各种下游分析需求对接,能做实时分析。实现中台数据汇总加实时数据分析。

应用 TiSpark 并不能解决所有问题,运行速度并不是很快,不能利用底层的统计信息做更精细的分析,再者在做有一定并发能力但并不复杂的场景时,分析能力不强,如利用 TiDB 做点查,有少量 key 的点查还是很快的,但是在 Spark 中并发达不到那么高,其 job 启动、调度并不轻量。其次其运维和使用没有 TiDB 简单,因为 TiDB 的接口管理使用与 MySQL 类似。

除了 TiSpark 之外,我们在 TiDB 本身也做了很多工作,优化 TiDB 优化器,早期做大数据分析要写很多 join 算子 hint、并发度多少、聚合算子选择都需要人为选择。一步步优化,从 RBO 到 CBO 到更好的 CBO,不但包括优化器本身、data 模型、plan 搜索,以及如何快速搜集大量统计信息,利用搜集的、扫描的真实数据信息来修正现有的统计信息。在执行模型方面,由一个纯的经典火山模型变成 Batch 加部分向量化的的执行引擎,当优化器预测基础 size 比较大时,在算子之间调用时,不是一次拿一条而是一次拿一批,batch size 可以实时调节,降低了内存消耗,因为内存分配次数比分配大小更重要些,对整个性能会有很好的优化。在 batch 内部通过 Apache Arrow 格式实现,数据会有一定的压缩,更经典,更重要的是可以对内存使用进行记账,分批次记账比一条条记账更高效;同时对部分算子进行向量化处理,后期将向图形向量化方向发展。TiDB 是一个混合负载机制,目的也是做一个通用负载数据库,不想借助其他特许硬件;同时也支持分区表,对数据进行裁剪。

TiDB 2.0 和 1.0 对比,TPC-H benchmark 结果中 query 运行速度明显要快很多,有些查询在1.0版本中无法查询现在也可以查询。但是在2.0中有些查询比较慢,原因是在 hash 聚合是一个单线程算子。在分析中比较重要的算子就是聚合和 join,join 在 tpch 中占一半,聚合占三分之一。我们的目标就是希望 TiDB 在不借助 Spark 情况下也能和主流分析工具媲美。在 TiDB 2.1 版本中,我们做了进一步的优化,特别是多线程的聚合算子,对 TPC-H 结果带来了进一步的提升。

核心矛盾

TiDB 2.1 版本在很多分析场景中已经跑的不错,但是在面对需要对大表进行全表扫描时,行存模式明显不满足需求,无论是 scan 或者简单 filter 或者 IO 压力都明显不如内存。还有在 HTAP 场景中需要解决 AP 负载不影响 TP 负载。TiDB 内部提供的解决方案就是让分析的负载优先级更低,可以运行低优先 SQL,还有就是依据优先级做作业调度等,还有就 TiDB 实例可以无限拓展,可以一部分给 TP 用,一部分给 AP 用,但是最终 TiKV 对磁盘、网络的占用还是无法避免;还有就是大查询执行下,有几个在线小查询,也没有 CPU 抢占机制;这些问题都是无法避免的。

TiFlash

TP 和 AP 之间对于存储格式的偏好不同,TP 一定要行存,需要低延迟,而 AP 一定要列存。一些数据库尝试过一个集群中同时有行存和列存,比如 Oracle 有列存插件。我们也开发了一个新项目——TiFlash,简单来说是一个 TiKV 的列存扩展。可以通过 Raft 日志将 TiKV 使用的数据实时同步出来,通过 Raft log 将数据同步到 TiFlash 实例,它会将行存的数据转到列存,然后存到本地列存引擎中。开发借用了部分 ClickHouse 代码,因为 ClickHouse 是一个以速度见长的引擎,在向量化方面做的比较细。通过 Raft 日志可以同步 TiKV 上所有数据,包括 MVCC、事务状态,能够实现数据的事务一致性读取,唯一影响的是 TiKV 要同步一分日志过来,Raft learner 只会一次写入不会参与投票,对线上写入延迟影响不大。

TiFlash 当前版本架构:数据从 TiDB 集群中写入进来后,通过 Raft 日志同步到 TiFlash 节点,通过 Spark 进行数据读取。TiFlash 以 Raft Learner 方式接入 Raft Group,使用异步方式传输数据,对 TiKV 产生非常小的负担。当数据同步到 TiFlash 时,会被从行格式拆解为列格式。在读取时会有一个校验机制,在读数据时会进行一个 RaftCommand 判断是否有读取数据,当数据同步到读取请求发送时间点的进度时,TiFlash 才提供读取。虽然有一次 Command,但是如果分析比较重,整个负载还是比较小的。

TiFlash 的下一个版本会做计算的融合,将入口切换转为一个,实现 TiDB 可以同时读取行存和列存副本。有些 SQL 部分选择列存部分选择行存,交给优化器来选择,原理是将列存当做一个特殊的索引,让列存读到与行存一样的数据。目前 TiFlash 已经进入 beta 版,在一些用户那边已经开始进行 PoC,预计在年内发布第一个正式版本。

目前的 TiDB

将存储引擎的数据通过 Raft learner 同步出去可以做更多的事情,保证日志是一个近实时同步方式。计算引擎也可以替换成不同的东西,寄存到自己的引擎中做自己的事情。希望用这套场景解决 AP 与 TP 相互干扰的状况,帮助用户简化使用。在 TP 中统计信息收集比较麻烦,当数据有上百亿表,这种收集统计信息是非常耗时的,当统计信息不准可能会导致业务选错索引。对于这种不需要完全实时,也不要求百分百准确的统计信息收集工作,可以让其后台执行,交给 TiFlash 引擎来做。

存储集群不再区分是何种引擎,是一个行列共存的存储引擎。可以实现数据实时同步,可以实现快速将几 T 或者几十 T 的数据快速导入集群中。有了这么多组件后维护起来比较麻烦,可以使用 TiDB Operator,让 K8s 帮助管理实例。

还有哪些问题?

目前的架构已经足够强大,但是还有些问题没解决。Spark+TiFlash 架构中,底层引擎还不支持 MR 模型,Spark 的 MR 模型还是略重,跑起来速度一般。后期希望自己写一套 MPP 引擎,让 TiFlash 节点之间可以交换数据,减少 Spark 端的工作任务,加快查询速度;还有 SQL 语句推下去做计算,统一协处理器层,让 TiKV 和 TiFlash 都能组成 MPP 集群。同一套代码,同一套引擎,让 TiFlash 也支持 SQL 行为,这样才能接上 MySQL 引擎。

还有写入还是要借助 TiKV,通过 TiDB 和 TiKV 写入行存,Raft 日志同步到列存中,在写入过程中吞吐不够,将数据从行存转入列存流水线较长,开销也会加大。

另一个就是需要加强写入,TiDB 对一次性写入的数据大小是有限制的,我们希望提供一套快速且支持大数据量的写入,支持几个 G 的数据原子写入。TiFlash+TiDB 能解决海量数据计算问题,但是计算后不能将数据原子写回,会导致整条业务逻辑不通。有了大数据量回写之后,TiFlash 可以承担 ETL 的功能。

最后就是可以用不同引擎解决不同问题,就像将行存列存引擎接到 TiKV 中来解决不同的问题,我们还可以添加更多的引擎。此外通过更好的调度,将不同的引擎调度到不同的机器实例上,实现完美的负载隔离,并且分散不同类型的业务负载到不同类型的引擎。

嘉宾介绍

申砾,PingCAP 技术 VP,TiDB Tech Lead,前网易有道、360 搜索资深研发。

——END——

分享嘉宾:申砾 PingCAP 技术VP

编辑整理:Hoh Xil

内容来源:大数据开源技术论坛 · 01

出品社区:DataFun

注:欢迎转发,转发请注明出处。