PostgreSQL LRU刷脏简析
关于 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;
}
elseif (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。




