写点什么

TiDB 知识点梳理 (PCTA 笔记分享)

  • 2023-12-08
    北京
  • 本文字数:10951 字

    阅读完需:约 36 分钟

作者: 魔人逗逗原文来源:https://tidb.net/blog/fe4f7b05

整体架构

TiDB 大体可以分为三个模块 TiDB Server, PD Cluster 和 Storage Cluster。


  • TiDB Server 的功能类似于 Mysql 中的 Server 端,用于 * 处理客户端的连接 *、* 解析和优化 SQL 语句 *、生成执行计划 等等。除此之外,因为 TiDB 底层存储是 LSM-Tree 而不是 B+ Tree,所以还需要将 * 关系型数据转换成 KV 格式 *。此外还有 *GC*、*DDL 执行 *、* 缓存 * 和 * 热点小表 * 等功能。

  • PD Cluster 是 TiDB 相比于传统数据库独有的一个模块,这个模块也是 TiDB 作为分布式系统的核心。主要的功能有 * 元数据的存储 *、TiDB 集群中 * 微服务的注册和调用 *、分布式事务 / 全局 ID(TSO) 的生成 和 集群的 * 负载均衡 *。为了实现这些,PD 集成了 etcd

  • Storage Cluster 是 TiDB 的存储模块,为了实现 HTAP(OLAP + OLTP),这部分又被分成两个存储模块——TiKV(行式存储) 和 TiFlash (列式存储)。

  • TiKV 为了支持 OLTP 业务,类似传统数据库引擎会有 MVCC 多版本并发管理、锁机制、算子下堆、事务隔离、ACID 等等功能,底层的存储结构使用的是 rocksdb 实现的 LSM-Tree。

  • TiFlash 是为了支持 OLAP 业务,TiFlash 的数据是基于 TiKV 异步复制过来的。


分模块梳理

上面我们对三个模块进行了梳理,下面我们具体剖开每个模块,看看具体的细节

TiDB Server

编译、优化、执行

Protocol Layer 是协议层,用于管理客户端的连接和身份认证


SQL 的解析和编译是 ParseCompile 模块处理的,主要的作用是:SQL → AST 语法树 → 执行计划 (Plan)


Parse 模块会将 SQL 语句解析成 AST 语法树,AST 语法树是pkg/parser/ast 目录中定义的基于 Node 的一组用于存储 SQL 语句信息的对象,比如 DMLNode 对象中会包含读写字段 (FieldList)、表名 (TableName)、条件信息 (OnCondition) 等等。


Compile 模块会将 AST 语法树转换成执行计划 (SQL Plan),这部分对应的代码在pkg/planner 目录下,Plan 对象会根据 ast.Node 即 AST 语法树来生成,并在创建 Plan 的过程中会进行 * 验证 *、逻辑优化 和 * 物理优化 *,也会创建对应的 session 上下文。


💡 还有一点需要注意,Compile 模块中提供了将 Plan 对象转为对应存储类型 (StoreType) 的执行器 (Executor),在执行器中包含 KV 结构数据,即Compile 模块会将解析 SQL 生成的关系型数据转换成对应的 KV 结构数据。




  • 经过上面的解析和编译 ,我们来到了执行Executor模块,它更像是一个 TiDB Server 中的调度模块,用于调度 TransactionKVDistSQL 模块。


每个 Exec 中会包含对应的 从 Plan 计划中传递过来的 session 上下文 (sessionctx),session 中会包含事务管理器 (TxnManager) 的调用方法,以便在执行过程中管理事务。


事务具体的实现都是封装在 Transaction 模块,对应代码中则是在 pkg/sessiontxn 目录下。


拿到 Executor 后,之后就是具体的执行阶段,针对比较简单的仅 根据主键或者唯一索引的等值查询 都是通过 KV 模块处理的;相对复杂的任务 (job) 会通过 DistSQL 将其转换成基于单表的子任务 (subjob)。


不论是KV还是 DistSQL 最终都会通过调用TiKV Client 来获取引擎层的数据。


💡 Executor模块既然负责了执行的调度,那有关 算子下推 的调度也是在此模块做的。具体的代码在pkg/executor/coprocessor.go


