TiDB HTAP 深度解读

Easter79
• 阅读 780

HTAP (Hybrid Transactional / Analytical Processing)是近些年需求不断受到关注的技术名词,它描述了一个数据库能够同时满足交易以及分析两种作业。TiDB 4.0 是一个针对 HTAP 进行了特别的设计和架构强化,这次给大家带来一篇 VLDB 2020 HTAP 主题的论文解读,比较特殊的是这篇论文是 PingCAP 写的,关于 TiDB HTAP 架构。所以这篇解读,是以作者团队(中的一部分)的视角来写的。原文在此,欢迎指正。

说重点

论文整体介绍了一下 TiDB 的架构和设计,对 TiDB 有兴趣的同学推荐完整看下,会对理解架构有很大帮助。不过既然重点是 HTAP,那么在我看来比较重要的地方是这三点:

  1. 实时更新的列存
  2. Multi-Raft 的复制体系
  3. 根据业务 SQL 智能选择行/列存储

后面我也会着重说一下这三部分。

先说存储

TP 和 AP 传统来说仰赖不同的存储格式:行存对应 OLTP,列存对应 OLAP。然而这两者的优劣差异在内存中会显得不那么明显,因此 SAP Hana 的作者 Hasso Plattner 提出使用 In-Memory + 列存技术同时处理 OLTP 和 OLAP。随后 2014 年 Gartner 提出的 HTAP 概念,也主要是针对内存计算。 这里有个关键信息,列存不合适 TP 类场景。这也许已经是很多人的常识,不过也许并不是所有人都想过为何列存不合适 TP。

数据快速访问需要仰赖 Locality,简单说就是希望根据你的访问模式,要读写的数据尽量放在一起。并不在一起的数据需要额外的 Seek 并且 Cache 效率更低。行存和列存,去除 encoding 和压缩这些因素,本质上是针对不同的访问模式提供了不同的数据 Locality。行存让同一行的数据放在一起,这样类似一次访问一整行数据就会得到很好的速度;列存将同一列的数据放在一起,那么每次只获取一部分列的读取就会得到加速;另一方面,列存在传统印象里更新很慢也部分是因为如果使用 Naive 的方式去将一行拆开成多列写入到应有的位置,将带来灾难性的写入速度。这些效应在磁盘上很明显,但是在内存中就会得以削弱,因此这些年以来我们提起 HTAP,首先想到的是内存数据库。

虽然内存价格在不断下降,但是仍然成本高企。虽说分析机构宣传 HTAP 带来的架构简化可以降低总成本,但实际上内存数据库仍然只是在一些特殊领域得到应用:若非那些无可辩驳的超低延迟场景,架构师仍然需要说服老板,HTAP 带来的好处是否真的值得使用内存数据库。这样, HTAP 的使用领域就受到很大的限制。

所以,我们还是以磁盘而非内存为设计前提。

之前并不是没有人尝试使用行列混合的设计。这种行列混合可以是一种折中格式如 PAX,也可以是在同一存储引擎中通过聪明的算法糅合两种形态。但无论如何,上面说的 Locality 问题是无法绕过的,哪怕通过超强的工程能力去压榨性能,也很难同时逼近两侧的最优解,更不用提技术上这将会比单纯考虑单一场景复杂数倍。

TiDB 并不想放弃 TP 和 AP 任何一侧,因此虽然也知道 Spanner 使用 PAX 格式做 HTAP,却没有贸然跟进。也许有更好的办法呢?

TiDB 整体一直更相信以模块化来化解工程问题,包括 TiDB 和 TiKV 的分层和模块切割都体现了这种设计倾向。这次 HTAP 的构思也不例外。经过各种前期的的 Prototype 实验,包括并不限于通过类似 Binlog 之类的 CDC 方案将 TP 的更新同步到易构的 AP 侧,但是这些效果都不尽如人意,我们最终选了通过 Raft 来剥离 / 融合行存和列存,而非在同一套引擎中紧耦合两种格式。这种方式让我们能单独思考两个场景,也无需对现有的引擎做太大的改变,让产品成型和稳定周期大大缩短。另一方面,模块化也使得我们可以更 好借助其他开源产品(ClickHouse)的力量,因为复杂的细节无需被封印在同一个盒子。

