暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

PolarDB PostgreSQL 扩展锁优化研究

PolarDB 2025-03-18
26

PolarDB PostgreSQL 扩展锁优化研究

关于 PolarDB PostgreSQL 版

PolarDB PostgreSQL 版是一款阿里云自主研发的云原生关系型数据库产品,100% 兼容 PostgreSQL,高度兼容Oracle语法;采用基于 Shared-Storage 的存储计算分离架构,具有极致弹性、毫秒级延迟、HTAP 、Ganos全空间数据处理能力和高可靠、高可用、弹性扩展等企业级数据库特性。同时,PolarDB PostgreSQL 版具有大规模并行计算能力,可以应对 OLTP 与 OLAP 混合负载。

前言

在进行并发 Insert
Copy ... From ...
的时候,为了防止多个进程同时对一个表文件进行扩展,造成数据覆盖。PostgreSQL 设计了表扩展锁防止并发表扩展,但表扩展锁的锁定范围过长,同时也造成了并发 Insert
和 Copy ... From ...
的性能下降。本文将说明 PolarDB PostgreSQL 表扩展锁的分析和优化过程。

批量扩展性能分析

PolarDB PostgreSQL 为了适配底层 PolarFS(以下简称为 PFS)4 MB 扩展较优的特性,相较于 PostgreSQL 的 8KB 扩展,设计了 4MB 的批量扩展功能。但在 4MB 批量扩展的过程中,会全程持有表扩展锁,对性能造成影响。下面将从批量扩展的执行流程、性能瓶颈分析来进行说明。

执行流程

首先,一个 tuple 的写入基本可以表示为如下图的几个步骤:

image.png

我们本次优化的部分主要是在页面扩展当中,而页面扩展的基本流程可以用下面的流程图来进行说明:

bulk_extend (1).png
  1. 对 Relation 上表扩展锁,防止多个进程同时对表进行扩展,实现函数为 LockRelationForExtension()
    ,该锁为互斥锁。

  2. 上完表扩展锁之后,文件的大小就只能由当前进程来修改,这时获取文件大小,确定扩展起点。

  3. 在 PostgreSQL 的设计中,我们对文件进行页面写入、扩展,都需要先写入 Buffer Pool 中这些文件页面对应的 Buffers,然后再由这些 Buffers 通过刷脏的方式写入到物理文件当中。所以批量扩展过程中需要对这些 Buffer 进行如下的步骤:

    1. 从 Buffer Pool 中申请 Buffer,申请的 Buffer 大小与要进行扩展的大小一致。批量扩展的大小是 4 MB,每个 Buffer 是 8 KB,一共要申请 512 个 Buffer。

    2. 如果申请出来的 Buffer 为 “脏”,也就是别的进程已经将其标记为脏页。则需要进行刷脏,将 Buffer 内的数据写入到文件当中,文件 I/O 是其中耗时较大的部分。

    3. 刷脏过后,则需要对这些 Buffer 通过 MemSet() 进行清零,保证写入扩展文件中的数据是全 0 的

    4. 对文件调用 smgr_bulkextend()
      进行批量物理扩展

    5. 对申请出来的这些全零 Buffer 通过 PageInit()
      进行初始化,写入页面头和当前页面的剩余空间,并加入到 FSM 当中(FSM 全称为 free space map,该结构的作用是加快对表结构中空闲块的搜索和设置),其他进程可以访问这些空闲页面

  4. 返回需要写入的 Buffer

  5. 释放表扩展锁

性能瓶颈分析

我们用 64 并发 Copy 来对批量扩展进行 Profiling 性能分析,具体的测试场景和参数在后文进行说明。通过 perf 和火焰图对 bulk_extend
进行了 ON-CPU 和 OFF-CPU 分析,由于在测试过程中看到 CPU 负载普遍在 30%~40% 之间,我们主要关注 OFF-CPU 的 profiling 结果

ON-CPU

image.png

OFF-CPU

image.png

可以从 OFF-CPU 上很明显的看到 LockRelationForExtension
表扩展锁的开销占据所有 OFF-CPU 的开销 98.70%。表扩展为主要的瓶颈,所有的进程均在等待表扩展。

优化表扩展锁

这部分表扩展锁优化主要参考了 PG16 的表扩展锁优化方法,资料链接如下:

