优化TiDB中写热点问题的策略与实践

网友投稿 318 2024-03-05



一.   什么是热点问题?

优化TiDB中写热点问题的策略与实践

所谓热点,就是说数据库在某一段时间内的读或写请求都在某一个节点或少数节点上,导致整个集群的负载不均衡而引起整体效率下降。举个最简单的例子,一个TiDB集群有10个TiDB Server节点和10个TiKV节点。对于TiDB Server而言,如果所有的连接均只分配到一个TiDB Server,那这个TiDB Server就是一个热点,对于这种情况我们可以引入负载均衡组件来实现将外部连接均衡分配给不同的TiDB Server。而对于TiKV节点而言,由于默认情况下读写请求由每个Region的Leader处理,如果Leader分布不均,那么就很容易导致TiKV节点的负载不均,不过PD的调度功能基本上可以保证TiKV节点之间的负载均衡。然而有一些情况是PD调度也无法解决的热点问题,最典型的就是递增写入场景,如下图所示,test1的主键字段a是AUTO_INCREMENT自增列,数据的写入按ID递增写入。

由于TiDB底层存储把二维关系结构映射为键值对,具体是把主键映射为Key以及把非主键字段组合映射为Value。TiKV可以看成是一个巨大有序的KV map,以Region为单位存储。对于上述test1表,由于数据写入是按ID递增写入,那么在任意一段连续的时间范围内写入请求都会集中在一个Region内,这个Region就会形成一个热点。而当这个Region写满时通过PD调度将这个Region分裂出一个新的空Region,后续的写入又会持续往新Region中写入导致一个新的热点。

二.解决写热点问题的思路?

解决写热点的问题需要考虑两个点:

保证被写入的表拆分为多个Region。表在刚创建时只有一个Region,因此无论是否按序递增写入都会落到这唯一的Region上,针对这种情况我们需要在建表时对表进行预拆分(pre split)。如果表原来已经有部分数据,我们需要通过专门的语法对表手动拆分(split)

避免键值数据递增写入。对于有主键表,Key主要使用主键值构成,如果主键类型为auto_increment,我们需要更换一种自动生成数值的方式,保证写入的数据是离散的。对于无主键表,Key是由数据库生成的隐式递增RowID,我们也需要有一种方式来生成非连续递增的RowID

三.手动拆分(split)与预拆分(presplit)怎么操作?

手动拆分

如果一个表已经创建好且可能有一些数据,我们需要使用TiDB提供的split语法对表进行手动拆分,具体的split语法可参考官网 Split Region 使用文档 | PingCAP 文档中心 。这里通过一个简单的事例来说明使用split拆分前后的变化,

mysql> show table test1 regions; +-----------+-----------+----------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | REGION_ID | START_KEY | END_KEY | LEADER_ID | LEADER_STORE_ID | PEERS | SCATTERING | WRITTEN_BYTES | READ_BYTES | APPROXIMATE_SIZE(MB) | APPROXIMATE_KEYS | SCHEDULING_CONSTRAINTS | SCHEDULING_STATE | +-----------+-----------+----------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | 5592 | t_176_ | 78000000 | 5594 | 2 | 5593, 5594, 5595 | 0 | 0 | 238025 | 4 | 24854 | | | +-----------+-----------+----------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ 1 row in set (0.01 sec) mysql> SPLIT TABLE test1 BETWEEN (0) AND (1000000000) REGIONS 4; +--------------------+----------------------+ | TOTAL_SPLIT_REGION | SCATTER_FINISH_RATIO | +--------------------+----------------------+ | 3 | 1 | +--------------------+----------------------+ 1 row in set (0.01 sec) mysql> show table test1 regions; +-----------+-------------------+-------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | REGION_ID | START_KEY | END_KEY | LEADER_ID | LEADER_STORE_ID | PEERS | SCATTERING | WRITTEN_BYTES | READ_BYTES | APPROXIMATE_SIZE(MB) | APPROXIMATE_KEYS | SCHEDULING_CONSTRAINTS | SCHEDULING_STATE | +-----------+-------------------+-------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | 5608 | t_176_ | t_176_r_250000000 | 5610 | 2 | 5609, 5610, 5611 | 0 | 0 | 0 | 1 | 0 | | | | 5612 | t_176_r_250000000 | t_176_r_500000000 | 5615 | 5 | 5613, 5614, 5615 | 0 | 0 | 0 | 1 | 0 | | | | 5616 | t_176_r_500000000 | t_176_r_750000000 | 5617 | 1 | 5617, 5618, 5619 | 0 | 0 | 0 | 1 | 0 | | | | 5592 | t_176_r_750000000 | 78000000 | 5594 | 2 | 5593, 5594, 5595 | 0 | 0 | 167635 | 4 | 24854 | | | +-----------+-------------------+-------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ 4 rows in set (0.00 sec)

