延迟的拆解分析

本文将 TiDB 中 SQL 语句的延迟拆解成各项监控指标,并从用户角度对指标进行分析,包括:

这些分析可以让你深入了解 TiDB 在执行 SQL 查询时的耗时情况,有助于诊断 TiDB 关键运行路径的问题。除了延迟的指标拆解之外,诊断场景小节介绍了如何在真实场景中分析延迟。

建议在阅读本文前,先阅读 TiDB 性能分析和优化方法。需要注意的是,在将延迟拆解成监控指标时,延迟的耗时数值采用的是均值,而非某几个特定慢查询对应的数值。许多指标都会以直方图的形式展示,以更好地展现耗时或者延迟的分布情况。你需要按如下公式使用总和 (sum) 及数量 (count) 来计算均值 (avg)。

  1. avg = ${metric_name}_sum / ${metric_name}_count

本文介绍的监控指标可以从 TiDB 的 Prometheus 管理界面中查询到。

通用 SQL 层

通用 SQL 层带来的延迟存在于 TiDB 的最顶部,所有 SQL 查询都具有这一部分的延迟。下面是通用 SQL 层操作的时间消耗图:

延迟的拆解分析 - 图1

  1. Diagram(
  2. NonTerminal("Token wait duration"),
  3. Choice(
  4. 0,
  5. Comment("Prepared statement"),
  6. NonTerminal("Parse duration"),
  7. ),
  8. OneOrMore(
  9. Sequence(
  10. Choice(
  11. 0,
  12. NonTerminal("Optimize prepared plan duration"),
  13. Sequence(
  14. Comment("Plan cache miss"),
  15. NonTerminal("Compile duration"),
  16. ),
  17. ),
  18. NonTerminal("TSO wait duration"),
  19. NonTerminal("Execution duration"),
  20. ),
  21. Comment("Retry"),
  22. ),
  23. )

通用 SQL 层的延迟可以使用 e2e duration 指标观察。它的计算方式是:

  1. e2e duration =
  2. tidb_server_get_token_duration_seconds +
  3. tidb_session_parse_duration_seconds +
  4. tidb_session_compile_duration_seconds +
  5. tidb_session_execute_duration_seconds{type="general"}
  • tidb_server_get_token_duration_seconds 代表令牌 (Token) 等待耗时,它通常小于 1 微秒,因而足以被忽略。
  • tidb_session_parse_duration_seconds 代表把 SQL 查询解析成抽象语法树 (AST, Abstract Syntax Tree) 的耗时。要跳过这部分的耗时,可以使用 PREPARE/EXECUTE 语句
  • tidb_session_compile_duration_seconds 代表把抽象语法树编译成执行计划的耗时。要跳过这部分的耗时,可以使用执行计划缓存
  • tidb_session_execute_duration_seconds{type="general"} 代表执行各种不同用户查询的耗时。这部分的耗时需要进行细粒度的拆解,以用来分析性能问题或瓶颈。

通常来说,OLTP (Online Transactional Processing) 工作负载可以分为读请求和写请求两类。下面的小节会分别对读请求写请求进行介绍。这两类请求共享了一些关键代码,但执行方式是不一样的。

读请求

读请求只有一种处理形式。

点查 (Point Get)

下面是点查操作的时间消耗图:

延迟的拆解分析 - 图2

  1. Diagram(
  2. Choice(
  3. 0,
  4. NonTerminal("Resolve TSO"),
  5. Comment("Read by clustered PK in auto-commit-txn mode or snapshot read"),
  6. ),
  7. Choice(
  8. 0,
  9. NonTerminal("Read handle by index key"),
  10. Comment("Read by clustered PK, encode handle by key"),
  11. ),
  12. NonTerminal("Read value by handle"),
  13. )

在点查过程中,tidb_session_execute_duration_seconds{type="general"} 使用如下方式计算:

  1. tidb_session_execute_duration_seconds{type="general"} =
  2. pd_client_cmd_handle_cmds_duration_seconds{type="wait"} +
  3. read handle duration +
  4. read value duration

pd_client_cmd_handle_cmds_duration_seconds{type="wait"} 代表从 PD 中读取 TSO 的耗时。在 auto-commit 事务模式下从聚簇索引主键或者快照中读取时,该数值为 0。

read handle durationread value duration 使用如下方式计算:

  1. read handle duration = read value duration =
  2. tidb_tikvclient_txn_cmd_duration_seconds{type="get"} =
  3. send request duration =
  4. tidb_tikvclient_request_seconds{type="Get"} =
  5. tidb_tikvclient_batch_wait_duration +
  6. tidb_tikvclient_batch_send_latency +
  7. tikv_grpc_msg_duration_seconds{type="kv_get"} +
  8. tidb_tikvclient_rpc_net_latency_seconds{store="?"}