https://www.postgresql.org/message-id/flat/20221029025420.eplyow6k7tgu6he3%40awork3.anarazel.de

表扩展锁锁定范围

我们再来回顾一下表扩展锁的具体作用,他的锁定范围中有哪些是必要,哪些不必要。

必要:

  1. 调用 smgrnblocks()
    获取当前文件大小

  2. 将 Buffers 写入到 buffer mapping table 当中

  3. 将 Buffers 标记为 IO_IN_PROCESS

原因解释:

  1. 上锁的目的是为了防止当前进程在确定扩展起点的时候,不会将其他进程已经扩展的页面作为扩展起点,防止重复扩展

2,3.    目的是为了防止其他非扩页进程,在我们进行扩展的过程中,访问到该页面,进行刷脏,然后当前扩页进程又重复刷脏

不必要:

页面申请、刷脏和清零,这三个步骤均属于进程获取 Buffer 的过程,这部分由 Buffer Content 和 BufHdr 锁来保护并发,无需表扩展锁保护。

优化后的扩展流程

有了上文对扩展锁的分析,我们可以对表扩展锁的锁定范围进行如下图所示的优化:

bulk_extend (3).png

伪代码如下所示:

1. 计算扩展页面数量,一般为 512 (4MB),至多只能扩满一个 segment。

2. VictimBuffer(), 刷脏, MemSet() 清零
for (i = 0->block_count)
{
  Get  victim_buffer from StrategyGetBuffer()
  Pin victim_buffer
  if (victim_buffer is dirty)
  {
    Flush();
  }
  InvalidateVictimBuffer
  {
    Lock old buffer mapping table
    Lock victim_buffer hdr
    Invalidate victim_buffer hdr
    Unlock victim_buffer hdr
    Delete victim_buffer from buffer mapping table
    Unlock old buffer mapping table
  }
  * Buffer 清零 */
  MemSet(victim_buffer,0);
}

3. 上表扩展锁
LockRelationForExtension(relation, ExclusiveLock);

4. 获取文件大小,确定扩展起点
first_block = smgrnblocks(bmr.smgr, fork);

5. victim_buffer 写入 BufTable 中,并且将 hdr 置为 IO_IN_PROGRESS
for (i = 0->block_count)
{
  Lock new buffer mapping table
  Insert victim_buffer to buffer mapping table
  Lock victim_buffer hdr
  Init victim_buffer hdr
  Unlock victim_buffer hdr
  Unlock new buffer mapping table

  StartBufferIO(victim_buffer);
}

6. 物理扩页
polar_smgrbulkextend();

7. 释放表扩展锁

8. TerminateBuffer()
for (i = 0->block_number)
{
  对 first_block 上 Buffer_content 锁
  TerminateBuffer(i);
}

9. 对 first_block 进行页面初始化,返回首个页面
对已经扩展的页面分为两类,一类即将使用的页面,另一类为不使用页面,不使用页面放入到 FSM 当中

复制

简单来说,就是将 Buffer 申请、刷脏、清零操作,放在表扩展外,来提升性能。

同时,我们对 FSM 争抢问题也进行了优化,即将使用的页面用链表进行维护,来给下一次扩展使用;不使用的页面放入到 FSM 当中,给其他进程使用。

优化效果

在上述所示的 64 并发 Copy 的场景中进行了测试,测试的场景和相关参数如下图所示:

测试工具
pgbench
并发数
64
测试时间
120s
测试 SQL
copy test from '/home/postgres/polardb_pg/copytest_data_text.copy';
文件大小
8.9 MB
预扩展大小
4 MB
相关参数
polar_enable_normal_bgwriter = on, bgwriter_delay=10ms, shared_buffers=8GB

TPS 由 35.73 提升到了 41.40 ,同样的,我们也对并发 Copy 进行了 ON-CPU 和 OFF-CPU 的分析。我们仍然较为关注 OFF-CPU 的性能。

ON-CPU

image.png

OFF-CPU

image.png

表扩展锁的 OFF-CPU 占比由 98% 降低到了 7.86%,这个时候 FlushBuffer 成为主要的性能瓶颈,占比 83%。

结论

通过表扩展锁优化,可以让并发 Copy 的性能提升约 15%,这部分的优化目前还仍处于 POC 阶段,后续将在 PolarDB PostgreSQL 进行上线。


文章转载自PolarDB,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论