市面上有其他设计采用了更紧密的耦合,例如 MemSQL 节点同时运行 TP 和 AP 两种业务,Spanner 选择 PAX 兼顾不同的读取模式,甚至传统数据库大多也在同一个引擎中添加了不同数据组织的支持。这样的架构会引入过于复杂的设计,也未必能在 TP 和 AP 任意一端取得好的收益。

由于选择了松耦合的设计,我们只需要专心解决一个问题就可以搞定存储:如何设计一个可根据主键实时更新的列存系统。事实上,列存多少都支持更新,只是这种更新往往是通过整体覆盖一大段数据来达到的,这也就是为什么多数传统的 OLAP 数据库只能支持批量的数据更新,。如果无需考虑实时主键更新,那么存储可以完全无需考虑数据的去重和排序:存储按照主键顺序整理不止是为了快速读取定位,也是为了写入更新加速。如果需要更新一笔数据,引擎至少需要让同一笔数据的新老版本能以某种方式快速去重,无论是读时去重还是直接写入覆盖。传统意义上分析型数据库或者 Hadoop 列存都抛弃了实时更新能力,因此无需在读或者写的时候负担这个代价,这也是它们得以支持非常高速批量加载和读取的原因之一。但这样的设计无法满足我们场景。要达到 HTAP 的目标,TiDB 的列存引擎必须能够支持实时更新,而且这个更新的速率不能低于行存。

事实上,我们肯定不是第一个在业界尝试实现列存更新的产品。业界对于列存更新,无论是何种变体,一个很通用的做法叫做 Delta Main。既然做列存更新效率不佳,那么我们何不使用写优化方式存储变更数据,然后逐步将更新部分归并到读优化的主列存区?只要我们保持足够的归并频率,那么整个数据的大部分比例都将以读优化的列存形态存在以保持性能。这是一个几乎从列存诞生起就有被想到的设计:你可以认为列存鼻祖 C-Store 就是某种意义上的 Delta Main 设计,它使用一个行存引擎做为写区,并不断将写区数据归并为列存。 TiDB HTAP 深度解读 我们的可更新列存引擎 DeltaTree 的设计也是非常类似的思路。宏观上,DeltaTree 将数据按照主键序排序切分,类似 TiDB 的 Region 概念那样,每一个数据范围单独形成一个片段,每当片段的物理大小超过阈值就会分裂。微观上来说,每个片段就如上图一般,分成 Delta 和 Stable Space 两部分。其中 Delta 部分以优化写入为主,他们是以写入顺序攒批排列的小数据块,以写入顺序排列而非主键顺序能使得写入大大加速,因为数据写入只需要不断追加。每当积攒了足够多的 Delta 数据,引擎就会将他们归并到 Stable 区,Stable 区的设计类似 Parquet,也是以行组(Row-Group)再按列切割,并排序后压缩存储。Stable 区无疑是对读取优化的,如果只考虑 Stable,那么速度将会很快。但实际上在读取时,仍未归并到 Stable 的 Delta 数据可能需要覆盖 Stable 中的老数据,因此读取会是一个在线归并过程。为了加速这个归并,引擎为 Delta 部分添加了内存中的辅助 B+Tree 索引,这样 Delta 虽然并非物理有序(保持 Delta 物理有序将大大降低写入性能),但仍然保持逻辑有序,免去了归并前排序的代价。同时,由于宏观上数据区间的划分,使得每次归并无需重写所有数据减轻了归并的压力。