tidb_tikvclient_request_seconds{type="Get"} 代表通过 gRPC 发往 TiKV 的批量 get 请求耗时。关于 tidb_tikvclient_batch_wait_durationtidb_tikvclient_batch_send_latencytidb_tikvclient_rpc_net_latency_seconds{store="?"} 等批量请求客户端的耗时计算方式,请参考批量请求小节。

tikv_grpc_msg_duration_seconds{type="kv_get"} 使用如下方式计算:

  1. tikv_grpc_msg_duration_seconds{type="kv_get"} =
  2. tikv_storage_engine_async_request_duration_seconds{type="snapshot"} +
  3. tikv_engine_seek_micro_seconds{type="seek_average"} +
  4. read value duration +
  5. read value duration(non-short value)

此时,请求已经到达 TiKV。TiKV 在处理 get 请求时,会进行一次 seek 和一到两次 read 操作。其中,短数据的键和值被编码在一个 write CF 中,因而只需要进行一次 read 操作。TiKV 在处理 get 请求前会先获取一个快照。关于 TiKV 快照耗时的计算方式,请参考 TiKV 快照小节。

read value duration(from disk) 使用如下方式计算:

  1. read value duration(from disk) =
  2. sum(rate(tikv_storage_rocksdb_perf{metric="block_read_time",req="get/batch_get_command"})) / sum(rate(tikv_storage_rocksdb_perf{metric="block_read_count",req="get/batch_get_command"}))

TiKV 采用 RocksDB 作为存储引擎。如果 block cache 中找不到要求的值,TiKV 需要从磁盘中读取。对于 tikv_storage_rocksdb_perf,get 请求可以是 get 或者 batch_get_command

批量点查 (Batch Point Get)

下面是批量点查操作的时间消耗图:

延迟的拆解分析 - 图3

  1. Diagram(
  2. NonTerminal("Resolve TSO"),
  3. Choice(
  4. 0,
  5. NonTerminal("Read all handles by index keys"),
  6. Comment("Read by clustered PK, encode handle by keys"),
  7. ),
  8. NonTerminal("Read values by handles"),
  9. )

在进行批量点查时,tidb_session_execute_duration_seconds{type="general"} 使用如下方式计算:

  1. tidb_session_execute_duration_seconds{type="general"} =
  2. pd_client_cmd_handle_cmds_duration_seconds{type="wait"} +
  3. read handles duration +
  4. read values duration

批量点查的过程几乎与点查一致。不同的是,批量点查会同时得到多个值。

read handles durationread values duration 使用如下方式计算:

  1. read handles duration = read values duration =
  2. tidb_tikvclient_txn_cmd_duration_seconds{type="batch_get"} =
  3. send request duration =
  4. tidb_tikvclient_request_seconds{type="BatchGet"} =
  5. tidb_tikvclient_batch_wait_duration(transaction) +
  6. tidb_tikvclient_batch_send_latency(transaction) +
  7. tikv_grpc_msg_duration_seconds{type="kv_batch_get"} +
  8. tidb_tikvclient_rpc_net_latency_seconds{store="?"}(transaction)

关于 tidb_tikvclient_batch_wait_durationtidb_tikvclient_batch_send_latencytidb_tikvclient_rpc_net_latency_seconds{store="?"} 等批量请求客户端耗时的计算方式,请参考批量请求小节。

耗时 tikv_grpc_msg_duration_seconds{type="kv_batch_get"} 使用如下方式计算:

  1. tikv_grpc_msg_duration_seconds{type="kv_batch_get"} =
  2. tikv_storage_engine_async_request_duration_seconds{type="snapshot"} +
  3. n * (
  4. tikv_engine_seek_micro_seconds{type="seek_max"} +
  5. read value duration +
  6. read value duration(non-short value)
  7. )
  8. read value duration(from disk) =
  9. sum(rate(tikv_storage_rocksdb_perf{metric="block_read_time",req="batch_get"})) / sum(rate(tikv_storage_rocksdb_perf{metric="block_read_count",req="batch_get"}))

TiKV 会首先得到一个快照,然后从同一个快照中读取多个值。read 操作的耗时和点查中的一致。当从磁盘中读取数据时,其平均耗时可以通过带有 req="batch_get" 属性的 tikv_storage_rocksdb_perf 来计算。

表扫描和索引扫描 (Table Scan 和 Index Scan)

下面是表扫描和索引扫描的时间消耗图:

