本文通过阐述一个高并发批量写入数据到 TiDB 的典型场景中,TiDB 中常见的问题,给出一个业务的最佳实践,避免业务在开发的时候陷入 TiDB 使用的 “反模式”。
面向的对象
场景
TiDB 数据分布原理
图 1 TiDB 数据概览
只要业务的写入没有 AUTO_INCREMENT 的主键或者单调递增的索引(也即没有业务上的写入热点,更多细节参见 TiDB 正确使用方式)。从原理上来说,TiDB 依靠这个架构,是可以线性扩展读写能力,并且可以充分利用分布式的资源的。这一点上 TiDB 尤其适合高并发批量写入场景的业务。
简单的例子
有一张简单的表:
CREATE TABLE IF NOT EXISTS TEST_HOTSPOT(
id BIGINT PRIMARY KEY,
age INT,
user_name VARCHAR(32),
email VARCHAR(128)
)
这个表结构非常简单,除了 id 为主键以外,没有额外的二级索引。写入的语句如下,id 通过随机数离散生成:
INSERT INTO TEST_HOTSPOT(id, age, user_name, email) values(%v, %v, '%v', '%v');
负载是短时间内密集地执行以上写入语句。
到目前为止,似乎已经符合了我们上述提到的 TiDB 最佳实践了,业务上没有热点产生,只要我们有足够的机器,就可以充分利用 TiDB 的分布式能力了。要验证这一点,我们可以在实验环境中试一试(实验环境部署拓扑是 2 个 TiDB 节点,3 个 PD 节点,6 个 TiKV 节点,请大家忽略 QPS,这里的测试只是为了阐述原理,并非 benchmark):
图 2 监控截图
客户端在短时间内发起了 “密集” 的写入,TiDB 收到的请求是 3K QPS。如果没有意外的话,压力应该均摊给 6 个 TiKV 节点。但是从 TiKV 节点的 CPU 使用情况上看,存在明显的写入倾斜(tikv - 3 节点是写入热点):
图 4 监控截图
Raft store CPU 代表 raftstore 线程的 CPU 使用率,通常代表着写入的负载,在这个场景下 tikv-3 是 raft 的 leader,tikv-0 跟 tikv-1 是 raft 的 follower,其他的 tikv 节点的负载几乎为空。
从 PD 的监控中也可以印证这一点:
图 5 监控截图
反直觉的原因
[CommonPrefix + TableID, CommonPrefix + TableID + 1)
对于在短时间内的大量写入,它会持续写入到同一个 Region。
图 6 TiKV Region 分裂流程
上图简单描述了这个过程,持续写入,TiKV 会将 Region 切分。但是由于是由原 Leader 所在的 Store 首先发起选举,所以大概率下旧的 Store 会成为新切分好的两个 Region 的 Leader。对于新切分好的 Region 2,3。也会重复之前发生在 Region 1 上的事情。也就是压力会密集地集中在 TiKV-Node 1 中。
在持续写入的过程中, PD 能发现 Node 1 中产生了热点,它就会将 Leader 均分到其他的 Node 上。如果 TiKV 的节点数能多于副本数的话,还会发生 Region 的迁移,尽量往空闲的 Node 上迁移,这两个操作在插入过程,在 PD 监控中也可以印证:
解决方法
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)]
读者可能会有疑问,为何 TiDB 不自动将这个切分动作提前完成?大家先看一下下图:
图 8 Table Region Range
从图 8 可以知道,Table 行数据 key 的编码之中,行数据唯一可变的是行 ID (rowID)。在 TiDB 中 rowID 是一个 Int64 整形。那么是否我们将 Int64 整形范围均匀切分成我们要的份数,然后均匀分布在不同的节点就可以解决问题呢?
答案是不一定,需要看情况,如果行 id 的写入是完全离散的,那么上述方式是可行的。但是如果行 id 或者索引是有固定的范围或者前缀的。例如,我只在 [2000w, 5000w) 的范围内离散插入,这种写入依然是在业务上没有热点的,但是如果按上面的方式切分,那么就有可能在开始也还是只写入到某个 Region。
作为通用的数据库,TiDB 并不对数据的分布作假设,所以开始只用一个 Region 来表达一个表,等到真实数据插入进来以后,TiDB 自动地根据这个数据的分布来作切分。这种方式是较通用的。
所以 TiDB 提供了 Split Region 语法,来专门针对短时批量写入场景作优化,下面我们尝试在上面的例子中用以下语句提前切散 Region,再看看负载情况。
由于测试的写入是在正数范围内完全离散,所以我们可以用以下语句,在 Int64 空间内提前将表切散为 128 个 Region:
SPLIT TABLE TEST_HOTSPOT BETWEEN (0) AND (9223372036854775807) REGIONS 128;
切分完成以后,可以通过 SHOW TABLE test_hotspot REGIONS; 语句查看打散的情况,如果 SCATTERING 列值全部为 0,代表调度成功。
也可以通过 table-regions.py 脚本,查看 Region 的分布,已经比较均匀了:
[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 监控截图
更复杂一些的情况
示例:
create table t (a int, b int) shard_row_id_bits = 4 pre_split_regions=·3;
参数配置
关闭 TiDB 的 Latch 机制
[txn-local-latches]
enabled = false
TiDB | 国产数据库
趋势所驱 拥抱开源
简单易用 面向未来
识别二维
查看更多课程详情
电话咨询:18501287439(同微信号)
东方龙马与PingCAP大学正式达成合作,共同助力开源数据库生态建设
| 北京 | 上海 | 广州 | 成都 |
4008-906-960