回头说之前提到的 LSM 列存方案。实际上你可以认为 LSM 也可以近似认为是一种 Delta Main。当数据写入 MemTable 时,也是以写优化的追加形式写入。那是否 LSM 也可以成为一种支持列存更新的设计呢?我们也尝试过,并非不可能,只是性能对比 DeltaTree 尚有差距:进行范围读取时,LSM 需要进行非常重的多路归并,因为任何上层的新数据都可能会覆盖下层的老数据,而层和层之间存在交集,因此 N 层的 LSM 也许需要进行 N 路归并才能获取一段数据。我们曾经实现过基于 ClickHouse MergeTree 改造的 LSM 列存引擎,对比新的 DeltaTree 将近慢了一倍。 TiDB HTAP 深度解读 至此为止,我们解决了可更新列存问题。

再说复制

既然选择了松耦合的存储引擎,行列存储并不在同一个模块内,那随之而来的问题必然是如何进行数据复制。对于传统的主从复制体系,我们往往使用比如 MySQL Binlog 这样的 High Level 层级进行复制。实际上,这种复制体系也是我们第一个原型迭代所使用的手段。基于 Binlog 的复制体系能很好封装不必要的细节,只要列存引擎 TiFlash 可以正常回放日志就可以,无需关心例如事务实现等等细节。这样我们很快得到了第一版 TiFlash,它通过 binlog 串联行存与列存,但是需要再往下实现容错,负载均衡等等一系列特性。更麻烦的是,TiDB 是一个分布式且多主的系统。每个 TiDB 服务器都会产生一份 binlog,如果要保持数据一致性,不会新老覆盖,binlog 实际上还需要经过一层汇聚和排序,这几乎将分布式降维打击成了单点吞吐,而排序管道也大大增加了数据到达的延迟。因此原型版的 TiFlash 是无法提供行列混合查询的:你只能单独查询行存或者列存,因为数据无法保证一致,在查询中混合两者会创造无穷无尽的不可知数据错误。

于是我们转而从更低层级的日志进行复制,是的,我们选了在 Raft 层进行对接。从更底层进行对接的好处显而易见,Raft Log 保留了数据复制所需的一切细节,我们得以将 TiFlash 设计成一种特异的 TiKV 节点,从而能够直接获得 Multi-Raft 体系所赋予的一切好处:数据变得可以通过 PD 进行透明迁移扩容,容错本身也完全无需操心全部交由 Raft 体系来完成,当副本丢失时,存储层会自动发起恢复,而复制本身的复杂一致性保障也变得无需操心。从面临自己完善基于 ClickHouse 的副本体系,到坐享其成,一切都是如此美好。当然在实际的工程实现上也有代价,由高层准 SQL 级的 Binlog 改为完全底层的 Raft Log,代价也是相当巨大的。我们需要在 ClickHouse 上实现所有 Multi-Raft 体系所需的复杂操作,例如 Region 的分裂与合并,以及迁移和读取容错。

新的设计是整个 HTAP 体系成立的关键,它给与 TiFlash 无缝接入整个存储层的能力。同一套复制体系,同一套调度体系,一样的事务模型,一样的一致性保障。它的复制设计是完全分布式,负载均衡且自动容错的。 相比通过主从复制或者同机器行列双写,它的 AP 和 TP 部分可以完全独立地运转,自由扩容:如果你需要更多 AP 算力,那请增加 TiFlash 节点;如果你需要增加 TP 算力,请增加 TiKV 节点。互不干扰,以 Workload 而言或者计算资源扩展而言都是。

与此同时,这种复制又是自动负载均衡且点对点直接链接的。每个 Region Leader 副本会单独与列存侧的副本进行沟通完全无需中间存储介质,当 Region 副本过大分裂时,列存副本也会跟着分裂;当副本因为热点打散进行迁移时,他们之间的复制管道也会跟着迁移。这对于 TiDB 的 Multi-Raft 体系来说都是已经实现的功能。 TiDB HTAP 深度解读 另外 Raft 体系带来的最大好处却是一致性和异步复制的共存。