延迟的拆解分析 - 图4

  1. Diagram(
  2. Stack(
  3. NonTerminal("Resolve TSO"),
  4. NonTerminal("Load region cache for related table/index ranges"),
  5. OneOrMore(
  6. NonTerminal("Wait for result"),
  7. Comment("Next loop: drain the result"),
  8. ),
  9. ),
  10. )

在进行表扫描和索引扫描时,耗时 tidb_session_execute_duration_seconds{type="general"} 使用如下方式计算:

  1. tidb_session_execute_duration_seconds{type="general"} =
  2. pd_client_cmd_handle_cmds_duration_seconds{type="wait"} +
  3. req_per_copr * (
  4. tidb_distsql_handle_query_duration_seconds{sql_type="general"}
  5. )
  6. tidb_distsql_handle_query_duration_seconds{sql_type="general"} <= send request duration

表扫描和索引扫描采用相同的方式处理。req_per_copr 是被分配的任务数量。由于执行协处理器和返回数据在不同的线程中运行,tidb_distsql_handle_query_duration_seconds{sql_type="general"} 是等待时间,并且小于 send request duration

send request durationreq_per_copr 使用如下方式计算:

  1. send request duration =
  2. tidb_tikvclient_batch_wait_duration +
  3. tidb_tikvclient_batch_send_latency +
  4. tikv_grpc_msg_duration_seconds{type="coprocessor"} +
  5. tidb_tikvclient_rpc_net_latency_seconds{store="?"}
  6. tikv_grpc_msg_duration_seconds{type="coprocessor"} =
  7. tikv_coprocessor_request_wait_seconds{type="snapshot"} +
  8. tikv_coprocessor_request_wait_seconds{type="schedule"} +
  9. tikv_coprocessor_request_handler_build_seconds{type="index/select"} +
  10. tikv_coprocessor_request_handle_seconds{type="index/select"}
  11. req_per_copr = rate(tidb_distsql_handle_query_duration_seconds_count) / rate(tidb_distsql_scan_keys_partial_num_count)

在 TiKV 中,表扫描的类型是 select,而索引扫描的类型是 indexselectindex 类型的内部耗时是一致的。

索引回表 (Index Look Up)

下面是通过索引回表操作的时间消耗图:

延迟的拆解分析 - 图5

  1. Diagram(
  2. Stack(
  3. NonTerminal("Resolve TSO"),
  4. NonTerminal("Load region cache for related index ranges"),
  5. OneOrMore(
  6. Sequence(
  7. NonTerminal("Wait for index scan result"),
  8. NonTerminal("Wait for table scan result"),
  9. ),
  10. Comment("Next loop: drain the result"),
  11. ),
  12. ),
  13. )

在通过索引回表的过程中,耗时 tidb_session_execute_duration_seconds{type="general"} 使用如下方式计算:

  1. tidb_session_execute_duration_seconds{type="general"} =
  2. pd_client_cmd_handle_cmds_duration_seconds{type="wait"} +
  3. req_per_copr * (
  4. tidb_distsql_handle_query_duration_seconds{sql_type="general"}
  5. ) +
  6. req_per_copr * (
  7. tidb_distsql_handle_query_duration_seconds{sql_type="general"}
  8. )
  9. req_per_copr = rate(tidb_distsql_handle_query_duration_seconds_count) / rate(tidb_distsql_scan_keys_partial_num_count)

一次通过索引回表的过程结合了索引查找和表查找,其中索引查找和表查找按流水线方式处理。

写请求

写请求有多个变种,因而比读请求复杂得多。下面是写请求的时间消耗图:

延迟的拆解分析 - 图6

  1. Diagram(
  2. NonTerminal("Execute write query"),
  3. Choice(
  4. 0,
  5. NonTerminal("Pessimistic lock keys"),
  6. Comment("bypass in optimistic transaction"),
  7. ),
  8. Choice(
  9. 0,
  10. NonTerminal("Auto Commit Transaction"),
  11. Comment("bypass in non-auto-commit or explicit transaction"),
  12. ),
  13. )
悲观事务乐观事务
Auto-commit执行 + 加锁 + 提交执行 + 提交
非 auto-commit执行 + 加锁执行

一次写请求可以被分解成以下三个阶段:

  • 执行阶段:执行并把更改写入 TiDB 的内存中
  • 加锁阶段:获取执行结果的悲观锁
  • 提交阶段:通过两阶段提交协议 (2PC) 来提交事务

在执行阶段,TiDB 在内存中修改数据,其延迟主要源自读取所需的数据。对于更新和删除查询,TiDB 先从 TiKV 读取数据,再更新或删除内存中的数据。

带有点查和批量点查的加锁时读取操作 (SELECT FOR UPDATE) 是一个例外。该操作会在单个 RPC (Remote Procedure Call) 请求中完成读取和加锁操作。

加锁时点查

