• PostgreSQL的WAL(1)--Buffer Cache


    为什么需要提前写日志

    DBMS处理的数据部分存储在RAM中,并异步写入磁盘(或其他非易失性存储)中。即写延迟了一段时间。这种情况发生的频率越低,输入/输出越少,系统运行越快。

    但是,如果发生故障(例如断电或DBMS或操作系统的代码错误),会发生什么? RAM的所有内容都会丢失,只有写入磁盘的数据才能幸存(磁盘也无法幸免于某些故障,如果磁盘上的数据受到影响,则只有备份可以提供帮助)。通常,可以以磁盘上的数据始终保持一致的方式来组织输入/输出,但这很复杂且效率不高(据我所知,只有Firebird选择了此选项)。

    通常,尤其是在PostgreSQL中,写入磁盘的数据似乎不一致,并且在故障后恢复时,需要采取特殊措施来恢复数据一致性。预写日志记录(WAL)只是一项使之成为可能的功能。

    buffer cache

    buffer cache不是存储在RAM中的唯一结构,而是其中最关键和最复杂的结构之一。理解其工作原理本身很重要;此外,我们将以它为例以熟悉RAM和磁盘如何交换数据。

    缓存在现代计算机系统中无处不在。一个处理器仅具有三级或四级缓存。通常,需要缓存来减轻两种内存之间的性能差异,其中一种相对较快,但是容量较小,循环不足;另一种相对较慢,但是容量足够。缓冲区缓存减轻了访问RAM的时间(纳秒)和磁盘存储的时间(毫秒)之间的差异。

    请注意,操作系统还具有解决相同问题的磁盘缓存。因此,数据库管理系统通常尝试通过直接访问磁盘而不是通过OS缓存来避免双重缓存。但是PostgreSQL并非如此:所有数据都是使用常规文件操作读取和写入的。

    此外,磁盘阵列的控制器甚至磁盘本身也具有自己的缓存。当我们讨论可靠性时,这将很有用。

    但是,让我们回到DBMS的buffer cache。

    每个buffer由数据页(块)的空间和header组成。header中包含:

    ·page在buffer中的位置(文件和块号)。

    ·page上数据更改的指示符,更改迟早需要将其写入磁盘(这样的缓冲区称为脏缓冲区)。

    ·buffer的使用计数。

    ·buffer的pin计数。

    buffer cache位于服务器的共享内存中,所有进程都可以访问它。为了处理数据,即读取或更新数据,这些进程会将页面读取到缓存中。当页面在缓存中时,我们在RAM中使用它并在磁盘访问中保存它。

    缓存最初包含空缓冲区,并且所有缓冲区都链接到空闲缓冲区列表中。缓存的哈希表用于快速找到您需要的页面。

    在cache中寻找一个page

    当进程需要读取页面时,它首先尝试通过哈希表在buffer cache中找到它。文件号和文件中的页面号用作哈希键。该进程在适当的哈希桶中找到buffer编号,并检查它是否确实包含所需的页面。像任何哈希表一样,此处可能会发生冲突,在这种情况下,该过程将不得不检查多个页面。

    哈希表的使用长期以来一直是人们抱怨的源头。像这样的结构可以快速按页查找缓冲区,但是,例如,如果您需要查找某个表占用的所有缓冲区,则哈希表绝对是无用的。但是还没有人建议好的替代品。

    如果在高速缓存中找到所需的页,则该进程必须通过增加pin计数来“pin”住缓冲区(多个进程可以同时执行此操作)。被固定的缓冲区(计数值大于零)时,它被认为是已使用并且具有无法“急剧”更改的内容。例如:一个新的元组可以出现在页面上-由于多版本并发和可见性规则,这对任何人都无害。但是无法将其他页面读入固定的缓冲区。

    Eviction

    可能会出现在缓存中找不到所需的页面的情况。在这种情况下,需要将该页从磁盘读入某个缓冲区。

    如果缓存中的空缓冲区仍然可用,则选择第一个空缓冲区。但是它们迟早会不够(数据库的大小通常大于为缓存分配的内存),然后我们将不得不选择一个已占用的缓冲区,将位于那里的页清除出去,并将新的页读入已释放的空间。

    清除技术基于这样一个事实:对于每次访问缓冲区,进程都会增加缓冲区header中的使用计数。因此,与其他缓冲区相比,使用频率较低的缓冲区的计数值较小,因此是清除的良好候选对象。

    时钟扫描算法循环地遍历所有缓冲区(使用指向«next victim»的指针),并将它们的使用量减少1。 为清除选择的第一个缓冲区要满足:

    ·使用计数是0

    ·pin计数也是0

    请注意,如果所有缓冲区都有一个非零的使用计数,那么算法将不得不在缓冲区中进行多次循环,减少计数的值算,直到其中一些减少到零为止。算法为了避免«做重叠»的操作,使用计数的最大值被限制为5。然而,对于大型的buffer cache,该算法可能会造成相当大的开销。

    找到缓冲区后,将对它执行以下操作。

    缓冲区被固定以显示使用它的其他进程。除了固定之外,还使用了其他锁定技术,但是我们将在后面更详细地讨论。

    如果缓冲区看起来是脏的,也就是说,包含已更改的数据,就不能直接删除页面——它需要首先保存到磁盘。 这很难说是一种好情况,因为要读取页面的进程必须等待其他进程的数据被写入,但是检查点和后台写入器进程缓解了这种影响,这将在后面讨论。

    然后将新页从磁盘读入选定的缓冲区。使用计数被设置为1。此外,必须将对已加载页面的引用写入哈希表,以便将来能够查找该页面。

    对«next victim»的引用现在指向下一个缓冲区,而刚刚加载的缓冲区有时间增加使用计数,直到指针循环地遍历整个缓冲区缓存并再次返回。

    自己验证一下

    和往常一样,PostgreSQL有一个扩展,可以让我们查看缓冲区缓存的内部。

    => CREATE EXTENSION pg_buffercache;
    

    让我们创建一个表并在那里插入一行。

    => CREATE TABLE cacheme(
      id integer
    ) WITH (autovacuum_enabled = off);
    => INSERT INTO cacheme VALUES (1);
    

    buffer cache将包含什么?至少,必须出现只添加了一行的页面。让我们用下面的查询来检查这个,它只选择与我们的表相关的缓冲区(通过relfilenode号),并解释relforknumber:

    => SELECT bufferid,
      CASE relforknumber
        WHEN 0 THEN 'main'
        WHEN 1 THEN 'fsm'
        WHEN 2 THEN 'vm'
      END relfork,
      relblocknumber,
      isdirty,
      usagecount,
      pinning_backends
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('cacheme'::regclass);
     bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
    ----------+---------+----------------+---------+------------+------------------
        15735 | main    |              0 | t       |          1 |                0
    (1 row)
    

    正如我们所想的那样:缓冲区只包含一个页面。它是脏的(isdirty),使用计数(usagecount)等于1,并且页面没有被任何进程固定(pinning_backends)。

    现在让我们再添加一行并重新运行查询。为了节省击键次数,我们将该行插入到另一个会话中,并使用g命令重新运行长查询。

    |  => INSERT INTO cacheme VALUES (2);
    

     

    => g
    

      

     bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
    ----------+---------+----------------+---------+------------+------------------
        15735 | main    |              0 | t       |          2 |                0
    (1 row)
    

      

    没有添加新的缓冲区:第二行适合同一页。注意增加的使用量。

    |  => SELECT * FROM cacheme;
    
    |   id
    |  ----
    |    1
    |    2
    |  (2 rows)
    

      

    => g
    
     bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
    ----------+---------+----------------+---------+------------+------------------
        15735 | main    |              0 | t       |          3 |                0
    (1 row)
    

    在读取页面之后,计数也会增加。

    但如果我们用vacuum呢?

    |  => VACUUM cacheme;
    

      

    => g
    
     bufferid | relfork | relblocknumber | isdirty | usagecount | pinning_backends
    ----------+---------+----------------+---------+------------+------------------
        15731 | fsm     |              1 | t       |          1 |                0
        15732 | fsm     |              0 | t       |          1 |                0
        15733 | fsm     |              2 | t       |          2 |                0
        15734 | vm      |              0 | t       |          2 |                0
        15735 | main    |              0 | t       |          3 |                0
    (5 rows)
    

      

    VACUUM创建了可见性map(一页)和空闲空间map(有三页,这是这样一个map的最小尺寸)。

    调优buffer cache的大小

    我们可以使用shared_buffers参数设置缓存大小。默认值是128mb,这是安装PostgreSQL后应该马上增加的参数之一。

    => SELECT setting, unit FROM pg_settings WHERE name = 'shared_buffers';
     setting | unit
    ---------+------
     16384   | 8kB
    (1 row)
    

    注意,更改此参数需要重新启动服务器,因为缓存的所有内存都是在服务器启动时分配的。

    即使是最大的数据库也只有一组有限的“热”数据,这些数据一直在被集中处理。理想情况下,必须在缓冲区缓存中容纳这个数据集(加上一些用于一次性数据的空间)。如果缓存大小较小,那么频繁使用的页面将不断地相互清除,这将导致过多的输入/输出。但是盲目地增加缓存也不好。当缓存很大时,维护它的开销将增加,除此之外,其他用途也需要RAM。

    因此,您需要为您的特定系统选择最佳的缓冲区缓存大小:这取决于数据、应用程序和负载。不幸的是,没有万能的值。

    通常建议使用1/4的内存作为第一个近似(低于10的PostgreSQL版本建议Windows使用更小的内存)。

    然后我们最好进行实验:增加或减少缓存大小,并比较系统特性。为此,您当然需要测试,并且应该能够重新生成工作负载。在生产环境中进行这样的实验是一种可疑的乐趣。

    但是,您可以通过相同的pg_buffercache扩展名获得一些关于您的系统上正在发生的事情的信息。 最重要的是要从正确的角度看问题。

    例如:你可以通过它们的使用来探索缓冲区的分布:

    => SELECT usagecount, count(*)
    FROM pg_buffercache
    GROUP BY usagecount
    ORDER BY usagecount;
     usagecount | count
    ------------+-------
              1 |   221
              2 |   869
              3 |    29
              4 |    12
              5 |   564
                | 14689
    (6 rows)
    

    在这种情况下,计数的多个空值对应于空缓冲区。对于一个什么都没有发生的系统来说,这并不奇怪。

    我们可以看到在我们的数据库中哪些表被缓存了,以及这些数据的使用频率有多高(在这个查询中,使用次数大于3的缓冲区指的是“集中使用”):

    => SELECT c.relname,
      count(*) blocks,
      round( 100.0 * 8192 * count(*) / pg_table_size(c.oid) ) "% of rel",
      round( 100.0 * 8192 * count(*) FILTER (WHERE b.usagecount > 3) / pg_table_size(c.oid) ) "% hot"
    FROM pg_buffercache b
      JOIN pg_class c ON pg_relation_filenode(c.oid) = b.relfilenode
    WHERE  b.reldatabase IN (
             0, (SELECT oid FROM pg_database WHERE datname = current_database())
           )
    AND    b.usagecount is not null
    GROUP BY c.relname, c.oid
    ORDER BY 2 DESC
    LIMIT 10;
              relname          | blocks | % of rel | % hot
    ---------------------------+--------+----------+-------
     vac                       |    833 |      100 |     0
     pg_proc                   |     71 |       85 |    37
     pg_depend                 |     57 |       98 |    19
     pg_attribute              |     55 |      100 |    64
     vac_s                     |     32 |        4 |     0
     pg_statistic              |     27 |       71 |    63
     autovac                   |     22 |      100 |    95
     pg_depend_reference_index |     19 |       48 |    35
     pg_rewrite                |     17 |       23 |     8
     pg_class                  |     16 |      100 |   100
    (10 rows)
    

    例如:我们在这里可以看到vac表占用了大部分空间,但是它没有被长时间访问,而且也没有被驱逐,这只是因为空缓冲区仍然可用。

    ·您需要多次重新运行此类查询:这些数字将在一定范围内变化。 ·您不应该连续运行这样的查询(作为监视的一部分),因为扩展会暂时阻塞对缓冲区缓存的访问。

    还有一点需要注意。不要忘记PostgreSQL通过常规的操作系统调用来处理文件,因此会发生双重缓存:页面同时进入DBMS和操作系统的缓存。因此,没有命中缓冲区缓存并不总是导致需要实际的输入/输出。但是操作系统的驱逐策略不同于DBMS:操作系统不知道读取数据的意义。

    Massive eviction

    批量读和写操作容易产生这样的风险,即有用的页面可能会被«一次性»从缓冲区缓存中快速驱逐。

    为了避免这种情况,使用了所谓的 buffer rings:只是为每个操作分配一小部分缓冲区缓存。驱逐仅在环内执行,因此缓冲区缓存中的其余数据不受影响。

    对于大型表(其大小大于缓冲区缓存的四分之一)的连续扫描,将分配32个页面。如果在扫描一个表的过程中,另一个进程也需要这些数据,那么它不会从头开始读取表,而是连接到已经可用的缓冲区环。在完成扫描之后,进程继续读取表的«missed»开头部分。

    让我们验证一下。创建一个表,以便一行占据整个页面——这样计数更方便。缓冲区缓存的默认大小为128 MB = 16384个页面(8 KB)。这意味着我们需要向表中插入超过4096行(即页)。

    => CREATE TABLE big(
      id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
      s char(1000)
    ) WITH (fillfactor=10);
    => INSERT INTO big(s) SELECT 'FOO' FROM generate_series(1,4096+1);
    

    我们来分析一下这个表

    => ANALYZE big;
    => SELECT relpages FROM pg_class WHERE oid = 'big'::regclass;
     relpages
    ----------
         4097
    (1 row)
    

    现在我们必须重新启动服务器以清除分析中读取的表数据的缓存。

    student$ sudo pg_ctlcluster 11 main restart
    

    重启后读取整个表:

    => EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
                                 QUERY PLAN                              
    ---------------------------------------------------------------------
     Aggregate (actual time=14.472..14.473 rows=1 loops=1)
       ->  Seq Scan on big (actual time=0.031..13.022 rows=4097 loops=1)
     Planning Time: 0.528 ms
     Execution Time: 14.590 ms
    (4 rows)
    

    让我们确保表页面在缓冲区缓存中只占用32个缓冲区:

    => SELECT count(*)
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('big'::regclass);
     count
    -------
        32
    (1 row)
    

    但如果我们禁止顺序扫描,表将读取使用索引扫描:

    => SET enable_seqscan = off;
    => EXPLAIN (ANALYZE, COSTS OFF) SELECT count(*) FROM big;
                                            QUERY PLAN                                         
    -------------------------------------------------------------------------------------------
     Aggregate (actual time=50.300..50.301 rows=1 loops=1)
       ->  Index Only Scan using big_pkey on big (actual time=0.098..48.547 rows=4097 loops=1)
             Heap Fetches: 4097
     Planning Time: 0.067 ms
     Execution Time: 50.340 ms
    (5 rows)
    

    在这种情况下,没有使用缓冲区环,整个表将进入缓冲区缓存(几乎整个索引):

    => SELECT count(*)
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('big'::regclass);
     count
    -------
      4097
    (1 row)
    

    缓冲区环以类似的方式用于vacuum过程(也是32页)和批量写操作copy和create table as select(通常为2048页,但不超过缓冲区缓存的1/8)。

    临时表

    临时表是常见规则中的一个例外。由于临时数据只对一个进程可见,因此不需要在共享缓冲区缓存中使用它们。 此外,临时数据只存在于一个会话中,因此不需要防止失败的保护。

    临时数据使用拥有该表的进程的本地内存中的缓存。由于这些数据只对一个进程可用,因此不需要使用锁保护它们。本地缓存使用正常的驱逐算法。

    与共享缓冲区缓存不同,本地缓存的内存是在需要时分配的,因为在许多会话中都不会使用临时表。单个会话中临时表的最大内存大小受到temp_buffers参数的限制。

    为cache预热

    在服务器重启后,缓存必须经过一段时间才能“预热”,也就是说,要填充活跃使用的数据。它可能有时看起来有用,立即读取某些表的内容到缓存中,一个专门的扩展是可用的:

    => CREATE EXTENSION pg_prewarm;
    

    以前,该扩展只能将某些表读入缓冲区缓存(或仅读入操作系统缓存)。但是PostgreSQL 11允许它将缓存的最新状态保存到磁盘,并在服务器重启后恢复。要使用它,需要将库添加到shared_preload_libraries并重新启动服务器。

    => ALTER SYSTEM SET shared_preload_libraries = 'pg_prewarm';
    
    student$ sudo pg_ctlcluster 11 main restart
    

    重启后,如果pg_prewarm.autoprewarm的值没有改变,autoprewarm主后台进程将启动,每隔pg_prewarm.autoprewarm_interval秒数完成一次刷新缓存中存储的页面列表。(在设置max_parallel_processes值时,不要忘记将新进程计算在内)。

    => SELECT name, setting, unit FROM pg_settings WHERE name LIKE 'pg_prewarm%';
                  name               | setting | unit
    ---------------------------------+---------+------
     pg_prewarm.autoprewarm          | on      |
     pg_prewarm.autoprewarm_interval | 300     | s
    (2 rows)
    

      

    postgres$ ps -o pid,command --ppid `head -n 1 /var/lib/postgresql/11/main/postmaster.pid` | grep prewarm
    
    10436 postgres: 11/main: autoprewarm master  
    

    现在缓存不包含big表:

    => SELECT count(*)
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('big'::regclass);
     count
    -------
         0
    (1 row)
    

    如果我们认为它的所有内容都是关键的,我们可以通过调用以下函数将其读入缓冲区缓存:

    => SELECT pg_prewarm('big');
     pg_prewarm
    ------------
           4097
    (1 row)
    

      

    => SELECT count(*)
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('big'::regclass);
     count
    -------
      4097
    (1 row)
    

    块列表被刷新到autoprewarm.blocks文件中。要查看列表,我们可以等到autoprewarm主进程第一次完成,或者我们可以手动启动刷新,如下所示:

    => SELECT autoprewarm_dump_now();
     autoprewarm_dump_now
    ----------------------
                     4340
    (1 row)
    

    刷新的页面数量已经超过4097;已被服务器读取的系统目录页被计算在内。这是文件:

    postgres$ ls -l /var/lib/postgresql/11/main/autoprewarm.blocks
    -rw------- 1 postgres postgres 102078 jun 29 15:51 /var/lib/postgresql/11/main/autoprewarm.blocks
    

    现在让我们重新启动服务器。

    student$ sudo pg_ctlcluster 11 main restart
    

    在服务器启动后,我们的表将再次位于缓存中。

    => SELECT count(*)
    FROM pg_buffercache
    WHERE relfilenode = pg_relation_filenode('big'::regclass);
     count
    -------
      4097
    (1 row)
    

    相同的autoprewarm主进程提供了这一点:它读取文件,按数据库划分页面,对它们进行排序(尽可能使从磁盘顺序读取),并将它们传递到一个单独的autoprewarm worker进程进行处理。

     

     

     

    原文地址:

    https://habr.com/en/company/postgrespro/blog/491730/

      

     

  • 相关阅读:
    linux系统中rsync+inotify实现服务器之间文件实时同步
    用Nginx搭建CDN服务器方法-开启Nginx缓存与镜像,自建图片服务器
    CentOS 搭建dns服务器 解析任意域名
    批量取控件的值
    我的一类库
    asp.net相关的一些代码
    C#的一些代码
    口算训练(唯一分解定理 + 二分+2018年女生赛)
    Codeforces Round #484 (Div. 2)
    Codeforces Round #483 (Div. 2) [Thanks, Botan Investments and Victor Shaburov!]
  • 原文地址:https://www.cnblogs.com/abclife/p/13684363.html
Copyright © 2020-2023  润新知