算子下推 指的是将一部分函数计算交给引擎层 (TiKV 和 TiFlash),以减轻 Server 层和引擎层在网络 IO 上的开销,它将本需要 Server 层处理的工作交给了引擎层,Server 层只需要对引擎层的结果进行聚合即可。由此可见,为了支持算子下推,引擎层也需要支持一部分函数执行。


  • TiKV 可以支持 TopN 和 Limit 的算子下推

  • TiFlash 可以支持 TableScan、Selection、Hash/Stream Aggregation、TopN、Limit、Project、HashJoin、Windows 等算子,支持了几乎所有的 SQL 函数的下推



online DDL

online DDL 相关模块——start job, workersschema load


💡 online DDL 仅在 TiDB v6.2.0 之前有效,这之后采取了并发 DDL 框架,详见 DDL 语句的执行原理及最佳实践


  • TiDB Server 集群中所有实例的start job 模块都可以接收 DDL 语句,并将其放入 TiKV 中的 job queue

  • 在 TiDB Server 的集群中,只有一个 Leader 节点,也可以称为 Owner 节点,只有 Owner 节点的workers 模块会生效,从 TiKV 中获取持久化的队列 ( 物理 DDL 队列general job queue 和 逻辑 DDL 队列add index job queue),执行 job,并在直接结束后记录到历史队列 history queue

  • schema load 是 TiDB 中表结构 schema 的缓存,主要用于 DDL 语句的查询


缓存

缓存模块——memBuffercache table


TiDB Server 的缓存主要是存储在 membuffer 模块,会缓存下面三个数据:SQL 结果、线程缓、元数据和统计数据,可以通过 tidb_mem_quota_query 阈值参数限制每条 SQL 的缓存占用大小,如果超过此阈值,可以通过 oom-action 参数设置返回 ERROR 或打印日志等 oom 动作。


cache table 主要是用于 * 热点小表缓存 *,此功能主要用于查询频繁数据量不大极少修改的场景,因此想使用热点小表的功能需要满足以下几个条件:


  1. 表的总数据量不大 (小于 64M)

  2. 表的读取频繁 (查询频繁、数据量小、极少修改)

  3. 不做 DDL,热点小表支持 DDL 操作,进行 DDL 需要先关闭热点小表缓存


💡 针对极少的写场景,热点小表缓存如何保证一致性? 设定 缓存租约 参数 tidb_table_cache_lease ,默认 5s。① 租约时间内,读操作直接读缓存,无法进行写操作 ② 租约到期时,缓存中的数据过期,写操作不在堵塞,读写操作都直接请求 TiKV ③ 数据更新完毕后后,租约继续开启,回到步骤 1

GC

  • GC 机制——GC 主要是清理 TiKV 中由 MVCC 产生的历史版本。GC 操作是仅由 TiDB Server 集群中 Leader 节点 发起,执行 GC 前,Server 会找到一个 safe point,小于 safe point 时间的数据将会被请求,默认是 safe point 是当前减去十分钟,可以通过 GC Left Time 设定,即 GC Left Time 默认是 10 分钟。

  • TiDB Server 集群每 10 分钟会进行一次 GC 操作。首先清除 表或者字段被 drop 的数据,其次清除 * 被 delete 的数据 *,最后清除这些数据相关的 * 锁信息 *。

PD

前面说到 PD 是 TiDB 分布式集群的大脑,提供的能力主要是服务整体集群的分布式一致性、高可用。PD 是基于etcd实现的,主要的功能大体可以分为以下几类:


  • 分布式集群中唯一键的生成:TSO 的生成、全局和事务 ID 的生成

  • 数据存储:存储集群中的服务信息 (元数据)、集群中的调度信息

  • 负载均衡策略:集群中的调度规则、标签 (label) 能力

数据存储

PD 会记录集群中每个实例的元数据信息、TiDB 集群的环境变量信息 (如 Server 中的 GC Left time)、集群的调度信息、使用过的 TSO、规则信息、资源组等等。具体是通过 leveldb 存储的。


TSO 的生成

TSO 是一个 int64 的整数型,主要是由 physical (unix 时钟,精确到毫秒) 和 logical (逻辑相对时钟) 组成,TSO 的分配都是由 PD 集群中的 Leader 节点 分配的。TiDB Server 在请求 PD 获取 TSO 的时候是调用 Leader 节点,通过异步 callable-future 的方式获取的,避免了 PD 生成 TSO 的等待时间。




还需要注意一点的是, PD 在分配 TSO 的时候会对 TSO 进行落盘,即会产生 IO 操作,所以在分配过程中会分配 一段时间窗口 的 TSO 并缓存到内存,在 TiDB Server 多次获取的过程中,只需要从内存中获取 TSO 即可,大大增加了并发性。源码在 pdpkg/tso/global_allocator.go


