共计 7478 个字符,预计需要花费 19 分钟才能阅读完成。
本文选自《TiDB 6.x in Action》,分为 TiDB 6.x 原理和个性、TiDB Developer 体验指南、TiDB 6.x 可管理性、TiDB 6.x 内核优化与性能晋升、TiDB 6.x 测评、TiDB 6.x 最佳实际 6 大内容模块,汇聚了 TiDB 6.x 新个性的原理、测评、试用心得等等干货。不论你是 DBA 运维还是利用开发者,如果你正在或有动向应用 TiDB 6.x,这本书都能够给你提供参考和实际指南。
TiFlash 是 TiDB 的剖析引擎,是 TiDB HTAP 状态的要害组件。TiFlash 源码浏览系列文章将从源码层面介绍 TiFlash 的外部实现。次要包含架构的演进,DAGRequest 协定、dag request 在 TiFlash 侧的解决流程以及 MPP 基本原理。
本文作者:徐飞,PingCAP 资深研发工程师
背景
上图是一个 TiDB 中 query 执行的示意图,能够看到在 TiDB 中一个 query 的执行会被分成两局部,一部分在 TiDB 执行,一部分下推给存储层(TiFlash/TiKV)执行。本文咱们次要关注在 TiFlash 执行的局部。
这个是一个 TiDB 的查问 request 在 TiFlash 外部的根本解决流程,首先 Flash service 会承受到来自 TiDB 的 RPC 申请,而后会从申请里拿到 TiDB 的 plan,在 TiFlash 中咱们称之为 DAGRequest,拿到 TiDB 的 plan 之后,TiFlash 须要把 TiDB 的 plan 编译成能够在 TiFlash 中执行的 BlockInputStream,最初在失去 BlockInputStream 之后,TiFlash 就会进入向量化执行的阶段。本文要讲的 TiFlash 计算层实际上是蕴含以上四个阶段的狭义上的计算层。
TiDB + TiFlash 计算层的演进
首先,咱们从 API 的角度来讲一下 TiDB + TiFlash 计算层的演进过程:
最开始在没有引入 TiFlash 时,TiDB 是用过 Coprocessor 协定来与存储层(TiKV)进行交互的,在上图中,root executors 示意在 TiDB 中单机执行的算子,cop executors 指下推给 TiKV 执行的算子。在 TiDB + TiKV 的计算体系中,有如下几个特点:TiDB 中的算子是在 TiDB 中单机执行的,计算的扩展性受限;TiKV 中的算子是在 TiKV 中执行的,而且 TiKV 的计算能力是能够随着 TiKV 节点数的减少而线性扩大的;因为 TiKV 中并没有 table 的概念,Coprocessor 是以 Region 为单位的,一个 region 一个 coprocessor request;每个 Coprocessor 都会带有一个用于 MVCC 读的 timestamp,在 TiFlash 中咱们称之为 start_ts。在 TiDB 4.0 中,咱们首次引入了 TiFlash:
在引入之初,咱们基本上就是只对接了现有的 Coprocessor 协定,能够看出下面这个图上之前 TiDB + TiKV 的图其实是一样的,除了存储层从 TiKV 变成了 TiFlash。然而实质上讲引入 TiFlash 之前 TiDB + TiKV 是一个面向 TP 的零碎,TiFlash 在简略对接 Coprocessor 协定之后,马上发现了一些对 AP 很不敌对的中央,次要有两点:Coprocessor 是以 region 为单位的,而 TiDB 中默认 region 大小是 96 MB,这样对于一个 AP 的大表,可能会蕴含成千上万个 region,这导致一个 query 就会有成千上万次 RPC;每个 Coprocessor 只读一个 region 的数据,这让存储层很多读相干的优化都用不上。在发现问题之后,咱们尝试对原始的 Coprocessor 协定进行改良,次要进行了两次尝试:BatchCommands:这个是 TiDB + TiKV 体系里就有的一个改良,原理就是在发送的时候将发送给同一个存储节点的 request batch 成一个,对于 TiFlash 来说,因为只反对 Coprocessor request,所以就是把一些 Coprocessor request batch 成了一个。因为 batch 操作是发送端最底层做的,所以 batch 在一起的 Coprocessor request 并没有逻辑上的分割,所以 TiFlash 拿到 BatchCoprocessor 之后也就是每个 Coprocessor request 顺次解决。所以 BatchCommands 只能解决 RPC 过多的问题。BatchCoprocessor:这个是 TiDB + TiFlash 特有的 RPC,其想法也很简略,就是对同一个 TiFlash 节点,只发送一个 request,这个 request 外面蕴含了所有须要读取的 region 信息。显然这个模式岂但能缩小 RPC,而且存储层能一次性的看到所有须要扫描的数据,也让存储层有了更大的优化空间。只管在引入 BatchCoprocessor 之后,Coprocessor 的两个次要毛病都失去了解决,然而因为无论是 BatchCoprocessor 还是 Coprocessor 都只是反对对单表的 query,遇到简单 sql,其大部分工作还是须要在 root executor 上单机执行,以上面这个两表 join 的 plan 为例:
只有 TableScan 和 Selection 局部能够在 TiFlash 中执行,而之后的 Join 和 Agg 都须要在 TiDB 执行,这显然极大的限度了计算层的扩展性。为了从架构层面解决这个问题,在 TiFlash 5.0 中,咱们正式引入了 MPP 的计算架构:
引入 MPP 之后,TiFlash 反对的 query 局部失去了极大的丰盛,对于现实状况下,root executor 间接进化为一个收集后果的 TableReader,剩下局部都会下推给 TiFlash,从而从根本上解决了 TiDB 中计算能力无奈横向扩大的问题。
DAGRequest 到 BlockInputStream
在 TiFlash 外部,接管到 TiDB 的 request 之后,首先会失去 TiDB 的 plan,在 TiFlash 中,称之为 DAGRequest,它是一个基于 protobuf 协定的一个定义,一些次要的局部如下:
值得一提的就是 DAGRequest 中有两个 executor 相干的 field:executors:这个是引入 TiFlash 之前的定义,其示意一个 executor 的数组,而且外面的 executor 最多就三个:一个 scan(tablescan 或者 indexscan),一个 selection,最初一个 agg/topN/limit;root_executors:显然下面那个 executors 的定义过于简略,无奈形容 MPP 时的 plan,所以在引入 MPP 之后咱们加了一个 root_executor 的 field,它是一个 executor 的 tree。在失去 executor tree 之后,TiFlash 会进行编译,在编译的时候有一个两头数据结构是 DAGQueryBlock,TiDB 会先将 executor tree 转成 DAGQueryBlock 的 tree,而后对 DAGQueryBlock 的 tree 进行后序遍从来编译。DAGQueryBlock 的定义和原始的 executor 数组很相似,一个 DAGQueryBlock 蕴含的 executor 如下:SourceExecutor Selection Having 其中 SourceExecutor 蕴含真正的 source executor 比方 tablescan 或者 exchange receiver,以及其余所有不合乎上述 executor 数组 pattern 的 executor,如 join,project 等。能够看进去 DAGQueryBlock 是从 Coprocessor 时代的 executor 数组倒退而来的,这个构造自身并没有太多的意义,而且也会影响很多潜在的优化,在不久的未来,应该会被移除掉。在编译过程中,有两个 TiDB 体系特有的问题须要解决:如何保障 TiFlash 的数据与 TiKV 的数据放弃强一致性;如何解决 Region error。对于第一个问题,咱们引入了 Learner read 的过程,即在 TiFlash 编译 tablescan 之前,会用 start_ts 向 raft leader 查问截止到该 start_ts 时,raft 的 index 是多少,在失去该 index 之后,TiFlash 会等本人这个 raft leaner 的 index 追上 leader 的 index。对于第二个问题,咱们引入了 Remote reader 的概念,即如果 TiFlash 遇到了 region error,那么如果是 BatchCoprocessor 和 MPP request,那 TiFlash 会被动向其余 TiFlash 节点发 Coprocessor request 来拿到该 region 的数据。在把 DAGRequest 编译成 BlockInputStream 之后,就进入了向量化执行的阶段,在向量化执行的时候,有两个根本的概念:Block:是执行期的最小数据单元,它由一个 column 的数组组成;BlockInputStream:相当于执行框架,每个 BlockInputStream 都有一个或者多个 child,执行时采纳了 pull 的模型。上面是执行时的伪代码:
BlockInputStream 能够分为两类:用于做计算的,例如:DMSegmentThreadInputStream:与存储交互的 InpuStream,能够简略了解为是 table scan;ExchangeReceiverInputStream:从远端读数据的 InputStream;ExpressionBlockInputStream:进行 expression 计算的 InputStream;FilterBlockInputStream:对数据进行过滤的 InputStream;ParallelAggregatingBlockInputStream:做数据进行聚合的 InputStream。用于并发管制的,例如:UnionBlockInputStream:把多个 InputStream 合成一个 InputStream;ParallelAggregatingBlockInputStream:和 Union 相似,不过还会做一个额定的数据聚合;SharedQueryBlockInputStream:把一个 InputStream 扩散成多个 InputStream。
用于计算的 InputStream 与用于并发管制的 InputStream 最大的不同在于用于计算的 InputStream 本人不治理线程,它们只负责在某个线程里跑起来,而用于并发管制的 InputStream 会本人治理线程,如上所示,Union,ParallelAggregating 以及 SharedQuery 都会在本人外部保护一个线程池。当然有些并发管制的 InputStream 本人也会实现一些计算,比方 ParallelAggregatingBlockInputStream。
MPP
在介绍完 TiFlash 计算层中根本的编译以及执行框架之后,咱们重点再介绍下 MPP。MPP 在 API 层共有三个:DispatchMPPTask:用于 TiDB 向 TiFlash 发送 plan;EstablishMPPConnectionSyncOrAsync:用于 MPP 中上游 task 向上游 task 发动读数据的申请,因为无论是读的数据量以及读的工夫会比拟长,所以这个 RPC 是 streaming 的 RPC;CancelMPPTask:用于 TiDB 端 cancel MPP query。在运行 MPP query 的时候,首先由 TiDB 生成 MPP task,TiDB 用 DispatchMPPTask 来将 task 分发给各个 TiFlash 节点,而后 TiDB 与 TiFlash 会用 EstablishMPPConnection 来建设起各个 task 之间的连贯。与 BatchCoprocessor 相比,MPP 的外围概念是 Exchange,用于 TiFlash 节点之间的数据交换,在 TiFlash 中有三种 exchange 的类型:Broadcast:行将一份数据 broadcast 到多个指标 mpp task;HashPartition:行将一份数据用 hash partition 的形式切分成多个 partition,而后发送给指标 mpp task;PassThrough:这个与 broadcast 简直一样,不过 PassThrough 的指标 task 只能有一个,通常用于 MPP task 给 TiDB 返回后果。
上图是 Exchange 过程中的一些要害数据结构,次要有如下几个:
接收端
ExchangeReceiver:用于向其余 task 建设连贯,接收数据并放在 result queue;
ExchangeReceiverInputStream:执行框架中的一个 InputStream,多个 ER Stream 独特持有一个 ExchangeReceiver,并从其 result queue 中读数据。
发送端
MPPTunnel:持有 grpc streaming writer,用于将计算结果发送给其余 task,目前有三种模式:
Sync Tunnel:用 sync grpc 实现的 tunnel;
Async Tunnel:用 async grpc 实现的 tunnel;
Local Tunnel:对于处于同一个节点的不同 task,他们之间的 Tunnel 不走 RPC,在内存里传输数据即可。
MPPTunnelSet:同一个 ExchangeSender 可能须要向多个 mpp task 传输数据,所以会有多个 MPPTunnel,这些 MPPTunnel 在一起组成一个 MPPTunnelSet。StreamingDAGResponseWriter:持有 MPPTunnelSet,次要做一些发送之前的数据预处理工作:将数据 encode 成协定规定的格局;
如果 Exchange Type 是 HashPartition 的话,还须要负责把数据进行 Hash partition 的切分。
ExchangeSenderBlockInputStream:执行框架中的一个 InputStream,持有 StreamingDAGResponseWriter,把计算的后果发送给 writer。
除了 Exchange,MPP 还有一个重要局部是 MPP task 的治理,与 BatchCoprocessor/Coprocessor 不同,MPP query 的多个 task 须要有肯定的通信合作,所以 TiFlash 中须要有对 MPP task 的治理模块。其次要的数据结构如下:
MPPTaskManager:全局的 instance 用来治理该 TiFlash 节点上所有的 MPP task;MPPQueryTaskSet:属于同一个 query 的所有 MPP task 汇合,在诸如 CancelMPPTask 时用于疾速找到所有的指标 task;MPPTask:一个 MPP query 中的最根本单元,不同 MPP task 之间通过 Exchange 来替换数据。
以上就是 TiFlash 中 MPP 的相干实现,能够看出目前这个实现还是比拟奢侈的。在随后的测试和应用中,咱们很快发现一些问题,次要有两个问题:第一个问题:对于一些 sql 自身很简单,然而数据量(计算量)却不大的 query,咱们发现,无论怎么减少 query 的并发,TiFlash 的 cpu 利用率始终会在 50% 以下。
通过一系列的钻研之后咱们发现 root cause 是目前 TiFlash 的线程应用是须要时申请,完结之后即开释的模式,而频繁的线程申请与开释效率非常低,间接导致了零碎 cpu 使用率无奈超过 50%。解决该问题的间接思路即应用线程池,然而因为咱们目前 task 应用线程的模式是非抢占的,所以对于固定大小的线程池,因为零碎中没有全局的调度器,会有死锁的危险,为此咱们引入了 DynamicThreadPool,在该线程池中,线程次要分为两类:固定线程:长期存在的线程动静线程:按需申请的线程,不过与之前的线程不同的是,该线程在完结当前任务之后会等一段时间,如果没有新的工作的话,才会退出第二个问题和第一个问题相似,也是线程相干的,即 TiFlash 在遇到高并发的 query 时,因为线程应用没有很好的管制,会导致 TiFlash server 遇到无奈调配出线程的问题,为了解决此问题,咱们必须管制 TiFlash 中同时应用的线程,在跑 MPP query 的时候,线程次要能够分为两局部:齐全分布式的调度器,仅依赖 TiFlash 节点本身的信息根本的原理为 MinTSOScheduer 保障 TiFlash 节点上最小的 start_ts 对应的所有 MPP task 能失常运行。因为全局最小的 start_ts 在各个节点上必然也是最小的 start_ts,所以 MinTSOScheduer 可能保障全局至多有一条 query 能顺利运行从而保障整个零碎不会有死锁,而对于非最小 start_ts 的 MPP task,则依据以后零碎的线程应用状况来决定是否能够运行,所以也能达到控制系统线程使用量的目标。
IO 线程:次要指用于 grpc 通信的线程,在减小 grpc 线程应用方面,咱们基本上是采纳了业界的成熟计划,即用 async 的形式,咱们实现了 async 的 grpc server 和 async 的 grpc client,大大减小了 IO 线程的使用量计算线程:为了管制计算线程,咱们必须引入调度器,该调度器有两个最低指标:不造成死锁以及最大水平控制系统的线程使用量,最初咱们在 TiFlash 里引入了 MinTSOScheduer:齐全分布式的调度器,仅依赖 TiFlash 节点本身的信息根本的原理为 MinTSOScheduer 保障 TiFlash 节点上最小的 start_ts 对应的所有 MPP task 能失常运行。因为全局最小的 start_ts 在各个节点上必然也是最小的 start_ts,所以 MinTSOScheduer 可能保障全局至多有一条 query 能顺利运行从而保障整个零碎不会有死锁,而对于非最小 start_ts 的 MPP task,则依据以后零碎的线程应用状况来决定是否能够运行,所以也能达到控制系统线程使用量的目标。
总结
本文次要系统性地介绍了 TiFlash 计算层的基本概念,包含架构的演进,TiFlash 外部对 TiDB plan 的解决以及 MPP 基本原理等,以冀望读者可能对 TiFlash 计算层有一个初步的理解。后续还会有一些具体实现诸如 TiFlash 表达式以及算子零碎的细节介绍,敬请期待。