DBA:当VACUUM通过时-我们手动清洁桌子

VACUUM只能从PostgreSQL中的一个表中“清理” 任何人都看不到的 -也就是说,没有一个活动查询在更改这些记录之前启动。

但是是否存在这种令人不快的类型(OLTP上的长期OLAP负载)? 如何清理被长查询包围而不是踩耙的活跃变化的表



我们撒了耙子


首先,我们确定它是什么以及我们要解决的问题如何产生。

通常,这种情况发生在相对较小的桌子上 ,但是其中有很多变化 。 通常,它们要么是不同的计数器/集合/等级 (经常在其上执行UPDATE),要么是用于处理某种持续运行的事件流的缓冲区队列 ,有关事件的记录始终为INSERT / DELETE。

让我们尝试使用等级重现该选项:

CREATE TABLE tbl(k text PRIMARY KEY, v integer); CREATE INDEX ON tbl(v DESC); --       INSERT INTO tbl SELECT chr(ascii('a'::text) + i) k , 0 v FROM generate_series(0, 25) i; 

并行地,在不同的连接中,开始进行长查询,收集一些复杂的统计信息,但不影响我们的表

 SELECT pg_sleep(10000); 

现在,我们多次更新计数器之一的值。 为了实验的纯正,我们将使用dblink在单独的事务中执行此操作 ,因为这实际上会发生:

 DO $$ DECLARE i integer; tsb timestamp; tse timestamp; d double precision; BEGIN PERFORM dblink_connect('dbname=' || current_database() || ' port=' || current_setting('port')); FOR i IN 1..10000 LOOP tsb = clock_timestamp(); PERFORM dblink($e$UPDATE tbl SET v = v + 1 WHERE k = 'a';$e$); tse = clock_timestamp(); IF i % 1000 = 0 THEN d = (extract('epoch' from tse) - extract('epoch' from tsb)) * 1000; RAISE NOTICE 'i = %, exectime = %', lpad(i::text, 5), lpad(d::text, 5); END IF; END LOOP; PERFORM dblink_disconnect(); END; $$ LANGUAGE plpgsql; 

 NOTICE: i = 1000, exectime = 0.524 NOTICE: i = 2000, exectime = 0.739 NOTICE: i = 3000, exectime = 1.188 NOTICE: i = 4000, exectime = 2.508 NOTICE: i = 5000, exectime = 1.791 NOTICE: i = 6000, exectime = 2.658 NOTICE: i = 7000, exectime = 2.318 NOTICE: i = 8000, exectime = 2.572 NOTICE: i = 9000, exectime = 2.929 NOTICE: i = 10000, exectime = 3.808 

怎么了 为什么即使对于单个记录的最简单的UPDATE ,运行时间也降低了7倍 -从0.524ms到3.808ms? 而且我们的评级正在越来越慢地建立。

MVCC是罪魁祸首


这全部与MVCC机制有关, 该机制会强制请求查看记录的所有先前版本。 因此,让我们从“死”版本中清除表:

 VACUUM VERBOSE tbl; 

 INFO: vacuuming "public.tbl" INFO: "tbl": found 0 removable, 10026 nonremovable row versions in 45 out of 45 pages DETAIL: 10000 dead row versions cannot be removed yet, oldest xmin: 597439602 

哦,没什么好清洁的! 并行查询使我们感到困扰 -毕竟,有一天,他可能想引用这些版本(如果?),那么他应该可以使用它们。 因此,即使VACUUM FULL也无法帮助我们。

“夹紧”桌子


但是我们可以肯定的是,我们的表不需要查询。 因此,让我们尝试将系统性能恢复到适当的框架,因为VACUUM通过之后,至少是“手动”地丢弃了表格中多余的所有内容。

为了更清楚,让我们考虑一个缓冲表的示例。 也就是说,有一个很大的INSERT / DELETE流,有时表是完全空的。 但是,如果那里不为空,则必须保存其当前内容

#0:评估情况


很明显,即使在每次操作之后,您都可以尝试对表进行操作,但这没有多大意义-维护开销显然会大于目标请求的吞吐量。

如果出现以下情况,我们将制定标准-“该采取行动了”

  • VACUUM已经运行很长时间了
    我们期望有很大的负载,因此距离上一次[自动] VACUUM为60秒
  • 物理表大小大于目标
    我们将其定义为相对于最小大小的页数(8KB块)的两倍- 每个堆1 blk +每个索引1 blk-对于潜在的空表。 如果我们期望一定数量的数据将始终“正常”保留在缓冲区中,那么收紧该公式是合理的。

验证请求
 SELECT relpages , (( SELECT count(*) FROM pg_index WHERE indrelid = cl.oid ) + 1) << 13 size_norm --    * current_setting('block_size')::bigint,     ?.. , pg_total_relation_size(oid) size , coalesce(extract('epoch' from (now() - greatest( pg_stat_get_last_vacuum_time(oid) , pg_stat_get_last_autovacuum_time(oid) ))), 1 << 30) vaclag FROM pg_class cl WHERE oid = $1::regclass -- tbl LIMIT 1; 

 relpages | size_norm | size | vaclag ------------------------------------------- 0 | 24576 | 1105920 | 3392.484835 

#1:真空


