写点什么

关于如何优化 TiDB 中的写热点问题

  • 2024-02-02
    北京
  • 本文字数:8214 字

    阅读完需:约 27 分钟

作者: 数据源的 TiDB 学习之路原文来源:https://tidb.net/blog/79afb8f2


一.   什么是热点问题?


所谓热点,就是说数据库在某一段时间内的读或写请求都在某一个节点或少数节点上,导致整个集群的负载不均衡而引起整体效率下降。举个最简单的例子,一个 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 中写入导致一个新的热点。


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


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


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

  2. 避免键值数据递增写入。对于有主键表,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)
复制代码


发布于: 刚刚阅读数: 3
用户头像

TiDB 社区官网:https://tidb.net/ 2021-12-15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
关于如何优化TiDB中的写热点问题_实践案例_TiDB 社区干货传送门_InfoQ写作社区