问题
进行测试时,预制完数据后立即进行SELECT测试查询场景,或者预制完数据立即重启服务,然后进行SELECT场景测试,监控磁盘负载时,发现写负载特别大。
这就有疑惑了,测试场景都是查询,没有INSERT\UPDATE\DELETE,哪来的写呢?
分析
最简单的方法就是在有负载时,跟踪其堆栈了,通过pstack跟踪checkpoint、pgwriter、stat进程,监控堆栈中是否有write了。有这个思路,立即做检查。
发现stat进程中有write,继续分析,发现该进程会更新统计信息持久化到pg_stat目录下文件。但是该文件更新数据量没那么大,监控到磁盘负载每秒达到大几十M,不太可能是因为这个导致的。
那么,继续跟踪用户连接上来后fork的进程,发现有大量的write。用户只有select,难道之前因为预制产生很多死记录,导致需要刷写脏页?那么测试前执行vacuum然后再进行测试是否还会有写负载呢?说做就做,立即执行vacuum,再进行测试。奇迹发生了,磁盘写负载立即下降下来了。到此,问题就知道在哪了,因为内存中有很多脏页,当进行select负载测试时,需要不断从磁盘加载数据,此时需要将内存中数据页进行替换以存储磁盘页,若该内存页为脏,需要先将它刷写下来。
编译一个debug版本,进行gdb跟踪可以详细了解到底write发生在哪个流程中。我们编译好后,立即替换postgres可执行文件,重启,在write函数上打断点,进行SELECT测试。
跟踪到heapam_index_fetch_tuple函数进行索引扫描时,会调用到smgrwrite。Smgrwrite函数为磁盘IO的函数,该函数对应mdwrite,继续调用FileWrite->pgwrite64。
根据堆栈信息,分析源码:
heapam_index_fetch_tuple-> hscan->xs_cbuf = ReleaseAndReadBuffer(hscan->xs_cbuf, hscan->xs_base.rel, ItemPointerGetBlockNumber(tid)); |- ReadBuffer |- ReadBufferExtended |- ReadBuffer_common |- bufHdr = BufferAlloc(smgr, relpersistence, forkNum, blockNum,strategy, &found); |- INIT_BUFFERTAG(newTag, smgr->smgr_rnode.node, forkNum, blockNum); newHash = BufTableHashCode(&newTag); newPartitionLock = BufMappingPartitionLock(newHash); buf_id = BufTableLookup(&newTag, newHash); if(buf_id >=0){ ... 内存中命中改页 return 该buf; } for(;;){//需要在内存找一个空闲页以供存储磁盘上加载的页 buf = StrategyGetBuffer(strategy, &buf_state); oldFlags = buf_state & BUF_FLAG_MASK; if (oldFlags & BM_DIRTY){ FlushBuffer(buf, NULL); } |- } FlushBuffer(buf, NULL); recptr = BufferGetLSN(buf); XLogFlush(recptr); smgrwrite(reln, buf->tag.forkNum, buf->tag.blockNum, bufToWrite, false); |- mdwrite->FileWrite->pgwrite64
和之前分析一致,读取数据页时,需要先从内存中找改页,若命中,则直接返回。若不命中,需要找一个空闲数据页,没有空闲页就会进行数据页驱逐,若此时该数据页是脏页,那么就需要先将它刷写下去。当然刷写前需要先将脏页对应的日志持久化。
这就需要注意了,进行测试时,预制完数据需要将其进行vacuum,消除后续进行vacuum对测试的影响。要不然,对测试结果影响太大了。