8.缓冲区管理器
冲区管理器管理着共享内存和持久存储之间的数据传输
8.1概览
PostgreSQL缓冲区管理器由缓冲表、缓冲区描述符和缓冲池组成。缓冲池层存储着数据文件页面,诸如表页与索引页,以及其相应的自由空间映射和可见性映射的页面。缓冲池是一个数组,数据的每个槽中存储数据文件的一页。缓冲池数组的序号索引称为buffer_id。
PostgreSQL中的每个数据文件页面都可以分配到unique的标签,即缓冲区标签。当缓冲区管理器收到请求时,PostgreSQL会用到目标页面的缓冲区标签。
缓冲区标签由三个值组成,分别是关系文件节点、关系分支编号和页面块号。第一个值分别代表了表空间、数据库和表的oid;第二个值代表关系表的分支号;最后一个值代表页面号。
#/pgdata/pgsql13/include/server/storage/buf_internals.h/* * Buffer tag identifies which disk block the buffer contains. * * Note: the BufferTag data must be sufficient to determine where to write the * block, without reference to pg_class or pg_tablespace entries. It's * possible that the backend flushing the buffer doesn't even believe the * relation is visible yet (its xact may have started before the xact that * created the rel). The storage manager must be able to cope anyway. * * Note: if there's any pad bytes in the struct, INIT_BUFFERTAG will have * to be fixed to zero them, since this struct is used as a hash key. */typedef struct buftag{ RelFileNode rnode; /* physical relation identifier */ ForkNumber forkNum; BlockNumber blockNum; /* blknum relative to begin of reln */} BufferTag;#/pgdata/pgsql13/include/server/storage/relfilenode.htypedef struct RelFileNode{ Oid spcNode; /* tablespace */ Oid dbNode; /* database */ Oid relNode; /* relation */} RelFileNode;
后端进程如何读取数据页
当后端进程修改缓冲池中的页面时(例如向页面插入元组),这种尚未刷新到持久存储,但已被修改的页面被称为脏页。
页面置换算法
当所有缓冲池槽位都被占用,且其中未包含所请求的页面时,缓冲区管理器必须在缓冲池中选择一个页面逐出,用于放置被请求的页面。
脏页刷盘
脏页最终应该被刷入存储,但缓冲区管理器执行这个任务需要额外帮助。在PostgreSQL中,检查点进程和后台写入器这两个后台进程负责此任务。
8.2缓冲区管理器的结构
8.2.1缓冲表
缓冲表可以在逻辑上分为三个部分,分别是散列函数、散列桶槽及数据项。
8.2.1缓冲区描述符
缓冲区描述符保存着页面的元数据,这些与缓冲区描述符相对应的页面保存在缓冲池槽中。缓冲区描述符的结构由BufferDesc结构定义。
# src/include/storage/buf_internals.h./* * Flags for buffer descriptors * * Note: BM_TAG_VALID essentially means that there is a buffer hashtable * entry associated with the buffer's tag. */#define BM_LOCKED (1U << 22) /* buffer header is locked */#define BM_DIRTY (1U << 23) /* data needs writing */#define BM_VALID (1U << 24) /* data is valid */#define BM_TAG_VALID (1U << 25) /* tag is assigned */#define BM_IO_IN_PROGRESS (1U << 26) /* read or write in progress */#define BM_IO_ERROR (1U << 27) /* previous I/O failed */#define BM_JUST_DIRTIED (1U << 28) /* dirtied since write started */#define BM_PIN_COUNT_WAITER (1U << 29) /* have waiter for sole pin */#define BM_CHECKPOINT_NEEDED (1U << 30) /* must write for checkpoint */#define BM_PERMANENT (1U << 31) /* permanent buffer (not unlogged, * or init fork) *//* * BufferDesc -- shared descriptor/state data for a single shared buffer. * * Note: Buffer header lock (BM_LOCKED flag) must be held to examine or change * the tag, state or wait_backend_pid fields. In general, buffer header lock * is a spinlock which is combined with flags, refcount and usagecount into * single atomic variable. This layout allow us to do some operations in a * single atomic operation, without actually acquiring and releasing spinlock; * for instance, increase or decrease refcount. buf_id field never changes * after initialization, so does not need locking. freeNext is protected by * the buffer_strategy_lock not buffer header lock. The LWLock can take care * of itself. The buffer header lock is *not* used to control access to the * data in the buffer! * * It's assumed that nobody changes the state field while buffer header lock * is held. Thus buffer header lock holder can do complex updates of the * state variable in single write, simultaneously with lock release (cleaning * BM_LOCKED flag). On the other hand, updating of state without holding * buffer header lock is restricted to CAS, which insure that BM_LOCKED flag * is not set. Atomic increment/decrement, OR/AND etc. are not allowed. * * An exception is that if we have the buffer pinned, its tag can't change * underneath us, so we can examine the tag without locking the buffer header. * Also, in places we do one-time reads of the flags without bothering to * lock the buffer header; this is generally for situations where we don't * expect the flag bit being tested to be changing. * * We can't physically remove items from a disk page if another backend has * the buffer pinned. Hence, a backend may need to wait for all other pins * to go away. This is signaled by storing its own PID into * wait_backend_pid and setting flag bit BM_PIN_COUNT_WAITER. At present, * there can be only one such waiter per buffer. * * We use this same struct for local buffer headers, but the locks are not * used and not all of the flag bits are useful either. To avoid unnecessary * overhead, manipulations of the state field should be done without actual * atomic operations (i.e. only pg_atomic_read_u32() and * pg_atomic_unlocked_write_u32()). * * Be careful to avoid increasing the size of the struct when adding or * reordering members. Keeping it below 64 bytes (the most common CPU * cache line size) is fairly important for performance. */typedef struct BufferDesc{ BufferTag tag; /* ID of page contained in buffer */ int buf_id; /* buffer's index number (from 0) */ /* state of the tag, containing flags, refcount and usagecount */ pg_atomic_uint32 state; int wait_backend_pid; /* backend PID of pin-count waiter */ int freeNext; /* link in freelist chain */ LWLock content_lock; /* to lock access to buffer contents */} BufferDesc;
8.2.3缓冲区描述符层
缓冲区描述符的集合构成了一个数组,本书称该数组为缓冲区描述符层。
PostgreSQL中的freelist完全不同于Oracle中freelist的概念。PostgreSQL的freelist只是空缓冲区描述符的链表。PostgreSQL中与Oracle中的freelist相对应的对象是空闲空间映射(FSM)
8.2.3缓冲池
缓冲池只是一个用于存储关系数据文件(例如表或索引)页面的简单数组。缓冲池数组的序号索引也就是buffer_id。
缓冲池槽的大小为8KB,等于页面大小,因而每个槽都能存储整个页面。
8.3缓冲区管理器锁
缓冲区管理器会出于不同的目的使用各式各样的锁,注意,本节描述的锁,指的是缓冲区管理器同步机制的一部分。它们与SQL语句和SQL操作中的锁没有任何关系。
8.3.1缓冲表锁
BufMappingLock保护整个缓冲表的数据完整性。它是一种轻量级的锁,有共享模式与独占模式。在缓冲表中查询条目时,后端进程会持有共享的BufMappingLock。插入或删除条目时,后端进程会持有独占的BufMappingLock。
BufMappingLock会被分为多个分区,以减少缓冲表中的争用(默认为128个分区)。每个BufMappingLock分区都保护着一部分相应的散列桶槽。
缓冲表也需要许多其他锁。例如,在缓冲表内部会使用自旋锁(spin lock)来删除数据项。
两个后端进程可以同时持有各自分区的BufMappingLock独占锁,以插入新的数据项。如果BufMappingLock是系统级的锁,那么其中一个进程就需要等待另一个进程完成处理。
8.3.2缓冲区描述符相关的锁
每 个 缓 冲 区 描 述 符 都 会 用 到 内 容 锁(content_lock)与 IO 进 行 锁(io_in_progress_lock)这两个轻量级锁,以控制对相应缓冲池槽页面的访问。当检查或更改描述符本身字段的值时,就会用到自旋锁。
内容锁 典型的强制限制访问的锁
当读取页面时,后端进程以共享模式获取页面相应缓冲区描述符中的content_lock.
执行以下操作之一时,则会获取独占模式的内容锁
IO进行锁
IO 进行锁(io_in_progress_lock)用于等待缓冲区上的I/O完成。当PostgreSQL进程加载/写入页面数据时,该进程在访问页面期间,持有对应描述符上独占的io_in_progres_lock。
自旋锁
当检查或更改标记字段与其他字段时,例如refcount和usage_count,会用到自旋锁。
例子1:
钉住缓冲区描述符:
LockBufHdr(bufferdesc); /* Acquire a spinlock */bufferdesc->refcont++;bufferdesc->usage_count++;UnlockBufHdr(bufferdesc); /* Release the spinlock */
例子2:
将脏位设置为”1”:
#define BM_DIRTY (1 << 0) /* data needs writing */#define BM_VALID (1 << 1) /* data is valid */#define BM_TAG_VALID (1 << 2) /* tag is assigned */#define BM_IO_IN_PROGRESS (1 << 3) /* read or write in progress */#define BM_JUST_DIRTIED (1 << 5) /* dirtied since write started */LockBufHdr(bufferdesc);bufferdesc->flags |= BM_DIRTY;UnlockBufHdr(bufferdesc);
缓冲区描述符的数据结构定义位置src/include/storage/buf_internals.h:BM_LOCKED
参考
8.4缓冲区管理器的工作原理
本节介绍缓冲区管理器的工作原理。当后端进程想要访问所需页面时,它会调用ReadBufferExtended函数。
函数 ReadBufferExtended的行为因场景而异,在逻辑上具体可以分为三种情况。
8.4.1访问存储在缓冲池中的页面
当从缓冲池槽中的页面里读取行时,PostgreSQL 进程获取相应缓冲区描述符的共享content_lock,因而缓冲池槽可以同时被多个进程读取。
当向页面插入(及更新、删除)行时,该 postgres后端进程获取相应缓冲区描述符的独占content_lock(注意,这里必须将相应页面的脏位置设为”1”)。访问完页面后,相应缓冲区描述符的引用计数值减1。
图8.8是访问存储在缓冲池中的页面示意图。
(1)创建所需页面的 buffer_tag(在本例中 buffer_tag 是’Tag_C’),并使用散列函数计算与描述符相对应的散列桶槽。
(2)获取相应散列桶槽分区上的BufMappingLock共享锁。
(3)查找标签为’Tag_C’的条目,并从条目中获取buffer_id。本例中buffer_id为2。
(4)将 buffer_id=2 的缓冲区描述符钉住,即将描述符的 refcount 和usage_count增加1。
(5)释放BufMappingLock。
(6)访问buffer_id=2的缓冲池槽。
8.4.2将页面从存储加载到空槽
在第二种情况下,假设所需页面不在缓冲池中,且freelist中有空闲元素(空描述符)。这时,缓冲区管理器将执行以下步骤:
(1)查找缓冲区表(本节假设页面不存在,找不到对应页面)。
(2)从 freelist 中获取空缓冲区描述符,并将其钉住。在本例中所获的描述符:buffer_id=4。
(3)以独占模式获取相应分区的BufMappingLock(此锁将在步骤(6)中被释放)。
(4)创建一条新的缓冲表数据项:buffer_tag=’Tag_E’, buffer_id=4,并将其插入缓冲区表中。
(5)将页面数据从存储加载至buffer_id=4的缓冲池槽中,如下所示:
(6)释放相应分区的BufMappingLock。
(7)访问buffer_id=4的缓冲池槽。
8.4.3将页面从存储加载到受害者缓冲池槽
在这种情况下,假设所有缓冲池槽位都被页面占用,且未存储所需的页面。图8.10、图8.11是将页面从存储加载到受害者缓冲池槽的示意图。
缓冲区管理器将执行以下步骤:
(1)创建所需页面的buffer_tag并查找缓冲表。在本例中假设buffer_tag是’Tag_M’ (且相应的页面在缓冲区中找不到)。
(2)使用时钟扫描算法选择一个受害者缓冲池槽位,从缓冲表中获取包含着受害者槽位buffer_id的旧表项,并在缓冲区描述符层将受害者槽位的缓冲区描述符钉住。本例中受害者槽的buffer_id=5,旧表项为Tag_F, id = 5。时钟扫描将在下一节介绍。
(3)如果受害者页面是脏页,则将其刷盘(write & fsync),否则进入步骤(4)。在使用新数据覆盖脏页之前,必须将脏页写入存储中。脏页的刷盘步骤如下:
在使用新数据覆盖脏页之前,必须将脏页写入存储中。脏页的刷盘步骤如下:
(4)以排他模式获取缓冲区表中旧表项所在分区上的BufMappingLock。
(5)获取新表项所在分区上的BufMappingLock,并将新表项插入缓冲表:
(6)从缓冲表中删除旧表项,并释放旧表项所在分区的BufMappingLock。
(7)将目标页面数据从存储加载至受害者槽位,然后用 buffer_id=5 更新描述符的标识字段,将脏位设置为0,并按流程初始化其他标记位。
(8)释放新表项所在分区上的BufMappingLock。
(9)访问buffer_id=5对应的缓冲区槽位。
8.4.4 页面替换算法:时钟扫描
NFU(Not Frequently Used)算法的变体,开销较少,能高效地选出较少使用的页面。
缓冲区描述符为黑色或灰色的方框,框中的数字显示每个描述符的 usage_count。而 nextVictimBuffer是一个 32位的无符号整型变量,它总是指向某个缓冲区描述符并按顺时针顺序旋转。
--为代码,时钟扫描 WHILE true(1) Obtain the candidate buffer descriptor pointed by the nextVictimBuffer(2) IF the candidate descriptor is unpinned THEN(3) IF the candidate descriptor's usage_count == 0 THEN BREAK WHILE LOOP /* the corresponding slot of this descriptor is victim slot. */ ELSE Decrease the candidate descriptpor's usage_count by 1 END IF END IF(4) Advance nextVictimBuffer to the next one END WHILE (5) RETURN buffer_id of the victim#(1)获取nextVictimBuffer指向的候选缓冲区描述符。#(2)如果候选描述符未被钉住,则进入步骤(3),否则进入步骤(4)。#(3)如果候选描述符的usage_count为0,则选择该描述符对应的槽作为受害者,并进入步骤(5),否则将此描述符的usage_count减1,并继续执行步骤(4)。#(4)将nextVictimBuffer迭代至下一个描述符(如果到末尾则回绕至头部)并返回步骤(1),重复直到找到受害者。#(5)返回受害者的buffer_id。
如上图,该算法的伪代码与算法描述如下:
当nextVictimBuffer扫过未钉住的描述符时,其usage_count会减1。因此只要缓冲池中存在未钉住的描述符,该算法总能在旋转若干次 nextVictimBuffer 后,找到一个usage_count为0的受害者。
8.5环形缓冲区
在读大表是,pg使用环形缓冲区,而非缓冲池,环形缓冲区是一个小的临时区域,当满足下面任意一个条件时,pg将在共享内存中分配一个环形缓冲区。
分配的环形缓冲区将在使用后被立即释放。
环形缓冲区的好处显而易见,如果后端进程在不使用环形缓冲区的情况下读取大表,则所有存储在缓冲池中的页面都会被移除,这会导致缓存命中率降低。环形缓冲区可以避免此问题。
为什么批量读取和清理过程的默认环形缓冲区大小为256 KB
源代码中缓冲区管理器目录下的README中解释了这个问题
顺序扫描使用256KB的环形缓冲区,它足够小,因而能放入L2缓存中,从而使得操作系统缓存到共享缓冲区的页面传输变得高效。通常更小一点也可以,但环形缓冲区需要足够大到能同时容纳扫描中被钉住的所有页面。
8.6脏页刷盘
除了置换受害者页面之外,检查点进程和后台写入器进程也会将脏页刷盘至存储中。尽管两个进程都具有相同的功能(脏页刷盘),但是它们有着不同的角色和行为。
检查点进程将检查点记录写入WAL段文件,并在检查点开始时进行脏页刷盘
后台写入器的目的是通过少量多次的脏页刷盘,减少检查点带来的密集写入的影响。后台写入器会一点点地将脏页落盘,尽可能减少对数据库活动造成的影响。在默认情况下,后台写入器每 200ms 被唤醒一次(由参数 bgwriter_delay 定义),且最多刷写 bgwriter_lru_maxpages个页面(默认为100个页面)。
本节英文版
https://www.rockdata.net/docs/13/
