TiDB Server
性能相关基本原理
CPU
CPU 是 TiDB 消耗的主要资源
- SQL 执行流程
- SQL:执行语句
- AST:抽象语法树
- Logical:逻辑优化
- Optimized Logical Plan:物理优化
- Cost Model:代价模型
- Selected Physical Plan:向下发的执行计划
- 火焰图
- go 的 pprof 抓取
go tool pprof -http 127.0.0.1:8080 http://127.0.0.1:10080/debug/pprof/profile
内存
- 内存分配次数过多 –> golang GC 压力大,其实 CPU 的使用过多,一般内存使用也会较多
- 火焰图抓取:
go tool pprof -http 127.0.0.1:8080 http://127.0.0.1:10080/debug/pprof/heap
网络
- 一些常见的参考值(PPT 中标黑的部分是:内存和网络读 1 MB 数据的耗时)
- 介绍下一个事务开启,走那些网络
- PD tso,全局唯一标识,两次(start ts, commit ts),点查例外。
- KV commit,需要走数据和索引两部分
- Raft 提交
介绍下例子(两阶段提交)
1 |
|
- 启动阶段:获取 start ts
- 读阶段:读一次索引,再读一次数据(也就是常说的回表)
- 提交阶段:获取 commit ts、两次 tikv 写入(数据+索引) * 2(prewrite + commit)、TiKV 之间 raft【写入 key 个数 * (副本数/2 + 1)】
- 上面的为协调者,TiKV 是参与者
锁
乐观事务模型下,只有提交才加锁
- prewirte 之后,会有一段时间才会进行 commit。
-
那么在这个时间中,当其他事务对相关数据进行读写请求,那么就会有一个等待解锁的过程。
- Backoff 机制:不断增加等待时间,直到超时(Backoff 是递增的)。
不同负载对性能的影响
- 读请求
- TiDB 内部计算
- SQL 优化
- KV 读
- 通过 TiKV Storage read pool 点查/更新/删除(2.1 的优化,性能损耗较低)
- Coprocessor 读(逻辑要比较复杂)
- TiKV Coprocessor
- TiDB 内部计算
- 写请求
- TiDB 内部计算(内部变换,将数据转换成要写入的数据)
- 事务提交
读请求和 MySQL 的差异
- TiDB 中没有任何读缓存(都会从 TIKV 中将数据读上来),MySQL 有 buffer pool
- TiDB 中有事务私有写缓存,事务内写的数据会缓存在事务自身的 buffer 中
- 当读请求也要读到缓存数据的时候,会与 TiKV 中结果进行合并
例子(一个 session 中):
Insert into test (i) values (1);
Begin;
Insert into test (i) values (2);
Select * from test; -- TiKV 中的结果(1) + 事务内的结果(2)
合并过程仅在 TiDB 单点内部处理,事务内数据量过大将会大大降低性能
写请求和 MySQL 的差异
- 事务提交前,TiDB 缓存所有即将提交的修改
- 写请求由于一致性检查的要求,也会进行大量的读
- TiDB 的 insert lazy check (优化:延迟检查)
- 普通 Insert
- Insert ignore?(写入是否有冲突)
- Insert on duplicate key update?(重复主键是否要更新)
例子:
CREATE TABLE T (I INT KEY);
INSERT INTO T VALUES (1);
BEGIN;
INSERT INTO T VALUES (1); -- MySQL 返回错误
INSERT INTO T VALUES (2);
COMMIT; -- TiDB 返回错误(发现主键冲突,直接回滚)
SELECT * FROM T; -- MySQL 返回 1 2;TiDB 返回 1
- 乐观事务模型(OCC)
- 对于 TiKV update 仅仅做了一次读。提交时冲突了怎么办?(重试,可关闭)
- 事务重试(对于客户无感知的,过多会有性能损耗)
- 重新取 start ts,重新执行事务内所有的写操作的 SQL。
- 相当于把事务启动时间向后挪动。
- 所有的操作都算在了 commit 语句自身的执行中,用户无感知。
例子:
session1 | session2 |
---|---|
Begin; | - |
Update t set i = 2 where i = 1; | - |
- | update t set i = 3 where i = 1; |
Commit; – 初次提交会失败,并进行自动重试 | - |
监控
- Duration
- QPS:系统 SQL(internal QPS) 和 业务 SQL
- 连接数(token-limit,默认 1000)
- Heap Memory Usage,内存使用,可抓火焰图分析
- Transaction Statement Num,排查事务语句太多带来的相关问题(事务中有多少 SQL)。
- Transaction Retry Num:事务冲突,提交异常慢的原因之一
- Backoff & Lock:事务冲突有关,Lock 是超过了限制的时间,那么就会解锁,事务被 kill
- KV Cmd Duration:区分 TIDB 和 TIKV 慢的关键
- PD TSO Wait/RPC Duration:wait duration 高,可能是 TiDB 自身慢(TiDB 获取 start ts,但是可能 TIDB 比较忙,一直没有用)。RPC duration 高,原因是 TiDB 与 PD 之间延迟太大(TiDB 从 PD 获取 TSO 的实际时间)。这两个监控如果基本类似,那么大概率是 TIDB 和 PD 之间网络延迟比较大。
慢日志及常用工具介绍
性能调参
- 配置参数
- 系统变量
TiKV Server
以 TIKV 角度来找性能瓶颈
TIKV 架构
- PD:存储元数据,分发 TSO,manager
- TIDB:计算
- TIKV:存储 + 计算下推,分布式计算
监控
- Grpc
- grpc message count:grpc 请求数
- grpc message duration:TIKV Grpc 通讯响应时间
- 如果 TiKV 端显示延迟不高,但是 TiDB 那边又说 TiKV 延迟比较高,说明中间环节可能有问题,比如网络,需要查看下网卡流量是否打满以及延迟情况。比如 ping 一下延迟是否比较高
TIKV 中数据存储
- 1、table 数据如何映射成 KV(key -> values)?
- 比如一张表,有主键、唯一索引、普通索引、普通列。一行数据的存储是什么样的呢?
- 行数据:row-key(行 key) -> column-1 + column-2 + column-3
- 索引数据:index-key -> row-id(uint64)
- 编码方式
- Row key 编码方式(保证全局唯一):table-id(占 8 个字节) + _r(row key 标志位) + row-id(uint64)
- Unique Index key 编码方式:table-id + _i(index key 标志位) + index-id + index-content -> row id
- No Unique Index key 编码方式:table-id + _i(index key 标志位) + index-id + index-content + row-id -> empty
- 说明
- TIDB 将所有的数据都放到 TIKV 上,对于 TIKV 而言,存储数据是一个全局的数据集,是从最小 key 到最大 key 的一个数据区间,没有表这个概念。
- 为了在 TIKV 中,不同表的数据容易区分,因此就有了 table-id。好处是:区分表、表数据连续(对 scan 操作友好)
- key 需要全局唯一,有序
- 这里的 KV 是逻辑 KV,当不考虑事务的话,我们编码好的 KV 对,就可以直接落盘。但是 TIDB 是支持分布式事务的,因此就需要一个两阶段的事务提交,不能直接将编码好的 KV 直接写盘,中间会有一些中间状态,因此要了解下 RocksDB 的 Column Family。
- 比如一张表,有主键、唯一索引、普通索引、普通列。一行数据的存储是什么样的呢?
- 2、RocksDB 的 Column Family
- 要了解两阶段提交的中间状态
- LMS tree 结构:
- 对于写,会先写 WAL(disk) 日志中
- 然后将写入的内容写入内存(memtable)
- 当 memtable 涨到一定的大小,就会将内存里面的数据,flush 成 SST 文件,新的写入会在新的 memtable 中写入
- 随着时间的推移,写的 SST 文件就会越来越多(有很多内容是有重叠的),这样对于读取是不友好的。
- 因此会对 SST 文件进行整理,这个过程就是 compaction。整理成一个塔型的结构,越到下面的数据越老,越往上就越新,memtable 中是最新的。
- 对于 RocksDB 的 Column Family 而言,他有三个 cf 空间,是完全隔离开的,但是共享了一个 WAL 日志
- 多个 cf 之间共享 WAL(保证原子性)
- 每个 cf 有单独的 memtable、block-cache 和 sst
- default cf:真正数据
- lock cf:锁信息
- write cf:CF 存储的是数据的版本信息 (MVCC) 以及索引相关的数据
- 3、KV 如何存储在 RocksDB 中,编码的逻辑 KV 如何落盘成物理 KV
- 物理 KV 如何存储,对于理解 TiKV 写入是有帮助的。
- KV 是如何存储在底层 RocksDB:
- default cf:一个 KV 过来,需要写对应的 Key 和 Value,Key 有对应的版本:key_start_ts(key+事务起始时间)
- lock cf:提交的第一阶段(prepare 阶段),会加锁,保证其他事务不会发生冲突
- 进入 commit 阶段:删除 lock cf(释放锁),提交记录写入 write cf。其中的内容就是:key + commit_ts(事务的提交时间) ->(指向) start_ts,通过 start_ts 可以在 default cf 找到对应的数据,并且记录了操作的类型(比如 PUT,也就是 insert)
- 整个过程总结:
- 通过 2PC 来支持分布式事务。
- prepare 阶段写 default 和 lock(两个操作是原子的)。
- commit 阶段删 lock 写 write(两个操作是原子的)。
- 细节优化 –> 短 value 处理:
- 通过 key 的编码方式我们知道,row key 大概在 20 多个 bytes 左右,index key 在 30 ~ 几百 bytes 左右。
- 如果 value 特别短,写多个 cf 导致的存储空间和写入开销都会增大。
- value <= 64 bytes, prepare 阶段把 value 携带在 lock 里面,commit 阶段写入到 write 里,也就是说 write cf 中也会有真正的数据。
- 这样我们少写一个 default cf,读的时候,只需要扫 write cf 数据即可(不需要通过 start_ts 去 default cf 中找真实数据了)。
写优化
1、TiKV 数据写入流程
- 写入请求到 TIKV,首先会先到 scheduler pool 组件:进行写之前的约束校验,比如写入的 ts 是否比已有版本的 ts 的更大(写写冲突)等等。(raw_put 和直接调用 TIKV 接口,会跳过该部分,使用 TIDB 写入数据,则不会)
- 然后请求就会到 raftstore 线程:处理 raft 任务:处理 raft 消息(复制给其他的 TIKV)和写 raft log(相关参数是 sync-log,默认为 true)
- raft 日志会 append log 落盘,日志落盘成功,就会回应 commit,这样 commit 成功后,就会进入 apply 线程,写入状态机,apply 到 db –> RocksDB data。(实际 commit = append + apply 成功)
- append 和 apply,单线程 –> 多线程。可能会成为瓶颈。
2、写入瓶颈分析
- CPU 使用是否高
- duration 是否高
- 有一个 stall 流控
3、写入热点问题分析
- 判断是否可优化
- 索引热点
- 组件热点
- 数据热点
4、写入参数优化
- RocksDB 写入相关参数
- 要考虑 L0 数据有重叠,导致不能简单的做 range。
- L0 compact 并发
- 压缩比:CPU 换 IO
- compression-per-level = [“no”, “no”, “lz4”, “lz4”, “lz4”, “zstd”, “zstd”] # 各个层的压缩算法
- level0-file-num-compaction-trigger = 4 # L0 compact 触发
- level0-slowdown-writes-trigger = 20 # L0 文件到达 20,RocksDB 流控控制写入速度
- level0-stop-writes-trigger = 36 # L0 文件到达 36,RocksDB 流控控制写入停止
- 当业务只是有高峰,时间较短,而且业务高峰时,对读的响应时间要求不高,那么可以放宽 RocksDB 流控的限制。
- Titan:将大 value 单独拆出来,存为 log 文件,减少写放大
读优化
1、TiKV 数据读取流程
- 读流程要比写简单一点
- KV 读:走 storage readpool(默认 4)
- 下推读:coprocessor readpool(默认 CPU 80%)
- 其中各对应 3 个 pool 配置,3 个优先级:
- high:高优先级请求,比如查系统变量等。
- normal:一些正常的请求。
- low:比如一些 scan
2、读取瓶颈分析
- 查看监控
- CPU:不同的池子(storage & coprocessor)
- 调整原则:太多 or 太低
- 有没有热点呢
- duration
- 请求时间
- 等待时间
- 处理时间
- CPU:不同的池子(storage & coprocessor)
3、RocksDB block-cache 原理与配置
- RocksDB Column Family
- 多个 cf 之间共享 WAL
- 每个 cf 有单独的 memtable、block-cache 和 sst
- 看下各个组件的作用(PPT 中有)
- 内存占用:
- memtable
- block cache
- 系统的 page cache