第 21 章 日常数据库维护任务
为了保持PostgreSQL服务器平稳运行,有一些必须定期执行的例行维护事务。这里讨论的任务本质上是重复性的,使用诸如cron脚本之类的标准 Unix 工具就可以轻松自动化。但是,建立合适的脚本并检查它们是否成功执行是数据库管理员的职责。
一项显而易见的维护任务是按固定的计划定期创建数据的备份副本。如果没有较新的备份,在灾难(磁盘故障、火灾、误删除关键表等)发生之后你就没有任何恢复的机会。PostgreSQL中可用的备份和恢复机制在第 22 章中有详细讨论。
另一类主要的维护任务是对数据库进行周期性的“清理”(vacuuming)。这项活动在第 21.1 节中讨论。
还有一件可能需要周期性关注的事情是日志文件管理。这在第 21.3 节中讨论。
与其他一些数据库管理系统相比,PostgreSQL 的维护需求较低。尽管如此,对这些任务给予适当的关注,将大大有助于确保你愉快而高效地使用这个系统。
21.1. 例行清理 #
PostgreSQL的VACUUM命令必须定期运行,原因有以下几点:
- 回收被更新或删除的行占用的磁盘空间。
- 更新PostgreSQL查询规划器使用的数据统计信息。
- 防止因事务 ID 回卷(transaction ID wraparound)而丢失非常旧的数据。
出于这些原因而执行的VACUUM操作的频率和范围,因各站点的需求而异。因此,数据库管理员必须理解这些问题并制定合适的维护策略。本节集中解释高层面的问题;关于命令语法等细节,请参阅VACUUM命令参考页。
从PostgreSQL 7.2 开始,标准形式的
VACUUM可以与正常的数据库操作(选择、插入、更新、删除,但不包括对表定义的更改)并行运行。因此,例行清理不像以前的版本那样具有侵入性,也就不必那么刻意地安排在一天中使用率低的时段进行。
21.1.1. 回收磁盘空间 #
在PostgreSQL的正常操作中,对一行执行UPDATE或DELETE并不会立即删除该行的旧版本。为了获得多版本并发控制的好处(见第 12 章),这种做法是必要的:在其他事务仍可能看到某个行版本时,不能删除它。但最终,一个过期或已删除的行版本不再被任何事务关注。它占用的空间必须被回收以供新行重用,以避免磁盘空间需求无限增长。这项工作通过运行VACUUM完成。
显然,频繁更新或删除的表需要比很少更新的表更经常地清理。设置只清理选定表的周期性cron任务可能有用,跳过已知不常变化的表。只有当你同时有大型的频繁更新表和大型的不常更新表时,这才可能有帮助——清理一个小表的额外开销不值得担心。
标准形式的VACUUM最适合以维持磁盘空间的平稳占用为目标。标准形式会找到旧的行版本并使其空间可在表内重用,但它并不十分努力地缩短表文件并把磁盘空间归还给操作系统。如果你需要把磁盘空间归还给操作系统,可以使用
VACUUM FULL——但释放出的磁盘空间如果很快又要重新分配,这样做又有什么意义呢?对于维护频繁更新的表,较频繁的标准VACUUM运行比不频繁的
VACUUM FULL运行是更好的做法。
对大多数站点,推荐的做法是每天在使用率低的时段安排一次数据库范围的VACUUM,必要时辅以对频繁更新表的更频繁清理。(如果集群中有多个数据库,别忘了对每个数据库都进行清理;vacuumdb程序可能会有帮助。)例行的空间回收清理应使用普通的VACUUM,而不是
VACUUM FULL。
在你确知已删除了一个表中大部分行的情况下,推荐使用
VACUUM FULL,这样表的稳态尺寸可以通过
VACUUM FULL更激进的方式大幅收缩。
如果你有一个表的内容会周期性地被完全删除,可以考虑用
TRUNCATE来完成,而不是DELETE后跟
VACUUM。
21.1.2. 更新规划器统计信息 #
PostgreSQL查询规划器依靠关于表内容的统计信息来为查询生成好的计划。这些统计信息由
ANALYZE命令收集,该命令可以单独调用,也可以作为
VACUUM的一个可选步骤调用。拥有相当准确的统计信息很重要,否则糟糕的计划选择可能降低数据库性能。
与为回收空间而清理一样,频繁更新统计信息对频繁更新的表比对不常更新的表更有用。但即使是频繁更新的表,如果数据的统计分布变化不大,也可能不需要更新统计信息。一个简单的经验法则想一想表中各列的最小值和最大值变化有多大。例如,一个包含行更新时间的timestamp列,随着行的添加和更新,其最大值会不断增加;这样的列可能比一个包含网站页面 URL 的列需要更频繁的统计更新。URL 列可能同样经常变化,但其值的统计分布可能变化相对较慢。
可以在特定的表甚至表的特定列上运行ANALYZE,因此如果你的应用需要,可以灵活地比其他统计信息更频繁地更新某些统计信息。然而在实践中,这个特性的用处值得怀疑。从PostgreSQL 7.2 开始,ANALYZE即使在大表上也是相当快的操作,因为它使用对表行的统计随机抽样而不是读取每一行。所以,隔一段时间就对整个数据库运行一次它,可能要简单得多。
提示
虽然按列调整ANALYZE的频率可能收效不大,但你很可能发现按列调整ANALYZE收集的统计信息的详细程度是值得的。在WHERE子句中大量使用且数据分布高度不规则的列,可能需要比其他列更细粒度的数据直方图。见ALTER TABLE SET STATISTICS。
对大多数站点,推荐的做法是每天在使用率低的时段安排一次数据库范围的ANALYZE;这可以有效地与每夜的
VACUUM结合进行。不过,表统计信息变化相对缓慢的站点可能会发现这样做过了头,不那么频繁的ANALYZE
运行就足够了。
21.1.3. 防止事务 ID 回卷故障 #
PostgreSQL的 MVCC 事务语义依赖于能够比较事务 ID(XID)号:一个插入 XID 大于当前事务 XID 的行版本处于“未来”,对当前事务不可见。但由于事务 ID 的长度有限(撰写本文时为 32 位),一个运行很长时间(超过 40 亿个事务)的集群将遭遇事务 ID 回卷:XID 计数器回卷到零,突然之间,过去的事务看起来处于未来——这意味着它们的输出变得不可见。简而言之,灾难性的数据丢失。(实际上数据还在那里,但如果你无法访问它,这也于事无补。)
在PostgreSQL 7.2 之前,防御 XID
回卷的唯一方法是至少每 40 亿个事务重新initdb一次。这对高流量站点当然不太令人满意,因此设计出了更好的解决方案。新方法允许服务器无限期保持运行,无需initdb或任何形式的重启。代价是这条维护要求:数据库中的每个表必须至少每 10 亿个事务被清理一次。
在实践中这并不是一个苛刻的要求,但由于不满足它的后果可能是完全的数据丢失(而不只是浪费磁盘空间或性能下降),因此专门做了一些规定来帮助数据库管理员跟踪自上次VACUUM
以来的时间。本节余下部分给出细节。
新的 XID 比较方法区分出两个特殊的 XID,即编号 1 和 2(BootstrapXID和FrozenXID)。这两个
XID 总是被认为比每个普通 XID 都旧。普通 XID(大于 2 的那些)使用模 231算术进行比较。这意味着对每个普通
XID 来说,有 20 亿个“更旧”的 XID 和 20 亿个“更新”的 XID;换一种说法,普通 XID 空间是循环的,没有终点。因此,一旦一个行版本以某个普通 XID 创建,无论我们谈论的是哪个普通 XID,该行版本在接下来的 20 亿个事务中都会显得处于“过去”。如果该行版本在超过 20 亿个事务后仍然存在,它将突然显得处于未来。为了防止数据丢失,必须在旧的行版本达到 20 亿个事务这一时限之前的某个时刻,把它们的 XID 重新指定为FrozenXID。一旦被指定了这个特殊的 XID,无论回卷问题如何,它们对所有普通事务都会显得处于“过去”,因此这样的行版本在被删除之前一直有效,不管时间多长。这种
XID 的重新指定由VACUUM处理。
VACUUM的常规策略是把FrozenXID重新指定给任何普通 XID 已在过去中超过 10 亿个事务的行版本。这个策略保留了原始的插入 XID,直到它不再可能被关注。(事实上,大多数行版本可能终生都不会被“冻结”。)按照这个策略,任何表上两次VACUUM运行之间的最大安全间隔正好是
10 亿个事务:如果你等得更久,某个上次还不够旧、未被重新指定的行版本现在可能已超过 20 亿个事务并回卷到了未来——也就是说,对你而言丢失了。(当然,再过 20 亿个事务后它会重新出现,但那无济于事。)
由于前述原因本来就需要周期性地运行VACUUM,任何一个表长达 10 亿个事务未被清理的情况不太可能发生。但为了帮助管理员确保满足这个约束,VACUUM把事务 ID 统计信息存储在系统表pg_database中。特别地,在完成任何数据库范围的清理操作(即不指定具体表的VACUUM)时,会更新数据库的pg_database行的
datfrozenxid列。该字段中存储的值是那次
VACUUM命令使用的冻结截止 XID。所有比该截止 XID
旧的普通 XID 都保证在该数据库中已被替换为FrozenXID。查看这些信息的一个便捷方法是执行查询
SELECT datname, age(datfrozenxid) FROM pg_database;
age列度量从截止 XID 到当前事务 XID 的事务数。
按照标准的冻结策略,一个刚清理过的数据库的age列将从 10 亿开始。当age接近 20 亿时,必须再次清理该数据库,以避免回卷故障的风险。推荐的做法是至少每 5 亿个事务清理每个数据库一次,以提供充足的安全余量。为了帮助满足这条规则,每次数据库范围的VACUUM都会在存在
pg_database项显示age超过 15 亿个事务时自动发出警告,例如:
play=# VACUUM; WARNING: some databases have not been vacuumed in 1613770184 transactions HINT: Better vacuum them within 533713463 transactions, or you may have a wraparound failure. VACUUM
带FREEZE选项的VACUUM使用更激进的冻结策略:足够旧、可被所有打开的事务视为有效的行版本都会被冻结。特别地,在一个完全空闲的数据库中执行
VACUUM FREEZE,可以保证该数据库中的所有行版本都被冻结。因此,只要该数据库不以任何方式被修改,就不需要后续的清理来避免事务 ID 回卷问题。这项技术被
initdb用来准备template0数据库。它也应当被用来准备任何要在pg_database中标记为
datallowconn = false的用户创建数据库,因为你无法连接的数据库没有便捷的清理方法。注意,VACUUM关于未清理数据库的自动警告消息将忽略datallowconn = false的
pg_database项,以避免对这些数据库发出虚假警告;因此,确保这样的数据库被正确冻结是你的责任。