下面是加锁时点查的时间消耗图:

延迟的拆解分析 - 图7

  1. Diagram(
  2. Choice(
  3. 0,
  4. Sequence(
  5. NonTerminal("Read handle key by index key"),
  6. NonTerminal("Lock index key"),
  7. ),
  8. Comment("Clustered index"),
  9. ),
  10. NonTerminal("Lock handle key"),
  11. NonTerminal("Read value from pessimistic lock cache"),
  12. )

在加锁时点查过程中,耗时 execution(clustered PK)execution(non-clustered PK or UK) 使用如下方式计算:

  1. execution(clustered PK) =
  2. tidb_tikvclient_txn_cmd_duration_seconds{type="lock_keys"}
  3. execution(non-clustered PK or UK) =
  4. 2 * tidb_tikvclient_txn_cmd_duration_seconds{type="lock_keys"}

加锁时点查会锁定键并获取其对应的值。相比先执行再获取锁的方式,该操作可以节省一次来回通信。加锁时点查的耗时可以看作与加锁耗时是一样的。

加锁时批量点查

下面是加锁时批量点查的时间消耗图:

延迟的拆解分析 - 图8

  1. Diagram(
  2. Choice(
  3. 0,
  4. NonTerminal("Read handle keys by index keys"),
  5. Comment("Clustered index"),
  6. ),
  7. NonTerminal("Lock index and handle keys"),
  8. NonTerminal("Read values from pessimistic lock cache"),
  9. )

在加锁时批量点查过程中,耗时 execution(clustered PK)execution(non-clustered PK or UK) 使用如下方式计算:

  1. execution(clustered PK) =
  2. tidb_tikvclient_txn_cmd_duration_seconds{type="lock_keys"}
  3. execution(non-clustered PK or UK) =
  4. tidb_tikvclient_txn_cmd_duration_seconds{type="batch_get"} +
  5. tidb_tikvclient_txn_cmd_duration_seconds{type="lock_keys"}

加锁时批量点查的执行过程与加锁时点查相似。不同的点在于,加锁时批量点查会在单个 RPC 请求中读取多个值。关于 tidb_tikvclient_txn_cmd_duration_seconds{type="batch_get"} 耗时的计算方式,请参考批量点查小节。

加锁阶段

本小节介绍加锁阶段的耗时。

  1. round = ceil(
  2. sum(rate(tidb_tikvclient_txn_regions_num_sum{type="2pc_pessimistic_lock"})) /
  3. sum(rate(tidb_tikvclient_txn_regions_num_count{type="2pc_pessimistic_lock"})) /
  4. committer-concurrency
  5. )
  6. lock = tidb_tikvclient_txn_cmd_duration_seconds{type="lock_keys"} =
  7. round * tidb_tikvclient_request_seconds{type="PessimisticLock"}

锁是按照两阶段锁的结构来获取的,带有流量控制机制。流量控制会按照 committer-concurrency(默认值为 128)来限制并发在线请求的数量。为了简单说明,流量控制可以看作是请求延迟 (round) 的倍增。

tidb_tikvclient_request_seconds{type="PessimisticLock"} 使用如下方式计算:

  1. tidb_tikvclient_request_seconds{type="PessimisticLock"} =
  2. tidb_tikvclient_batch_wait_duration +
  3. tidb_tikvclient_batch_send_latency +
  4. tikv_grpc_msg_duration_seconds{type="kv_pessimistic_lock"} +
  5. tidb_tikvclient_rpc_net_latency_seconds{store="?"}

关于 tidb_tikvclient_batch_wait_durationtidb_tikvclient_batch_send_latencytidb_tikvclient_rpc_net_latency_seconds{store="?"} 等批量请求客户端耗时的计算方式,请参考批量请求小节。

