autovacuum 是 postgresql 里非常重要的一个服务端进程,能够自动地执行,在一定条件下自动地对 dead tuples 进行清理并对表进行分析。

1.1 什么是autovacuum?

autocuum是启动postgresql时自动启动的后台实用程序之一。

在生产环境中不应该将其关闭

autovacuum=on #默认on
track_counts=on #默认on

1.2 为什么需要autovacuum

  • 需要vacuum来移除死元组
  • 防止死元组膨胀
  • 更新表的统计信息进行分析,以便提供优化器使用
  • autovacuum launcher使用Stats Collector的后台进程收集的信息来确定autovacuum的候选表列表

1.3 记录autovacuum

log_autovacuum_min_duration=-1 # -1表示不记录,0表示记录所有的,大于0(如250ms,1s,1min,1h,1d)表示真空操作大于此值的操作

1.4 什么时候做autovacuum

Autovacuum实际操作的内容

  1. Autovacuum

    触发条件(如果由于更新或删除,表中的实际死元组数超过此有效阈值,则该表将成为autovacuum的候选表)

  2. Autovacuum ANALYZE

    thresold a table = autovacuum_analyze_scale_factor * number of tuples + autovacuum_analyze_threshold

autovacuum_vacuum_threshold = 50       # min number of row updates before vacuum
autovacuum_analyze_threshold = 50      # min number of row updates before analyze
autovacuum_vacuum_scale_factor = 0.2   # fraction of table size before vacuum
autovacuum_analyze_scale_factor = 0.1  # fraction of table size before analyze

举个例子

test表有1000行记录。

上面的公式作为参考:

当test表成为autovacuum vacuum的候选者,满足的条件如下:

更新/删除的记录:0.2*1000 + 50 = 250

插入/更新/删除的记录:0.1*1000 + 50 = 150

1.5 这是不是一个问题

table1=100行

其触发analyze和vacuum的阈值分别是:60和70。

table2=100w行

其触发analyze和vacuum的阈值分别是:100050和200050.

1.6 如何确定需要调整其autovacuum setting的表

为了单独调整表的autovacuum,必须知道一段时间内表上的插入/删除/更新数。

postgres=# select n_tup_ins,n_tup_upd,n_tup_del,n_live_tup,n_dead_tup from pg_stat_user_tables where schemaname = 'public' and relname = 'test';
 n_tup_ins | n_tup_upd | n_tup_del | n_live_tup | n_dead_tup 
-----------+-----------+-----------+------------+------------
      1000 |         0 |       249 |        751 |          0
(1 row)

表autovacuum setting的设置

可以通过设置单个表的存储参数来重写此行为,这样会忽略全局设置。

postgres=# alter table test set (autovacuum_vacuum_cost_limit=500);
ALTER TABLE
postgres=# alter table test set (autovacuum_vacuum_cost_delay=10);
ALTER TABLE
postgres=# \d+ test
                         Table "public.test"
 Column |  Type   | Modifiers | Storage  | Stats target | Description 
--------+---------+-----------+----------+--------------+-------------
 id     | integer |           | plain    |              | 
 info   | text    |           | extended |              | 
Has OIDs: no
Options: autovacuum_vacuum_cost_limit=500, autovacuum_vacuum_cost_delay=10

1.7 一次可以运行多个autovacuum过程

在可能包含多个数据库的实例/集群上,一次运行的autovacuum进程数不能超过下面参数设置的值:

autovacuum_max_workers = 3             # max number of autovacuum subprocesses
                                        # (change requires restart)
autovacuum_naptime = 1min         # time between autovacuum runs

启动下一个autovacuum之前的等待时间:

autovacuum_naptime/N,其中N是实例中数据库的总数。真空是IO密集型操作,设置了一些参数来最小化真空对IO的影响。

# 可达到的总成本限制,结合所有的autovacuum作业
autovacuum_vacuum_cost_limit = -1      # default vacuum cost limit for
                                        # autovacuum, -1 means use
                                        # vacuum_cost_limit
# 当一个清理工作达到autovacuum_vacuum_cost_limit指定的成本限制时,autovacuum将休眠数毫秒
autovacuum_vacuum_cost_delay = 20ms    # default vacuum cost delay for
                                        # autovacuum, in milliseconds;
                                        # -1 means use vacuum_cost_delay
                                        