负载均衡

对于负载均衡所需要的实例状态信息,由 TiKV 节点周期性地通过心跳 (heart beat) 的方式传递给 PD。其中这部分信息包含 store heartbead(TiKV 实例的状态信息) **** 和 region heartbeat (TiKV 中每个 Region 的状态信息)。


基于这些信息,PD 支持读写均衡、region 容量分配均衡、热点数据打散、扩容缩容、故障恢复等等功能,这些属于比较常规的负载均衡策略,这里不做详细说明。下面主要说明一下 PD 提供的 label 标签 功能。


针对大规模的分布式集群业务,服务会部署在不同的 IDC 机房、不同的子网上,为了提供用户对于跨机房业务的调度配置,TiDB 在 PD 中提供了 label,具体的代码在pkg/schedule/labeler




PD 中定义了 ReplicationConfig 来读取 label 相关的配置,比较重要的几个变量有:


  • max-replicas:每个区域的副本数

  • location-labels:标签列表,列表中的顺序代表标签的优先级

  • isolation-level:显式强制隔离副本的 label,即在此配置中的 label key 至少在 3 个不同的 label value 中有副本

TiKV

rocksdb

TiKV 的存储数据结构是基于rocksdb 的 LSM-Tree (全称是 Log-Structured Merge-tree),存储的格式是 KV 键值对,通过分块 + 二分查找的方式找到对应的记录。


LSM-Tree 会在内存中分配 1 个MemTable块和若干个 immutable MemTable,这些块以链表的形式连接在一起;同理磁盘中的 SSTable 也会按照层级串联起来且大小不断递增;需要注意的是 Level 0 的 SSTable 数据是 immutable MemTable 数据的复刻 (rocksdb 会尽可能块地将 immutable MemTables 刷盘到 L0 的 SSTable 中)。


内存中的immutable MemTable = 磁盘中的 Level 0 SSTable


MemTable 填满时,会刷盘到immutable MemTable,后台进程会将immutable MemTable 的数据刷新到磁盘中 Level 0 的 SSTable ,当 Level 0 的 SSTABLE 文件达到 4 个时,会进行压缩并存储到 Level 1,以此类推。


写操作时,直接将写请求记录在内存的MemTable 和磁盘的 WAL 日志即可


读操作时,TiKV 在 MemTable 上有提供了一个Block Cache 的缓存,用于缓存最近最常读取的数据,Block Cache 中没有再依次读取MemTableimmutable MemTableSSTable


还需要注意一点数据写入都是到内存的 MemTable,那么如何保证一致性?


答案是:WAL —— Write Ahead Log



既然 LSM-Tree 是 KV 形式存储,那么一个表中的不同索引要怎么查找?


这里就需要引入列簇 Column Families,即每个 column 对应一组MemTableimmutable MemTableSSTable


下面我们从 rocksdb 源码的数据结构,来串一下上面的功能


// [db/column_family.h]// 列簇集合class ColumnFamilySet {  private:    friend class ColumnFamilyData; // 默认主键列簇    // 列簇集合    UnorderedMap<std::string, uint32_t> column_families_;    UnorderedMap<uint32_t, ColumnFamilyData*> column_family_data_;    }
// [db/column_family.h]// 单列簇数据class ColumnFamilyData { private: friend class ColumnFamilySet; // 列信息 uint32_t id_; const std::string name_; // 版本信息 Version* dummy_versions_; // Head of circular doubly-linked list of versions. Version* current_; // == dummy_versions->prev_
MemTable* mem_; // MemTable MemTableList imm_; // immutable MemTable SuperVersion* super_version_; // 所有版本(当前+历史版本) // ColumnFamily 形成双向链表 ColumnFamilyData* next_; ColumnFamilyData* prev_;
}
// [db/memtable_list.h]// 所有 immutable memtables 的集合的引用,数组存在 MemTableListVersion 中// 官方注释也说明 MemTableList即imm 会尽快刷新到 L0 的SST中class MemTableList { const int min_write_buffer_number_to_merge_; MemTableListVersion* current_; // 仍需刷盘的elements数量 int num_flush_not_started_; // 当前内存使用量 size_t current_memory_usage_;}
// [db/memtable_list.h]// 保存了 immutable memtables 数组class MemTableListVersion { // 没有刷盘的 Immutable MemTables std::list<MemTable*> memlist_; // 已经落盘的 MemTables std::list<MemTable*> memlist_history_;}
// [db/memtable.h]class MemTable { // MemtableRep 里包含了 SkipListRep 和 HashSkipListRep // 具体可以看 [db/memtablerep.h] const size_t kArenaBlockSize; ConcurrentArena arena_; std::unique_ptr<MemTableRep> table_; std::unique_ptr<MemTableRep> range_del_table_;
// flush 相关 bool flush_in_progress_; // started the flush bool flush_completed_; // finished the flush uint64_t file_number_; // filled up after flush is complete std::atomic<FlushStateEnum> flush_state_;}
复制代码


