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

PostgreSQL LRU刷脏简析

内核开发者 2023-12-05
2313

关于 PolarDB PostgreSQL 版

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

功能介绍

原生的PostgreSQL中刷脏主要由background writer进程来完成的。background writer进程主要存在两个被唤醒的条件:1)当buffer alloc淘汰buffer时会唤醒background writer进程;2)间隔BgWriterDelay时间也会自动被唤醒。background writer进程通过循环调用刷脏函数,将缓存中脏页刷到磁盘上。同时,刷脏策略主要考虑了当前buffer alloc对应的时钟扫描的位置,来保证领先buffer淘汰的速度,从而可以减少backend进程的刷脏次数,提高业务的效率。

原理概述

刷脏策略的主要实现在BgBufferSync函数中:

Bgwriter进程会记录上一轮扫描的这些信息prev_strategy_passes、prev_strategy_buf_id。从而可以计算在上一轮与这一轮之间页面淘汰算法总共扫描了多少buffer

int32		passes_delta = strategy_passes - prev_strategy_passes;

strategy_delta = strategy_buf_id - prev_strategy_buf_id;
strategy_delta += (long) passes_delta * NBuffers;

Bgwriter进程主要策略是希望领先淘汰算法一轮,所以会根据当前刷脏的位置,包括轮次和buffer id,来计算本轮需要刷脏的页面数bufs_to_lap、以及开始的位置(next_to_clean、next_passes),计算如下:

if ((int32) (next_passes - strategy_passes) > 0)
{
    /* 领先buffer淘汰轮次 */
    bufs_to_lap = strategy_buf_id - next_to_clean;
}
else if (next_passes == strategy_passes &&
         next_to_clean >= strategy_buf_id)
{
    /* 在同一轮次,但是buffer id领先 */
    bufs_to_lap = NBuffers - (next_to_clean - strategy_buf_id);
}
else
{
    /* 刷脏进程落后与buffer淘汰 */
    next_to_clean = strategy_buf_id;
    next_passes = strategy_passes;
    bufs_to_lap = NBuffers;
}

同时也会考虑扫描多少个页面才会获取一个有用的页面,strategy_delta表示在一轮刷脏的间隔里共扫描的页面数,而recent_alloc表示在一轮刷脏的间隔期间buffer alloc的次数;同时会平滑scans_per_alloc,使得刷脏更加平滑变动

scans_per_alloc = (float) strategy_delta / (float) recent_alloc;
smoothed_density += (scans_per_alloc - smoothed_density) /
        smoothing_samples;

同时如果刷脏策略已经领先于页面淘汰策略,则会计算领先的页面数多少可以用作有效的buffer:

bufs_ahead = NBuffers - bufs_to_lap;
reusable_buffers_est = (float) bufs_ahead / smoothed_density;

最终通过需要刷脏多个页面才会领先一轮,以及下一轮可能需要分配的页面数作为条件,来决定最终需要刷脏的页面数:

while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
{
    /* ... */
}

buffer淘汰的时钟算法:

在StrategyControl共享内存变量中会记录下一个可能淘汰的buffer id以及当前时钟已经走过的轮次

typedef struct
{
    /* ... */

	/*
	 * Clock sweep hand: index of next buffer to consider grabbing. Note that
	 * this isn't a concrete buffer - we only ever increase the value. So, to
	 * get an actual buffer, it needs to be used modulo NBuffers.
	 */
	pg_atomic_uint32 nextVictimBuffer;

	/* ... */

	uint32		completePasses; /* Complete cycles of the clock sweep */

    /* ... */
} BufferStrategyControl;

上述通过Nbuffers这个环,来获取需要淘汰的buffer。从nextVictimBuffer获取需要淘汰的buffer,当发现这个buffer没有被pin住,同时最近没有被使用,则会尝试淘汰该buffer,如果最近被使用,则将use_count减1,继续沿着时钟往下找。如果发现buffer被pin住,则会判断尝试的次数trycounter,如果为0说明已经查找很多buffer了但是未找到有效buffer会输出ERRO,否则--trycounter后会继续沿着时钟找到下一个buffer,直到找到满足条件的buffer。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论