大量使用临时表带来的系统表如pg_attribute膨胀问题,替代方案,以及如何擦屁股 - Greenplum, PostgreSQL最佳实践

3 minute read

背景

以前有使用Greenplum的朋友遇到元表膨胀的问题,总结了原因写了一篇文章如下。

建议先阅读这篇文档,再阅读本文,里面有些原理我在本文就不讲了。

http://blog.163.com/digoal@126/blog/static/1638770402014616113353555

近日,又有使用PostgreSQL的朋友也遇到类似的问题,一些元表膨胀非常厉害。

用户大量的使用临时表,频繁的创建(PG的临时表是需要随时用随时建的,每个会话都要自己建,而且每个临时表会在pg_class,pg_attribute 中留下痕迹,用完还需要从元表中 delete 这些元数据),因此元表pg_attribute, pg_rewrite, pg_class 会出现大量的dead tuple。

同时用户的业务需要在数据库长期跑长事务,这个也是膨胀的关键点。

图说问题, 一针见血

目前PostgreSQL的版本,在回收垃圾时,只能回收比现存最早事务更老的垃圾tuple。

如图

1

这张图表示,当前XID=99999,当前数据库集群中未结束的事务是XID=1000的事务,假设在XID=1000后系统产生了100GB的垃圾数据(如UPDATE了一个大表,或者删除或更新了一大批数据)。

那么这100GB是没有办法被vacuum回收的,只能回收XID=1000以前的垃圾。

这就是膨胀的起因。 (长事务同时伴随大量更新或删除产生垃圾)

PostgreSQL 9.6会引入快照过旧的功能,有点类似于Oracle 的undo snapshot too old的玩意。来防止因存在很长的事务带来的膨胀问题。

如图

2

例如用户设置了只保留最近1000个事务产生的垃圾,当老的事务访问到已经被vacuum清除的数据时,会报snapshot too old的错误。 这个方法大大降低了膨胀的概率。

如何戒掉临时表(替代方案)

回到本文的主题,用户除了用临时表,还有什么方法?

因为PG的临时表是随用随建的(会话断开又得建), 有什么好的替代方案,不要动到元数据吗?

答案是,当然有。

如果你是在存储过程中使用临时表,你可以建立composite type,然后用数组来存储这些临时数据,这样就不会动到元数据了。

例子 :

postgres=# create type t as (id int, info text, crt_time timestamp);  
CREATE TYPE  
  
create or replace function f() returns void as $$                                                                      
declare   
  tmp_t t[];  
  tmp t;  
begin  
  -- 产生一批临时记录,聚合后存入数组,原来你可能要存入临时表  
  select array_agg((t1)::t) into tmp_t from (select id,md5(random()::text),clock_timestamp() from generate_series(1,100) t(id)) t1;  
  
  -- 把数组当表使用, unnest数组,   
  -- select (unnest(tmp_t)).*;  
    
  -- 你可能会用临时数据做一些JOIN操作, 用unnest和subquery就能和其他表一样JOIN操作  
  -- select t2.* from t2 join (select (unnest(tmp_t)).*) t1 on t1.id=t2.id where ....;  
  -- update t2 set xx=xx from (select (unnest(tmp_t)).*) t1 where t2.xx=t1.xx and t1.xx=xx and t2.xx=xx;  
  
  -- 你可能会对临时数据做一些处理  
  for i in 1..array_length(tmp_t, 1) loop  
    raise notice '%', tmp_t[i];  
    tmp := tmp_t[i];  
    tmp.info := 'new '||tmp.info;  
    tmp_t[i] := tmp;  
    raise notice '%', tmp_t[i];  
    -- 逻辑处理可以放在这里做  
  end loop;  
end;  
$$ language plpgsql strict;  
  
...  
NOTICE:  (91,dc3606d6b017f60fc10b945ab00b02bd,"2016-06-15 17:53:41.749235")  
NOTICE:  (91,"new dc3606d6b017f60fc10b945ab00b02bd","2016-06-15 17:53:41.749235")  
...  

使用composite type的数组完全能满足原来使用临时表才能满足的需求。

