TiDB 写入慢流程排查系列(三)— TiDB Server 写入流程

4. 生成 + 运行 Executor

TiDB 会将 plan 转换成 executor,运行时会将所有的物理 executor 构造一个树状结构,每一层通过调用下一层获取结果。

监控说明

  • 位置:TiDB – Executor – Execution Duration 面板
    executor duration 是指 parse 结束之后到执行出结果的时间,即从优化 SQL 到 tidb 计算出结果这个时间,这个时间长可能是 tikv 数据到 tidb 汇聚计算时间比较长

4.1.获取 Key 所在的 Region 和 TSO

TiDB 通过向 PD 发送请求实现 region 的定位以及获取 TSO。

Region Cache

在 TiDB 中每个 kv 请求都需要根据 Key 定位到能处理该 Key 请求所在的 Store 地址, 而 Key 到 Store 的映射信息实际存分散存在于每个 kv 中且集中收集于 PD 中, 理论上对于每个请求 TiDB 可以通过每次向 PD 查询 Key 对应哪个 Store 的信息, 但出于性能考虑在 TiDB 侧我们维护了一个内存 Cache 来缓存映射信息避免重复的查询。TiDB 会先访问 pd 获取 tso,然后访问 tidb-server 本地 Region Cache,然后按照获得的路由信息,将请求发给 TiKV,如果 TiKV 返回错误说明路由信息过旧,这个时候 tidb-server 会去 pd 重新取 region 的最新路由信息,并更新 region cache。如果,请求发送到 follower 了,TiKV 会返回 not leader 的错误并把谁是 leader 的信息返回给 tidb-server,tidb-server 会进行重试,根据返回的 leader 信息到所在的 TiKV 读取数据。 然后更新 Region Cache。

获取 TSO

所有 TiDB 与 PD 交互的逻辑都是通过一个 PD Client 的对象进行的,这个对象会在服务器启动时创建 Store 的时候创建出来,创建之后会开启一个新线程,专门负责批量从 PD 获取 TSO。对于这些 TSO 请求,分为三个阶段:

  • 将一个 TSO 请求放入 channel,对应的函数为 GetTSAsync,调用这个函数会得到一个 tsFuture 对象
  • TSO 线程从 channel 拿到请求向 PD 发起 RPC 请求,获取 TSO,获取到 TSO 之后分配给相应的请求
  • TSO 分配给相应的请求之后,就可以通过持有的 tsFuture 获取 TSO(调用 tsFuture.Wait())
监控说明

目前系统没有对第一个阶段设置相应的监控信息,这个过程通常很快,除非 channel 满了,否则 GetTSAsync 函数很快返回,而 channel 满表示 RPC 请求延迟可能过高,所以可以通过 RPC 请求的 Duration 来进一步分析。

第二阶段中 TiDB 向 PD 发起 RPC 请求的 Duration 对应的监控项是 PD TSO RPC Duration。

  • 位置:TiDB – PD Client – PD TSO RPC Duration 面板
  • PD TSO RPC Duration : 反应向 PD 发起 RPC 请求的耗时,如果 duration 比较高,可能的原因如下:
    ➢ TiDB 和 PD 之间的网络延迟高,可以通过 Blackbox exporter Dashboard 下面的 Network Status - Ping Latency 监控项确认。
    ➢ PD 负载太高,不能及时处理 TSO 的 RPC 请求,可以通过 PD Dashboard 下的 TiDB - handle requests duration 监控项确认。

第一阶段拿到 tsFuture 对象后到第三阶段调用 tsFuture.Wait() 这段过程的 Duration 对应的监控项是 TSO Async Duration;拿到 tsFuture 之后还需要进行 SQL Parse 和 Compile 成执行计划,真正执行的时候才会调用 tsFuture.Wait()

  • 位置:TiDB – PD Client – PD Client CMD Duration (wait)
  • TSO Async Duration:从获取 ts future,到开始 wait ts future 的耗时,如果 duration 比较高,可能的原因如下:
    ➢ 这个 SQL 很复杂,Parse 花费了很长的时间
    ➢ Compile 花费了很长时间