根据上面的数据结构,我们可以得到下面这张图


分布式事务 (MVCC + 2PC)

分布式事务的设计思想沿用了 Google Percolator (Google.Inc. (2010). Large-scale Incremental Processing Using Distributed Transactions and Notifications.) 提出的分布式事务,主要分为两部分


  1. 基于 MVCC 机制的快照读

  2. 事务的两阶段提交 prewrite-commit


稍微总结一下 Percolator


  • 数据结构层面,定义了一种带 TSO 时间戳的 KV 格式,其中 key 是行关键字 (row),列关键字 (column),以及时间戳 (timestamp) 的组合,value 是任意的 byte 数组

  • 架构层面分为 3 个组件:Client、 TSOBigtable,其中 Client 是分布式事务流程的控制者、两阶段提交的协调者,TSO 全局唯一且递增的时间戳,Bigtable 是持久化的分布式存储


关于 Percolator 更详细的内容可以看这篇文章,这里不在展开说明—— PolarDB 数据库内核月报. 11(2018). Database · 原理介绍 · Google Percolator 分布式事务实现原理解读


对应到 TiDB,首先我们需要知道 TiDB 中的 KV 格式是如何设计的。TiDB 为表的每个索引分配了一个索引 ID,用IndexId表示。


对于主键和唯一索引,可以根据键值 key 快速定位到对应的 RowID;如果是唯一索引,value 对应 RowID 主键的值,如果是主键则是具体的数据:


Key:   tablePrefix{tableID}_indexPrefixSep{indexID}_indexedColumnsValueValue: RowID
复制代码


对于不需要满足唯一性约束的普通二级索引,一个键值可能对应多行,需要根据键值范围查询对应的 RowID:


Key:   tablePrefix{TableID}_indexPrefixSep{IndexID}_indexedColumnsValue_{RowID}Value: null
复制代码


生成 IndexKey 这部分代码在 tidb 中的pkg/tablecodec/tablecodec.go


var (  tablePrefix     = []byte{'t'}  recordPrefixSep = []byte("_r")  indexPrefixSep  = []byte("_i")  metaPrefix      = []byte{'m'})
// 生成 index keyfunc GenIndexKey(loc *time.Location, tblInfo *model.TableInfo, idxInfo *model.IndexInfo, phyTblID int64, indexedValues []types.Datum, h kv.Handle, buf []byte) (key []byte, distinct bool, err error) { // 判断 unique if idxInfo.Unique { for _, cv := range indexedValues { if cv.IsNull() { distinct = false break } } } TruncateIndexValues(tblInfo, idxInfo, indexedValues) // 分配定长buf key = GetIndexKeyBuf(buf, RecordRowKeyLen+len(indexedValues)*9+9) // table id, 返回 t[tableID]_i key = appendTableIndexPrefix(key, phyTblID) // index id,返回 t[tableID]_i[indexId] key = codec.EncodeInt(key, idxInfo.ID) // indexedColumnsValue key, err = codec.EncodeKey(loc, key, indexedValues...) if err != nil { return nil, false, err } if !distinct && h != nil { // 二级索引 // RowID if h.IsInt() { key = append(key, codec.IntHandleFlag) key = codec.EncodeInt(key, h.IntValue()) } else { key = append(key, h.Encoded()...) } } return}
复制代码


在 MVCC 的加持下,会在 Key 中记录 version 信息,,例如:


Key1_Version1 -> ValueKey2_Version2 -> ValueKey2_Version1 -> Value
复制代码


