TiDB 高并发写入常见热点问题及规避方法
本文通过阐述一个高并发批量写入数据到 TiDB 的典型场景中,TiDB 中常见的问题,给出一个业务的最佳实践,避免业务在开发的时候陷入 TiDB 使用的 “反模式”。
面向的对象
本文主要面向对 TiDB 有一定了解的读者,读者在阅读本文之前,推荐先阅读讲解 TiDB 原理的三篇文章(讲存储,说计算,谈调度),以及 TiDB Best Practice。
场景
高并发批量插入场景,通常存在于业务系统中的批量任务中,例如清算以及结算等业务。它存在以下显著的特点:
数据量大
需要短时间内将历史数据入库
需要短时间内读取大量数据
这就对 TiDB 提出了一些挑战:
写入/读取能力是否可以线性水平扩展
数据在持续大并发写入,性能是否稳定不衰减
对于分布式数据库来说,除了本身的基础性能之外,最重要的就是充分利用所有节点能力,避免出现单个节点成为瓶颈。
TiDB 数据分布原理
如果要解决以上挑战,需要从 TiDB 数据切分以及调度的原理开始讲起。这里只是作简单的说明,详细请大家参见:谈调度。
图 1 TiDB 数据概览
但是软件世界里,没有银弹。具体的事情还需要具体分析。我们接下来就通过一些简单的负载来探讨 TiDB 在这种场景下,需要如何被正确的使用,才能达到此场景理论上的最佳性能。
简单的例子
有一张简单的表:
CREATE TABLE IF NOT EXISTS TEST_HOTSPOT(
id BIGINT PRIMARY KEY,
age INT,
user_name VARCHAR(32),
email VARCHAR(128)
)
INSERT INTO TEST_HOTSPOT(id, age, user_name, email) values(%v, %v, '%v', '%v');
图 2 监控截图
图 4 监控截图
图 5 监控截图
反直觉的原因
上面这个现象是有一些违反直觉的,造成这个现象的原因是:刚创建表的时候,这个表在 TiKV 只会对应为一个 Region,范围是:
[CommonPrefix + TableID, CommonPrefix + TableID + 1)
图 6 TiKV Region 分裂流程
在持续写入一段时间以后,整个集群会被 PD 自动地调度成一个压力均匀的状态,到那个时候才会真正利用整个集群的能力。对于大多数情况来说,这个是没有问题的,这个阶段属于表 Region 的预热阶段。
但是对于高并发批量密集写入场景来说,这个却是应该避免的。
那么我们能否跳过这个预热的过程,直接将 Region 切分为预期的数量,提前调度到集群的各个节点中呢?
解决方法
TiDB 在 v3.0.x 版本以及 v2.1.13 以后的版本支持了一个新特性叫做 Split Region。这个特性提供了新的语法:
SPLIT TABLE table_name [INDEX index_name] BETWEEN (lower_value) AND (upper_value) REGIONS region_num
SPLIT TABLE table_name [INDEX index_name] BY (value_list) [, (value_list)]
图 8 Table Region Range
SPLIT TABLE TEST_HOTSPOT BETWEEN (0) AND (9223372036854775807) REGIONS 128;
SHOW TABLE test_hotspot REGIONS;
语句查看打散的情况,如果 SCATTERING 列值全部为 0,代表调度成功。[root@172.16.4.4 scripts]# python table-regions.py --host 172.16.4.3 --port 31453 test test_hotspot
[RECORD - test.test_hotspot] - Leaders Distribution:
total leader count: 127
store: 1, num_leaders: 21, percentage: 16.54%
store: 4, num_leaders: 20, percentage: 15.75%
store: 6, num_leaders: 21, percentage: 16.54%
store: 46, num_leaders: 21, percentage: 16.54%
store: 82, num_leaders: 23, percentage: 18.11%
store: 62, num_leaders: 21, percentage: 16.54%
我们再重新运行插入负载:
图 11 监控截图
可以看到已经消除了明显的热点问题了。
当然,这里只是举例了一个简单的表,还有索引热点的问题。如何预先切散索引相关的 Region?
这个问题可以留给读者,通过 Split Region 文档 可以获得更多的信息。
更复杂一些的情况
如果表没有主键或者主键不是 int 类型,用户也不想自己生成一个随机分布的主键 ID,TiDB 内部会有一个隐式的 _tidb_rowid 列作为行 id。在不使用 SHARD_ROW_ID_BITS 的情况下,_tidb_rowid 列的值基本上也是单调递增的,此时也会有写热点存在。(查看什么是 SHARD_ROW_ID_BITS)
要避免由 _tidb_rowid 带来的写入热点问题,可以在建表时,使用 SHARD_ROW_ID_BITS 和 PRE_SPLIT_REGIONS 这两个建表 option(查看什么是 PRE_SPLIT_REGIONS)。
SHARD_ROW_ID_BITS 用来把 _tidb_rowid 列生成的行 ID 随机打散,pre_split_regions 用来在建完表后就预先 split region。注意:pre_split_regions 必须小于等于 shard_row_id_bits。
示例:
create table t (a int, b int) shard_row_id_bits = 4 pre_split_regions=·3;
SHARD_ROW_ID_BITS = 4 表示 tidb_rowid 的值会随机分布成 16 (16=2^4) 个范围区间。
pre_split_regions=3 表示建完表后提前 split 出 8 (2^3) 个 region。
在表 t 开始写入后,数据写入到提前 split 好的 8 个 region 中,这样也避免了刚开始建表完后因为只有一个 region 而存在的写热点问题。
参数配置
关闭 TiDB 的 Latch 机制
TiDB 2.1 版本中在 SQL 层引入了 latch 机制,用于在写入冲突比较频繁的场景中提前发现事务冲突,减少 TiDB 跟 TiKV 事务提交时写写冲突导致的重试。对于跑批场景,通常是存量数据,所以并不存在事务的写入冲突。可以把 TiDB 的 latch 关闭,以减少细小内存对象的分配:
[txn-local-latches]
enabled = false
💡文中划线部分均有跳转,点击【阅读原文】查看原版文章