0
4
1
0
专栏/.../

一篇文章说透缓存表

 jiyf  发表于  2022-05-05

使用场景

对于数据量极少的小表,数据往往只会存在于一个 Region 上,而 Region 是 TiDB 数据调度的最小单位,因此无法进行更细粒度的调度。

当这样的小表出现大量的读查询时,会给小表数据所在的 TiKV 节点造成很大的查询压力,出现明显的读热点问题,造成集群中各个 TiKV 节点负载的不均衡,严重时会导致集群性能出现抖动、成为集群负载的瓶颈。

缓存表主要就是为了解决小表读热点的问题,并不针对小表的写热点,它把整张表的数据从 TiKV 加载到 TiDB Server 中进行缓存,当处理查询请求时候,直接从 TiDB Server 缓存数据中就可以完成数据查询,避免了每个查询都要访问 TiKV 节点。

针对这样的读热点小表,改为缓存表以后:

  • 减少 TiDB 节点到 TiKV 节点 rpc 调用
  • 不会给 TiKV 节点带来读压力
  • 减小查询的时延

缓存表使用局限性:

  • 要求表很少被修改,对于写极不友好
  • 不允许在缓存表上执行 DDL 操作(先改为普通表执行完 DDL 再改为缓存表)
  • 表数据总大小限制不超过 64MB(以编码后 kv entry 总大小为统计方式,包括索引数据)

缓存一致性

缓存表采用 lease 机制保证从各个 TiDB Server 缓存读取跟从 TiKV 读取数据的一致性。

lease 代表租约,在对应的 lease 周期内,尤其是 WRITE 操作,只在 WRITE lease 内允许,一致性通过以下方式确保:

  • 在 READ lease 内,不允许更改操作,那么获取 READ lease 内的 snapshot 从缓存中读取可以保证一致性

  • 在 WRITE lease 内:

    • 读取 WRITE lease 内的 snapshot 从 TiKV 读取
    • 写操作(事务提交)会检查事务 commit ts 要求在 WRITE lease 范围内,那么保证了旧缓存基于 snapshot 依然一致

lease 说明

mysql> select tb.TABLE_SCHEMA, tb.TABLE_NAME, cache.* from mysql.table_cache_meta as cache inner join information_schema.TABLES as tb on cache.tid = tb.TIDB_TABLE_ID;
+--------------+------------+-----+-----------+--------------------+--------------------+
| TABLE_SCHEMA | TABLE_NAME | tid | lock_type | lease              | oldReadLease       |
+--------------+------------+-----+-----------+--------------------+--------------------+
| sbtest1      | sbtest1    | 650 | WRITE     | 432986910940987392 | 432986909630267392 |
+--------------+------------+-----+-----------+--------------------+--------------------+
1 row in set (0.00 sec)

table_cache_meta 各个列信息:

  • lock_type:缓存表 lease 的锁类型,有以下几种:

    • NONE:没有锁,一般只有在新建的缓存表会有此类型
    • READ:读锁,在 lease 内,不能进行表更新操作
    • INTEND:写操作的意向锁,抢占式锁类型。如果当前锁类型为读锁时,需要进行写操作,添加 INTEND 阻止读操作延长 lease,使得写操作能顺利获取写锁。INTEND 类型时 oldReadLease 列生效,代表 READ lease 的时间,到期后开始允许写操作,lease 列代表期望后面获取的 WRITE lease 的时间。
    • WRITE:写锁,在 lease 内可以进行表更新操作
  • lease:锁到期时间,是 tso 类型,代表当前 lock_type 的有效期,如果 lease 过期,锁将会无效

  • oldReadLease:只有加 INTEND 锁的时候才会更新此列,代表读操作的结束时间,到期后可以执行写操作

读操作

  • 优先选择 Point_Get、Batch_Point_Get 算子
  • 其次 UnionScan 算子

目前版本 6.0.0 对于简单的查询,当可以通过主键或者唯一索引检索数据时候,显示走 Point_Get、Batch_Point_Get 的执行计划(有缓存数据可用,直接读缓存)。

