PostgreSQL码农集散地

PostgreSQL 18 preview - bitmap Heap Scan支持AIO批量读

PostgreSQL 18 preview - bitmap Heap Scan支持AIO批量读

HeapBitmapScan 是一种扫描表的方法。它首先通过一个或多个索引找到匹配条件的行的物理位置(TID - Tuple Identifier),将这些 TID 收集到一个位图(Bitmap)中。然后,根据这个位图(排序后的block number)去访问表(堆)中对应的页面,并获取实际的行数据(元组)。

Bitmap Heap Scan 启用 AIO 批量模式后,当 Bitmap Heap Scan 需要从磁盘读取大量数据页面时,其 I/O 性能有望得到提升。

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=459e7bf8e2f8

Remove HeapBitmapScan's skip_fetch optimization  
author Andres Freund <[email protected]>   
Wed, 2 Apr 2025 18:25:17 +0000 (14:25 -0400)  
committer Andres Freund <[email protected]>   
Wed, 2 Apr 2025 18:54:20 +0000 (14:54 -0400)  
commit 459e7bf8e2f8ab894dc613fa8555b74c4eef6969  
tree d89ead863ddc22c0615d244c97ce26d3cf9cda32 tree  
parent 0dca5d68d7bebf2c1036fd84875533afef6df992 commit | diff  
Remove HeapBitmapScan'
s skip_fetch optimization  

The optimization does not take the removal of TIDs by a concurrent vacuum into  
account. The concurrent vacuum can remove dead TIDs and make pages ALL_VISIBLE  
while those dead TIDs are referenced in the bitmap. This can lead to a  
skip_fetch scan returning too many tuples.  

It likely would be possible to implement this optimization safely, but we  
don't have the necessary infrastructure in place. Nor is it clear that it's  
worth building that infrastructure, given how limited the skip_fetch  
optimization is.  

In the backbranches we just disable the optimization by always passing  
need_tuples=true to table_beginscan_bm(). We can't perform API/ABI changes in  
the backbranches and we want to make the change as minimal as possible.  

Author: Matthias van de Meent <[email protected]>  
Reported-By: Konstantin Knizhnik <[email protected]>  
Discussion: https://postgr.es/m/CAEze2Wg3gXXZTr6_rwC+s4-o2ZVFB5F985uUSgJTsECx6AmGcQ@mail.gmail.com  
Backpatch-through: 13  

https://git.postgresql.org/gitweb/?p=postgresql.git;a=commit;h=67be093562b6b345c170417312dff22f467055ba

Use AIO batchmode for bitmap heap scans  
author Melanie Plageman <[email protected]>   
Thu, 3 Apr 2025 22:23:02 +0000 (18:23 -0400)  
committer Melanie Plageman <[email protected]>   
Thu, 3 Apr 2025 22:23:02 +0000 (18:23 -0400)  
commit 67be093562b6b345c170417312dff22f467055ba  
tree 1dfc38467f362e64f01f6e3dc86d4de8526dd1ba tree  
parent 54a3615f15b9b2e1433bc47cfb150317885d7c1f commit | diff  
Use AIO batchmode for bitmap heap scans  

Previously bitmap heap scan was not AIO batchmode safe because of the  
visibility map reads potentially donefor the "skip fetch" optimization  
(which skipped fetching tuples from the heap if the pages were all  
visible and none of the columns were used in the query).  

The skip fetch optimization implementation was found to have bugs and  
was removed in 459e7bf8e2f8, so we can safely enable batchmode for
bitmap heap scans.  

AI 解读