# - Cost-Based Vacuum Delay -
#vacuum_cost_delay = 0                  # 0-100 milliseconds
# 读取已在共享缓冲区中且不需要磁盘读取的页的成本
vacuum_cost_page_hit = 1               # 0-10000 credits
# 获取不在共享缓冲区中的页的成本
vacuum_cost_page_miss = 10             # 0-10000 credits
# 在每一页中发现死元组时写入该页的成本
vacuum_cost_page_dirty = 20            # 0-10000 credits
#vacuum_cost_limit = 200                # 1-10000 credits                                        

1秒(1000ms)中会发生什么

在读取延迟为0毫秒的最佳情况下,autovacuum可以唤醒并进入睡眠50次(1000毫秒/20毫秒),因为唤醒之间的延迟需要20毫秒。

由于在共享缓冲区中每次读取一个页面的相关成本时1,因此在每个唤醒中可以读取200个页面(因为上面把总成本限制设置为200),在50个唤醒中可以读取50*200个页面。

如果在共享缓冲区中找到了所有具有死元组的页,并且autovacuum代价延迟为20毫秒,则它可以在每一轮中读取:((200/vacuum_cost_page_hit)*8)kb,这需要等待autovacuum代价延迟时间量。

因此,考虑到块大小为8192字节,autovacuum最多可以读取:50 * 200 * 8kb=78.13 MB/s。

如果块不在共享缓冲区中,需要从磁盘读取,则autovacuum最多可以读取:50 * 200 * 8kb=7.81 MB/s。

现在,为了从页/块中删除死元组,写操作的开销是:vacuum_cost_page_dirty,默认20。

一个autovacuum每秒最多可以写/脏:50 * (200/vacuum_cost_page_dirty) * 8kb=3.9 MB/s。

谨慎设置autovacuum_max_workers

通常,此成本平均分配给实例中运行的所有autovacuum过程的autovacuum_max_workers数。因此增加vacuum_max_workers可能会延迟当前运行的autovacuum workers的autovacuum执行。而增加autovacuum_vacuum_cost_limit可能会导致IO瓶颈。可以通过设置单个表的存储参数来重写此行为,这样会忽略全局设置。

1.8 冻结txids

Transaction ID

  • 每当事物开始时,事务管理器就会分配一个唯一的标识符,称为事物id(txid);

  • PostgreSQL的txid是一个32位无符号整数,大约42亿;

  • 冻结txid的目的就是为了行可见性;

    1、删除指向相应死元组的死元组和索引元组;

    2、清除CLOG中不必要的部分。

    3、冻结旧txids;

    4、更新FSM,VM和统计信息。

  • 请注意,BEGIN命令没有指定txid。在PostgreSQL中,当第一个命令在BEGIN命令执行后执行时,事务管理器会分配一个txid,然后分配它的事物开始。

postgres=# begin;
BEGIN
postgres=# select txid_current();
 txid_current 
--------------
         100
(1 row)
  • 0表示无效的 txid。
  • 1表示Bootstrap txid,仅在数据库集群初始化时使用。
  • 2表示Frozen txid。
  • Txid 可以相互比较。例如,在 txid 100 的观点上,大于 100 的 txid 是“未来”,从 txid 100 看是不可见的;小于 100 的 txid 是“过去的”并且可见(图 5.1 a))。

PostgreSQL 中的事务 ID

image

元组结构

image

HeapTupleHeaderData 结构在src/include/access/htup_details.h中定义。

  • t_xmin保存插入此元组的事务的 txid。
  • t_xmax保存删除或更新此元组的事务的 txid。如果此元组尚未删除或更新,则 t_xmax 设置为0,表示无效。
  • t_cid保存的是命令id(cid),表示从0开始在当前事务中执行该命令之前执行了多少条SQL命令。例如,假设我们在单个事务中执行了三个INSERT命令:’BEGIN; INSERT; INSERT; INSERT; END;’。如果第一个命令插入此元组,则 t_cid 设置为 0。如果第二个命令插入此元组,则 t_cid 设置为 1,依此类推。
  • t_ctid保存指向自身或新元组的元组标识符(tid)。tid用于标识表中的元组。当这个元组更新时,这个元组的t_ctid指向新的元组;否则, t_ctid 指向它自己。

插入、删除和更新元组

为了专注于元组,下面没有表示页眉和行指针。