如果 Parser 和 Compile 过程很快完成了,第三阶段中调用 tsFuture.Wait() 时 PD 的 TSO RPC 还没有返回,就需要等待,这段等待时间对应的监控项是 PD TSO Wait Duration

  • 位置:TiDB – PD Client – PD TSO Wait Duration 面板
  • PD TSO Wait Duration:调用 wait ts future 之后等待 future 返回的耗时,受网络和 runtime 影响

注意:读取只向 PD拿一次 TSO 就够了。写入的话,需要拿两次,因为写流程两阶段提交,需要取两次 tso,包括:start_ts + commit_ts。并且暂时不会支持通过 start_ts 计算出 commit_ts 减少拿的次数。所以在压测场景下,写入场景一般会比读场景的 PD TSO Wait Duration 的指标值高很多。

DistSQL API

请求的分发与汇总会有很多复杂的处理逻辑,比如上小节说的出错重试、获取路由信息、控制并发度以及结果返回顺序,为了避免这些复杂的逻辑与 SQL 层耦合在一起,TiDB 抽象了一个统一的分布式查询接口,称为 DistSQL API,位于 distsql 这个包中。
DistSQL 是位于 SQL 层和 Coprocessor 之间的一层抽象,它把下层的 Coprocessor 请求封装起来对上层提供一个简单的 Select 方法。执行一个单表的计算任务。最上层的 SQL 语句可能会包含 JOIN,SUBQUERY 等复杂算子,涉及很多的表,而 DistSQL 只涉及到单个表的数据。一个 DistSQL 请求会涉及到多个 Region,我们要对涉及到的每一个 Region 执行一次 Coprocessor 请求。

监控说明

  • 位置:TiDB – DistSQL 面板
    ➢ Distsql Duration:Distsql 处理的时长
    ➢ Distsql QPS:Distsql 的数量统计
    ➢ Distsql Partial QPS:每秒 Partial Results 的数量
    ➢ Scan Keys Num:每个 Query 扫描的 Key 的数量
    ➢ Scan Keys Partial Num:每一个 Partial Result 扫描的 Key 的数量
    ➢ Partial Num:每个 SQL 语句 Partial Results 的数量

  • 位置:TiDB – KV Request 面板 & Overview - TiDB 面板
    ➢ KV Duration(KV Request Duration 999 by store/type、KV Cmd Duration 99/999)
    KV Request Duration 999 by store:KV Request 执行时间,根据 TiKV 显示
    KV Request Duration 999 by type:KV Request 执行时间,根据请求类型显示
    KV Cmd Duration 99/999:KV 命令执行的时间

  • KV Count
    ➢KV Cmd OPS(Overview - TiDB 面板):KV 命令执行数量统计
    ➢KV Txn OPS(TiDB - Transaction 面板):启动事务的数量统计
    ➢Txn Regions Num 90(TiDB - Transaction 面板):事务使用的 Region 数量统计
    ➢Txn Max Write Size Bytes 100:事务写入的字节数统计
    ➢Txn Max Write KV Num 100:事务写入的 KV 数量统计
    ➢Load SafePoint OPS(TiDB - Transaction 面板):更新 SafePoint 的数量统计





4.3 executor 执行写入

输入的 SQL 会经过 parse,compile 等过程最后变成一个 Executor
最终会对应于 InsertExec / UpdateExec / DeleteExec 等
以 insert 写入为例,Insert Executor 会通过调用 Table 接口写入数据,Table 接口写入有以下作用:

  • 将 Row 转化成 Key-value
  • 维护数据和索引的一致性
  • 保障 DDL 变更过程中的 schema 可见性
    在将 Row 转化成 Key-value 的过程中,会通过 Add/Update/Remove 等接口函数调用 tablecodec 编码规则将数据写入类似 Key-value 的容器 unionstore,包括 snapshot + membuffer 两部分。此时数据的所有修改在未提交之前都缓存在内存中,那么在读取的时候,需要先读取缓存,再去读快照。

大事务

