批量写库操作,如何优化?
引言
数据库插入操作的语句如下:
insert into table values (a1, b1)
涉及到SQL层和存储层,其中SQL层需要解析SQL语句,生成抽象语法树(AST),计算表达式等,存储层需要判断主键冲突,包括增量数据和基线数据上的主键冲突,如果是非重复主键,则将数据插入到增量数据中。
上条插入语句只插入一行数据,称之为单条插入,相应地,还可以在一条语句中插入多行数据,称之为批量插入。
insert into table values (a1, b1), (a2, b2), (a3, b3)
批量插入的多行数据作为一个事务,所有数据插入成功,或者所有数据插入失败,不会出现部分数据插入成功的情况。批量插入相对于单条插入在性能上有很大优势,SQL解析只需要做一次,事务只需要做一次,因此理应在相同的时间内插入更多行数据。
1. 单行插入引擎
此前,OceanBase的单条插入与批量插入使用的是同一套接口,从SQL层读取一行,检查冲突,插入数据,然后反复重复这个过程,直到没有数据为止。这样的代码看起来非常优雅,却没有利用到批量插入的特点而做针对性的优化。
2. 批量插入引擎
批量插入引擎每次可以读取一批数据,比如500行,然后做批量检查冲突,再批量插入到增量数据中(内存B+树),目前做的只有批量读和检查冲突,批量插入留到以后再做。看似很简单的优化,性能却提升了很多,在递增插入场景,Sysbench bulk insert的单线程测试中,无基线数据时,性能提升30%,有基线数据时,性能提升了100%。性能提升的原因有如下几点:
2.1 系统层面
正在处理的一批数据可以始终在CPU Cache中,L1 Cache的大小是32KB,一行的大小为32 bytes(元数据,指针等),可以存储1024行,而读L1 Cache的性能是读内存性能的100倍。
CPU不仅可以Cache数据,还可以Cache指令,在单条插入的时候,在一定时间内总是执行不同的指令,因此很难Cache,每次都需要从内存中取指令,将指令解码后,才能再去取数据,而在批量插入中,在一个紧凑的循环中,每次都是执行相同的指令,因此这些指令基本上可以在Cache中。
CPU访问内存的过程为,进程的虚拟内存地址通过查找TLB(硬件高速缓存,空间较小),Page Table(内存中)转化为内存的物理地址,若TLB中找不到对应的虚拟地址,需要访问内存中的Page Table。若同时处理一个500行的数组,TLB的命中率会大很多,而访问TLB的速度是内存的100倍。
CPU有预取内存功能,当从SQL中读到的行需要转换为存储层中的行时,以前是读内存,转换,读内存,转换,而现在是完全并行起来的,转换完一行之后,后面的行已经从内存中被预取到CPU Cache中了,而且CPU读内存的单位是Cache Line是64 bytes,每次可以读两行,而以前单行处理的时候,是把这个能力浪费了的。
存储层从SQL拿数据的时候,会调用一个虚函数get_next_row,C++里虚函数是通过虚函数表实现的,对象里有一个指向虚函数表的指针,每次调用函数的时候,需要通过指针找到这个表,然后在表里再通过一个指针,找到相应的函数实现,也就是每次调用get_next_row都有两次随机内存访问,而改成批量之后,就少了大量的这种操作,比如有4万行数据,以前需要4万次虚函数调用,而现在只需要80次。
2.2 算法层面
检查主键冲突的时候,由于基线数据是静态的,最大值不变,而后面插入的数据往往是越来越大的,因此只需要比较一下这一批数据的最小值和静态数据的最大值即可,减少了大量的冲突检测。
单行插入内存B+树时,每一行都需要从根节点搜索,直到相应的叶子节点,需要多次加读锁写锁,批量插入后,对一批数据做一个排序,然后将相应的数据直接插入到相应的叶子节点而不再从根节点搜索,减少了大量的比较和加锁操作,而且同一批数据基本在少量的叶子节点中,因此叶子节点基本都可以在CPU Cache中。
·END·
相关阅读:
有没有那么一瞬间,你也曾有过“失业焦虑”? 浅析分布式系统中的补偿机制设计问题 聊聊分布式日志系统的设计与实践 执行个 DEL 竟然也会阻塞 Redis?深挖一下果然不简单 PHP 中数组是如何灵活支持多数据类型的? 一文带你看通透,MySQL事务ACID四大特性实现原理
参考文章:https://www.modb.pro/db/508155
版权申明:内容来源网络,仅供学习研究,版权归原创者所有。如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!
专注架构技术研究,一起跨越职业瓶颈!
关注公众号,免费领学习资料
如果您觉得还不错,欢迎关注和转发~