在数据库中管理膨胀

在数据库中管理膨胀

Greenplum数据库的堆表使用PostgreSQL的多版本并发控制(MVCC)存储实现。被删除或更新的行被从数据库逻辑删除,但是该行的一个不可见映像保留在表中。这些被删除的行(也被称为过期行)由一个空闲空间映射跟踪。运行VACUUM会把过期行标记为可以被后续插入重用的空闲空间。

如果空闲空间映射不足以容纳所有的过期行,VACUUM就不能从导致空闲空间映射溢出的过期行回收空间。磁盘空间只能通过运行VACUUM FULL恢复,这个操作会锁住表,逐行拷贝到文件的开头,然后截断文件。这是一种昂贵的操作,对于大型的表,它可能需要超乎想象的时间来完成。应该只在较小的表上使用这种操作。如果使用者尝试杀死一次VACUUM FULL操作,系统可能会被破坏。

重要:

在大型的UPDATE以及DELETE操作之后非常有必要运行VACUUM,这样可以避免运行VACUUM FULL

如果空闲空间映射溢出并且需要恢复空间,推荐使用CREATE TABLE...AS SELECT命令把该表拷贝为一个新表,这将会创建一个新的紧凑的表。然后删除原始表并且重命名拷贝的表。

对于频繁更新的表来说,有少量或者中等数量的过期行以及空闲空间很正常,空闲空间将随着新数据的加入而被重用。但是当表被允许长得非常大以至于活动数据只占空间的一小部分时,该表就明显地“膨胀”了。膨胀的表要求更多磁盘存储以及可能拖慢查询执行的额外I/O。

膨胀影响堆表、系统目录和索引。

在表上定期运行VACUUM语句可以防止它们长得过大。如果表确实出现了明显的膨胀,必须使用VACUUM FULL语句(或者可替代的过程)来紧缩文件。如果一个大型表变得明显膨胀,更好的方法是使用从数据库表移除膨胀中描述的方法之一来移除膨胀。

注意:
绝不运行VACUUM FULL <database_name>并且不要在Greenplum数据库中的大型表上运行VACUUM FULL

确定空闲空间映射的尺寸

当使用者运行VACUUM时,堆表中的过期行会被增加到一个共享的空闲空间映射中。这个空闲空间映射必须有足够的大小来容纳这些行。如果空闲空间映射不够大,导致空闲空间映射溢出的行所占据的空间将不能通过常规的VACUUM语句回收。使用者将不得不使用VACUUM FULL或者另一种替代方法来恢复那些空间。

使用者可以通过定期运行VACUUM语句来避免空闲空间映射溢出。表膨胀得越厉害,空闲空间映射中要跟踪的行越多。对于有很多对象的超大数据库来说,使用者可能需要增加空闲空间映射的尺寸以防止溢出。

max_fsm_pages配置参数设置在共享空闲空间映射中跟踪其空闲空间的磁盘页面的最大数目。每一个页槽消耗共享内存的六个字节。max_fsm_pages的默认值是200,000。

max_fsm_relations配置参数设置在共享空闲空间映射中跟踪其空闲空间的关系的最大数目。它应该被设置为一个大于数据库中表、索引和系统表总数的值。每个Segment实例的每个关系会消耗大约60字节的内存。

有关这些配置参数的详情请见Greenplum数据库参考指南

检测膨胀

ANALYZE语句所收集的统计信息可以被用来计算存储一个表所要求的磁盘页面的预计数量。页面的预计数量和实际数量之间的差别就是膨胀的度量。gp_toolkit方案提供了一个gp_bloat_diag视图,它通过预计页数和实际页数的比率来确定表膨胀。要使用这个视图,确定为数据库中所有的表都收集了最新的统计信息。然后运行下面的SQL:
gpadmin=# SELECT * FROM gp_toolkit.gp_bloat_diag;
 bdirelid | bdinspname | bdirelname | bdirelpages | bdiexppages |                bdidiag                
----------+------------+------------+-------------+-------------+---------------------------------------
    21488 | public     | t1         |          97 |           1 | significant amount of bloat suspected
(1 row)

其结果只包括发生了中度或者显著膨胀的表。当实际页面数和预期页面的比率超过4但小于10时,就会报告为中度膨胀。当该比率超过10时就会报告显著膨胀。

gp_toolkit.gp_bloat_expected_pages视图会为每个数据库对象列出其已用页面的实际数量和预期数量。
gpadmin=# SELECT * FROM gp_toolkit.gp_bloat_expected_pages LIMIT 5;
 btdrelid | btdrelpages | btdexppages 
----------+-------------+-------------
    10789 |           1 |           1
    10794 |           1 |           1
    10799 |           1 |           1
     5004 |           1 |           1
     7175 |           1 |           1
(5 rows)

btdrelid是该表的对象ID。btdrelpages列报告该表使用的页面数,btdexppages列是预期的页面数。再有,报出的数字是基于表统计信息的,因此要确保在已经被更改的表上运行ANALYZE

从数据库表移除膨胀

VACUUM命令会把过期行加入到共享的空闲空间映射中,这样这些空间能被重用。当在被频繁更新的表上定期运行VACUUM时,过期行所占用的空间可以被迅速地重用,从而防止表文件长得更大。在空闲空间映射被填满之前运行VACUUM也很重要。对于更新密集的表,用户可能需要每天运行VACUUM一次来防止表膨胀。