耗时 tikv_grpc_msg_duration_seconds{type="kv_pessimistic_lock"} 使用如下方式计算:

  1. tikv_grpc_msg_duration_seconds{type="kv_pessimistic_lock"} =
  2. tikv_scheduler_latch_wait_duration_seconds{type="acquire_pessimistic_lock"} +
  3. tikv_storage_engine_async_request_duration_seconds{type="snapshot"} +
  4. (lock in-mem key count + lock on-disk key count) * lock read duration +
  5. lock on-disk key count / (lock in-mem key count + lock on-disk key count) *
  6. lock write duration
  • 自 TiDB v6.0 起,TiKV 默认使用内存悲观锁。内存悲观锁会跳过异步写入的过程。

  • tikv_storage_engine_async_request_duration_seconds{type="snapshot"}是快照类型耗时,详情请参考 TiKV 快照小节.

  • lock in-mem key countlock on-disk key count 使用如下方式计算:

    1. lock in-mem key count =
    2. sum(rate(tikv_in_memory_pessimistic_locking{result="success"})) /
    3. sum(rate(tikv_grpc_msg_duration_seconds_count{type="kv_pessimistic_lock"}}))
    4. lock on-disk key count =
    5. sum(rate(tikv_in_memory_pessimistic_locking{result="full"})) /
    6. sum(rate(tikv_grpc_msg_duration_seconds_count{type="kv_pessimistic_lock"}}))

    内存和磁盘中被加锁的键数量可以从内存锁计数中计算得出。TiKV 在得到锁之前会读取键对应的值,其读取耗时可以从 RocksDB performance context 中计算得出:

    1. lock read duration(from disk) =
    2. sum(rate(tikv_storage_rocksdb_perf{metric="block_read_time",req="acquire_pessimistic_lock"})) / sum(rate(tikv_storage_rocksdb_perf{metric="block_read_count",req="acquire_pessimistic_lock"}))
  • lock write duration 是写入磁盘锁的耗时,具体计算方式请参考异步写入小节。

提交阶段

本小节介绍提交阶段的耗时。下面是提交操作的时间消耗图:

延迟的拆解分析 - 图9

  1. Diagram(
  2. Stack(
  3. Sequence(
  4. Choice(
  5. 0,
  6. Comment("use 2pc or causal consistency"),
  7. NonTerminal("Get min-commit-ts"),
  8. ),
  9. Optional("Async prewrite binlog"),
  10. NonTerminal("Prewrite mutations"),
  11. Optional("Wait prewrite binlog result"),
  12. ),
  13. Sequence(
  14. Choice(
  15. 1,
  16. Comment("1pc"),
  17. Sequence(
  18. Comment("2pc"),
  19. NonTerminal("Get commit-ts"),
  20. NonTerminal("Check schema, try to amend if needed"),
  21. NonTerminal("Commit PK mutation"),
  22. ),
  23. Sequence(
  24. Comment("async-commit"),
  25. NonTerminal("Commit mutations asynchronously"),
  26. ),
  27. ),
  28. Choice(
  29. 0,
  30. Comment("committed"),
  31. NonTerminal("Async cleanup"),
  32. ),
  33. Optional("Commit binlog"),
  34. ),
  35. ),
  36. )

提交的耗时使用如下方式计算:

  1. commit =
  2. Get_latest_ts_time +
  3. Prewrite_time +
  4. Get_commit_ts_time +
  5. Commit_time
  6. Get_latest_ts_time = Get_commit_ts_time =
  7. pd_client_cmd_handle_cmds_duration_seconds{type="wait"}
  8. prewrite_round = ceil(
  9. sum(rate(tidb_tikvclient_txn_regions_num_sum{type="2pc_prewrite"})) /
  10. sum(rate(tidb_tikvclient_txn_regions_num_count{type="2pc_prewrite"})) /
  11. committer-concurrency
  12. )
  13. commit_round = ceil(
  14. sum(rate(tidb_tikvclient_txn_regions_num_sum{type="2pc_commit"})) /
  15. sum(rate(tidb_tikvclient_txn_regions_num_count{type="2pc_commit"})) /
  16. committer-concurrency
  17. )
  18. Prewrite_time =
  19. prewrite_round * tidb_tikvclient_request_seconds{type="Prewrite"}
  20. Commit_time =
  21. commit_round * tidb_tikvclient_request_seconds{type="Commit"}

提交的耗时可以拆解为以下四个指标:

  • Get_latest_ts_time 代表异步提交或单阶段 (1PC) 提交事务中获取最新 TSO 的耗时。
  • Prewrite_time 代表预先写阶段的耗时。
  • Get_commit_ts_time 代表普通两阶段 (2PC) 事务的耗时。
  • Commit_time 代表提交阶段的耗时。需要注意的是,异步提交和单阶段 (1PC) 事务没有此阶段。

与悲观锁一样,流量控制充当延迟的放大,即上述公式中的 prewrite_roundcommit_round

tidb_tikvclient_request_seconds{type="Prewrite"}tidb_tikvclient_request_seconds{type="Commit"} 的耗时使用如下方式计算:

  1. tidb_tikvclient_request_seconds{type="Prewrite"} =
  2. tidb_tikvclient_batch_wait_duration +
  3. tidb_tikvclient_batch_send_latency +
  4. tikv_grpc_msg_duration_seconds{type="kv_prewrite"} +
  5. tidb_tikvclient_rpc_net_latency_seconds{store="?"}
  6. tidb_tikvclient_request_seconds{type="Commit"} =
  7. tidb_tikvclient_batch_wait_duration +
  8. tidb_tikvclient_batch_send_latency +
  9. tikv_grpc_msg_duration_seconds{type="kv_commit"} +
  10. tidb_tikvclient_rpc_net_latency_seconds{store="?"}

