ICode9

精准搜索请尝试: 精确搜索
首页 > 数据库> 文章详细

PostgreSQL的MVCC(8)--Freezing

2020-09-13 20:33:51  阅读:291  来源: 互联网

标签:冻结 事务 PostgreSQL -- age Freezing freeze xmin tfreeze


事务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/

 

 

 

 

  

 

标签:冻结,事务,PostgreSQL,--,age,Freezing,freeze,xmin,tfreeze
来源: https://www.cnblogs.com/abclife/p/13663001.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有