但是也需要注意array是在内存中的,如果临时数据非常庞大,使用数组可能OOM。

虽然临时表也是在内存中的,但是它太大的时候可以落盘,不会OOM。

数组求记录数,使用 array_length就可以了,千万不要unnest转成行再count。

学会擦屁股

如果元数据已经膨胀了怎么处理?该擦的屁股还得擦。

对于PostgreSQL 9.5 你可以放心大胆的擦,(其他版本暂时没有验证过)。

可选的方法有 vacuum full, cluster, reindex.

如果要回收空间,同时重建索引,使用vacuum full。

如果要重排元表顺序,使用cluster。

如果要重新建索引,使用reindex 。

根据你自己的情况选择就可以了。

当你使用的是Greenplum时,要特别注意。

就是pg_class,这个元表是比较特殊的,因为其他元表的记录也记录在这里,包括索引的元表也记录在pg_class里面。

对它进行vacuum full, reindex, cluster都会导致有几个元表的记录会排到这张表的最后面去。

例如我的系统中有几百万张表,这样快速生成几百万表

CREATE OR REPLACE FUNCTION public.f(id integer)  
 RETURNS void  
 LANGUAGE plpgsql  
 STRICT  
AS $function$   
declare   
  sql text;  
  i int;   
begin   
  for i in id..id+100 loop   
    sql='create table if not exists tbl'||i||' (id int)';  
    execute sql;  
  end loop;  
end;  
$function$;  
  
vi test.sql  
\setrandom id 1 1000000000  
select f(:id);  
  
pgbench -M prepared -n -r -P 1 -f ./test.sql -c 20 -j 20 -T 1000  

对pg_class这样操作后,有几条元数据就会跑到pg_class的最后面去。

1.

vacuum full pg_class;    
  
然后你会发现这几个小家伙排到最后了  
postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;  
   oid    |              relname              |    ctid      
----------+-----------------------------------+------------  
     1259 | pg_class                          | (92424,30)  
     3455 | pg_class_tblspc_relfilenode_index | (92424,29)  
     2663 | pg_class_relname_nsp_index        | (92424,28)  
     2662 | pg_class_oid_index                | (92424,27)  

2.

cluster pg_class using pg_class_oid_index;    

– 你可能会想,oid=这几个的是小的,应该到前面的行了吧。 但实际并不是如此。
这几个小家伙排到最后了

postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;  
   oid    |              relname              |    ctid      
----------+-----------------------------------+------------  
     1259 | pg_class                          | (92424,30)  
     3455 | pg_class_tblspc_relfilenode_index | (92424,29)  
     2663 | pg_class_relname_nsp_index        | (92424,28)  
     2662 | pg_class_oid_index                | (92424,27)  
 13089945 | test_pkey                         | (92424,25)  
 13089944 | pg_toast_13089939_index           | (92424,24)  
 13089942 | pg_toast_13089939                 | (92424,23)  
 13089939 | test                              | (92424,22)  

3.

reindex table pg_class;    

– 别看它只是重建索引,但是实际上它也会影响 pg_class_oid_index , pg_class_relname_nsp_index , pg_class_tblspc_relfilenode_index 这三在pg_class的顺序。
这几个小家伙又奇迹般的排到最后了

postgres=# select oid,relname,ctid from pg_class order by ctid desc limit 10;  
   oid    |              relname              |    ctid      
----------+-----------------------------------+------------  
     3455 | pg_class_tblspc_relfilenode_index | (92424,33)  
     2663 | pg_class_relname_nsp_index        | (92424,32)  
     2662 | pg_class_oid_index                | (92424,31)  
     1259 | pg_class                          | (92424,30)  
 13089945 | test_pkey                         | (92424,25)  

以上诡异的问题,在Greenplum数据库中就是致命的(不知道现在的版本改进了没有,反正PostgreSQL 9.5是没这个问题的)。

如果你在GP中有几百万的表,这几条元数据因以上原因跑pg_class 的最后几页去了,那问题就大了(后面会讲回天的方法)。

