高并发写入最佳实践
分布式融合数据库HTAP支持线性扩展,通过均衡调度可以尽可能将业务的写负载均匀地分布到不同的计算或存储节点上,更好地利用上整体系统资源,以满足业务高并发写入场景下的数据存储需求。然而,在某些场景下,仍然可能会出现部分业务写负载不能被很好地分散,导致某些节点负载过高,引发性能瓶颈或不均衡的情况,无法发挥出分布式架构的优势,从而影响了整体的性能,这种情况通常称为写热点问题。
下文阐述了分布式融合数据库HTAP的数据分布原理,高并发写入场景下热点产生的原因,以及如何规避或者优化写热点问题。
数据分布原理
数据分片
分布式融合数据库HTAP采用 Key-Value 模型作为数据的存储模型,数据按范围分片存储,每个数据分片负责存储一段 Key 范围(从 StartKey 到 EndKey 的左闭右开区间)的数据,每个行存节点会负责多个数据分片。当一个分片持续写入数据,导致分片数据过大,超过一定阈值,系统会将该数据分片分裂为两个数据分片。相反的,如果相邻的两个数据分片过小,系统则会自动触发将它们合并成一个数据分片。
数据采用多副本存储,通过 Multi-Raft 协议实现数据强一致性,确保少数副本发生故障时不影响可用性。每个分片对应一个Raft Group,由 Raft Leader 负责处理数据的读写。分布式融合数据库HTAP的管理节点会动态地将分片 Raft Leader 调度到不同的存储节点上,实现负载均衡。
SQL到KV映射
分布式融合数据库HTAP对外兼容 MySQL 协议和 SQL 语法,存储的数据包括以下两种:
- 表数据:表中每一行的数据(聚簇索引也是一种特殊的表数据)。
- 索引数据:除聚簇索引以外的其他索引数据。
术语解释
- TableID:系统为每个表分配的一个 ID,目的是保证同一个表数据放在一起,方便全表扫描、范围查询等查找任务。
- RowID:表内标识一行记录的唯一 ID。当表有单列整型主键时,会直接使用该主键值来作为 RowID;当表没有主键或表主键是非聚簇索引时,系统会自动分配一个隐式 RowID。
- IndexID: 与 TableID 类似,分布式融合数据库HTAP会为表中每个索引分配了一个索引 ID。
- indexedColumnsValue:由索引列值组成的字符串。
表数据与 KV 映射关系
数据表中的每行数据,都会映射成一个 (Key, Value) 键值对,映射规则存在以下两种情况:
-
表没有主键,或表有单列整型主键或其他非聚簇索引主键(含联合主键、非整型单列主键)。
Key: t{TableID}_r{RowID} Value: [col1, col2, col3, col4, ...]
当表有单列整型主键时,会直接使用该主键值来作为 RowID。
-
聚簇索引主键。
Key: t{TableID}_i{IndexID}_indexedColumnsValue Value: [col1, col2, col3, col4, ...]
索引数据与 KV 映射关系
分布式融合数据库HTAP支持非聚簇索引型主键和二级索引(包括唯一索引和非唯一索引)。按照索引是否唯一,有如下两种情况,存储规则如下:
-
表的唯一索引或非聚簇索引型主键,Key 存储的是索引信息,而 Value 则存储上述行数据中的 RowID。用户可以根据索引查询到表数据 RowID,并回表去查询对应的行记录。
Key: t{TableID}_i{IndexID}_indexedColumnsValue Value: RowID
-
表的非唯一索引映射规则如下。由于索引非唯一,一个索引值可能对应多行数据,因此 Key 加上了 RowID 以保证 Key 的唯一。
Key: t{TableID}_i{IndexID}_indexedColumnsValue_{RowID} Value: null
最佳实践
从上述数据映射规则可知,存在着以下这些写热点场景:
- 系统为表分配的隐式 RowID 存在写热点问题。
当表没有主键,或表有非聚簇索引作为主键(含联合主键、非整型单列主键)时,系统会为表数据自动分配 RowID。由于隐式 RowID 是递增的,在表 insert 的过程中插入的行只会追加到表数据最后一个分片,导致该分片成为热点。 - 表采用递增型单列整型主键可能存在写热点问题。
对于单列整型主键,系统直接使用该值作为 RowID。如果主键值是递增的,也会引发跟系统分配的隐式 RowID 一样的写热点问题。单列整型主键常见的热点场景包括自增 ID 作为主键、采用基于时间戳生成的 ID(如雪花算法)作为主键等。 - 当表其他索引的值出现递增趋势时,也存在写入热点问题。
业务在插入、修改、删除数据时,如果生成的联合主键、单列非整型主键或其他索引的值出现递增、频繁重复或相邻的情况,也会导致表数据或索引数据的某个数据分片成为写入热点。
以下是针对写热点问题的最佳实践,旨在帮助用户更有效地使用数据库,避免在设计中引入写热点问题,并通过一定的优化措施缓解已出现的写热点问题。
设计规避
- 创建数据表时一定要创建主键,且优先使用聚簇索引主键,这样可以规避表数据的系统隐式 RowID 热点问题。
- 对于单列整型主键,避免使用自增ID,建议使用随机值,或在建表时添加 AUTO_RANDOM 属性指定由系统自动生成随机值,如下所示:
# 建表时主键指定 AUTO_RANDOM 属性 CREATE TABLE t (a BIGINT PRIMARY KEY AUTO_RANDOM, b varchar(255)); # 插入新记录 INSERT INTO t (b) VALUES ("test"); # 获取上次分配的主键值 SELECT LAST_INSERT_ID();
- 除了单列整型主键,对于其他主键或索引,应避免频繁插入相邻值或大量重复值。
热点优化
- 对于系统自动分配隐式 RowID 导致热点的场景,可以通过设置 SHARD_ROW_ID_BITS 把 RowID 打散写入多个不同的分片进行优化,缓解表热点问题,使用方法如下所示:
# SHARD_ROW_ID_BITS = 4 表示 2^4=16 个分片 ALTER TABLE t SHARD_ROW_ID_BITS = 4;
- 新建表的大批量写入热点,通过 Split Region 预切分来解决。
对于新建的表,开始时整个表只有一个分片,如果发生大批量写入则会造成热点。为了解决这种场景中的热点问题,可以使用 Split Region 方法预先为表切分出多个分片,并打散到各个存储节点上。Split Region 支持均匀切分和不均匀切分两种用法:# 均匀切分:BETWEEN lower_value AND upper_value REGIONS region_num 语法是通过指定数据的上、下边界和分片数量,然后在上、下边界之间均匀切分出 region_num 个分片。 SPLIT TABLE table_name [INDEX index_name] BETWEEN (lower_value) AND (upper_value) REGIONS region_num # 不均匀切分:BY value_list… 语法将手动指定一系列的点值,然后根据这些指定的点值切分出多个分片,适用于数据不均匀分布的场景。 SPLIT TABLE table_name [INDEX index_name] BY (value_list) [, (value_list)] ...