PAX:你一直在寻找的缓存性能

13 阅读9分钟

PAX:你一直在寻找的缓存性能

摘要: PAX(分区属性扩展)是一种存储布局,将数据库页面内的数据组织成面向列的迷你页,将典型 NSM(n 元存储模型)中 94% 的缓存污染降至几乎可忽略的水平。通过在每个 8KB 页面内将列值分组存放,同时保持同一页面上的所有列以便于元组重建,PAX 实现了纯列式存储 80% 的优势,仅需 5% 的实现复杂度,非常适合宽表的混合 OLTP/OLAP 工作负载和选择性查询。

原文链接


感谢 Boris Novikov,是他最初引导我走向 PAX 方向,并进行了多次富有见地的技术讨论。我感激他所花费的时间以及我们进行过和将继续进行的精彩讨论。

多年来,我对数据库存储布局有一种执念。不是那种光鲜的执念,而是那种凌晨 3 点醒来想着"为什么我们要加载 60 个无用字节到缓存中,却只读取 4 个字节?"

这正是 PostgreSQL 每天在全球生产服务器上数十亿次做的事情。我们就……接受它吗?不,我不接受它。让我给你讲讲 PAX。

让我抓狂的图书馆

想象一下:你走进一家图书馆。每本书就是一条数据库记录。PostgreSQL 的传统存储(NSM - N 元存储模型)将完整的书存放在每个书架上:第一章、第二章、第三章,全部装订在一起。

这就是让我夜不能寐的问题:当你只需要 1000 本书中的第三章时,你必须把每本完整的书从书架上取下来,翻过第一章和第二章(你永远不会读),取出第三章,然后移到下一本书。

你在浪费时间。你在浪费精力。你在浪费缓存带宽。

现在,列式存储(比如 Parquet)说"去他的,让我们把书拆了重新排列!"把所有第一章放在书架 A,所有第二章放在书架 B,所有第三章放在书架 C。这对快速读取第三章很棒!

但问题来了:当你需要同一本书的第一章和第三章时,你必须访问多个书架,收集碎片,然后重新装订书。用数据库术语来说,这是一个昂贵的连接操作。对于 OLTP 工作负载?别想了。太慢了。

**PAX 说:**把书放在同一个书架上(便于重新组装),但在该书架内将章节组织在一起(实现快速顺序访问)。

为什么 NSM 让我想尖叫

让我给你看看我们每天都在经历的灾难。

PostgreSQL 页面内部是什么

PostgreSQL 以 8KB 页面存储数据。一个典型的 NSM 页面如下所示:

image.png**注意:**这是一个简化视图。实际上,元组之间可能存在间隙(由于更新、删除或对齐),但关键点仍然是:每个元组包含所有连续存储的列。这是我们的缓存问题。

元组从底部(pd_special)增长,行指针从顶部(在标题之后)增长,中间是空闲空间。很简单,对吧?

现在想象这个简单的查询:

SELECT age FROM users WHERE age > 30;

缓存灾难

现代 CPU 不会获取单个字节。它们加载缓存行(通常一次 64 字节)。就像去杂货店一样:即使你只需要牛奶,你也要提着一个能装 64 件商品的购物篮。

当 PostgreSQL 读取一个元组来检查 age 列(4 字节)时,以下是实际加载到缓存中的内容:

image.png**注意:**我要为这张图的粗糙之处道歉。我没有时间按比例绘制。它只显示列数据。实际上,每个元组包含一个 23 字节的头部,浪费更严重(总共 83 字节,跨越 2 个缓存行)。

我们想要 age。我们得到了一切。

那是 94% 的缓存污染!94% 👏 的 👏 污染 👏(是的,我喜欢拍手强调词语,这里感觉特别恰当,我忍不住。)

在 100 万行的表扫描上:

  • NSM 触发 100 万次缓存未命中
  • 传输的有用数据:4MB(仅 age 列)
  • 浪费的带宽:56MB(其他无人询问的列)

我见过生产环境中的 DBA 玩俄罗斯方块——重新排列数据类型以最小化填充开销,寻找每个元组浪费的 2-4 字节。

与此同时,PostgreSQL 正在将每行 56 个无用字节加载到缓存中。

就像在泰坦尼克号上重新排列甲板上的躺椅。

Anastasia Ailamaki 在 2001 年在一台 Pentium II Xeon 上测量过这个(是的,我足够老还记得那些)。她发现 75% 的缓存未命中完全可以通过 PAX 避免。

你知道更糟的是什么吗?现代系统有 128 字节的缓存行,这让问题更大!我们正在倒退!

PAX 登场:我们一直忽略的解决方案

PAX 的不同之处在于。它不是存储完整的元组,而是将每个 8KB 页面的内部重新组织成面向列的"迷你页":

image.png每个迷你页将一列的所有值分组在一起。PAX 头部包含指向每个迷你页位置的偏移量。当需要 age 列时,PostgreSQL 只读取"age"迷你页,而不是整行。

但有趣的地方来了。还记得 NSM 中 94% 的缓存浪费吗?看看用 PAX 查询相同数据时会发生什么:

image.png一个缓存行现在加载 16 个 age 值,而不是 1 个。这不是 16% 的改进——而是缓存利用率提高了 16 倍!