补丁 1:Remove HeapBitmapScan's skip_fetch optimization (移除堆位图扫描的 skip_fetch 优化)

  • 核心内容: 这个补丁移除了 HeapBitmapScan(堆位图扫描)中的一个名为 skip_fetch 的优化。
  • HeapBitmapScan 简介: 这是一种扫描表的方法。它首先通过一个或多个索引找到匹配条件的行的物理位置(TID - Tuple Identifier),将这些 TID 收集到一个位图(Bitmap)中。然后,根据这个位图(排序后的block number)去访问表(堆)中对应的页面,并获取实际的行数据(元组)。
  • skip_fetch 优化是什么: 这个优化试图在某些情况下避免实际去堆(heap)中读取数据页。具体条件是:
    • 如果这两个条件都满足,优化器就认为不需要真正去读取这个堆页面,只需根据位图中指向该页面的 TID 数量来计数即可,从而节省 I/O。
  1. 查询本身不需要从表中获取任何列的数据(例如 SELECT COUNT(*))。
  2. 位图扫描器检查对应的堆页面的可见性映射(Visibility Map, VM),发现该页面被标记为 ALL_VISIBLE(表示页面上所有元组对所有当前事务都可见)。
  • 为什么要移除这个优化(Bug 的原因):
    • 并发 VACUUM 的问题: 这个优化没有正确处理与并发运行的 VACUUM 命令之间的交互。
    • 竞争条件(Race Condition):
    1. 一个 Bitmap Heap Scan 开始执行,它扫描索引并构建了一个 TID 位图。这个位图可能包含一些指向当时“死亡”(dead)但尚未被物理移除的元组的 TID。
    2. 与此同时(并发地),一个 VACUUM 命令开始清理同一个表。VACUUM 会物理移除这些死亡的元组,并可能将它们所在的页面标记为 ALL_VISIBLE(因为页面上所有剩余的元组现在都可见了)。
    3. Bitmap Heap Scan 继续执行,它检查到某个页面的 VM 状态是 ALL_VISIBLE,并且(如果查询不需要列数据)决定启用 skip_fetch 优化。
    4. 此时,它会根据 原始位图 中指向该页面的 所有 TID 进行计数。但问题在于,这些 TID 中有一部分可能刚刚被 VACUUM 清理掉了!
    5. 结果:skip_fetch 优化导致扫描返回了错误的、过多的元组数量(因为它计算了那些已经被并发 VACUUM 删除的 TID)。
  • 解决方案和未来:
    • 补丁作者认为,虽然理论上可能实现一个安全的 skip_fetch 优化,但目前 PostgreSQL 缺乏必要的基础设施(例如更精细的锁或同步机制来处理这种情况)。
    • 考虑到构建这种基础设施的复杂性以及 skip_fetch 优化本身带来的好处有限,决定直接移除这个有问题的优化。
  • 向后移植 (Backpatching): 在旧版本的分支中,为了保持 API/ABI(应用程序接口/二进制接口)的稳定性,不能直接移除代码或修改函数签名。因此,采用了更简单的方法:在调用 table_beginscan_bm() 函数时,强制将 need_tuples 参数设置为 true。这实际上就禁用了 skip_fetch 优化,因为该优化只在 need_tuples 为 false 时才可能触发。
  • 补丁 2:Use AIO batchmode for bitmap heap scans (为位图堆扫描启用 AIO 批量模式)

    • 核心内容: 这个补丁为 Bitmap Heap Scan 启用了 AIO(异步 I/O)的批量模式(batchmode)。
    • AIO 批量模式是什么: 这是一种 I/O 优化技术。操作系统可以一次性接收多个读写请求(一个批次),然后异步地、可能并行地处理它们,而不是一个接一个地同步等待。这通常能提高 I/O 密集型操作的性能,尤其是在读取大量分散的磁盘块时。
    • 为什么以前不能为 Bitmap Heap Scan 启用 AIO 批量模式:
      • 正如第一个补丁所讨论的,之前的 Bitmap Heap Scan 包含 skip_fetch 优化。
      • 这个 skip_fetch 优化需要在扫描过程中读取可见性映射(VM)来判断页面是否 ALL_VISIBLE。
      • 这种对 VM 的读取操作,与 AIO 批量模式主要针对批量读取 堆页面 的逻辑存在冲突或不兼容,使得在存在 skip_fetch 的情况下安全地实现 AIO 批量模式变得困难或不可能。
    • 为什么现在可以启用了:
      • 关键在于第一个补丁(提交号 459e7bf8e2f8)已经移除了有问题的 skip_fetch 优化。
      • 由于不再需要在扫描过程中读取 VM 来做 skip_fetch 决策,阻碍 Bitmap Heap Scan 使用 AIO 批量模式的主要障碍被清除了。
    • 好处: 启用 AIO 批量模式后,当 Bitmap Heap Scan 需要从磁盘读取大量数据页面时,其 I/O 性能有望得到提升。

    总结关系:

    这两个补丁是相互关联的:

    1. 第一个补丁 发现并移除了 Bitmap Heap Scan 中一个存在并发问题的 skip_fetch 优化。
    2. 第二个补丁 利用第一个补丁移除 skip_fetch 优化(及其伴随的 VM 读取)这一事实,为 Bitmap Heap Scan 启用了一项重要的性能优化——AIO 批量模式,这在之前是不安全的。

    简单来说,第一个补丁修复了一个 bug 并移除了一项有问题的优化,这为第二个补丁安全地引入另一项性能改进(AIO 批量模式)铺平了道路。