zoukankan      html  css  js  c++  java
  • PostgreSQL的MVCC(8)--Freezing

    事务ID包装

    PostgreSQL使用32位事务ID。 这是一个相当大的数字(大约40亿),但是随着服务器的大量工作,这个数字并不是不可能被耗尽。例如:每秒处理1000次事务,这种情况最少要在一个半月的连续工作中发生。

    但是我们已经提到,多版本并发控制依赖于顺序编号,这意味着在两个事务中,数值较小的事务可以被认为是较早开始的。 因此,很明显,重置计数器并从头开始编号不是一个选择。

     

    但是为什么不使用64位的事务id——它不会完全消除这个问题吗?问题是每个元组的header(如前所述)存储两个事务id: xmin和xmax。header部分相当大,因为它至少23个字节,扩大事务id将导致header增加额外的8个字节,完全没有理由这么做。

    但是,为什么不使用64位事务处理ID-不就能完全消除问题?问题是每个元组的标头(如前所述)存储两个事务ID:xmin和xmax。标头相当大-至少23个字节,并且位大小的增加将使标头增加额外的8个字节。这是完全没有道理的。

    那该怎么办呢?与其按顺序(按数字)顺序排列事务标id,不如想象一个圆圈或一个钟盘。以与比较时钟读数相同的方式比较事务ID。也就是说,对于每个事务,事务ID的“逆时针”部分被认为与过去有关,而“顺时针”部分被视为与未来有关。

    事务的age定义为自系统中发生事务以来(不考虑事务ID绕行)开始运行的事务数。为了确定一个事务是否比另一个事务更早,我们比较了它们的age而不是ID。 (顺便说一下,正是由于这个原因,没有为xid数据类型定义 «greater»和«less»操作)

    但是这种循环的安排很麻烦。一个在past,距离很远的事务(图中的事务1),一段时间后将进入与未来相关的圆的一半。这当然会破坏可见性规则并导致问题:事务1所做的更改将会消失在视线之外。

    元组的freeze和可见性原则

    为了防止这种从past到future的“旅行”发生,vacuum操作还需要执行另一项任务(除了释放页面空间之外)。它会找到相当old和“cold”元组(在所有快照中可见,并且不太可能更改),并以特殊的方式标记它们,即“freeze”它们。冻结的元组被认为比任何普通数据都旧,并且在所有快照中始终可见。并且不再需要查看xmin事务编号,并且可以安全地重用该编号。 因此冻结的元组始终保留在过去。

    为了跟踪冻结的xmin事务,两个提示位都设置为:commited和aborted。

    请注意,xmax事务不需要冻结。它的存在表明该元组不再是live的。 当它不再在数据快照中可见时,该元组将被清除。

    让我们为实验创建一个表。为其指定最小填充因子(fillfactor),以便每页仅容纳两行-这使我们更方便地观察正在发生的事情。 让我们也关闭autovacuum以自行控制vacumm时间。

    => CREATE TABLE tfreeze(
      id integer,
      s char(300)
    ) WITH (fillfactor = 10, autovacuum_enabled = off);
    

    我们已经创建了使用«pageinspect»扩展来显示位于页面上的元组的函数的几个变体。现在我们将创建这个函数的另一个变体:它将一次显示多个页面并输出xmin事务的age(使用age系统函数):

    => CREATE FUNCTION heap_page(relname text, pageno_from integer, pageno_to integer)
    RETURNS TABLE(ctid tid, state text, xmin text, xmin_age integer, xmax text, t_ctid tid)
    AS $$
    SELECT (pageno,lp)::text::tid AS ctid,
           CASE lp_flags
             WHEN 0 THEN 'unused'
             WHEN 1 THEN 'normal'
             WHEN 2 THEN 'redirect to '||lp_off
             WHEN 3 THEN 'dead'
           END AS state,
           t_xmin || CASE
             WHEN (t_infomask & 256+512) = 256+512 THEN ' (f)'
             WHEN (t_infomask & 256) > 0 THEN ' (c)'
             WHEN (t_infomask & 512) > 0 THEN ' (a)'
             ELSE ''
           END AS xmin,
          age(t_xmin) xmin_age,
           t_xmax || CASE
             WHEN (t_infomask & 1024) > 0 THEN ' (c)'
             WHEN (t_infomask & 2048) > 0 THEN ' (a)'
             ELSE ''
           END AS xmax,
           t_ctid
    FROM generate_series(pageno_from, pageno_to) p(pageno),
         heap_page_items(get_raw_page(relname, pageno))
    ORDER BY pageno, lp;
    $$ LANGUAGE SQL;
    

    注意,committed和aborted的提示位集都表示冻结(我们用圆括号«f»表示)。多个来源(包括文档)提到了一个专门的ID来表示冻结的事务:FrozenTransactionId = 2。这个方法在9.4之前的PostgreSQL版本中就已经存在了,现在它被提示位所取代。这允许在元组中保留初始事务号,这便于维护和调试。但是,在旧系统中仍然可以遇到ID = 2的事务,甚至被升级到最新版本也可能遇到。

    我们还需要«pg_visibility»扩展,它使我们能够查看visibility map:

    => CREATE EXTENSION pg_visibility;
    

      

    在9.6前的PostgreSQL版本中,visibility map每页只包含一位;这个map只跟踪那些有«pretty old»行版本的页面,这在所有数据快照中都是可见的。这背后的思想是,如果页面在可见性映射中被跟踪,则不需要检查其元组的可见性规则。

    从9.6版本开始,每个页面的all-frozen bit被添加到visibility map中。all-frozen bit跟踪所有元组都被冻结的页面。

    让我们在表中插入几行,然后立即对要创建的可见性映射进行vacuum处理:

    => INSERT INTO tfreeze(id, s)
      SELECT g.id, 'FOO' FROM generate_series(1,100) g(id);
    => VACUUM tfreeze;
    

      

    我们可以看到,这两个页面都是可见的,但不是all-frozen:

    => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno)
    ORDER BY g.blkno;
     blkno | all_visible | all_frozen 
    -------+-------------+------------
         0 | t           | f
         1 | t           | f
    (2 rows)
    

      

    创建行的事务的age(xmin_age)等于1——这是系统中执行的上一个事务:

    => SELECT * FROM heap_page('tfreeze',0,1);
     ctid  | state  |  xmin   | xmin_age | xmax  | t_ctid 
    -------+--------+---------+----------+-------+--------
     (0,1) | normal | 697 (c) |        1 | 0 (a) | (0,1)
     (0,2) | normal | 697 (c) |        1 | 0 (a) | (0,2)
     (1,1) | normal | 697 (c) |        1 | 0 (a) | (1,1)
     (1,2) | normal | 697 (c) |        1 | 0 (a) | (1,2)
    (4 rows)
    

      

    Minimum age for freezing

    控制freeze的主要参数有三个,我们将逐一讨论。

    让我们从vacuum_freeze_min_age开始,它定义了元组可以被冻结的xmin事务的最小age。这个值越小,额外的开销可能就越多:如果我们处理热数据,密集更新,冻结新的和更新的元组将会很难。在这种情况下最好等一等。

    这个参数的默认值指定了一个事务开始被冻结时,自它发生以来有5000万个其他事务在运行:

    => SHOW vacuum_freeze_min_age;
     vacuum_freeze_min_age 
    -----------------------
     50000000
    (1 row)
    

      

    为了观察freeze,让我们将该参数的值降低为1。

    => ALTER SYSTEM SET vacuum_freeze_min_age = 1;
    => SELECT pg_reload_conf();
    

      

    更新第0页上的一行。新版本将在相同的页面,因为小的填充因子。

    => UPDATE tfreeze SET s = 'BAR' WHERE id = 1;
    

      

    这是我们现在在数据页上看到的:

    => SELECT * FROM heap_page('tfreeze',0,1);
     ctid  | state  |  xmin   | xmin_age | xmax  | t_ctid 
    -------+--------+---------+----------+-------+--------
     (0,1) | normal | 697 (c) |        2 | 698   | (0,3)
     (0,2) | normal | 697 (c) |        2 | 0 (a) | (0,2)
     (0,3) | normal | 698     |        1 | 0 (a) | (0,3)
     (1,1) | normal | 697 (c) |        2 | 0 (a) | (1,1)
     (1,2) | normal | 697 (c) |        2 | 0 (a) | (1,2)
    (5 rows)
    

      

    在0页面上,有一个版本被冻结,但是vacuum操作根本没有查看第1页。因此,如果页面上只剩下活动元组,那么清理将不会访问此页面,也不会冻结它们。

    => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno)
    ORDER BY g.blkno;
     blkno | all_visible | all_frozen 
    -------+-------------+------------
         0 | t           | f
         1 | t           | f
    (2 rows)
    

      

    我们已经讨论过,vacuum只查看visibility map中没有跟踪的页面。

    => VACUUM tfreeze;
    => SELECT * FROM heap_page('tfreeze',0,1);
     ctid  |     state     |  xmin   | xmin_age | xmax  | t_ctid 
    -------+---------------+---------+----------+-------+--------
     (0,1) | redirect to 3 |         |          |       | 
     (0,2) | normal        | 697 (f) |        2 | 0 (a) | (0,2)
     (0,3) | normal        | 698 (c) |        1 | 0 (a) | (0,3)
     (1,1) | normal        | 697 (c) |        2 | 0 (a) | (1,1)
     (1,2) | normal        | 697 (c) |        2 | 0 (a) | (1,2)
    (5 rows)
    

      

    在0页面上,有一个版本被冻结,但是vacuum操作根本没有查看第一页。因此,如果页面上只剩下活动元组(live tuples),那么清理将不会访问此页面,也不会冻结它们。

    => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno)
    ORDER BY g.blkno;
     blkno | all_visible | all_frozen 
    -------+-------------+------------
         0 | t           | f
         1 | t           | f
    (2 rows)
    

      

    Age to freeze the entire table

    为了冻结那些留在页面内,常规vacuum不会扫描的元组,提供了第二个参数:vacuum_freeze_table_age。

    每个页面都会存储一个事务id,比该事务老的事务都被认为是冻结的(pg_class.relfrozenxid)。这就是与vacuum_freeze_table_age参数的值进行比较的存储事务的年龄。

    => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
     relfrozenxid | age 
    --------------+-----
              694 |   5
    (1 row)
    

      

    在PostgreSQL 9.6之前,每次清理都会对表进行完全扫描,以确保能够访问所有页面。 对于大表,此操作很长。 更糟的是,因为如果无法完成清理工作(例如,急躁的管理员打断了该命令),则该过程必须从头开始。

    从9.6版开始,由于使用了all-frozen bit (我们可以在pg_visibility_map输出的all_frozen列中看到),清理仅对尚未设置该位的页面进行。这样不仅可以确保工作量少得多,而且可以确保中断可忍受:如果清理过程停止并重新启动,则不必再次查看上次已将其设置为all-frozen bit的页面。

    无论如何,所有表页面每相隔(vacuum_freeze_table_age − vacuum_freeze_min_age)个事务都会冻结一次。 使用默认值时,这种情况每一百万个事务发生一次:

    => SHOW vacuum_freeze_table_age;
     vacuum_freeze_table_age 
    -------------------------
     150000000
    (1 row)
    

      

    很明显,vacumm_freeze_min_age设置太大不是一个好的选择,因为这将增加开销而不是减少开销。

    让我们看看如何冻结整个表,为此,我们将vacuum_freeze_table_age减小为5,以便满足冻结条件。

    => ALTER SYSTEM SET vacuum_freeze_table_age = 5;
    => SELECT pg_reload_conf();
    

      

    开始做freeze:

    => VACUUM tfreeze;
    

      

    现在,由于已经确定检查了整个表,冻结的事务的ID可以增加,因为我们确信页面上没有旧的未冻结的事务。

    => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
     relfrozenxid | age 
    --------------+-----
              698 |   1
    (1 row)
    

    现在,第一页上的所有元组都被冻结:

    => SELECT * FROM heap_page('tfreeze',0,1);
     ctid  |     state     |  xmin   | xmin_age | xmax  | t_ctid 
    -------+---------------+---------+----------+-------+--------
     (0,1) | redirect to 3 |         |          |       | 
     (0,2) | normal        | 697 (f) |        2 | 0 (a) | (0,2)
     (0,3) | normal        | 698 (c) |        1 | 0 (a) | (0,3)
     (1,1) | normal        | 697 (f) |        2 | 0 (a) | (1,1)
     (1,2) | normal        | 697 (f) |        2 | 0 (a) | (1,2)
    (5 rows)
    

    此外,第一页已知是全冻结的:

    => SELECT * FROM generate_series(0,1) g(blkno), pg_visibility_map('tfreeze',g.blkno)
    ORDER BY g.blkno;
     blkno | all_visible | all_frozen 
    -------+-------------+------------
         0 | t           | f
         1 | t           | t
    (2 rows)
    

      

    Age for «aggressive» freezing

    及时冻结元组至关重要。如果未冻结的事务可能会遇到一些风险,为防止可能出现的问题PostgreSQL将关闭。

    为什么会发生这种情况?原因有很多。

    ·autovacum可能已关闭,VACUUM也未启动。我们已经提到不应该这样做,但这在技术上是可行的。 ·即使打开了autovacuum,也不会对有些数据库执行(请记住track_counts参数和«template0»数据库)。 ·正如我们上次观察到的,清理操作会跳过仅添加数据但不删除或更改数据的表。

    为了响应这些问题,提供了«aggressive» freezing,这由autovacuum_freeze_max_age参数控制。如果某个数据库中的表的未冻结事务可能早于age参数中指定的年龄,则将启动强制autovacuum(即使已关闭),并且迟早会处理到有问题的表。

    默认值非常保守:

    => SHOW autovacuum_freeze_max_age;
     autovacuum_freeze_max_age 
    ---------------------------
     200000000
    (1 row)
    

    autovacuum_freeze_max_age的限制为20亿个事务,但使用的值小10倍。这是有道理的:通过增加该值,我们还增加了autovacuum的风险,因为在剩余时间间隔内无法冻结所有必要的行。

    此外,此参数的值确定XACT结构的大小:由于系统不得保留可能需要了解其状态的较旧事务,因此,自动清空将通过删除不需要的XACT段文件来释放空间。

    让我们看一下vacuum是如何处理只是append-only的表的。该表的autovacuum功能已关闭,但即使这样也不会受到阻碍。

    更改autovacuum_freeze_max_age参数要求服务器重新启动。但是,您也可以通过存储参数在单独的表级别设置所有上述参数。通常只有在表确实需要特殊处理的情况下,才有意义。

    因此,我们将在表级别设置autovacuum_freeze_max_age(并同时恢复为正常的fillfactor)。不幸的是,最小可能值为100 000:

    => ALTER TABLE tfreeze SET (autovacuum_freeze_max_age = 100000, fillfactor = 100);
    

    不幸的是,因为我们必须执行10万个事务才能重现你所关注的情况。 但是,对于实际使用,这无疑是一个极低的值。

    由于我们要添加数据,因此让我们将10万行插入到表中,每个行都有自己的事务。 再次提醒,在实际情况下应避免这样做。 但是我们只是在研究,所以我们被允许。

    => CREATE PROCEDURE foo(id integer) AS $$
    BEGIN
      INSERT INTO tfreeze VALUES (id, 'FOO');
      COMMIT;
    END;
    $$ LANGUAGE plpgsql;
    
    => DO $$
    BEGIN
      FOR i IN 101 .. 100100 LOOP
        CALL foo(i);
      END LOOP;
    END;
    $$;
    

    我们可以看到,表中最后一个被冻结的事务的年龄超过了阈值:

    => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
     relfrozenxid |  age   
    --------------+--------
              698 | 100006
    (1 row)
    

    但是现在如果我们等待一段时间,一个记录将出现在`automatic aggressive vacuum of table "test.public.tfreeze"的消息日志中。冻结事务的数量将会改变,其年龄将不再超出界限:

    => SELECT relfrozenxid, age(relfrozenxid) FROM pg_class WHERE relname = 'tfreeze';
     relfrozenxid | age 
    --------------+-----
           100703 |   3
    (1 row)
    

      

    还有multixact冻结技术,但我们将在讨论锁之前暂停讨论,以避免讨论得太多。

    Freezing manually

    有时,手动控制冻结而不是依靠autovacuum似乎很方便。

    您可以通过VACUUM FREEZE命令手动启动冻结。它将冻结所有元组,而不理会事务的年龄(就像autovacuum_freeze_min_age参数等于零)。 使用VACUUM FULL或CLUSTER命令重写表时,所有行也将被冻结。

    要冻结所有数据库,可以使用该实用程序:

    vacuumdb --all --freeze
    

    如果指定了FREEZE参数,则在最初由COPY命令加载数据时也可以将其冻结。 为此,必须在与COPY相同的事务中创建表(或用TRUNCATE命令清空)。

    由于可见性规则中冻结的行存在例外,因此这些行将在其他事务的快照中可见,这违反了正常的隔离规则(这与具有“可重复读”或“可序列化”级别的事务有关)。

    为确保这一点,在另一个会话中,让我们以“可重复读”隔离级别启动事务:

    |  => BEGIN ISOLATION LEVEL REPEATABLE READ;
    |  => SELECT txid_current();
    

    注意,该事务创建了数据快照,但没有访问“ tfreeze”表。 现在,我们将截断«tfreeze»表,并在一个事务中将新行加载到该表中。 如果并行事务读取《 tfreeze》的内容,则TRUNCATE命令将被锁定到该事务的末尾。

    => BEGIN;
    => TRUNCATE tfreeze;
    => COPY tfreeze FROM stdin WITH FREEZE;
    1  FOO
    2  BAR
    3  BAZ
    .
    => COMMIT;
    

    现在并发事务看到了新的数据,尽管这违反了隔离:

    |  => SELECT count(*) FROM tfreeze;
    |   count 
    |  -------
    |       3
    |  (1 row)
    |  => COMMIT;
    

    但是,由于这种数据加载不太可能定期发生,因此这几乎不是问题。

    更糟糕的是,COPY WITH FREEZE无法与可见性图一起使用-加载的页面没有被跟踪为仅包含所有人可见的元组。 因此,当vacuum操作首先访问该表时,它必须再次处理所有表并创建可见性图。 更糟糕的是,数据页在其自己的header中具有全可见的指示器,因此,清理不仅读取整个表,还完全重写它以设置所需的位。 不幸的是,可以期望不早于版本13(讨论)解决该问题。

     

     

     

     

    原文地址:

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

     

      

  • 相关阅读:
    软工个人项目作业
    软工个人博客作业(2)
    软工第一次作业-热身
    OO第四单元
    OO第三单元
    OO第二单元总结
    面向对象UML单元总结及课程总结
    JML单元总结
    面向对象设计与构造第二单元总结
    OO第一单元小结
  • 原文地址:https://www.cnblogs.com/abclife/p/13663001.html
Copyright © 2011-2022 走看看