当然这部分也是基于上面提到的 GenIndexKey 方法上,添加 TSO 时间戳;这部分代码在 tidb 的pkg/store/helper/helper.go ,但实际获取 mvcc 的 key 是通过请求 tikv 拿到的,因为中间可能会涉及加锁,具体在 tikv 的src/storage/txn/commands/mvcc_by_key.rssrc/storage/mvcc, 这里只展示部分代码


// GetMvccByEncodedKeyWithTS get the MVCC value by the specific encoded key, if lock is encountered it would be resolved.func (h *Helper) GetMvccByEncodedKeyWithTS(encodedKey kv.Key, startTS uint64) (*kvrpcpb.MvccGetByKeyResponse, error) {  // 构造 tikv 请求  tikvReq := tikvrpc.NewRequest(tikvrpc.CmdMvccGetByKey, &kvrpcpb.MvccGetByKeyRequest{Key: encodedKey})  for {    ...    kvResp, err := h.Store.SendReq(bo, tikvReq, keyLocation.Region, time.Minute)    ...    mvccResp := kvResp.Resp.(*kvrpcpb.MvccGetByKeyResponse)    ...        // Try to resolve the lock and retry mvcc get again if the input startTS is a valid value.    if startTS > 0 && mvccResp.Info.GetLock() != nil {      ...      lockInfo := mvccResp.Info.GetLock()      lock := &txnlock.Lock{        Key:             []byte(encodedKey),        Primary:         lockInfo.GetPrimary(),        TxnID:           lockInfo.GetStartTs(),        TTL:             lockInfo.GetTtl(),        TxnSize:         lockInfo.GetTxnSize(),        LockType:        lockInfo.GetType(),        UseAsyncCommit:  lockInfo.GetUseAsyncCommit(),        LockForUpdateTS: lockInfo.GetForUpdateTs(),      }      ...        }    return mvccResp, nil  }  }
复制代码


use crate::storage::{    mvcc::MvccReader,    txn::{        commands::{find_mvcc_infos_by_key, Command, CommandExt, ReadCommand, TypedCommand},        ProcessResult, Result,    },    types::MvccInfo,    Snapshot, Statistics,};
复制代码


💡 总结一下上面的内容,key 的生成是在 TiDB Server 中完成的,然后 TiDB Server 拿到了 TSO 后 (startTs),会请求 TiKV 获取 MVCC 的信息


前面用大量篇幅说了 TiDB 的 key 生成和 TiKV 中 MVCC key,下面讲一下 2PC 流程。


2PC 流程会涉及到 乐观锁悲观锁,最主要的区别在于悲观锁会在用户发起 2PC 前 (执行阶段),请求 TiKV 获取并持久化一个悲观锁 (for updata ts),具体流程如下:


  1. 用户开启事务,TiDB 请求 PD 获取 start_ts

  2. 用户执行 SQL,通过start_ts 从 TiKV 获取数据,并写入 TiDB 内存

  3. 从 PD 获取当前 tso 作为当前锁的 for_update_ts,并将锁信息写入内存 (悲观锁)

  4. 使用 for_update_ts 并发地对所有涉及到的 Key 发起加悲观锁(acquire pessimistic lock)请求 (悲观锁)

  5. 用户发起提交,TiDB Server 开启两阶段提交

  6. prewrite:TiDB 请求 TiKV 将数据写入 MemTable,并写入 start_ts

  7. commit:TiDB 从 PD 获取 TSO,作为 commit_ts;请求 TiKV,并给写入数据添加 commit_ts


最后推荐一下 TiDB 社区的专栏文章 (TiDB 悲观锁实现原理 2021.06.21)


Region

前面我们讲到了相对微观的持久化的结构、事务等等,现在我们再从相对宏观的角度再来看整个 TiKV 集群。


TiDB 中每个表会对应多个 Region,Region 划分的规则是


  • Region 达到 96M 后,会另起一个 Region

  • 对于历史数据的写入,会导致原已

  • 达到 96M 的 Region 增大 / 减小

  • 如果 Region 增加到 144M 后,一个 Region 会分裂成两个 Region

  • 如果 Region 过小时,Region 会做合并


TiKV 集群中,每个 Region 至少会有 3 个副本 (1 个 leader 2 个 follower),TiDB 只和 leader 节点进行通信,leader 和 follower 之间是通过 Raft 协议同步的,所以和 Raft 协议类似:


  • leader 会定期 (heartbeat time internal) 向 follower 发起心跳,并将写日志同步到 follower

  • 如果一定时间内 (election timeout) follower 没收到心跳,会将自己的状态转为 candidate 候选人