关于 tidb_tikvclient_batch_wait_durationtidb_tikvclient_batch_send_latencytidb_tikvclient_rpc_net_latency_seconds{store="?"} 等批量请求客户端耗时的计算方式,请参考批量请求小节。

tikv_grpc_msg_duration_seconds{type="kv_prewrite"} 使用如下方式计算:

  1. tikv_grpc_msg_duration_seconds{type="kv_prewrite"} =
  2. prewrite key count * prewrite read duration +
  3. prewrite write duration
  4. prewrite key count =
  5. sum(rate(tikv_scheduler_kv_command_key_write_sum{type="prewrite"})) /
  6. sum(rate(tikv_scheduler_kv_command_key_write_count{type="prewrite"}))
  7. prewrite read duration(from disk) =
  8. sum(rate(tikv_storage_rocksdb_perf{metric="block_read_time",req="prewrite"})) / sum(rate(tikv_storage_rocksdb_perf{metric="block_read_count",req="prewrite"}))

与 TiKV 中的锁一样,预先写在读取和写入阶段均进行了处理。读取阶段的耗时可以从 RocksDB performance context 计算。有关写入阶段耗时的计算方式,请参考异步写入部分。

tikv_grpc_msg_duration_seconds{type="kv_commit"}使用如下方式计算:

  1. tikv_grpc_msg_duration_seconds{type="kv_commit"} =
  2. commit key count * commit read duration +
  3. commit write duration
  4. commit key count =
  5. sum(rate(tikv_scheduler_kv_command_key_write_sum{type="commit"})) /
  6. sum(rate(tikv_scheduler_kv_command_key_write_count{type="commit"}))
  7. commit read duration(from disk) =
  8. sum(rate(tikv_storage_rocksdb_perf{metric="block_read_time",req="commit"})) / sum(rate(tikv_storage_rocksdb_perf{metric="block_read_count",req="commit"})) (storage)

kv_commit 的耗时与 kv_prewrite 几乎一致。关于写入阶段耗时的计算方式,请参考异步写入小节。

批量请求

下面是批量请求客户端的时间消耗图:

延迟的拆解分析 - 图10

  1. Diagram(
  2. NonTerminal("Get conn pool to the target store"),
  3. Choice(
  4. 0,
  5. Sequence(
  6. Comment("Batch enabled"),
  7. NonTerminal("Push request to channel"),
  8. NonTerminal("Wait response"),
  9. ),
  10. Sequence(
  11. NonTerminal("Get conn from pool"),
  12. NonTerminal("Call RPC"),
  13. Choice(
  14. 0,
  15. Comment("Unary call"),
  16. NonTerminal("Recv first"),
  17. ),
  18. ),
  19. ),
  20. )
  • 总体的发送请求耗时看作 tidb_tikvclient_request_seconds
  • RPC 客户端为每个存储维护各自的连接池(称为 ConnArray),每个连接池都包含带有一个发送批量请求 channel 的 BatchConn
  • 绝大多数情况下,当存储是 TiKV 并且 batch 大小为正数时,批量请求开启。
  • 批量请求 channel 的大小是 tikv-client.max-batch-size 的值(默认值为 128)。请求入队的耗时看作 tidb_tikvclient_batch_wait_duration
  • 一共有 CmdBatchCopCmdCopStreamCmdMPPConn 三种流式请求。流式请求会引入一个额外的 recv() 调用来获取流中的第一个响应。

tidb_tikvclient_request_seconds 大致使用如下方式计算(部分延迟不包含在内):

  1. tidb_tikvclient_request_seconds{type="?"} =
  2. tidb_tikvclient_batch_wait_duration +
  3. tidb_tikvclient_batch_send_latency +
  4. tikv_grpc_msg_duration_seconds{type="kv_?"} +
  5. tidb_tikvclient_rpc_net_latency_seconds{store="?"}
  • tidb_tikvclient_batch_wait_duration 记录批量请求系统的等待耗时。
  • tidb_tikvclient_batch_send_latency 记录批量请求系统的编码耗时。
  • tikv_grpc_msg_duration_seconds{type="kv_?"} 是 TiKV 的处理耗时。
  • tidb_tikvclient_rpc_net_latency_seconds 记录网络延迟。

TiKV 快照

下面是 TiKV 快照操作的时间消耗图:

延迟的拆解分析 - 图11

  1. Diagram(
  2. Choice(
  3. 0,
  4. Comment("Local Read"),
  5. Sequence(
  6. NonTerminal("Propose Wait"),
  7. NonTerminal("Read index Read Wait"),
  8. ),
  9. ),
  10. NonTerminal("Fetch A Snapshot From KV Engine"),
  11. )

