海量写入的恶性循环
摘要: 本文探讨了 PostgreSQL 中的海量写入操作如何通过 WAL(预写式日志)生成、检查点、全页写入和自动清理(autovacuum)过程,形成自我强化的性能下降恶性循环。
我多次遇到客户定期出现海量写入峰值的情况。海量意味着系统难以消化这些写入,而且在前一个峰值还未消化完之前,下一个峰值又出现了。
这种情况可能由不同原因引起,但我发现大多数时候这是应用程序的问题。无论如何,我发现 PostgreSQL 由于其设计方式,可能会让情况变得更糟。
WAL 文件、检查点和全页写入
首先,你需要知道,为了防止任何数据丢失(也因为写入顺序文件比直接写入数据文件更快),数据在向应用程序/用户发送提交确认之前,会先写入 WAL(Write-Ahead Log,预写式日志)文件。这是一种标准的做法。Oracle 也是如此,SQL Server 也一样。实际上我想不出有哪个 RDBMS(关系数据库管理系统)会用其他方式。
时不时地,会进行检查点操作。RDBMS 会将数据从顺序 WAL 文件复制到数据文件中。
从逻辑上讲,如果你大量写入数据库,你就会大量写入 WAL 文件,这将导致更多检查点,从而导致更多数据文件写入。这不应该让你感到困惑。
这引出了下一个问题:全页写入。你可能在 PostgreSQL 中遇到过 full_page_writes 参数(更多信息请参阅 PostgreSQL 文档)。除非你的数据是可扩展的,否则不要禁用 full_page_writes。
为了防止数据损坏,检查点操作后的第一次写入会将完整页面写入 WAL,而不仅仅是更改的数据块。但在海量写入操作的情况下,这可能意味着生成多个 WAL,甚至两个检查点之间的所有 WAL。
因此,海量写入会导致生成更多 WAL,这将导致更多检查点,进而导致生成更多 WAL!这就是我们进入恶性循环的方式。
自动清理(autovacuum)登场
但情况更糟!在 PostgreSQL 中,MVCC(多版本并发控制)的实现方式是:每次需要更新一行时,我们都会通过逻辑删除旧版本并在同一数据文件中写入新版本来写入新的行版本。这在发生回滚时非常方便,因为旧版本的行始终可达。它还避免了你在 Oracle 中长时间运行事务时可能遇到的著名的"snapshot too old"(快照过旧)问题。
因此,时不时地,我们需要"清理"那些被逻辑删除且对任何人都不可见的行... 这就是清理(vacuum)发生的时机。为了简化维护,创建了自动清理守护进程(专业提示:永远不要杀死一个运行了很长时间的自动清理进程,否则你将不得不重新开始)。
本文不打算详细解释自动清理,关于它还有很多可说的... 如果你对这个主题感兴趣,请阅读这个清理处理章节。
无论如何,由于自动清理会产生写入,它也会创建更多 WAL 文件。由于自动清理是由自上次自动清理以来更改的行数触发的,更改的行数越多,触发自动清理进程的概率就越高。
你看出这会导致什么了吗?我们又迎来了另一个恶性循环。
这里有一个小图来解释:
如何解决?
很抱歉我只能回答"视情况而定"。最好的解决方案是理解为什么应用程序如此频繁地向数据库发送这么多写入。如果应用程序存在反模式(antipattern),比如将应用程序状态保存在数据库中,那么解决问题的最佳方法是修复应用程序...(是的,我知道,说起来容易,但也许下次你不会陷入那种反模式了。)另一种解决方案可能是升级你的硬件,使用更快的磁盘、更多内存和更多 CPU,如果你的硬件已知较慢且老旧的话。
你也可以尝试通过调整 PostgreSQL 来减少检查点频率,但请记住,如果你的海量写入是由于不良原因造成的(剧透提醒,这种情况超过 99%),那你的尝试无异于用滴管注满浴缸。
要调整检查点,你可以查看 PostgreSQL 文档页面。我还发现减少 shared_buffers 会减慢 PostgreSQL 的速度,从而减少每分钟创建的 WAL 文件数量。