使用 NSM,扫描 100 行查找"age > 30"会触发 100 次缓存未命中。使用 PAX?只有 7 次缓存未命中(100 ÷ 16 ≈ 7)。相同的数据,相同的查询,缓存未命中减少 93%。

这就是页面级别列式存储的魔力。PAX 将所有列保持在同一页面上(便于元组重建),但为顺序访问组织它们(缓存友好)。你获得了纯列式存储 80% 的优势,而无需昂贵的跨文件连接。

PAX 和 Parquet:没人谈论的家族树

当我弄清楚这件事时,我的mind被震撼了:Parquet 基于 PAX。

Parquet 没有发明页面级别的列式存储。Anastasia Ailamaki 在 2001 年就发明了。Twitter 和 Cloudera 只是将其应用于 2013 年的整个文件。

时间线:

  • 2001:Ailamaki 发明 PAX(事务数据库中的 8KB 页面)
  • 2013:Twitter/Cloudera 创建 Parquet(PAX 扩展到 128MB+ 文件)

那有什么区别?

特性PAX(原始)ParquetPostgreSQL 需要什么
粒度页面(8KB)行组(128MB+)页面(8KB)
能 UPDATE 吗?是(MVCC)否(文件不可变)是(MVCC)
所在位置OLTP+OLAP DBMS数据湖/批处理OLTP+OLAP DBMS
花哨的编码不太RLE、字典、Delta、全部未来工作
压缩否(论文中没有)Snappy、Gzip、Zstd未来工作

这是每个人都忽略的关键见解:Parquet 证明了 PAX 在大规模下有效,但牺牲了可变性来达到这一点。你不能 UPDATE Parquet 文件——它是仅追加或整个文件替换。

PostgreSQL 需要带有完整 ACID 保证的 PAX。这意味着坚持 8KB 页面,保留 MVCC,并使其与 WAL、vacuum 和缓冲区管理器一起工作。

PAX 与其他一切

让我明确说明 PAX 的位置:

特性NSMPAX纯列式
缓存局部性极差优秀优秀
记录重建免费便宜(同页)昂贵(多文件连接)
适合 OLTP?相当好不适合
适合 OLAP?不太好相当好非常适合
存储开销基线相同可变

PAX 让你以 5% 的成本获得列式存储 80% 的优势。

这是我愿意接受的权衡。

什么时候应该(以及不应该)使用 PAX

我不会告诉你 PAX 是万能的。它不是。

最佳场景

宽表(12+ 列)带选择性查询:

-- 非常适合 PAX
SELECT event_time, revenue 
FROM analytics_events  -- 25 列的表
WHERE user_id = 12345;
-- 访问 25 列中的 2 列 → 92% 缓存节省

混合 OLTP/OLAP 工作负载:

  • OLTP:单行更新保持快速(所有内容在同一页上)
  • OLAP:范围扫描飞起(持续的缓存局部性)

频繁的顺序扫描,低投影:

-- NSM 的噩梦,PAX 的天堂
SELECT COUNT(*)
FROM logs
WHERE timestamp > NOW() - INTERVAL '1 hour';

PAX 表现不佳的地方

  1. 窄表(< 8 列): 重建开销开始接近 NSM 的简单性。不值得。
  2. SELECT * 查询: 无论如何您都必须读取所有微页面。PAX 只是增加开销。
  3. 通过索引的重度随机访问: 索引查找 → 读取完整 tuple。与 NSM 成本相同。 (尽管索引-only 扫描确实受益于更好的 VM 覆盖。)

您可以期待怎样的性能提升?

Ailamaki 在 2001 年在 Pentium II Xeon 上测量了:

  • 顺序扫描时数据缓存未命中减少 75%
  • 范围选择时执行时间减少 17-25%
  • TPC-H 查询(混合连接/聚合)减少 11-48%

现代硬件(DDR5、128 字节缓存行、巨大的 L3 缓存)上,绝对数字不同,但原则仍然成立。实际上,它们更加成立:

  • 更大的缓存行 = NSM 浪费更多带宽
  • NVMe 终结了 I/O 主导地位 → CPU/缓存成为瓶颈

我保守的生产估算:

  • 宽表上的 OLAP 查询:快 10-30%
  • OLTP 查询:慢 0-5%(微页面开销)
  • 混合工作负载:净收益 5-15%

但我想诚实地说:在有人构建 POC 并进行基准测试之前,这些都是有根据的猜测。

下一步是什么?

PAX 解决了一个真正的问题。缓存污染问题不会消失——随着每一代 CPU,它只会变得更糟。

但关键是:说"PAX 会很棒"很容易。在 PostgreSQL 中实际实现它?那就有趣了。

死 tuple 可能很难管理。WAL 日志记录如果不想每次都使用完整页面写入,会变得非常棘手。Vacuum 肯定会增加挑战。

这是理论工作——目前还没有 POC。但我想把这些想法抛出去。也许比我聪明的人会采纳它。

因为说实话?PostgreSQL 应该得到比 94% 缓存污染更好的东西。


参考文献:

Ailamaki, A., DeWitt, D. J., Hill, M. D., & Skounakis, M. (2001). Weaving Relations for Cache Performance. Proceedings of the 27th International Conference on Very Large Data Bases (VLDB) .