传统意义上,如果需要复制保持副本一致,就必须采用同步复制。这样,无论是列存节点的高压,还是网路延迟加大,都会对 TP 业务带来巨大冲击:为了保持数据一致性,行存事务必须等待列存确实完成写入才能返回,否则期间的故障将会带来数据丢失和不一致。另外新增任何列存节点也会加大遭遇网络延迟的概率。虽然诸多 HTAP 产品并不会态度考虑 AP 和 TP 互相影响的问题,我们仍然希望娇弱的 TP 能收到更大程度的保护。

这,恰恰可以通过 Raft 解决。TiFlash 通过 Learner 角色接入 Raft 体系,这允许列存以不投票只异步抄写的方式加入集群,这意味着它不会因为自身的稳定干扰正常 TP 业务的运转。当 TP 侧有事务写入,TiKV 无需等待 TiFlash 的数据同步,仅仅在完成正常的行存副本容错复制就可以返回客户端完成事务。那你也许要问了,这样是否数据无法保证一致性,是否行存和列存之间也许存在数据延迟?是也不是。物理上来说,确实存在,一个系统理论上并无可能做到异步复制仍然能同时物理上保持副本一致。但实际上我们也无需保证数据每时每刻在物理上一致,我们只需要提供一种一致的逻辑读取结果就行了。这也是 Raft 本身的核心特点之一,虽然多个副本并非全都每时每刻保持一致,但是只要读取的时候能得到最新的一致性数据即可。当实际读取发生时,列存副本会向行存的 Leader 发起校对请求,这个请求本身很简单:请告诉我在你收到请求的瞬间,最新日志序号是多少。而 TiFlash 会等待数据复制进度追上校对结果。仅此而已。这就使得 TiFlash 能够保证取得足够新鲜的数据,新鲜到保证囊括上一个瞬间写入的信息。是的,从 TiKV 写入的最新数据保证能从 TiFlash 被读取,这形成了读取的水位线。而通过时间戳和 MVCC 配合,TiFlash 的异步同步也可以提供与 TiKV 一样的强一致保证。这使得 TiFlash 列存表现得并不像一套异构复制体系,而更像是一种特殊的列存索引,也使得我们可以放心大胆地在同一个查询中混合两种不同引擎,而无需担心是否会由不一致带来微妙难以追查的错误。 TiDB HTAP 深度解读

智能选择

智能选择放在最后说,是因为它也的确是我们最后实现的。TiDB 的行列存智能选择就是通过代价优化自动选择行存或者列存。说起来这部分也很简单,犹如使用统计信息选择索引,我们也可以通过代价公式估算列存的使用代价。综合各个访问路径的开销,我们就能知道需要选择何种方式读取数据,而列存只是其中一种,并无特殊性。 TiDB HTAP 深度解读 技术上来说,这并没有太多新意。但通过自动选择,TiDB 的 HTAP 体系从 TP + 报表的用况一下子拓展到了 HTAP 混合业务。一些边界模糊的业务系统,通过 TiFlash 加持,变得架构简单。例如物流系统,用户希望能够在同一套查询平台检索个别单号以及投递明细,又希望能统计某时间段不同货物类别的收发情况。明细查询对于 TiDB 来说并无任何障碍,但以往没有列存的时候,大数据集下的多维分析性能对比真的分析型产品仍有不小的差距。有了 TiFlash 之后,这样 AP 和 TP 边界模糊的业务就立马变得圆润完整起来。反倒是原始计划中的 TiSpark 读取,由于 TiDB 更贴近业务和 DBA 而非大数据的特点,相较之下显得并没有那么多。

最后

这篇文章并不完全讲述了我们论文的内容。缺失的部分是 TiDB 非 HTAP 部分的设计,有兴趣的同学可以点击原文在此。另外,也欢迎大家使用我们的产品,各位的使用和宝贵意见是 TiDB 发展最基本的推动力。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k