测试发现PostgreSQL在bitmap index scan时,如果要读入大量堆page,读IO的速度会远低于正常的顺序读,影响性能。
下面用一个例子说明这个问题。
台式机上的CentOS7.1虚机
消费级SSD
blockdev --setra设置为2048
创建测试表
每个page大概有107条记录,c1的范围是0~199,也就是大概每2个page中就有一个c1相同的记录,即按page算,c1的选择性差不多是50%。
数据是2GB,shared_buffers是256MB。
现在用c1作为条件,使用不同的执行计划进行查询,并且每次查询前都清一次OS缓存并重起PostgreSQL。
Bitmap index扫描145秒。
每个IO大小是16个扇区(8K),没有看到大的IO合并,IO队列深度也小于1,判断磁盘预读没有生效。
顺序扫描74秒。
index扫描76秒
通过上面的数据可以看出,顺序扫描和索引扫描都可以利用磁盘预读,但bitmap索引扫描不行。
微观上通过strace -p跟踪postgres后端进程看看索引扫描和bitmap索引扫描各自调用的API有什么区别。
以下是索引扫描调用的API片段
bitmap索引扫描调用的API片段
从上面的调用看,应该是fadvise64()使得磁盘预读失效。fadvise64()调用是由effective_io_concurrency参数控制的预读功能,effective_io_concurrency的默认值为1,它只对bitmap index scan有效。
下面将effective_io_concurrency禁用,发现性能有所提高,执行时间102秒,磁盘预读也生效了。
除了禁用,还有一个办法,就是将effective_io_concurrency设得更大,完全代替磁盘预读。这次效果更好,46秒就出结果了。
采用这个办法,IO请求大小还是16个扇区,但是队列深度也就是IO的并行度提高了。
再看看调用的API,fadvise64()和read()仍然是交替的,但fadvise64()会提前好几个周期就将相应的预读请求发给IO设备。
上面的场景需要扫描50%的堆page,下面看看只需扫描少量堆page的情况。
构造扫描1%堆page的查询,下面在不同effective_io_concurrency值的情况下bitmap index scan的执行结果。
从这个结果看,effective_io_concurrency的值为0还是为1,性能差别不大。
下面看看其它扫描方式的执行结果。
从这儿看好像Index Scan总比bitmap index Scan快,其实这和测试数据有关。测试数据的导入的方式决定了从index里取出的堆元组已经是按page顺序排列好的,所以没有发挥出bitmap调整元组顺序的效果。
PostgreSQL的预读依赖OS的磁盘预读和posix_fadvise()调用。(MySQL利用的是libaio,机制不同)
posix_fadvise()也就是effective_io_concurrency生效时,磁盘预读会失效,对于bitmap heap scan需要扫描大量位置相邻page的场景,性能不佳。
为优化bitmap heap scan的大量读IO,根据情况可以将effective_io_concurrency设为0或者设置为一个比较大的值。
上面这个例子中性能优化效果看上去并不是很大,但在另一个环境中用相同的方法优化tpch Q6查询效果就更加明显了。优化前执行时间1050秒,effective_io_concurrency设为0,250秒。effective_io_concurrency设为100,116秒。
如果不考虑effective_io_concurrency使磁盘预读失效的性能下降,bitmap heap scan的代价估算还是蛮合理的。 但是考虑这个因素的话,对于需要读取大量堆page的时候,比如20%以上随机分布的page,顺序读会更好。
根据相关文章的解释,如果read不连续,将会使磁盘预读失效。
http://os.51cto.com/art/200910/159067.htm
但是,实际检验的结果,通过特殊的索引和查询条件,构造一个完全跳跃式的read()序列,read()和lseek()交替出现,没有2个连续的read(),结果预读仍然有效。可见Linux对顺序读的判断并没有字面上那么简单。
http://blog.chinaunix.net/uid-20726500-id-5747918.html
http://blog.163.com/digoal@126/blog/static/163877040201421392811622/
http://mysql.taobao.org/monthly/2015/05/04/