其他查询会生成带有 UnionScan 算子的执行计划,针对缓存表生成的 UnionScan 算子,封装了下层的执行逻辑:

  1. 如果缓存数据可用,通过 tidb server 缓存数据查询
  2. 缓存数据不可用,访问 tikv 查询数据(普通的快照读)

通过 trace 命令查看 UnionScan 算子下是走的缓存还是常规 tikv 快照查。

以 sbtest1 表做测试,表结构如下(CACHED ON 说明当前是缓存表):

mysql> show create table sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `k` int(11) NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
  KEY `k_1` (`k`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=469041 /* CACHED ON */
1 row in set (0.00 sec)

举例1:通过主键查询,走 Point_Get 算子

mysql> explain select * from sbtest1 where id = 10000;
+-------------+---------+------+---------------+---------------+
| id          | estRows | task | access object | operator info |
+-------------+---------+------+---------------+---------------+
| Point_Get_1 | 1.00    | root | table:sbtest1 | handle:10000  |
+-------------+---------+------+---------------+---------------+
1 row in set (0.00 sec)

举例2:其他会走缓存表逻辑,走 UnionScan 算子

mysql> explain select * from sbtest1 where k = 10000;  
+----------------------------------+---------+-----------+-----------------------------+---------------------------------------+
| id                               | estRows | task      | access object               | operator info                         |
+----------------------------------+---------+-----------+-----------------------------+---------------------------------------+
| UnionScan_6                      | 1.97    | root      |                             | eq(sbtest1.sbtest1.k, 10000)          |
| └─IndexLookUp_12                 | 1.97    | root      |                             |                                       |
|   ├─IndexRangeScan_10(Build)     | 1.97    | cop[tikv] | table:sbtest1, index:k_1(k) | range:[10000,10000], keep order:false |
|   └─TableRowIDScan_11(Probe)     | 1.97    | cop[tikv] | table:sbtest1               | keep order:false                      |
+----------------------------------+---------+-----------+-----------------------------+---------------------------------------+
4 rows in set (0.00 sec)

举例3:等待缓存过期,通过 trace 命令查看到执行从 tikv 查询数据

mysql> trace select * from sbtest1 where k = 10000;
+-------------------------------------------------------------------------+-----------------+------------+
| operation                                                               | startTS         | duration   |
+-------------------------------------------------------------------------+-----------------+------------+
| trace                                                                   | 13:06:40.722793 | 1.753585ms |
|   ├─session.ExecuteStmt                                                 | 13:06:40.722798 | 314.799µs  |
|   │ ├─executor.Compile                                                  | 13:06:40.722806 | 177.778µs  |
|   │ └─session.runStmt                                                   | 13:06:40.723000 | 94.216µs   |
|   │   └─UnionScanExec.Open                                              | 13:06:40.723038 | 26.677µs   |
|   │     ├─buildMemIndexLookUpReader                                     | 13:06:40.723046 | 1.454µs    |
|   │     └─memIndexLookUpReader.getMemRows                               | 13:06:40.723054 | 4.785µs    |
|   ├─*executor.UnionScanExec.Next                                        | 13:06:40.723122 | 1.39117ms  |
|   │ ├─*executor.IndexLookUpExecutor.Next                                | 13:06:40.723125 | 1.367836ms |
|   │ │ ├─distsql.Select                                                  | 13:06:40.723161 | 26.253µs   |
|   │ │ │ └─regionRequest.SendReqCtx                                      | 13:06:40.723237 | 698.852µs  |
|   │ │ │   └─rpcClient.SendRequest, region ID: 1372, type: Cop           | 13:06:40.723256 | 634.524µs  |
|   │ │ ├─distsql.Select                                                  | 13:06:40.724022 | 15.073µs   |
|   │ │ │ └─regionRequest.SendReqCtx                                      | 13:06:40.724089 | 331.329µs  |
|   │ │ │   └─rpcClient.SendRequest, region ID: 1372, type: Cop           | 13:06:40.724104 | 286.651µs  |
|   │ │ ├─*executor.TableReaderExecutor.Next                              | 13:06:40.724072 | 378.053µs  |
|   │ │ └─*executor.TableReaderExecutor.Next                              | 13:06:40.724464 | 4.925µs    |
|   │ └─*executor.IndexLookUpExecutor.Next                                | 13:06:40.724508 | 835ns      |
|   └─*executor.UnionScanExec.Next                                        | 13:06:40.724521 | 7.952µs    |
|     └─*executor.IndexLookUpExecutor.Next                                | 13:06:40.724525 | 474ns      |
+-------------------------------------------------------------------------+-----------------+------------+
20 rows in set (0.01 sec)

举例4:缓存数据有效,通过 trace 命令查看到执行从缓存中查询数据,不用访问 tikv.

mysql> trace select * from sbtest1 where k = 10000;
+-------------------------------------------------+-----------------+------------+
| operation                                       | startTS         | duration   |
+-------------------------------------------------+-----------------+------------+
| trace                                           | 13:06:43.415779 | 474.048µs  |
|   ├─session.ExecuteStmt                         | 13:06:43.415783 | 434.126µs  |
|   │ ├─executor.Compile                          | 13:06:43.415791 | 258.235µs  |
|   │ └─session.runStmt                           | 13:06:43.416065 | 126.674µs  |
|   │   └─UnionScanExec.Open                      | 13:06:43.416105 | 55.186µs   |
|   │     ├─buildMemIndexLookUpReader             | 13:06:43.416109 | 1.624µs    |
|   │     └─memIndexLookUpReader.getMemRows       | 13:06:43.416117 | 36.707µs   |
|   │       └─memTableReader.getMemRows           | 13:06:43.416135 | 14.088µs   |
|   ├─*executor.UnionScanExec.Next                | 13:06:43.416225 | 3.651µs    |
|   └─*executor.UnionScanExec.Next                | 13:06:43.416234 | 1.31µs     |
+-------------------------------------------------+-----------------+------------+
10 rows in set (0.00 sec)

写操作

当数据量大于 64MB 时候,禁止对表的 INSERT、UPDATE,允许 DELETE

对缓存表进行 DML 操作时,TiDB Server 要获取表的 WRITE lease,代表在这个租约内,可以进行表的写操作,如果要查询的数据的 snapshot 在租约内,那就不能直接使用缓存的数据,因为 TiKV 数据可能已经更新,造成缓存数据不一致,这时候需要直接查询 TiKV 获取一致的数据。

在 WRITE lease 内,读查询退化为跟普通表一样的从 TiKV 读取

使用 sysbench 压测 select 查询过程中,对表执行一个 dml 操作,表的 WRITE lease 期间,缓存失效,从 tikv 查询数据,表现出压测 tps 出现下降,latency 变长的情景;当 dml 执行结束,表重新回到 READ lease 时,tps 又回升到之前的数值。

[ 16s ] thds: 32 tps: 63156.73 qps: 63156.73 (r/w/o: 63156.73/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 17s ] thds: 32 tps: 63100.18 qps: 63100.18 (r/w/o: 63100.18/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 18s ] thds: 32 tps: 62388.72 qps: 62388.72 (r/w/o: 62388.72/0.00/0.00) lat (ms,95%): 0.80 err/s: 0.00 reconn/s: 0.00
[ 19s ] thds: 32 tps: 62373.22 qps: 62373.22 (r/w/o: 62373.22/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 20s ] thds: 32 tps: 32795.91 qps: 32795.91 (r/w/o: 32795.91/0.00/0.00) lat (ms,95%): 1.86 err/s: 0.00 reconn/s: 0.00
[ 21s ] thds: 32 tps: 31120.99 qps: 31120.99 (r/w/o: 31120.99/0.00/0.00) lat (ms,95%): 1.89 err/s: 0.00 reconn/s: 0.00
[ 22s ] thds: 32 tps: 31001.04 qps: 31001.04 (r/w/o: 31001.04/0.00/0.00) lat (ms,95%): 1.89 err/s: 0.00 reconn/s: 0.00
[ 23s ] thds: 32 tps: 30879.05 qps: 30879.05 (r/w/o: 30879.05/0.00/0.00) lat (ms,95%): 1.96 err/s: 0.00 reconn/s: 0.00
[ 24s ] thds: 32 tps: 28217.93 qps: 28217.93 (r/w/o: 28217.93/0.00/0.00) lat (ms,95%): 2.07 err/s: 0.00 reconn/s: 0.00
[ 25s ] thds: 32 tps: 52939.06 qps: 52939.06 (r/w/o: 52939.06/0.00/0.00) lat (ms,95%): 1.14 err/s: 0.00 reconn/s: 0.00
[ 26s ] thds: 32 tps: 61600.98 qps: 61600.98 (r/w/o: 61600.98/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 27s ] thds: 32 tps: 60956.91 qps: 60956.91 (r/w/o: 60956.91/0.00/0.00) lat (ms,95%): 0.83 err/s: 0.00 reconn/s: 0.00
[ 28s ] thds: 32 tps: 63911.26 qps: 63911.26 (r/w/o: 63911.26/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 29s ] thds: 32 tps: 63052.79 qps: 63052.79 (r/w/o: 63052.79/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00

由于要先获取 WRITE lease,也就是在 DML 时候,要等待获取 lease 后才能进行事务提交,所以对于缓存表,执行更改可能耗时较长,正常等待获取 lease 的时间在 0 ~ tidb_table_cache_lease 的范围,也就是 INTEND lock 的持有时间,等待 READ lease 过期。

在 DML 事务 commit 阶段尝试获取 WRITE lease,也就是在提交时延时较大。

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
​
mysql> delete from sbtest1 where id = 100;
Query OK, 1 row affected (0.00 sec)
​
mysql> commit;
Query OK, 0 rows affected (5.32 sec)

这里 commit 语句耗时 5.32 秒,因为在提交阶段等待获取 WRITE lease 耗时较长。

压力测试

使用 sysbench 对缓存表单表进行稳定性压力测试,压测线程数固定为 32,每个压测表的行数会变,从 50 ~ 204800 递增,表结构如下:

mysql> show create table sbtest1\G
*************************** 1. row ***************************
       Table: sbtest1
Create Table: CREATE TABLE `sbtest1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `k` int(11) NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`) /*T![clustered_index] CLUSTERED */,
  KEY `k_1` (`k`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin AUTO_INCREMENT=106519 /* CACHED ON */
1 row in set (0.00 sec)

sysbench 压测语句如下:

SELECT c FROM sbtest1 WHERE k = sb_rand(1, oltp_table_size);

压测结果(count 是表的数据行数):

 count               tps                     qps                 latency
    50          54516.24                54516.24                  1.27ms
   100          54934.17                54934.17                  1.25ms
   200          54061.46                54061.46                  1.30ms
   400          53859.76                53859.76                  1.32ms
   800          53083.76                53083.76                  1.34ms
  1600          53464.69                53464.69                  1.34ms
  3200          53157.89                53157.89                  1.32ms
  6400          53531.27                53531.27                  1.27ms
 12800          56076.71                56076.71                  1.06ms
 25600          58441.74                58441.74                  0.95ms
 51200          59734.40                59734.40                  0.87ms
102400          61787.46                61787.46                  0.78ms
204800          64656.75                64656.75                  0.72ms

压测过程中间数据:

[ 50s ] thds: 32 tps: 62284.70 qps: 62284.70 (r/w/o: 62284.70/0.00/0.00) lat (ms,95%): 0.77 err/s: 0.00 reconn/s: 0.00
[ 51s ] thds: 32 tps: 62437.31 qps: 62437.31 (r/w/o: 62437.31/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 52s ] thds: 32 tps: 62268.66 qps: 62268.66 (r/w/o: 62268.66/0.00/0.00) lat (ms,95%): 0.77 err/s: 0.00 reconn/s: 0.00
[ 53s ] thds: 32 tps: 62325.43 qps: 62325.43 (r/w/o: 62325.43/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 54s ] thds: 32 tps: 62224.41 qps: 62224.41 (r/w/o: 62224.41/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 55s ] thds: 32 tps: 62159.53 qps: 62159.53 (r/w/o: 62159.53/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 56s ] thds: 32 tps: 61839.63 qps: 61839.63 (r/w/o: 61839.63/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 57s ] thds: 32 tps: 59084.72 qps: 59084.72 (r/w/o: 59084.72/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 58s ] thds: 32 tps: 62009.57 qps: 62009.57 (r/w/o: 62009.57/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 0.00
[ 59s ] thds: 32 tps: 60203.24 qps: 60203.24 (r/w/o: 60203.24/0.00/0.00) lat (ms,95%): 0.77 err/s: 0.00 reconn/s: 0.00
[ 60s ] thds: 32 tps: 62814.70 qps: 62814.70 (r/w/o: 62814.70/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 61s ] thds: 32 tps: 59874.28 qps: 59874.28 (r/w/o: 59874.28/0.00/0.00) lat (ms,95%): 0.80 err/s: 0.00 reconn/s: 0.00
[ 62s ] thds: 32 tps: 62260.65 qps: 62260.65 (r/w/o: 62260.65/0.00/0.00) lat (ms,95%): 0.80 err/s: 0.00 reconn/s: 0.00
[ 63s ] thds: 32 tps: 61401.04 qps: 61401.04 (r/w/o: 61401.04/0.00/0.00) lat (ms,95%): 0.87 err/s: 0.00 reconn/s: 0.00
[ 64s ] thds: 32 tps: 62067.33 qps: 62067.33 (r/w/o: 62067.33/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 65s ] thds: 32 tps: 62575.07 qps: 62575.07 (r/w/o: 62575.07/0.00/0.00) lat (ms,95%): 0.80 err/s: 0.00 reconn/s: 0.00
[ 66s ] thds: 32 tps: 62451.52 qps: 62451.52 (r/w/o: 62451.52/0.00/0.00) lat (ms,95%): 0.78 err/s: 0.00 reconn/s: 0.00
[ 67s ] thds: 32 tps: 62328.75 qps: 62328.75 (r/w/o: 62328.75/0.00/0.00) lat (ms,95%): 0.80 err/s: 0.00 reconn/s: 0.00
[ 68s ] thds: 32 tps: 62482.39 qps: 62482.39 (r/w/o: 62482.39/0.00/0.00) lat (ms,95%): 0.84 err/s: 0.00 reconn/s: 0.00
[ 69s ] thds: 32 tps: 62094.58 qps: 62094.58 (r/w/o: 62094.58/0.00/0.00) lat (ms,95%): 0.81 err/s: 0.00 reconn/s: 

从压测数据来看,性能比较平稳,没有出现明显的波动。

总结

  • 缓存表适用于表数据量小、查询多、又极少有 DML 的场景
  • 缓存表可以解决适用场景下的读热点问题
  • 当有 DML 时,缓存读回退到普通的 TiKV snapshot 读,这时候读 Latency 增大、TPS 降低
  • 不能直接对缓存表执行 DDL,有 DDL 需求时先改为普通表,执行 DDL 后再改回缓存表
  • 对于 DML,在事务提交阶段,由于要等待 READ lease 过期,可能会被阻塞导致耗时较长
  • 如果缓存加载较慢,可以适当延长 tidb_table_cache_lease 值;增大可能会导致 DML 事务提交阻塞更久,对于极少 DML 的缓存表场景,建议适当调大 tidb_table_cache_lease 值

0
4
1
0

版权声明:本文为 TiDB 社区用户原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文出处链接和本声明。

评论
暂无评论