一个 TiKV 快照的总体耗时可以从 tikv_storage_engine_async_request_duration_seconds{type="snapshot"} 指标查看,它的计算方式如下:

  1. tikv_storage_engine_async_request_duration_seconds{type="snapshot"} =
  2. tikv_coprocessor_request_wait_seconds{type="snapshot"} =
  3. tikv_raftstore_request_wait_time_duration_secs +
  4. tikv_raftstore_commit_log_duration_seconds +
  5. get snapshot from rocksdb duration

当 leader lease 过期时,TiKV 会在从 RocksDB 获取快照之前提出读索引命令。tikv_raftstore_request_wait_time_duration_secstikv_raftstore_commit_log_duration_seconds 是提交读索引命令的耗时。

从 RocksDB 获取快照通常是一个快速操作,因此 get snapshot from rocksdb duration 的耗时可以被忽略。

异步写入

异步写入是 TiKV 通过回调将数据异步写入基于 Raft 的复制状态机 (Replicated State Machine) 的过程。

  • 下面是异步 IO 未开启时,异步写入过程的时间消耗图:

    延迟的拆解分析 - 图12

    1. Diagram(
    2. NonTerminal("Propose Wait"),
    3. NonTerminal("Process Command"),
    4. Choice(
    5. 0,
    6. Sequence(
    7. NonTerminal("Wait Current Batch"),
    8. NonTerminal("Write to Log Engine"),
    9. ),
    10. Sequence(
    11. NonTerminal("RaftMsg Send Wait"),
    12. NonTerminal("Commit Log Wait"),
    13. ),
    14. ),
    15. NonTerminal("Apply Wait"),
    16. NonTerminal("Apply Log"),
    17. )
  • 下面是异步 IO 开启时,异步写入过程的时间消耗图:

    延迟的拆解分析 - 图13

    1. Diagram(
    2. NonTerminal("Propose Wait"),
    3. NonTerminal("Process Command"),
    4. Choice(
    5. 0,
    6. NonTerminal("Wait Until Persisted by Write Worker"),
    7. Sequence(
    8. NonTerminal("RaftMsg Send Wait"),
    9. NonTerminal("Commit Log Wait"),
    10. ),
    11. ),
    12. NonTerminal("Apply Wait"),
    13. NonTerminal("Apply Log"),
    14. )

异步写入耗时的计算方式如下:

  1. async write duration(async io disabled) =
  2. propose +
  3. async io disabled commit +
  4. tikv_raftstore_apply_wait_time_duration_secs +
  5. tikv_raftstore_apply_log_duration_seconds
  6. async write duration(async io enabled) =
  7. propose +
  8. async io enabled commit +
  9. tikv_raftstore_apply_wait_time_duration_secs +
  10. tikv_raftstore_apply_log_duration_seconds

异步写入可以拆解为以下三个阶段:

  • 提案阶段 (Propose)
  • 提交阶段 (Commit)
  • 应用阶段 (Apply):对应上面公式中的 tikv_raftstore_apply_wait_time_duration_secs + tikv_raftstore_apply_log_duration_seconds

提案阶段耗时的计算方式如下:

  1. propose =
  2. propose wait duration +
  3. propose duration
  4. propose wait duration =
  5. tikv_raftstore_store_wf_batch_wait_duration_seconds
  6. propose duration =
  7. tikv_raftstore_store_wf_send_to_queue_duration_seconds -
  8. tikv_raftstore_store_wf_batch_wait_duration_seconds

Raft 过程以瀑布方式记录,因此,提案阶段的耗时是根据 tikv_raftstore_store_wf_send_to_queue_duration_secondstikv_raftstore_store_wf_batch_wait_duration_seconds 两个指标之间的差值计算的。

提交阶段耗时的计算方式如下:

  1. async io disabled commit = max(
  2. persist log locally duration,
  3. replicate log duration
  4. )
  5. async io enabled commit = max(
  6. wait by write worker duration,
  7. replicate log duration
  8. )

从 TiDB v5.3.0 开始,TiKV 支持通过 StoreWriter 线程池写入 Raft 日志,即异步 IO。异步 IO 会改变提交过程,仅在 store-io-pool-size 数值大于 0 时启用。耗时 persist log locally durationwait by write worker duration 的计算方式如下:

  1. persist log locally duration =
  2. batch wait duration +
  3. write to raft db duration
  4. batch wait duration =
  5. tikv_raftstore_store_wf_before_write_duration_seconds -
  6. tikv_raftstore_store_wf_send_to_queue_duration_seconds
  7. write to raft db duration =
  8. tikv_raftstore_store_wf_write_end_duration_seconds -
  9. tikv_raftstore_store_wf_before_write_duration_seconds
  10. wait by write worker duration =
  11. tikv_raftstore_store_wf_persist_duration_seconds -
  12. tikv_raftstore_store_wf_send_to_queue_duration_seconds