上述输出说明表test1在开始时只有一个region,然后使用split语法将0-1000000000的数据范围平均拆分为4个region。需要注意的是,这种拆分方式需要提前了解表中的数据分布,否则可能会导致拆分后数据分布仍然不均匀。

另外补充一点,split不仅可以对表进行拆分,也可以对索引进行拆分,避免索引上的热点问题。同时,split也可以作用于分区表上,当作用于分区表时,会对每个分区的region都进行同样的拆分规则。

特别提醒:TiDB中的PD组件会定期调度合并一些小的Region,如果希望拆分的表不会被PD调度合并,需要在表上添加一个属性,具体方法如下所示。

ALTER TABLE test1 ATTRIBUTES merge_option=deny;

预拆分

从某种程度上说,split语法也可以作为一个预拆分的方法,比如当你新建一张空表后立即使用split进行手工拆分。不过,TiDB还提供了另外一种方式可以在建表DDL语句时通过专门的语法来进行预拆分。

关于预拆分的方法,具体可以参考官网 Split Region 使用文档 | PingCAP 文档中心, 这里还是使用一个示例来说明。需要注意的是,预拆分的语法目前只能在有shard_row_id_bits定义的表上才生效。(shard_row_id_bits后续介绍)

mysql> create table test4(a bigint, b varchar(20), c int) shard_row_id_bits=4 pre_split_regions=2; Query OK, 0 rows affected (0.52 sec) mysql> show table test4 regions; +-----------+-----------------------------+-----------------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | REGION_ID | START_KEY | END_KEY | LEADER_ID | LEADER_STORE_ID | PEERS | SCATTERING | WRITTEN_BYTES | READ_BYTES | APPROXIMATE_SIZE(MB) | APPROXIMATE_KEYS | SCHEDULING_CONSTRAINTS | SCHEDULING_STATE | +-----------+-----------------------------+-----------------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ | 5632 | t_182_ | t_182_r_2305843009213693952 | 5634 | 2 | 5633, 5634, 5635 | 0 | 0 | 0 | 1 | 0 | | | | 5636 | t_182_r_2305843009213693952 | t_182_r_4611686018427387904 | 5638 | 2 | 5637, 5638, 5639 | 0 | 39 | 0 | 1 | 0 | | | | 5640 | t_182_r_4611686018427387904 | t_182_r_6917529027641081856 | 5642 | 2 | 5641, 5642, 5643 | 0 | 39 | 0 | 1 | 0 | | | | 5592 | t_182_r_6917529027641081856 | 78000000 | 5594 | 2 | 5593, 5594, 5595 | 0 | 15205 | 87886 | 4 | 24862 | | | +-----------+-----------------------------+-----------------------------+-----------+-----------------+------------------+------------+---------------+------------+----------------------+------------------+------------------------+------------------+ 4 rows in set (0.01 sec)

四.如何生成离散而非连续递增数据?

拆分只能做到表被拆分成多个Region,这些Region虽然可以被分配到不同的数据节点,然而要彻底实现写均衡还是要保证写入的数据是离散的而不是连续的,实现写入离散有几种方式。

主键为自增列时,使用AUTO_RANDOM代替AUTO_INCREMENT

