开发者社区> 绛云> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

PostgreSQL ring buffer策略

简介: PostgreSQL ring buffer策略 When running a query that needs to access a large number of pages just once,such as VACUUM or a large sequential scan, a different strategy is used.
+关注继续查看

PostgreSQL ring buffer策略

When running a query that needs to access a large number of pages just once,such as VACUUM or a large sequential scan, a different strategy is used.A page that has been touched only by such a scan is unlikely to be needed again soon, so instead of running the normal clock sweep algorithm and blowing out the entire buffer cache, a small ring of buffers is allocated using the normal clock sweep algorithm and those buffers are reused for the whole scan. This also implies that much of the write traffic caused by such a statement will be done by the backend itself and not pushed off onto other processes.

引用了src/backend/storage/buffer/README中对ring buffer的介绍,ring buffer就是在当需要大量访问页面的情况下如vacuum或者大量的全表扫描时采用的一种特殊的策略。不会像正常的时钟扫描算法交换出整个缓冲区,而是在一小块缓冲区上使用时钟扫描算法,并会重用缓冲区完成整个扫描。这样就会避免大量全表扫描带来的缓冲区命中率的下降。

一、使用ring buffer策略的场景

/src/backend/storage/buffer/freelist.c/GetAccessStrategy

GetAccessStrategy(BufferAccessStrategyType btype)
{
    BufferAccessStrategy strategy;
    int            ring_size;

    /*
     * Select ring size to use.  See buffer/README for rationales.
     *
     * Note: if you change the ring size for BAS_BULKREAD, see also
     * SYNC_SCAN_REPORT_INTERVAL in access/heap/syncscan.c.
     */
    switch (btype)
    {
        case BAS_NORMAL:
            /* if someone asks for NORMAL, just give 'em a "default" object */
            return NULL;

        case BAS_BULKREAD:
            ring_size = 256 * 1024 / BLCKSZ;
            break;
        case BAS_BULKWRITE:
            ring_size = 16 * 1024 * 1024 / BLCKSZ;
            break;
        case BAS_VACUUM:
            ring_size = 256 * 1024 / BLCKSZ;
            break;

        default:
            elog(ERROR, "unrecognized buffer access strategy: %d",
                 (int) btype);
            return NULL;        /* keep compiler quiet */
    }

    /* Make sure ring isn't an undue fraction of shared buffers */
    ring_size = Min(NBuffers / 8, ring_size);

    /* Allocate the object and initialize all elements to zeroes */
    strategy = (BufferAccessStrategy)
        palloc0(offsetof(BufferAccessStrategyData, buffers) +
                ring_size * sizeof(Buffer));

    /* Set fields that don't start out zero */
    strategy->btype = btype;
    strategy->ring_size = ring_size;

    return strategy;
}

1.BAS_BULKREAD

批量读的情况,会分配256KB的内存区域,README中解释了这个大小的原因,原因是这个大小刚好适合L2缓存,这会让OS缓存到共享缓存传输效率更高,那什么情况才算批量读呢?

src/backend/access/heap/heapam.c/

if (!RelationUsesLocalBuffers(scan->rs_rd) &&
        scan->rs_nblocks > NBuffers / 4)
    {
        allow_strat = scan->rs_allow_strat;
        allow_sync = scan->rs_allow_sync;
    }
    else
        allow_strat = allow_sync = false;

    if (allow_strat)
    {
        /* During a rescan, keep the previous strategy object. */
        if (scan->rs_strategy == NULL)
            scan->rs_strategy = GetAccessStrategy(BAS_BULKREAD);
    }

这个意思就是说如果表不是临时表并且扫描的块数大于shared_buffer的1/4的块数,就会使用BAS_BULKREAD策略。

2.BAS_BULKWRITE

批量写的情况下,会分配16MB的空间,原因是较小的ring buffer会频繁的进行wal flush,降低写入的效率。批量写的情况为如下场景:

  • COPY FROM 命令
  • CREATE TABLE AS 命令
  • CREATE MATERIALIZED VIEW或者 REFRESH MATERIALIZED VIEW 命令
  • ALTER TABLE 命令

