MySQL调优原创
# 10亿条数据如何快速插入MySQL
- 单条数据1kb
- 10亿数据不是一个大文件,而是若干个文件
- 有序导入
# 数据表单表能支持10亿吗
答案是不能,单表推荐是2000w以下,MySQL索引数据结构是B+树,全量数据存储在主键索引,也就是聚簇索引的叶子结点上,B+树插入和查询的性能和B+树层数直接相关,2000W以下是3层索引,而2000w以上则可能为四层索引
Mysql b+索引的叶子节点每页大小16K。当前每条数据正好1K,所以简单理解为每个叶子节点存储16条数据。b+索引每个非叶子节点大小也是16K,但是其只需要存储主键和指向叶子节点的指针,我们假设主键的类型是 BigInt,长度为 8 字节,而指针大小在 InnoDB 中设置为 6 字节,这样一共 14 字节,这样一个非叶子节点可以存储 16 * 1024/14=1170。
层数 | 最大数据量 |
---|---|
2 | 1170 * 16 = 18720 |
3 | 1170 * 1170 * 16 = 21902400 = 2000w |
4 | 1170 * 1170 * 1170 * 16 = 25625808000 = 256亿 |
# 如何高效的写入数据库
相比较于单条,批量写入性能更高,每次写入的数量根据机器的性能决定
批量写入数据如何保证成功? 使用事务,保证同时成功、同时失败
事务失败需要重试,当重试了N次后,可以考虑单条或者减少批次的数量查看原因
写入数据库时不要建任何辅助索引,索引排序有较大的消耗
# 要不要分库
mysql 单库的并发写入是有性能瓶颈的
数据使用SSD存储,性能会更好,如果是HDD,顺序读写性能会更好,但是并发支持差,单磁头需要反复寻道写入,耗时大大增加;SSD厂商不同,性能也表现不同,没有测试的情况下,其实并不知道实际的性能
所以在设计上要更加灵活,需要支持以下能力
- 支持配置数据库的数量
- 支持配置并发写表的数量,(如果MySQL是HDD磁盘,只让一张表顺序写入,其他任务等待)
# 怎么保证写入的数据有序
使用自定义的主键
根据文件后缀,如
- index_90.txt 被写入 数据库database_9,table_0 ,
- index_67.txt被写入数据库 database_6,table_7。
并发执行,配合任务id taskID
# 怎么协调文件任务和写数据库任务
读取文件任务与写入数据任务分离,假设100个读取任务,每个任务读取一批数据,立即写入数据库是否可以呢?机器由于有瓶颈,无法满足1个库同时并发大批量写入10个表,所以100个任务同时写入数据库,势必导致每个库同时有10个表同时在顺序写,这加剧了磁盘的并发写压力。为尽可能提高速度,减少磁盘并发写入带来的性能下降, 需要一部分写入任务被暂停的。那么读取任务需要限制并发度吗?不需要。
假设写入任务和读取任务合并,会影响读取任务并发度。初步计划读取任务和写入任务各自处理,谁也不耽误谁。但实际设计时发现这个方案较为困难。
最初的设想是引入Kafka,即100个读取任务把数据投递到Kafka,由写入任务消费kafka写入DB。100个读取任务把消息投递到Kafka,此时顺序就被打乱了,如何保证有序写入数据库呢?我想到可以使用Kafka partition路由,即读取任务id把同一任务的消息都路由到同一个partition,保证每个partition内有序消费。
要准备多少个分片呢?100个很明显太多,如果partition小于100个,例如10个。那么势必存在多个任务的消息混合在一起。如果同一个库的多个表在一个Kafka partition,且这个数据库只支持单表批量写入,不支持并发写多个表。这个库多个表的消息混在一个分片中,由于并发度的限制,不支持写入的表对应的消息只能被丢弃。所以这个方案既复杂,又难以实现。
所以最终放弃了Kafka方案,也暂时放弃了将读取和写入任务分离的方案。
最终方案简化为 读取任务读一批数据,写入一批。即任务既负责读文件、又负责插入数据库。
# 如何保证任务的可靠性
如果读取任务进行到一半,宕机或者服务发布如何处理呢?或者数据库故障,一直写入失败,任务被暂时终止,如何保证任务再次拉起时,再断点处继续处理,不会存在重复写入呢?
刚才我们提到可以 为每一个记录设置一个主键Id,即 文件后缀index+文件所在行号。可以通过主键id的方式保证写入的幂等。
所以也无需数据库自增主键ID,可以在批量插入时指定主键ID。
如果另一个任务也需要导入数据库呢?如何实现主键ID隔离,所以主键ID还是需要拼接taskId。例如{taskId}{fileIndex}{fileRowNumber} 转化为Long类型。
如果taskId较大,拼接后的数值过大,转化为Long类型可能出错。
最重要的是,如果有的任务写入1kw,有的其他任务写入100W,使用Long类型无法获知每个占位符的长度,存在冲突的可能性。而如果拼接字符串{taskId}{fileIndex}{fileRowNumber} ,新增唯一索引,会导致插入性能更差,无法满足最快导入数据的诉求。所以需要想另一个方案。
可以考虑使用Redis记录当前任务的进度。例如Redis记录task的进度,批量写入数据库成功后,更新 task进度。
INCRBY KEY_NAME INCR_AMOUNT
指定当前进度增加100,例如 incrby task_offset_{taskId} 100。如果出现批量插入失败的,则重试插入。多次失败,则单个插入,单个更新redis。要确保Redis更新成功,可以在Redis更新时 也加上重试。
如果还不放心Redis进度和数据库更新的一致性,可以考虑 消费 数据库binlog,每一条记录新增则redis +1 。
如果任务出现中断,则首先查询任务的offset。然后读取文件到指定的offset继续 处理。
# 如何协调读取任务的并发度
为了避免单个库插入表的并发度过高,影响数据库性能。可以考虑限制并发度。如何做到呢?
既然读取任务和写入任务合并一起。那么就需要同时限制读取任务。即每次只挑选一批读取写入任务执行。
# 设计最小任务调度模型--golang实现
小型调度模型,不考虑中间件
- 任务队列
- 已就绪处理单元
- 重试队列
初始化任务队列,以及初始化处理单元列表,两个数据结构都是可扩容的列表,每一个处理单元都由一个协程构成,