如果表的主键字段是AUTO_INCREMENT递增序列,那么一段连续的写入都会落在同一个Region,此Region就形成写热点。我们可以将字段定义为AUTO_RANDOM类型,AUTO_RANDOM的值既具有随机性又具有唯一性,因此它在保证主键唯一性的基础上也能够实现数据的离散性。下面图片模拟分别使用AUTO_INCREMENT和AUTO_RANDOM定义主键字段时的流量可视化情况,可以看到使用AUTO_INCREMENT时有明显阶梯状的明亮斜线,这是因为写入总出现在Region末端,一个Region写完之后写一个新的Region。而使用AUTO_RANDOM之后写入则比较离散,说明热点已经被打散。

无主键表或非聚簇索引主键表时,使用SHARD_ROW_ID_BITS

对于无主键表或非聚簇索引主键表时,TiDB会使用一个隐式自增RowID来映射到底层的Key值,因此也会有上述所说的写入热点问题。针对这种情况,我们可以在表定义时通过指定SHARD_ROW_ID_BITS来将RowID打散写入不同的Region。关于SHARD_ROW_ID_BITS用法可参考 SHARD_ROW_ID_BITS | PingCAP 文档中心。本篇幅我们仍然给出一个使用示例,定义三张无主键表,对比三张表在写入时的流量可视化效果。

mysql> show create table test1; +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | test1 | CREATE TABLE`test1` ( `a` bigint(20) NOT NULL, `b` varchar(20) DEFAULT NULL, `c`int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> show create table test2; +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | test2 | CREATE TABLE`test2` ( `a` bigint(20) NOT NULL, `b`varchar(20) DEFAULT NULL,`c` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T! SHARD_ROW_ID_BITS=4 */ | +-------+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> show create table test3; +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | test3 | CREATE TABLE`test3` ( `a` bigint(20) NOT NULL, `b` varchar(20) DEFAULT NULL, `c` int(11) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin /*T! SHARD_ROW_ID_BITS=4 PRE_SPLIT_REGIONS=4*/ | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec)

依次对三张表写入1千万条记录,我们发现test1表(默认递增RowID)具有明显的写入热点情况。test2表(shard_row_id_bits=4)的写入热点比test1要好一些,因为虽然test2表的写入具有离散性,但是表未做预拆分。test3表(shard_row_id_bits=4 &pre_split_regions=4)的写入热点问题基本不存在,因为它的写入具有离散性,且表做了预拆分。

五.聚簇索引表与非聚簇索引表?

前面有提到非聚簇索引,这里补充说明一下相关概念。聚簇索引是TiDB 5.0版本中开始支持的,用来控制有主键表的数据存储方式。聚簇索引与非聚簇索引的区别在于:

聚簇索引表。表对应TiKV存储的Key值由主键列数据构成,存储一行是一个键值对。因此聚簇索引表的优势是插入效率更高(减少一次索引写入)主键查询效率更高(减少索引回表动作)。聚簇索引的缺点是可能会产生写热点问题,而这个问题可以通过非聚簇索引表一定程度来规避。

非聚簇索引表。表对应TiKV存储的Key值由TiDB隐式生成的RowID构成,而主键本质上是一个唯一索引。

TiDB当前的版本中默认创建的有主键表均为聚簇索引表(受@@global.tidb_enable_clustered_index参数影响),如果想创建为非聚簇索引表,可以在建表语句中增加NONCLUSTERED 关键字。

mysql> create table test6(a int primary key nonclustered, b int); Query OK, 0 rows affected (0.52 sec) mysql> show create table test6; +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | test6 | CREATE TABLE`test6` ( `a` int(11) NOT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`a`) /*T![clustered_index] NONCLUSTERED */ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin | +-------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> create table test7(a int primary key, b int); Query OK, 0 rows affected (0.52 sec) mysql> show create table test7; +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Table | Create Table | +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | test7 | CREATE TABLE`test7` ( `a` int(11) NOT NULL, `b` int(11) DEFAULT NULL, PRIMARY KEY (`a`) /*T![clustered_index] CLUSTERED */ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin | +-------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 1 row in set (0.01 sec)

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:从多套MySQL到单套TiDB的转变 骏伯网络管理平台应用案例
下一篇:体验 TiDB v6.0.0 之 Clinic 功能介绍与应用场景解析
相关文章