Buffer Pool(缓冲池)
Aim:保存那些需要重用的数据库文件中的page。
被用于读写想要的pages的access methods,如sequential scan,indexed retrieval 和 hashing。它使用文件管理函数去获取与table对应的data files。
Buffer pool的操作:
1 . request_page(pid) 2.release_page(pid)
一定程度上request_page()替代getBlock() or get_page(), release_page()替代putBlock() or put_page()。 release_page是指不再常用这个page了,如果buffer pool 满了则被换掉。Buffer pool比较稳定,一般读次数大于写。
Buffer pool的数据结构:
- frames… NBUFS Pages(8K) buffers的数组
- directory… NBUFS FrameData items的数组,给出每个frame的信息。
Directory里Info比较小 ,包括frame里是哪些pages,它们被修改了吗(dirty bit),哪些transactions在用它们(pin count),最近访问的时间戳等等。
Frames里包含page里的data,或者是空的。
PageID = BufferTag = (mode,forkNum,blockNum)
PS:fork是指额外的data file。
Scan without Buffer Pool(不用Buffer Pool的Scan):
Buffer buf;
//how many pages of data contained in this relation
int N = numberOfBlocks(Rel);
//Scan each of those pages of data
for (i=0; i<N; i++){
//get constructed pageID
pageID = makePageID(db,Rel,i);
//get block of data in page and feed into buffer
getBlock(pageID, buf);
for (j=0; j<nTuples(buf); j++){
process(buf, j)
}
}
需要读 N pages。如果再读一次,还是需要读N pages。
Scan with Buffer pool(使用 Buffer Pool的Scan):
Buffer buf;
int N = numberOfBlocks(Rel);
for (i = 0; i<N; i++){
pageID = makePageID(db,Rel,i);
//Get Index into buffer pool
bufID = request_page(pageID);
//Get the data of buffer in frame
buf = frame[bufID]
for (j=0; j< nTuples(buf); j++){
process(buf, j)
}
// When finishing the frame,release it
release_page(pageID);
}
第一次需要读N Pages。再次读时,0<=page<=N,因为已经在buffer pool里的无需读。
Buffer pool 数据结构:
typedef char Page[PAGESIZE];
typedef ... PageID; //defined earlier
typedef struct_FrameData{
PageID pid; // which page is in frame
int pin_count; //how many processes using page
int dirty; //page modified since loaded
Time last_used; //when page was last accessed
}FrameData;
Page frame[NBUFS]; //actual buffers
FrameData directory[NBUFS];
Request_page()的实现:
int request_page(pageID pid)
{
bufID = findInPool(pid)
if (bufId == NOT_FOUND){
if (no free frames in Pool){
//kick out the old free frame
bufID = findFrameToReplace()
if (directory[bufID].dirty){
old = directory[bufID].page
put_page(old,frame[bufID])
}
}
//read data into frame and set relevant entry
bufID = index of freed frame
directory[bufID].page = pid
directory[bufID].pin_count = 0
directory[bufID].dirty = 0
get_page[pid, frame[bufID]]
}
directory[bufID].pin_count++
}
对比Virtual file descriptor pool 满了会扩1倍,buffer pool在PostgreSQL启动时创建,比较大,基本不会扩张。
**Release_page(pid):**对指定page减少pin count。
(如不需要替换,此操作对disk和buffer里的内容无影响,buffer里的页面状态也和disk无关)
make_page (pid): 修改page时,为指定page 设置dirty bit。
(不是真正的写入disk,只是指出page改变过了)
flush_page(pid): 用write()将一个page写入disk。
(high-level的DBMS不用常用它)
Evicting a page:
尽量减少read() 和 write()。
找到frame满足:
1. pin count = 0 (即没有人用它)
2. dirty bit = 0 (不改变)
如果选中的page被改变,将frame flush进disk。
将directory entry标记为“frame empty”
如果有多个frames可被释放,则需要一个policy决定哪一个是best的。
Page Replacement Policies:
常用的policies:
- Least Recently Used(LRU)最近最少使用算法
- Most Recently Used (MRU)最近最常用算法
- First in First Out(FIFO)
- Random
但postgreSQL不用以上任何一种policy。
LRU/MRU 需要知道pages最后被访问的时间。两种方式:
6. 更新usage当你释放这个page。(一般选择这个去track usage time)。
7. 更新usage当你标记这个page。
n frame 的 Buffer pool 的Cost benefit由以下决定:
8. 可用的frame的数量(越多越好)
9. 替换策略(replacement strategy) vs 页获取模式(page access pattern)
eg.
Case a.连续扫描(sequential scan), 使用LRU或MRU,当n(buffer pool中的page数量)>=b(file(relation)中的page数量)。
第一次扫描的代价是b reads,但接下来无扫描代价。
Case b.连续扫描, 使用MRU, 当n<b。
第一次扫描的代价是b reads,接下来代价是 b-n reads(在池中的只需要request)。
Case c.连续扫描,使用LRU,当n<b。
所有的scans的代价都是b reads,也就是 sequential flooding(当n个page对应完所有的frame后,n+1 page会代替第一个page的位置,然后以此类推)。
Buffer management的影响
找到既是customer也是emplyees的query:
select c.name
from Customer c,Employee e
where c.ssn = e.ssn;
也可以通过nested loops在DBMS中实现:
for each tuple t1 in Customer{
for each tuple t2 in Employee{
if (t1.ssn == t2.ssn){
append(t1.name) to result set
}
}
}
在实际系统中,对于page-level的操作:
Rel rC = openRelation("Customer");
Rel rE = openRelation("Employee");
for (int i =0l i< nPages(rC);i++){
PageID pid1 = makePageID(db,rC,i);
Page p1 = request_page(pid1);
for (int j =0; j< nPages(rE); j++){
PageID pid2 = makePageID(db,rE,j);
Page p2 = request_page(pid2);
//compare all pairs of tuples from p1,p2, fast since loading in memory.
//construct solution set from matching pairs
release_page(pid2);
}
release_page(pid1);
}
Exercise 5:Buffer Cost Benefit
当没有buffer pool时(最坏情况):
需要Customer表中读每一个page到一个buffer,
对于每个Customer的page,还要读Employee的每个page。
eg:
C0 then E0,E1,E2,E3
C1 then E0,E1,E2,E3
…
代价是:reads = 10(Outer)+10*4(inner)=50 reads
优化:
C0 then E0,E1,E2,E3
C1 then E3,E2,E1,E0
C2 then E0,E1,E2,E3
…
reads = 10(Emp) + 4(Cust) +9*3(Cust) = 41reads
当有20个buffer时(最好情况):
每次只读一次Customer的每个的 page
每次只读一次Emplyee的每个page
reads = 10+4 =14 reads
C0 E0 E1 E2 E3 C1 C2…
当有n>2个buffer且用MRU策略时:
eg:如果有3个buffer pool:
当出现C和E的pair时,process这个pair,然后释放 pair中的E。当没有E了就释放C。当缓冲池(buffer pool)满了,优先替换缓冲池最后一个位置(Most recent)。PS:对于已经在缓冲池中但被释放的不需要再读。
操作 Buffer pool Notes
初始 _ _ _
request C0 C0 _ _
request E0 C0 E0 _
process C0+E0
release E0 MRU: E0
request E1 C0 E0 E1
process C0+E1
release E1 MRU E1->E0
request E2 C0 E0 E2
process C0+E2
…
因为以上太tedious(枯躁),可以写C程序模拟buffer pool的usage,参数:argv[1] :给出“outer”table的pages的数量。
argv[2]: 给出“inner” table的pages的数量。
argv[3]: 给出buffer pool中槽的数量。
argv[4]: 给出替换策略 (LRU, MRU, FIFO-Q)。
PostgreSQL 的 Buffer Manager:
1.对于所有的后端都提供一个共享的内存缓冲池( a shared pool of memory buffers for all backends)。
2.通过buffer manager有所有的从disk获取data的方式。
因为同一时间很多进程都在获取buffer pool的数据,所以Buffers 被放到于 一个大的shared memory的区域。
定义:src/include/storage/buf*.h
函数:src/backend/storage/buffer/*.c
注释:backend/storage/buffer/README
PostgreSQL 的Buffer pool组成:
BufferDescriptors (即directory):shared fixed array of NBuffers X BufferDesc。Server启动前的配置。
BufferBlocks (即frames): shared fixed array of NBuffers X buffers (each BLCKSZ bytes.) (cd postgresql-12.1/src–>grep -r ‘define BLCKSZ’ include–> 得到BLCKSZ的值是8192。)
Buffer = 以上数组们的索引:
indexes: global buffers 1…NBuffers; local buffers negatives.
PostgreSQL的shared buffer的索引是1到NBuffers-1,因为索引0指向的是BufferDescriptors。
Buffer pool的大小在postgresql.conf中设置:
shared_buffer = 16KB //min 128KB, 16*8KB buffers
BufferTag:pageID, 指出buffer pool 的某个frame中是哪个page
typedef struct buftag{
RelFileNode rnode; /* physical relation identifier*/
ForkNumber forkNum;
BlockNumber blockNum; /* relatice to start of relation */
}BufferTag;
typedef struct BufferDesc{ //(simplified)
BufferTag tag; //ID of page contained in buffer
int buf_id; //buffer's index number (from 0)
Bits32 state; //dirty, refcount, usage
int freeNext; //link is freelist chain
... //others related to concurrency
}BufferDesc;
Buffer Pool Functions
Buffer ReadBuffer(Relation r, BlockNumber n)–>相当于request
确保relation r的文件中的blocknumber是n的page被加载
累加buffer的pin count 和 usage count
返回buffer pool中被加载page的索引(Buffer value)
**BufferDesc BufferAlloc( Relation r, ForkNumber f, BlockNumber n, bool found)
用于ReadBuffer去找到一个buffer(r,f,n)
如果buffers都不available,则选择哪个buffer将被替代。
void ReleaseBuffer(Buffer buf)
递减buffer的pin count
如果pin count为0,确保buffer中所有activity返回前完成(没有用户使用page)。
void MarkBufferDirty(Buffer buf)
标记buffer被修改过
要求buffer被pinned且locked
真正的write操作在之后完成(比如当buffer的替换)
…
Reference
comp9315 week3 lectures