3.BAS_VACUUM

vacuum会使用256KB的内存区域,类似顺序扫描。如果有脏页,wal必须被写入才能重用这个buffer。

二、测试

pg_buffercache可以帮助我们观察shared_buffer中使用的情况

1.BAS_BULKREAD测试

#创建pg_buffercache插件
postgres=# create extension pg_buffercache ;
CREATE EXTENSION

#查看shared_buffer大小
postgres=# show shared_buffers ;
 shared_buffers
----------------
 128MB
(1 row)

#创建表
postgres=# create table test as  select generate_series(1,1000000);
SELECT 1000000

#查看表大小,表大小需要大于shared_buffer的1/4
postgres=# \dt+ test
                   List of relations
 Schema | Name | Type  |  Owner   | Size  | Description
--------+------+-------+----------+-------+-------------
 public | test | table | postgres | 35 MB |
(1 row)

#重启数据库,清空shared_buffer
pg_ctl restart

#关闭并行
postgres=# set max_parallel_workers_per_gather =0;
SET

#查询shared_buffer中test表中的块的信息
postgres=# select * from pg_buffercache where relfilenode ='test'::regclass;
 bufferid | relfilenode | reltablespace | reldatabase | relforknumber | relblocknumber | isdirty | usagecount | pinning_backends
----------+-------------+---------------+-------------+---------------+----------------+---------+------------+------------------
(0 rows)

#进行全部扫描
postgres=# select count(1) from test;
  count
---------
 1000000
(1 row)

#查询shared_buffer中test表中的块的信息
postgres=# select count(1) from pg_buffercache where relfilenode ='test'::regclass;
 count
-------
    32
(1 row)

发现有32个块,刚好与256KB(32*8)大小对应

2.BAS_BULKWRITE

#导出数据文件
postgres=# copy test to '/home/postgres/test.csv' with csv;
COPY 1000000

#重启数据库
pg_ctl restart

#查询shared_buffer中test表中的块的信息
postgres=# select count(1) from pg_buffercache where relfilenode ='test'::regclass;
 count
-------
     0
(1 row)

#copy from 导入test表中
postgres=# copy test from '/home/postgres/test.csv';
COPY 1000000

#查询shared_buffer中test表中的块的信息
postgres=# select count(1) from pg_buffercache where relfilenode='test'::regclass;
 count
-------
  2051
(1 row)

批量写入分配的大小为16MB,预想的块数应该为2048(16*1024/8),发现与预想有点差别,下面我们看下原因

pg_buffercache中有个relforknumber这个字段,这个定义如下

/src/include/common/relpath.h

typedef enum ForkNumber
{
    InvalidForkNumber = -1,
    MAIN_FORKNUM = 0,
    FSM_FORKNUM,
    VISIBILITYMAP_FORKNUM,
    INIT_FORKNUM

    /*
     * NOTE: if you add a new fork, change MAX_FORKNUM and possibly
     * FORKNAMECHARS below, and update the forkNames array in
     * src/common/relpath.c
     */
} ForkNumber;

0就是main数据文件,1就是fsm文件,2就是vm文件。

我们在查下pg_buffercache

postgres=# select relforknumber,count(1) from pg_buffercache where relfilenode='test'::regclass group by relforknumber;
 relforknumber | count
---------------+-------
             0 |  2048
             1 |     3
(2 rows)

发现数据文件buffer块与我们之前预想的结果是一致的,多的三个块是访问fsm文件的块。

postgres=# select relfilenode,relforknumber,relblocknumber,isdirty,usagecount,pinning_backends from pg_buffercache where relfilenode='test'::regclass and relforknumber!='0';
 relfilenode | relforknumber | relblocknumber | isdirty | usagecount | pinning_backends
-------------+---------------+----------------+---------+------------+------------------
       16437 |             1 |              3 | f       |          5 |                0
       16437 |             1 |              0 | f       |          5 |                0
       16437 |             1 |              2 | f       |          5 |                0