元组的表示

image

插入

通过插入操作,一个新的元组被直接插入到目标表的一页中。

元组插入

image

假设一个 tuple 被一个 txid 为 99 的事务插入到一个页面中。在这种情况下,插入的 tuple 的 header 字段设置如下。

  • 元组_1:
    • t_xmin设置为 99,因为这个元组是由 txid 99 插入的。
    • t_xmax设置为 0,因为该元组尚未被删除或更新。
    • t_cid设置为 0,因为这个元组是 txid 99 插入的第一个元组。
    • t_ctid设置为 (0,1),指向自身,因为这是最新的元组。

删除

在删除操作中,目标元组被逻辑删除。执行 DELETE 命令的 txid 的值设置为元组的 t_xmax。

元组删除

image

假设 Tuple_1 被 txid 111 删除,此时 Tuple_1 的头部字段设置如下。

  • 元组_1:
    • t_xmax设置为 111。

如果提交 txid 111,则不再需要 Tuple_1。通常,不需要的元组在 PostgreSQL 中被称为死元组。

死元组最终应该从页面中删除。清理死元组被称为VACUUM处理。

更新

在更新操作中,PostgreSQL 在逻辑上删除最新的元组并插入一个新元组。

更新该行两次

image

假设已经被 txid 99 插入的行被 txid 100 更新了两次。

当执行第一个 UPDATE 命令时,通过将 txid 100 设置为 t_xmax 逻辑删除Tuple_1,然后插入 Tuple_2。然后,将Tuple_1的 t_ctid 重写为指向 Tuple_2。Tuple_1 和 Tuple_2 的头字段如下。

  • 元组_1:
    • t_xmax设置为 100。
    • t_ctid从 (0, 1) 重写为 (0, 2)。
  • 元组_2:
    • t_xmin设置为 100。
    • t_xmax设置为 0。
    • t_cid设置为 0。
    • t_ctid设置为 (0,2)。

当执行第二个 UPDATE 命令时,与第一个 UPDATE 命令一样,逻辑上删除 Tuple_2 并插入 Tuple_3。Tuple_2 和 Tuple_3 的头字段如下。

  • 元组_2:
    • t_xmax设置为 100。
    • t_ctid从 (0, 2) 重写为 (0, 3)。
  • 元组_3:
    • t_xmin设置为 100。
    • t_xmax设置为 0。
    • t_cid设置为 1。
    • t_ctid设置为 (0,3)。

与删除操作一样,如果提交 txid 100,则 Tuple_1 和 Tuple_2 将是死元组,如果中止 txid 100,则 Tuple_2 和 Tuple_3 将是死元组。

页面检查

PostgreSQL 提供了一个扩展pageinspect,它是一个贡献模块,用于显示数据库页面的内容。

testdb=# CREATE EXTENSION pageinspect;
CREATE EXTENSION
testdb=# CREATE TABLE tbl (data text);
CREATE TABLE
testdb=# INSERT INTO tbl VALUES('A');
INSERT 0 1
testdb=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |     99 |      0 |     0 | (0,1)
(1 row)
postgres=# delete from tbl;
DELETE 1
postgres=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |   4965 |   4966 |     0 | (0,1)
(1 row)

postgres=# truncate table tbl;
TRUNCATE TABLE
postgres=# INSERT INTO tbl VALUES('A');
INSERT 0 1
postgres=# begin;
BEGIN
postgres=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |   4979 |      0 |     0 | (0,1)
(1 row)

postgres=# update tbl set data = 'B';
UPDATE 1
postgres=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |   4979 |   4980 |     0 | (0,2)
     2 |   4980 |      0 |     0 | (0,2)
(2 rows)

postgres=# update tbl set data = 'C';
UPDATE 1
postgres=# SELECT lp as tuple, t_xmin, t_xmax, t_field3 as t_cid, t_ctid 
                FROM heap_page_items(get_raw_page('tbl', 0));
 tuple | t_xmin | t_xmax | t_cid | t_ctid 
-------+--------+--------+-------+--------
     1 |   4979 |   4980 |     0 | (0,2)
     2 |   4980 |   4980 |     0 | (0,3)
     3 |   4980 |      0 |     1 | (0,3)
(3 rows)
posted on 2023-09-20 23:15  jl1771  阅读(28)  评论(0编辑  收藏  举报