http://cncc.bingj.com/cache.aspx?q=osdba+pg_index+reindex&d=5025474051580513&mkt=zh-CN&setlang=zh-CN&w=pa4BQX_-Pu7tUlh6kM34Dv-xu2uzi4m6

以前老唐在Greenplum上对元表做了这样的操作,结果很感人。

greenplum中reindex pg_class后发生狂读pg_class的问题分析

当访问任何一个表时,需要从pg_class表中提取这个表的元数据信息,而在pg_class中查找表的原数据信息,需要访问索引pg_class_oid_index,而访问索引pg_class_oid_index,也需要从pg_class表中获得索引pg_class_oid_index的元数据信息,而获得索引pg_class_oid_index自己的元数据信息,就不能再通过索引自己去查找自己的信息了,这里就只能从头扫描表pg_class来获得,而由于索引pg_class_oid_index的条目被移到了最后的位置,所以导致需要几乎把pg_class从头 扫描到尾才能找到pg_class_oid_index的条目,这样就大大的降低了数据库的性能。

以前也做过pg_class,为什么没有出来这个问题呢?我和任振中也做了测试发现,当原先的0号块中有空闲空间时,做reindex时,索引pg_class_oid_index的 条目仍会在0号块中,这样就不会出现上面的问题了。

由此可知,在greenplum中是不能随便对pg_class表做reindex了。

如果不小心把pg_class_oid_index等几个元表弄到pg_class 的末端了咋办

其实还是可以回天的,而且方法依旧很简单。

把头几个页的数据更新掉,然后用 vacuum回收前几页,然后把要挪动的记录更新一下,FSM会随机选择空页面给你放,就有机会回到前面的页。

例子

当前他们在这里

postgres=# select ctid,oid,relname from pg_class where oid in (1259,3455,2663,2662);  
    ctid    | oid  |              relname                
------------+------+-----------------------------------  
 (92424,42) | 1259 | pg_class  
 (92424,43) | 2662 | pg_class_oid_index  
 (92424,44) | 2663 | pg_class_relname_nsp_index  
 (0,1)      | 3455 | pg_class_tblspc_relfilenode_index  
(4 rows)  

把前面5页的记录都更新一遍

postgres=# update pg_class set relkind=relkind where ctid::text ~ '^\(0,' or ctid::text ~ '^\(1,' or ctid::text ~ '^\(2,' or ctid::text ~ '^\(3,' or ctid::text ~ '^\(4,' or ctid::text ~ '^\(5,';  
UPDATE 207  

回收垃圾

postgres=# vacuum verbose pg_class;  

更新要挪动的记录

postgres=# update pg_class set relkind=relkind where oid in (1259,3455,2663,2662);  
UPDATE 4  

已经挪到前面的页了,恭喜你

postgres=# select ctid,oid,relname from pg_class where oid in (1259,3455,2663,2662);  
  ctid  | oid  |              relname                
--------+------+-----------------------------------  
 (0,46) | 1259 | pg_class  
 (0,48) | 2662 | pg_class_oid_index  
 (1,1)  | 2663 | pg_class_relname_nsp_index  
 (1,2)  | 3455 | pg_class_tblspc_relfilenode_index  
(4 rows)  

参考

src/backend/catalog/postgres.bki

create pg_proc 1255 bootstrap rowtype_oid 81  
...  
insert OID = 1242 ( boolin 11 10 12 1 0 0 0 f f f f t f i 1 0 16 "2275" _null_ _null_ _null_ _null_ _null_ boolin _null_ _null_ _null_ )  
insert OID = 1243 ( boolout 11 10 12 1 0 0 0 f f f f t f i 1 0 2275 "16" _null_ _null_ _null_ _null_ _null_ boolout _null_ _null_ _null_ )  
...  

注意事项

1. 因为vacuum full和cluster都是DDL操作,需要注意锁等待和本身需要运行的时间,建议空闲时间操作。

并且设置语句超时或锁超时。

2. 对于Greenplum 千万别对pg_class执行vacuum full, reindex, cluster操作,否则可能是致命伤害。

万一你不小心这么做了,可以使用我上面教你的挪动记录的方法来补救。

Flag Counter

digoal’s 大量PostgreSQL文章入口