在 4.0 之前的版本,由于 TiDB 两阶段提交的要求,修改数据的单个事务过大时会存在以下问题:

  • 客户端在提交之前,数据都写在内存中,而数据量过多时易导致 OOM (Out of Memory) 错误。
  • 在第一阶段写入数据耗时增加,与其他事务出现写冲突的概率会指数级增长。
  • 最终导致事务完成提交的耗时增加。
    因此,TiDB 对事务做了一些限制:
  • 单个事务包含的 SQL 语句不超过 5000 条(默认)
  • 每个键值对不超过 6 MB
  • 键值对的总数不超过 300000
  • 键值对的总大小不超过 100 MB
    为了使性能达到最优,建议每 100~500 行写入一个事务。
    在 4.0 之后的版本开始支持大事务,取消了单个事务的键值对的总数量不超过 30 万条的限制,同时之前默认事务的 100MB 大小可以通过 txn-total-size-limit 进行修改,最大支持 10 GB。即 4.0 之后事务大小限制为:
  • 每个键值对不超过 6 MB
  • txn-total-size-limit 控制键值对总大小,最大 10G
    需要注意的是,由于在提交前事务的数据修改还是缓存在内存里,因此实际的单个事务大小限制还取决于服务器剩余可用内存的大小,执行事务时 TiDB 进程的内存消耗大约是事务大小的 6 倍以上。

悲观锁

如果使用悲观事务模型,在悲观锁加锁的过程中也有可能会因为重试和锁等待而引起写入变慢的情况。
悲观事务在乐观事务基础上实现,在 Prewrite 之前增加了 Acquire Pessimistic Lock 阶段:

  • 每个 DML 都会加悲观锁,锁写到 TiKV 里,同样会通过 raft 同步。
  • 悲观事务在加悲观锁时检查各种约束,如 Write Conflict、key 唯一性约束等。
  • 悲观锁不包含数据,只有锁,只用于防止其他事务修改相同的 key,不会阻塞读,但 Prewrite 后会阻塞读(和 Percolator 相同)。
  • 提交时同 Percolator,悲观锁的存在保证了 Prewrite 不会发生 Write Conflict,保证了提交一定成功。

悲观事务会用 for update ts 读和检查 Write Conflict,效果等同于可更新的 start ts,当悲观事务执行 DML 发现 Write Conflict 时,就会更新 for update ts 再重试这个 DML。

悲观事务遇到锁时要等待锁释放,为了高效的实现锁等待,每个 TiKV 都有 Waiter Manager。当 Acquire Pessimistic Lock 遇到 KeyIsLocked 时,会在 Waiter Manager 里等锁释放(commit/rollback)。悲观锁支持 innodb_lock_wait_timeout,默认单条 DML 等锁 50s 会报等锁超时错误。

悲观事务相关监控:

  • 位置:TiDB – Transcation 面板
    ➢ Duration:事务执行的时间
    ➢ Statement Lock Keys:单个语句的加锁个数
    ➢ Acquire Pessimistic Locks Duration:加锁所消耗的时间
    ➢ Pessimistic Statement Retry OPS:悲观语句重试次数统计。当语句尝试加锁时,可能遇到写入冲突,此时,语句会重新获取新的 snapshot 并再次加锁

事务提交:

在所有的事务修改完成之后会进行事务的提交,在 TiDB 中事务提交为两阶段提交,详细介绍如下:

通过前面的处理,对于写入算子 Insert / Update / Delete 在执行过程中将待写入的数据编码为 Key-Value 并先写入事务的 In-Memory Buffer,写入的数据包含两类:

  1. 普通数据插入
  2. 索引数据插入

在事务缓存中,普通数据和索引数据使用不同的编码方式转换 Key 和 Value,统一理解为 Key-Value 即可。两阶段提交的本质就是将 In-Memory Buffer 中的 Key-Value 通过 tikvclient 写入到 TiKV 中。

两阶段提交顾名思义就是将事务的提交分成两个阶段:

1、Prewrite

2、Commit

在每个 Transaction 开启时会获取一个 TSO 作为 start_ts,在 Prewrite 成功后 Commit 前获取 TSO 作为 commit_ts,如下图:

事务相关监控

  • Transaction 面板
    ➢ Transaction OPS:事务执行数量统计
    ➢ Duration:事务执行的时间(如果启用 Latches,则包含 Latches 的时间)
    ➢ Transaction Retry Num:事务重试次数
    ➢ Transaction Statement Num:一个事务中的 SQL 语句数量
    ➢ Session Retry Error OPS:事务重试时遇到的错误数量

  • KV Count 面板
    ➢ KV Txn OPS:启动事务的数量统计
    ➢ Txn Regions Num 90:事务使用的 Region 数量统计
    ➢ Txn Write Size Bytes 100:事务写入的字节数统计
    ➢ Txn Write KV Num 100:事务写入的 KV 数量统计

  • KV Errors
    ➢ KV Retry Duration:KV 重试请求的时间
    ➢ TiClient Region Error OPS:TiKV 返回 Region 相关错误信息的数量
    ➢ KV Backoff OPS:TiKV 返回错误信息的数量(事务冲突等)
    ➢ Lock Resolve OPS:事务冲突相关的数量

可能出现的错误

发送请求过程中,根据 RegionCache 中的 Region 信息切分,但是 Region 信息可能已经过期,包括

Region 发生了分裂

Region 发生了合并

Region 发生了调度

一个请求中的部分 Key 上可能有锁

其他错误

错误是如何处理的?

如果是 Region 相关的错误,会先进行 Backoff,然后进行重试,比如:

Region 信息过期会使用新的 Region 信息重试任务

Region 的 leader 切换,会把请求发送给新的 Leader

如果部分 Key 上有锁,会进行 Resolve lock

如果有其他错误,则立即向上层返回错误,中断请求

错误相关的监控有哪些?

KV Errors

KV Retry Duration:KV 重试请求的时间

TiClient Region Error OPS:TiKV 返回 Region 相关错误信息的数量

KV Backoff OPS(TiDB - KV Errors):TiKV 返回错误信息的数量(事务冲突等)

Lock Resolve OPS:事务冲突相关的数量

Other Errors OPS:其他类型的错误数量,包括清锁和更新 SafePoint

一些常见问题补充:

  1. Backoff 是什么意思?→ Backoff 就是本次请求失败了,sleep 一小段时间再进行重试

  2. Backoff sleep 的时间长短如何确定?→ TiDB 中的 Backoff 对于不同的请求以及错误类型使用不同的 Backoff 算法,具体可以参考文档1 参考文档2

  3. Backoff 会一直进行吗?→ 不会

  4. Backoff 什么时候终止?→ 每一次失败重试之前都会 sleep 一段时间,Backoff 终止的依据是多次重试的 sleep 时间总和大于阈值

  5. Backoff 的阈值如何确定的?→ 目前是写死在代码之中,参考以下列表,单位毫秒

  6. copBuildTaskMaxBackoff = 5000

  7. tsoMaxBackoff = 15000

  8. scannerNextMaxBackoff = 20000

  9. batchGetMaxBackoff = 20000

  10. copNextMaxBackoff = 20000

  11. getMaxBackoff = 20000

  12. prewriteMaxBackoff = 20000

  13. cleanupMaxBackoff = 20000

  14. GcOneRegionMaxBackoff = 20000

  15. GcResolveLockMaxBackoff = 100000

  16. deleteRangeOneRegionMaxBackoff = 100000

  17. rawkvMaxBackoff = 20000

  18. splitRegionBackoff = 20000

  19. maxSplitRegionsBackoff = 120000

  20. scatterRegionBackoff = 20000

  21. waitScatterRegionFinishBackoff = 120000

  22. locateRegionMaxBackoff = 20000

  23. pessimisticLockMaxBackoff = 10000

  24. pessimisticRollbackMaxBackoff = 10000

  25. Region Error 和 Lock 可以通过监控排查,其他错误怎排查?→ 在日志中搜索 “other error”

举一个简单的例子:

一个请求发送到 TiKV 的超时时间是 60s,如果返回了错误之后每次 backoff 1s,总的 Backoff 时间是 5s,那么这种情况可以重试 5 次。考虑一种情况:前四次请求失败,且每次失败都是可重试错误,最后一次请求成功,每次请求返回时间都是 50s,那么这个请求的总的时间为 50 * 4 + 1 * 4 + 50。

这个例子想要说明的问题是什么呢?如果发现 Duration 耗时很长,不但需要去看 Backoff 的监控,还需要去查看单次 KV Duration 的耗时,已经 KV Errors 的监控进一步排查问题。

2 个赞