记工作中遇到的一个性能问题

 

工作中遇到一个问题,更新schema的过程发现耗时太长,30多s。导致其他一系列流程阻住。记录一下这个问题的分析解决。

开始觉得估计是数据量大sql执行时间过长,整个过程分为3条sql,也就是schema被分表成为的三个表的查询:

    select * from __all_table;  //主键table_id
    select * from __all_column; //主键table_id,column_id
    select * from __all_join;//主键left_table_id, let_column_id

之后在内存里面做Join。由于是有序的,所以join过程直接采用了二分查找。之于没有用散列做Join或者直接 SQL语句里做Join的原因,只能说是历史遗留问题了。

在三个执行过程中加计时,跑一下,发现结果如下:

    all_table表查询,建立TableSchema对象并插入Array,20+s
    all_column查询, 二分找到对应的TableSchema,填入Column,10+s
    all_join查询,二分找到对应的TableSchema,填入JoinInfo,0.xs,忘了

问题很明显出在第一个上面。Perf分析一下程序执行过程,发现大量时间耗费在TableSchema的Operator=上面。我们的编程习惯是自己实现Operator=,拷贝构造直接调用自己的=重载。所以要么是拷贝构造占用了大量时间,要么是赋值占用了大量时间。

同时,在命令行执行一下SQL查询,发现第一条SQL查询花费1s左右,而第二条花费10s,第三条表里没有数据。从数据量来看,大概也是这个数量级,因为column表里面存放所有table的列信息,意味着平均每个表有10个列。这条SQL在客户端默认的超时时间下会直接Timeout。

考虑这个过程里面,运算符=的调用确实也只可能发现在第一个过程,因为后面都是直接填充里面的属性值。赋值的操作只有可能发生在动态表扩展。为了屏蔽一下扩展再分析,我们直接在Array建立前Reserve一下内存空间。也就是考虑到有18000+张表,直接Reserve(20000)。

重跑发现总时间直接降到了13s,去掉第二条sql的执行时间(第三个过程忽略),第一个过程在1s之内返回。所以问题就处在动态表扩展上。

我们知道算法上的动态表设计,每次扩展的时候是两倍申请空间,拷贝之前的对象到新地址空间,再析构之前的对象。这也就意味着20000张表扩展了log2(20000),15次的样子(可能有个默认reserve数值)。当然问题也可以通过reserve这么解决了,但是流式接口,不能在数据迭代完毕之前知道有多少行数据。所以需要额外发起一次count()查询。但是再查询一次的开销还不如浪费点空间。

继续分析,减少reserve值到10000发现根本没有效果,还是一样的耗费20s,这就意味着之前的所有expand的影响都没有最后一次大。但是这个明显不合理,因为1+2+4+…2^n = 2^(n+1) - 1,意味着之前所有的拷贝合并起来一定跟最后一次拷贝数量相当。时间上也应该占用一半。考虑到大内存分配跟小内存分配在我们的内存池里面开销差异不大,所以还是扩展上的问题。

在Array的extend上加日志,继续跑。发现问题就在这里了,有大量的expand,这就意味着扩展的开销远大于我们考虑的情形。看ObArray的源码,发现实现上并不是我们理解的二倍扩展,ObArray的实现是每次扩展一个固定的大小(block_size/元素大小) ,而这个blocksize默认值是64k,算算我们的TableSchema有3k大小,也就是每20个就扩展了一次。在18000+的表构建过程中,扩展了差不多1000次。

针对这个问题,我们把ObArray的扩展大小从64k提升到2M,扩展次数降低到100次以内。时间上,虽然不如直接reserve(20000),但是已经回到2s,整个过程在14s内。基本达到要求。

后续就需要分析SQL查询是否能加快了。当然我把_all_column加入TableSchema的那个过程也reserve了一下,避免TableSchema里面column数组在每次push一个列的时候引起扩展,因为平均看,一个table所包含的平均列数也就10左右。性能待测试。

从这个问题吸取的教训就是,动态表能之前确定长度的最好直接Reserve,否则相当于有多次申请内存-析构-拷贝构造(或赋值,我们之前是赋值)-释放内存的开销。同时,Perf是个好工具,虽然我还不是很熟悉。

关于vector的扩容见这里