代码改变世界

10 Things I Hate About PostgreSQL

2021-06-21 10:30  abce  阅读(182)  评论(0编辑  收藏  举报

看到一篇题为《Ten Things I Hate About PostgreSQL》的文章。

原文地址: https://rbranson.medium.com/10-things-i-hate-about-postgresql-20dbab8c2791

这里只是大概的翻译了一下,对原文观点不做任何讨论。此外,增加了一些自己的笔记。感兴趣的请回看原文。

 

1.Disastrous XID Wraparound.

XID的wraparound是灾难性的。该问题导致过很多长时间停机的故障,长达数天。

PostgreSQL的MVCC事务语义依赖于能够比较事务ID (XID)号:行版本的插入XID大于当前事务的XID就表示是“将来的”,并且不应该对当前事务可见。XID即transaction id。在一个postgresql cluster中,xid的长度是受限制的(32位)。如果长时间运行,超过了40亿个,就会发生事务id的wraparound:xid计数器会重新回到0计数,这样就会出现事务明明是之前老的事务,却一下子变成了"将来的"事务,也就意味着变成了不可见的了。一句话,发生了灾难性的数据丢失。(实际上数据还是在那里,只是对你不可见了)为了避免这个问题,在每发生20亿个事务后,就至少需要对每个数据库中的所有表执行vacuum操作。

定期对表vacuum可以解决这个问题的原因是,vacuum会将行标记为frozen状态,表示这些行是很久之前的事务插入的,从而可以对当前事务和以后的事务可见。常规xids的比较是使用对2的32次方进行取模的算法,这样就有20亿个xid是“旧的”、20亿个xid是“新的”;换个方法说,就是xid空间是循环的、没有端点。postgresql保留了一个特殊的xid,FrozenTransactionId,这个特殊id不会遵守常规xid的比较规则,FrozenTransactionId被认为比任一常规xid都“旧”。被标记为frozen状态的行的xid被认作FrozenTransactionId,这样这些行就会出现在所有常规事务的“过去”,无论这些行记录存在多久的时间,都不用担心wraparound问题。

在postgresql 9.4之前,freeze是通过使用FrozenTransactionId替换行的插入xid实现的,在行的xmin系统列中是可见的。新版本中,只是设置一个标志位,保留行原来的xmin。从9.4之前版本升级过来的,可能还会看到xmin等于FrozenTransactionId的行。

 

2.Failover Will Probably Lose Data.

failover会有数据丢失的风险。

如果运行的主库突然出现故障,普通的流复制几乎肯定会丢失已经提交的数据。可能有人会说,这就是异步复制的代价,你可以不使用异步复制呀。postgresql支持同步复制

PostgreSQL支持使用仲裁提交的同步复制以实现容错持久性,但是它有一个更严的性能影响,这使得它的应用实现更加复杂。等待不会使用系统资源,但是持续持有事务锁,直到传输被确认。结果就是,不谨慎地使用同步复制会降低性能,因为会增加响应时间和更高的竞争。

 

3.Inefficient Replication That Spreads Corruption.

流复制是目前使用最多的复制机制。属于物理复制的一种,复制的是磁盘上二进制数据的变化。

每次需要通过写操作修改磁盘上的数据页(4KB)时,哪怕是只修改一个字节,也要将根据请求的修改进行编辑的整个页的副本写入预写日志(WAL)。物理流复制利用现有的WAL基础框架作为流到副本的更改日志。

使用物理复制,大的索引构建会创建大量WAL条目,从而很容易成为流复制的瓶颈。

页粒度的读-修改-复制过程可能会导致主机上由硬件引起的数据损坏,更容易传播到副本,我本人在生产环境中见过好几次这种情况。

这与逻辑复制相反,逻辑复制仅复制逻辑数据更改。至少从理论上讲,大的索引构建只会导致在网络上复制单个命令。尽管PostgreSQL已经支持逻辑复制已有相当长的一段时间了,但是大多数部署都使用物理流复制,因为它更健壮,支持范围更广并且更易于使用。

 

4.MVCC Garbage Frequently Painful.

和大多数主流数据库一样,PostgreSQL使用多版本并发控制(MVCC)来实现并发事务。但是,它的特定实现经常会带来关于垃圾行版本及其清理(vacuum)的操作难题。一般来说,INSERT和update操作会创建任何修改行的新副本(或“行版本”),将旧版本留在磁盘上,直到它们可以被清除。

虽然这种情况在过去几年稳步改善,但它是一个复杂的系统,对于第一次处理这个问题的人来说,它有点像一个黑盒子。 例如,了解Heap-Only Tuples (HOT)以及它何时起作用,对于大量就地更新的工作负载(比如在一行中维护一致的计数器列)可能是决定成败的因素。 默认的autovacuum设置在大多数情况下都是有效的,但是有时也会不起作用。

相比之下,MySQL和Oracle使用redo和undo日志。它们不需要类似的后台垃圾收集进程。它们所做的权衡主要是增加事务提交和回滚操作的延迟。

 

5.Process-Per-Connection = Pain at Scale.

PostgreSQL为每一个连接fork一个进程,而大多数其他数据库使用更高效的连接并发模型。这就造成了相对困难的调优问题,因为在一个相对较低的阈值下,增加更多的连接会降低性能,而较高的阈值(难以估计,高度依赖于工作负载)则会导致性能急剧下降。

使用连接池程序的当然可以解决问题,但是引入额外的体系会增加结构复杂性。在一个特别大的部署中,我最终不得不添加第二个pgbouncer层。 一层运行在应用服务器上,另一层运行在数据库服务器上。它总共为大约100万个客户端进程聚合了连接。这时候对于调优工作来说,大概只有40%的技术含量,剩下大概要花40%的蛮力,还需要10%的碰运气了。

进程的可伸缩性在每一个主要版本中都在不断提高,但是最终,与MySQL中使用的“thread-per-connection”这样的架构相比,这种架构的性能有一些硬性限制。

 

6.Primary Key Index is a Space Hog.

PostgreSQL中的表有一个主键索引,使用单独的heap进行存储。其他数据库将这些集成在一起,或者支持“索引组织表”。在这种设计中,主键查找过程直接获取行数据,而不需要通过二级返回(secondary fetch)获取完整的行,也不需要额外的CPU和I/O。

PostgreSQL中的CLUSTER命令根据索引重新组织表以提高性能,但对于大多数真实的OLTP情况并不是真的有效。它在排他锁下重写整个表,阻塞任何读或写操作。PostgreSQL不为新数据维护clustered layout,因此必须定期运行此操作。因此,只有当你能够定期长时间使数据库离线时,它才真正有用。

但更关键的是,索引组织的表节省了空间,因为索引不需要行数据的单独副本。对于主要由主键覆盖的小行表,例如连接表,这可以很容易地将表的存储占用减少一半。

CREATE TABLE likes ( object_type INTEGER NOT NULL, object_id BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY, user_id BIGINT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ( object_type, object_id, user_id ) );

PostgreSQL将维护一个与基表存储分离的主键索引。 这个索引将包含每一行的object_type、object_id和user_id列的完整副本。每行28个字节中有20个(~70%)将被重复。如果PostgreSQL支持索引组织表,它就不会消耗所有额外的空间。

 

7.Major Version Upgrades Can Require Downtime.

一些大版本升级需要数小时的停机时间来转换大型数据库的数据。使用典型的流复制机制,不可能通过升级副本和故障转移来优雅地完成这一任务。 磁盘上的二进制格式在大版本之间是不兼容的,因此主副本之间的有线协议实际上也是不兼容的。

希望逻辑复制最终完全取代流复制,这将提供在线滚动升级策略。当我从事大规模水平扩展部署时,我们在定制基础设施上进行了大量工程性投资,使用另一个基于触发器的复制系统(也用于分片迁移)完成这些升级,而不需要停机。

 

8.Somewhat Cumbersome Replication Setup.

相对而言,MySQL的out-of-the-box的复制是更麻烦,但相比一些NoSQL存储如MongoDB和Redis,或一些cluster-oriented复制系统(比如MySQL组复制和Galera集群,从易用性和sharp-edge-avoidance的角度来看,设置复制在PostgreSQL还有很多需要改进。虽然从理论上讲,逻辑复制为第三方解决方案提供了更大的灵活性来掩盖这些缺陷,但到目前为止,使用逻辑复制代替流复制还存在一些相当大的问题。

 

9.Ridiculous No-Planner-Hints Dogma.

计划器提示(planner hints)允许指导查询计划器使用其本身不会使用的查询策略。在计划器提示看起来是一种高效灵活的编译器的争论中,PostgreSQL开发团队多年来一直拒绝支持查询计划器提示。

我理解他们,主要是防止用户使用查询提示来解决问题,而那些问题应该通过编写恰当的查询来解决。然而,当你看到生产数据库由于突然的、意想不到的查询计划变化而完全崩溃时,这种哲学似乎是太过武断了。

在许多这种情况下,给计划器一个提示可以在分分钟内解决问题,为工程团队节省数小时或数天的时间。虽然有一些间接的变通方法涉及禁用某些查询计划器策略,但它们是有风险的,在任何类型的时间压力下绝对不应该使用。

 

10.No Block Compression.

MySQL的InnoDB的页压缩通常会减少一半的存储空间,并且从性能的角度来看是不受影响的。PostgreSQL会自动压缩较大的值,但这对于在关系数据库中存储数据的最常见方式没有用处。对于大多数RDBMS中,一行通常只有几百个字节或更少,这意味着压缩只有在跨多行或多个块应用时才真正有效。

Postgres 的页面(page)大小是固定的 8k,同一行的数据必须在同一个页面内,但是 Postgres 需要支持变长的数据类型(如 varchar),是可能超过 8k 的。解决方案是所谓的 TOAST (The Oversized-Attribute Storage Technique, 过长字段存储技术)。

对于PostgreSQL的核心数据结构来说,块压缩确实很难实现,但是MySQL的InnoDB存储引擎所采用的“打孔(hole punch)”策略在实践中似乎工作得很好,尽管也有一些缺点。

感兴趣的可以参考文章:

http://mysql.taobao.org/monthly/2016/02/01/

http://mysql.taobao.org/monthly/2015/08/01/

PostgreSQL中唯一广泛使用的通用块压缩设置利用了ZFS,它似乎对很有效。ZFS现在在Linux中已经是生产级的了,但无疑也带来了一些管理开销,而这些开销对于XFS或ext4这样的“开箱即用”文件系统来说是不存在的。

 

最后

你可能仍然应该使用 PostgreSQL 而不是其他东西来存储你最喜欢的数据。一般来说,我建议从PostgreSQL开始,然后尝试找出为什么它不适用于你的用例。

PostgreSQL 非常成熟,设计精良,功能丰富,并且对于绝大多数用例都非常高效。它还不受占主导地位的企业赞助商的阻碍,包括出色的文档,并拥有专业、包容的社区。

好消息是,通过使用诸如 Heroku PostgreSQL、Compose PostgreSQL、Amazon RDS for PostgreSQL 或 Google Cloud SQL for PostgreSQL 之类的托管数据库服务,可以减轻或消除由本文中提出的许多问题引起的痛苦。

我很自豪地说,我已经在 PostgreSQL 之上构建软件将近 20 年了,尽管存在缺陷,但我仍然是坚定的拥护者。鉴于我多年来见证了其令人难以置信的开发团队所取得的进展,我可以说大多数(如果不是全部)这些问题都将在适当的时候得到解决。