leader 和 follower 之间的写日志同步是基于raft log 的,和 rocksdb kv 存放数据不同,rocksdb raft 仅存放 raft log,同步具体分为 3 步:


  1. Propose:当 Region leader 收到写请求的时候,leader 会将写请求变成 raft log

  2. Append:将 raft log 写入到本地的 rocksdb raft 进行持久化

  3. Replicate:leader 通过 raft 算法,将自己的日志复制到 follower,follower 收到日之后写入到自己本地的rocksdb raft ,最后 follower 将成功的消息返回给 leader

  4. Committed:当 leader 收到收到过半数发送的成功消息后,则完成 commit


这里的 commit 不是指 2PC 中的 commit 阶段完成,而只是 raft 日志复制完成,实际上 raft 复制这一步是在 2PC 第二部 commit 中的一个步骤,raft 日志复制完成后,2PC 的 commit 操作才能完成。

TiFlash

TiFlash 是负责列式存储的模块,主要的核心功能如下:


  • 异步复制:TiFlash 数据来源

  • 一致性:TiFlash 中的 MVCC 快照读

  • 智能选择:TiDB 自动选择 TiKV 或者 TiFlash 混用以提供最佳的查询速度,这也是 HTAP 的特性之一

  • 计算加速:这部分主要是算子下推的功能,之前在介绍 TiDB 的时候也有所涉及

异步复制

在对 TiDB 集群进行写入的过程中,并不会直接写 TiFlash,也不会像 TiKV 的 follower 节点那样在写入的时候通过日志复制保持强一致性。TiFlash 是作为 raft learner 角色对 TiKV 中的数据进行异步复制


步骤如下:


  1. 同一个集群内的 TiDB 会维护一个 TiFlash Replica Manager (注:此 TiFlash Replica Manager 功能在 21 年 12 月之前是放在 TiFlash 中的,之后迁到 TiDB Server 模块,参考11.18.2021 ddl: Move TiFlash cluster manager module into TiDB)

  2. 当 TiDB 感知到写操作时,会转化为 PD 的 Placement Rule

  3. PD 通过 Placement Rule 令 TiKV 分裂出制定 Key 范围的 Region,为其添加 Learner Peer 并异步调度到集群中的 TiFlash 节点



下面我们可以看一下部分代码,TiFlash Replica Manager 这部分主要在 TiDB 的pkg/domain/infosync/tiflash_manager.go 中,这里定义了 ManagerCtx 的结构