(3 rows)

访问fsm的调用关系是CopyFrom->CopyFromInsertBatch->heap_multi_insert->RelationGetBufferForTuple->GetPageWithFreeSpace->fsm_search。如果插入buffer的块数=1000或者行的大小加起来大于64KB就会触发一次刷写,这时就会去访问fsm文件寻找空余的空间,具体怎么访问fsm文件,这里就不具体展开讨论了。

src/backend/commands/copy.c/CopyFrom

if (useHeapMultiInsert)
                {
                    /* Add this tuple to the tuple buffer */
                    if (nBufferedTuples == 0)
                        firstBufferedLineNo = cstate->cur_lineno;
                    bufferedTuples[nBufferedTuples++] = tuple;
                    bufferedTuplesSize += tuple->t_len;

                    /*
                     * If the buffer filled up, flush it.  Also flush if the
                     * total size of all the tuples in the buffer becomes
                     * large, to avoid using large amounts of memory for the
                     * buffer when the tuples are exceptionally wide.
                     */
                    if (nBufferedTuples == MAX_BUFFERED_TUPLES ||
                        bufferedTuplesSize > 65535)
                    {
                        CopyFromInsertBatch(cstate, estate, mycid, hi_options,
                                            resultRelInfo, myslot, bistate,
                                            nBufferedTuples, bufferedTuples,
                                            firstBufferedLineNo);
                        nBufferedTuples = 0;
                        bufferedTuplesSize = 0;
                    }
                }

3.BAS_VACUUM

#查询shared_buffer中test表中的块的信息
postgres=# select count(1) from pg_buffercache where relfilenode ='test'::regclass;
 count
-------
     0
(1 row)

#vacuum操作
postgres=# vacuum test ;
VACUUM
postgres=# select count(1) from pg_buffercache where relfilenode ='test'::regclass;
 count
-------
    37
(1 row)

预想结果应该是32,结果是37,我们再看下原因

postgres=# select relfilenode,relforknumber,relblocknumber,isdirty,usagecount,pinning_backends from pg_buffercache where relfilenode='test'::regclass and relforknumber!='0';
 relfilenode | relforknumber | relblocknumber | isdirty | usagecount | pinning_backends
-------------+---------------+----------------+---------+------------+------------------
       16437 |             2 |              0 | t       |          2 |                0
       16437 |             1 |              2 | f       |          5 |                0
       16437 |             1 |              3 | t       |          5 |                0
       16437 |             1 |              0 | t       |          1 |                0
       16437 |             1 |              1 | t       |          1 |                0
(5 rows)

这里可以看到有四次是对fsm文件不同块进行的访问,有1次对vm文件进行的访问,减掉5刚好就是32个块。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
webgl系列之 buffer的使用
什么是buffer buffer, 这是啥东西啊 ,或者说为什么要有buffer 这个东西 对于webgl 来说。我们先看下官方定义: 「WebGLBuffer」 接口属于 WebGL API 的一部分,表示一个不透明的缓冲区对象,储存诸如顶点或着色之类的数据。 官方的这个解释,作为一个小白来说, 啥哇,你这是在为难我胖虎嘛??? 图片 为难我胖虎 没事由我来大家解读,首先第一句话就是一句废话, 属于「webgl API 的一部分」, 这我他🐴的也知道哇,我们重点分析 一下这句话,是一个不透明的缓冲区对象, 什么是缓冲区对象 「webgl提供一种很方便的机制 ,就是缓冲区对象」,它可
155 0
log buffer 中的组提交
 【本文根据itpub的帖子整理】    数据库中同时存在许多事务,LGWR进程在写入重做日志文件时可以采取“组提交”的方式。        一个用户提交事务时,LGWR进程开始将该进程的重做记录写入硬盘。
572 0
+关注
绛云
阿里云数据库专家服务组PostgreSQL dba,热爱pg,正在学习pg内核的相关的代码,欢迎大家和我讨论,一起共同成长
15
文章
1
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载