警告:当表出现显著膨胀时,在运行VACUUM之前先运行ANALYZE会更好。由于ANALYZE使用块级采样,如果一个表中不含有效行的块的比例很高,就会导致ANALYZEpg_class系统目录的reltuples列设置为不准确的值或者0,这可能会导致优化得不好的查询。VACUUM命令会产生更加准确的计数,在ANALYZE之后运行它将更正不准确的行计数估计。

当表积累了显著的膨胀时,运行VACUUM命令并不足够。对于小型表,运行VACUUM FULL <table_name>能够回收导致空闲空间映射溢出的行所使用的空间并且减小表文件的尺寸。不过,VACUUM FULL语句是一种昂贵的操作,它要求一个ACCESS EXCLUSIVE锁并且可能需要异常长的时间完成。比起在一个大型表上运行VACUUM FULL,采用另一种方法从大型文件中移除膨胀会更好。注意每一种从大型表中移除膨胀的方法都是资源密集型的,并且只应该在极端情况下完成。

第一种从大型表中移除膨胀的方法是创建一个将过期行排除在外的表拷贝,删掉原始的表并且把这个拷贝重命名为表名。这种方法使用CREATE TABLE <table_name> AS SELECT语句创建新表,例如:
gpadmin=# CREATE TABLE mytable_tmp AS SELECT * FROM mytable;
gpadmin=# DROP TABLE mytable;
gpadmin=# ALTER TABLE mytabe_tmp RENAME TO mytable;
第二种从表移除膨胀的方法是重新分布该表,这会把该表重建为不含过期行。按照以下步骤:
  1. 把表的分布列记下来。
  2. 把该表的分布策略改为随机:
    ALTER TABLE mytable SET WITH (REORGANIZE=false) 
    DISTRIBUTED randomly;

    这会为该表更改分布策略,但不会移除任何数据。该命令应该会立即完成。

  3. 将分布策略改回其初始设置:
    ALTER TABLE mytable SET WITH (REORGANIZE=true) 
    DISTRIBUTED BY (<原始分布列>);

    这一步会重新分布数据。因为表之前是用同样的分布键分布的,表中的行只需要简单地在同一Segment上重写即可,同时排除过期行。

从索引移除膨胀

VACUUM命令只会从表中恢复空间。要从索引中恢复空间,需要使用REINDEX命令重建它们。

要在一个表上重建所有的索引,可运行REINDEX table_name;。要重建一个特定的索引,可运行REINDEX index_name;REINDEX不会为该索引更新reltuplesrelpages统计信息,因此有必要在重建索引后ANALYZE这个表更新这些统计信息。

从系统目录移除膨胀

Greenplum数据库系统目录也是堆表并且也可能随着时间推移变得膨胀。随着数据库对象被创建、修改或者删除,过期行会留在系统目录中。使用gpload装载数据会加剧膨胀,因为gpload会创建并且删除外部表(为了避免使用gpload,推荐使用gpfdist装载数据。

系统目录中的膨胀会导致扫描表所需的时间增加,例如在创建解释计划时需要扫描系统目录。系统目录会被频繁扫描,那么如果它们变得膨胀,整体的系统性能都会退化。

推荐每晚在系统目录上运行VACUUM,或者至少每周运行一次。同时,运行REINDEX SYSTEM从索引中移除膨胀。此外,还可以使用带-s--system)选项的reindexdb工具对系统目录重建索引。在重建索引之后,还有必要运行ANALYZE,因为REINDEX命令重构的索引的统计信息为空。

下面的脚本在系统目录上运行VACUUMREINDEXANALYZE
#!/bin/bash
DBNAME="<database_name>"
SYSTABLES="' pg_catalog.' || relname || ';' from pg_class a, pg_namespace b \
where a.relnamespace=b.oid and b.nspname='pg_catalog' and a.relkind='r'"
psql -tc "SELECT 'VACUUM' || $SYSTABLES" $DBNAME | psql -a $DBNAME
reindexdb -s -d $DBNAME
analyzedb -s pg_catalog -d $DBNAME

如果系统目录膨胀得很显著,使用者就必须执行一次大强度的系统目录维护过程。移除膨胀的CREATE TABLE AS SELECT以及重新分布键方法不能被用于系统目录。使用者必须转而在计划的停机时段运行VACUUM FULL。在此期间,停止系统上所有的目录活动,VACUUM FULL会对系统目录取得排他锁。定期运行VACUUM能够阻止对这种代价更高的过程的需要。

从追加优化表移除膨胀

对追加优化表的处理与堆表有很大不同。尽管追加优化表允许更新、插入和删除,但它们并非为这些操作而优化,因此不推荐对追加优化表使用这些操作。如果使用者采纳这一建议并且为一次装载/多次读取负载使用追加优化,追加优化表上的VACUUM几乎会即刻运行。

如果使用者确实在追加优化表上运行了UPDATE或者DELETE命令,过期行会由一个辅助位图而不是空闲空间映射来跟踪。VACUUM是唯一能恢复空间的方式。在有过期行的追加优化表上运行VACUUM会通过把整个表重写成没有过期行的表以紧缩该表。不过,如果表中过期行的百分数超过了gp_appendonly_compaction_threshold配置参数的值,则不会执行任何行动,该参数的默认值是10(10%)。每个Segment上都会检查该阈值,因此VACUUM语句可能会在某些Segment上对追加优化表进行紧缩而在其他Segment上不做紧缩。通过将gp_appendonly_compaction参数设置为no可以禁用对追加表的紧缩。