我们无法事先知道并行查询是否确实在阻碍我们-确切地说,自并行查询问世以来,已有多少记录已“过时”。 因此,无论如何,当我们决定以某种方式处理表时,都应首先在其上运行VACUUM-与VACUUM FULL不同,它不会干扰并行的数据读写过程。

同时,他可以立即清除我们要删除的大部分内容。 是的,此表的后续请求将在“热缓存”中发送给我们,这将减少它们的持续时间-因此,减少了通过我们的服务交易阻止其他人的总时间。

#2:有人在家吗?


让我们检查一下-表格中是否有任何内容:

 TABLE tbl LIMIT 1; 

如果仅剩一条记录,那么我们可以节省很多时间-只需执行TRUNCATE即可

它的作用与对每个表的无条件DELETE命令相同,但速度更快,因为它实际上并不扫描表。 此外,它会立即释放磁盘空间,因此在此之后无需执行VACUUM操作。
是否需要同时重置表序列的计数器(RESTART IDENTITY),请自己决定。

#3:一切-反过来!


由于我们在竞争激烈的环境中工作,因此在这里检查表中是否没有条目时,已经有人可以在其中写一些东西了。 我们不应该丢失这些信息,那又如何呢? 没错,必须这样做,以确保没有人可以确定录制。

为此,我们需要为我们的事务启用SERIALIZABLE隔离(是的,在这里我们开始事务)并“紧密”锁定表:

 BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE; 

此级别的阻止是由于我们要对其执行的操作。

#4:利益冲突


我们来到这里,我们想“锁定”平板电脑-例如,如果有人当时正在使用平板电脑,请从平板电脑上阅读吗? 我们将“吊死”以期望释放此块,而其他希望阅读的人将已经埋在我们里面...

为了防止这种情况的发生,请“牺牲自己”-如果我们在一定时间内(允许的很小时间内)仍然无法获得锁,那么我们将从数据库中获取一个异常,但是至少我们不会打扰其余的人。

为此,设置会话变量lock_timeout (对于9.3+版本)或/和statement_timeout 。 要记住的主要事情是,statement_timeout的值仅适用于下一条语句。 也就是说,像这样粘贴时, 它将无法工作

 SET statement_timeout = ...;LOCK TABLE ...; 

为了避免以后恢复该变量的“旧”值,我们使用SET LOCAL表格,该表格将设置范围限制为当前事务。

请记住,statement_timeout适用于所有后续请求,因此,如果表中有很多数据,则事务不能扩展到不可接受的值。

#5:复制数据


如果表并非完全为空,则必须通过辅助临时标签重新保存数据:

 CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl; 

ON COMMIT DROP签名意味着在事务结束时,临时表将不存在,并且您不需要在连接上下文中手动删除它。

由于我们假设没有太多“实时”数据,因此该操作应该足够快。

好,仅此而已! 请记住在事务完成后运行ANALYZE来标准化表的统计信息(如有必要)。

我们收集最终脚本


我们使用这样的“伪python”:

 #     stat <- SELECT relpages , (( SELECT count(*) FROM pg_index WHERE indrelid = cl.oid ) + 1) << 13 size_norm , pg_total_relation_size(oid) size , coalesce(extract('epoch' from (now() - greatest( pg_stat_get_last_vacuum_time(oid) , pg_stat_get_last_autovacuum_time(oid) ))), 1 << 30) vaclag FROM pg_class cl WHERE oid = $1::regclass -- table_name LIMIT 1; #      VACUUM   if stat.size > 2 * stat.size_norm and stat.vaclag is None or stat.vaclag > 60: -> VACUUM %table; try: -> BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; #         1s -> SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s'; -> LOCK TABLE %table IN ACCESS EXCLUSIVE MODE; #          row <- TABLE %table LIMIT 1; #       ""  -   ,    - ""      if row is None: -> TRUNCATE TABLE %table RESTART IDENTITY; else: #      - -> CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE %table; #      -> TRUNCATE TABLE %table; #         -> INSERT INTO %table TABLE _tmp_swap; -> COMMIT; except Exception as e: #    ,     "" -   if not isinstance(e, InterfaceError): -> ROLLBACK; 

而且您不能第二次复制数据吗?
原则上,如果表的oid本身不与BL端的其他活动或DB端的FK绑定,则是可能的:
 CREATE TABLE _swap_%table(LIKE %table INCLUDING ALL); INSERT INTO _swap_%table TABLE %table; DROP TABLE %table; ALTER TABLE _swap_%table RENAME TO %table; 

让我们在源表上运行脚本并检查指标:
 VACUUM tbl; BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE; SET LOCAL statement_timeout = '1s'; SET LOCAL lock_timeout = '1s'; LOCK TABLE tbl IN ACCESS EXCLUSIVE MODE; CREATE TEMPORARY TABLE _tmp_swap ON COMMIT DROP AS TABLE tbl; TRUNCATE TABLE tbl; INSERT INTO tbl TABLE _tmp_swap; COMMIT; 

 relpages | size_norm | size | vaclag ------------------------------------------- 0 | 24576 | 49152 | 32.705771 

一切顺利! 该表已缩小了50倍,并且所有UPDATE再次快速运行。

Source: https://habr.com/ru/post/zh-CN481866/


All Articles