大量使用临时表带来的问题,替代方案,以及如何擦屁股

以前有使用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。
如图
大量使用临时表带来的问题,替代方案,以及如何擦屁股
这张图表示,当前XID=99999,当前数据库集群中未结束的事务是XID=1000的事务,假设在XID=1000后系统产生了100GB的垃圾数据(如UPDATE了一个大表,或者删除或更新了一大批数据)。
那么这100GB是没有办法被vacuum回收的,只能回收XID=1000以前的垃圾。
这就是膨胀的起因。 (长事务同时伴随大量更新或删除产生垃圾)

PostgreSQL 9.6会引入快照过旧的功能,有点类似于Oracle 的undo snapshot too old的玩意。来防止因存在很长的事务带来的膨胀问题。
如图
大量使用临时表带来的问题,替代方案,以及如何擦屁股
例如用户设置了只保留最近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操作,否则可能是致命伤害。
万一你不小心这么做了,可以使用我上面教你的挪动记录的方法来补救。

上一篇:Redis基础学习笔记


下一篇:[20150529]使用bbed解决丢失的归档.txt