是否开启异步 IO 的区别在于本地持久化日志的耗时。使用异步 IO 可以直接从瀑布指标中计算本地持久化日志的耗时,忽略批处理等待耗时。

replicate log duration 代表 quorum 副本中日志持久化的耗时,其中包含 RPC 耗时和大多数日志持久化的耗时。replicate log duration 耗时的计算方式如下:

  1. replicate log duration =
  2. raftmsg send wait duration +
  3. commit log wait duration
  4. raftmsg send wait duration =
  5. tikv_raftstore_store_wf_send_proposal_duration_seconds -
  6. tikv_raftstore_store_wf_send_to_queue_duration_seconds
  7. commit log wait duration =
  8. tikv_raftstore_store_wf_commit_log_duration -
  9. tikv_raftstore_store_wf_send_proposal_duration_seconds

Raft DB

下面是 Raft DB 的时间消耗图:

延迟的拆解分析 - 图14

  1. Diagram(
  2. NonTerminal("Wait for Writer Leader"),
  3. NonTerminal("Write and Sync Log"),
  4. NonTerminal("Apply Log to Memtable"),
  5. )
  1. write to raft db duration = raft db write duration
  2. commit log wait duration >= raft db write duration
  3. raft db write duration(raft engine enabled) =
  4. raft_engine_write_preprocess_duration_seconds +
  5. raft_engine_write_leader_duration_seconds +
  6. raft_engine_write_apply_duration_seconds
  7. raft db write duration(raft engine disabled) =
  8. tikv_raftstore_store_perf_context_time_duration_secs{type="write_thread_wait"} +
  9. tikv_raftstore_store_perf_context_time_duration_secs{type="write_scheduling_flushes_compactions_time"} +
  10. tikv_raftstore_store_perf_context_time_duration_secs{type="write_wal_time"} +
  11. tikv_raftstore_store_perf_context_time_duration_secs{type="write_memtable_time"}

commit log wait duration 是 quorum 副本中最长的耗时,可能大于 raft db write duration

从 TiDB v6.1.0 开始,TiKV 默认使用 Raft Engine 作为日志存储引擎,这将改变写入日志的过程。

KV DB

下面是 KV DB 的时间消耗图:

延迟的拆解分析 - 图15

  1. Diagram(
  2. NonTerminal("Wait for Writer Leader"),
  3. NonTerminal("Preprocess"),
  4. Choice(
  5. 0,
  6. Comment("No Need to Switch"),
  7. NonTerminal("Switch WAL or Memtable"),
  8. ),
  9. NonTerminal("Write and Sync WAL"),
  10. NonTerminal("Apply to Memtable"),
  11. )
  1. tikv_raftstore_apply_log_duration_seconds =
  2. tikv_raftstore_apply_perf_context_time_duration_secs{type="write_thread_wait"} +
  3. tikv_raftstore_apply_perf_context_time_duration_secs{type="write_scheduling_flushes_compactions_time"} +
  4. tikv_raftstore_apply_perf_context_time_duration_secs{type="write_wal_time"} +
  5. tikv_raftstore_apply_perf_context_time_duration_secs{type="write_memtable_time"}

在异步写入过程中,提交的日志需要应用到 KV DB 中,应用耗时可以根据 RocksDB performance context 进行计算。

诊断场景

前面的部分详细介绍了 SQL 查询过程中执行时间的细粒度指标。本小节主要介绍遇到慢读取或慢写入查询时常见的指标分析过程。所有指标均可在 Performance Overview 面板的 Database Time 中查看。

慢读取查询

如果 SELECT 语句占 Database Time 的很大一部分,你可以认为 TiDB 在读查询时速度很慢。

慢查询的执行计划可以在 TiDB Dashboard 中的 Top SQL 语句 区域查看。要分析慢读取查询的耗时,你可以根据前面的描述分析点查批量点查表扫描和索引扫描的耗时情况。

慢写入查询

在分析慢写入查询之前,你需要查看 tikv_scheduler_latch_wait_duration_seconds_sum{type="acquire_pessimistic_lock"} by (instance) 指标来确认冲突的原因:

  • 如果这个指标在某些特定的 TiKV 实例中很高,则在热点区域可能会存在冲突。
  • 如果这个指标在所有实例中都很高,则业务中可能存在冲突。

如果是业务中存在冲突,那么你可以分析加锁提交阶段的耗时。