// 管理 PD 和 TiFlash 副本信息type TiFlashReplicaManagerCtx struct {  etcdCli              *clientv3.Client  sync.RWMutex         // protect tiflashProgressCache  tiflashProgressCache map[int64]float64  codec                tikv.Codec}
type TiFlashReplicaManager interface { ... // 维护 PlacementRule SetPlacementRule(ctx context.Context, rule placement.TiFlashRule) error DeletePlacementRule(ctx context.Context, group string, ruleID string) error GetGroupRules(ctx context.Context, group string) ([]placement.TiFlashRule, error) // 管理和获取 TiFlash 副本信息 GetStoresStat(ctx context.Context) (*pd.StoresInfo, error) CalculateTiFlashProgress(tableID int64, replicaCount uint64, TiFlashStores map[int64]pd.StoreInfo) (float64, error) UpdateTiFlashProgressCache(tableID int64, progress float64) GetTiFlashProgressFromCache(tableID int64) (float64, bool) DeleteTiFlashProgressFromCache(tableID int64) ...}
复制代码

一致性

我们知道 TiFlash 的数据是通过异步调度的,但 TiDB 是支持智能选择的,这就会产生一个问题:如何保证智能选择的时候保证 TiFlash 和 TiKV 的数据一致性?


在查询 TiFlash 的时候,TiFlash 会请求 TiKV leader 节点来确认最新的 raft log idx ,如果自己不是最新的会等待最新的 raft log 同步过来,以保证自己的数据和 TiKV 数据的一致性。具体的例子如下:


  • T0 时刻:写入数据到 TiKV Leader

  • T1 时刻:请求 TiFlash 获取数据

  • T2 时刻:TiFlash 请求 TiKV leader 获取最新的 raft log idx 并等待当前 TiFlash 中的 idx 追上,这个步骤也被称为 Learner Read

智能选择

TiDB 可以经由优化器自主选择行列,选择的逻辑是:优化器根据统计信息估算读取数据的规模,并对比列存和行存访问开销,然后做出最优选择。所以这部分是在 Cpomplie 阶段,即把 AST 语法树转换成SQL Plan 的时候完成的,对应的代码应该是 pkg/planner/core/optimizer.go ,这里就不具体展开了。


分功能梳理

我们在上面按服务模块挨个介绍了每个模块中的主要功能,下面这部分主要是想通过 DML 和 DDL 流程把上面的模块都穿起来。

DML - 读操作

  1. SQL 会被 TiDB Server 中的Protocol Layer 协议层接收

  2. TiDB Server 请求 PD 节点获取 TSO,标记为 start_ts 开始执行时间

  3. 进入Parse 解析模块,将 SQL 语句解析为 AST 语法树

  4. 将 AST 语法树交给Compile 模块,通过编译优化后,生成对应的SQL Plan 执行计划

  5. SQL Plan 进入Execute 模块,Execute 先去information schema 缓存中获取表的元数据信息。如果是简单的点查,会下发到 kv 模块;如果是相对复杂的任务,会下发到 DistSQL 模块,由它们具体调用 client 请求 TiKV

  6. 读请求到达 TiKV 后,会生成 snapshot 快照,进入UnifyRead Pool线程池

  7. TiKV 会从read pool中拿到快照,并请求 rocksdb kv 执行查询

DML - 写操作

前 4 步和读操作基本类似,这里我们直接从第 5 步开始


  1. Execute 模块去 TiKV 查询需要修改的数据,此步骤和读操作类似

  2. TiDB Server 拿到需要修改的原始数据后,将数据更新到 memBuffer 缓存;如果此时发生写冲突,需要请求 PD 获取 for_update_ts 时间戳 (悲观锁)

  3. 用户发起提交后,调用 Transaction 模块发起两阶段提交 (2PC),写操作都是Transaction 通过KV模块调用 TiKV

  4. 写操作会先发送给 TiKVScheduler模块,它负责负责协调事务并发写入冲突,并将收到的写请求向下传递给Raftsotre

  5. Raftsotre 模块拿到请求后会生成 raft log ,将raft log写入本地 (leader) 的 rocksdb raft 后,同步给 follower

  6. 当过半 follower 将raft log写入本地rocksdb raft 并返回成功后,调用 Apply 模块将数据持久化到 rocksdb kv ,成功后返回 TiDB Server,至此 2PC 的第一步完成

  7. TiDB Server 再次请求 PD 获取 commit_ts 提交的 TSO,再次调用 TiKV 写入commit_ts

  8. 最终将提交成功返回给用户


DDL

之前在 TiDB 的online DDL 我们讲了相关模块,这里我们主要按照流程步骤描述一下:


  1. 用户给任一的 TiDB Server 发送 DDL 请求

  2. 经过解析、编译、优化之后调用start job

  3. 如果当前的 TiDB Server 是 Owner 节点则直接调用workers 执行

  4. 否则将 DDL 任务持久化到 TiKVjob queue(物理 DDL) 或 add index queue(逻辑 DDL)。TiDB Server Owner 节点的works 模块从 TiKV 中的 queue 获取任务并执行

  5. 执行完毕后,将历史任务持久化到 TiKV 的 history queue



但是这种方式有几个问题:


  • TiDB Server 集群中的 Owner 节点只有一个,影响执行效率

  • job queue或者add index queue 是先进先出,DDL 任务会存在积压的情况

  • 对于互相不影响的 DDL 任务,在执行的时候依然会被阻塞


所以在 TiDB v6.2 版本之后,推出了并发 DDL 的框架


首先针对 Owner 节点添加了判断:


  • 涉及一张表的 DDL 相互阻塞

  • DROP DATABASE 和 数据库内所有 DDL 互相阻塞

  • 逻辑 DDL 需要等待之前正在执行的逻辑 DDL 执行完才能执行 (逻辑 DDL 本身执行较快不怎么消耗资源)


在以上的限制后,进行了如下的升级:


  • 集群中可以有多个 Owner 并行执行执行 DDL 任务

  • DDL Job 不再是先进先出,而是通过选择可执行的 Job 集合


发布于: 刚刚阅读数: 4
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
TiDB知识点梳理 (PCTA 笔记分享)_TiDB 底层架构_TiDB 社区干货传送门_InfoQ写作社区