批量插入性能优化

ywz888
发布于 2022-10-10 16:13
浏览
0收藏

引言

数据库插入操作的语句为insert into table values (a1, b1),涉及到SQL层和存储层,其中SQL层需要解析SQL语句,生成抽象语法树(AST),计算表达式等,存储层需要判断主键冲突,包括增量数据和基线数据上的主键冲突,如果是非重复主键,则将数据插入到增量数据中。上条插入语句只插入一行数据,称之为单条插入,相应地,还可以在一条语句中插入多行数据,insert into table values (a1, b1), (a2, b2), (a3, b3),称之为批量插入。批量插入的多行数据作为一个事务,所有数据插入成功,或者所有数据插入失败,不会出现部分数据插入成功的情况。批量插入相对于单条插入在性能上有很大优势,SQL解析只需要做一次,事务只需要做一次,因此理应在相同的时间内插入更多行数据。

单行插入引擎

此前,OceanBase的单条插入与批量插入使用的是同一套接口,从SQL层读取一行,检查冲突,插入数据,然后反复重复这个过程,直到没有数据为止。这样的代码看起来非常优雅,却没有利用到批量插入的特点而做针对性的优化。

批量插入引擎

批量插入引擎每次可以读取一批数据,比如500行,然后做批量检查冲突,再批量插入到增量数据中(内存B+树),目前做的只有批量读和检查冲突,批量插入留到以后再做。看似很简单的优化,性能却提升了很多,在递增插入场景,Sysbench bulk insert的单线程测试中,无基线数据时,性能提升30%,有基线数据时,性能提升了100%。性能提升的原因有如下几点:

系统层面:

  1. 正在处理的一批数据可以始终在CPU Cache中,L1 Cache的大小是32KB,一行的大小为32 bytes(元数据,指针等),可以存储1024行,而读L1 Cache的性能是读内存性能的100倍。
  2. CPU不仅可以Cache数据,还可以Cache指令,在单条插入的时候,在一定时间内总是执行不同的指令,因此很难Cache,每次都需要从内存中取指令,将指令解码后,才能再去取数据,而在批量插入中,在一个紧凑的循环中,每次都是执行相同的指令,因此这些指令基本上可以在Cache中。
  3. CPU访问内存的过程为,进程的虚拟内存地址通过查找TLB(硬件高速缓存,空间较小),Page Table(内存中)转化为内存的物理地址,若TLB中找不到对应的虚拟地址,需要访问内存中的Page Table。若同时处理一个500行的数组,TLB的命中率会大很多,而访问TLB的速度是内存的100倍。
  4. CPU有预取内存功能,当从SQL中读到的行需要转换为存储层中的行时,以前是读内存,转换,读内存,转换,而现在是完全并行起来的,转换完一行之后,后面的行已经从内存中被预取到CPU Cache中了,而且CPU读内存的单位是Cache Line是64 bytes,每次可以读两行,而以前单行处理的时候,是把这个能力浪费了的。
  5. 存储层从SQL拿数据的时候,会调用一个虚函数get_next_row,C++里虚函数是通过虚函数表实现的,对象里有一个指向虚函数表的指针,每次调用函数的时候,需要通过指针找到这个表,然后在表里再通过一个指针,找到相应的函数实现,也就是每次调用get_next_row都有两次随机内存访问,而改成批量之后,就少了大量的这种操作,比如有4万行数据,以前需要4万次虚函数调用,而现在只需要80次。

算法层面:

  1. 检查主键冲突的时候,由于基线数据是静态的,最大值不变,而后面插入的数据往往是越来越大的,因此只需要比较一下这一批数据的最小值和静态数据的最大值即可,减少了大量的冲突检测。
  2. 单行插入内存B+树时,每一行都需要从根节点搜索,直到相应的叶子节点,需要多次加读锁写锁,批量插入后,对一批数据做一个排序,然后将相应的数据直接插入到相应的叶子节点而不再从根节点搜索,减少了大量的比较和加锁操作,而且同一批数据基本在少量的叶子节点中,因此叶子节点基本都可以在CPU Cache中。


文章转载自公众号:OceanBase

分类
标签
已于2022-10-10 16:13:10修改
收藏
回复
举报
回复
    相关推荐