圖解PostgreSQL--buffer管理

一、共享緩衝區數據結構

圖解PostgreSQL--buffer管理

1、Buffer由數組BufferDescriptor[]數組進行管理。該數組由函數InitBufferPool創建,大小為NBuffers個成員即BufferDesc。該數組創建後由StrategyControl進行管理,firstFreeBuffer為鏈表頭,指向鏈表第一個成員;lastFreeBuffer指向鏈表尾;所有free list中成員由freeNext串起來,該值為數組下標。

2、BufferDescriptor數組是共享內存中申請,所有進程共享。

<code>進程1:

(gdb) p BufferDescriptors

$1 = (BufferDescPadded *) 0xa615fb80

(gdb) p *BufferDescriptors

$2 = {bufferdesc = {tag = {rnode = {spcNode = 1664, dbNode = 0,

relNode = 1262}, forkNum = MAIN_FORKNUM, blockNum = 0}, buf_id = 0,

state = {value = 2199126016}, wait_backend_pid = 0, freeNext = -2,

content_lock = {tranche = 53, state = {value = 536870912}, waiters = {

head = 2147483647, tail = 2147483647}}}, pad = "\\200"}

進程2

(gdb) p BufferDescriptors

$1 = (BufferDescPadded *) 0xa615fb80

(gdb) p *BufferDescriptors

$2 = {bufferdesc = {tag = {rnode = {spcNode = 1664, dbNode = 0,

relNode = 1262}, forkNum = MAIN_FORKNUM, blockNum = 0}, buf_id = 0,

state = {value = 2199126016}, wait_backend_pid = 0, freeNext = -2,

content_lock = {tranche = 53, state = {value = 536870912}, waiters = {

head = 2147483647, tail = 2147483647}}}, pad = "\\200"}/<code>

3、同時還會通過一個環形區進行管理這些數組成員。當進行大表掃描時使用。由strategy->buffers[]數組管理,該數組存儲的是BufferDescriptors[]數組的下標+1後的值,而每次取buf描述符時,從strategy->current值開始進行選擇。選出的不可用後,依次向後進行遍歷,遍歷到頭後從頭再來進行選擇,即形成一個環。是否可用的標準後文詳述。

4、下面說下BufferDesc成員變量。

1)BufferTag tag為一個描述符對應磁盤物理頁的映射。即space ID+database ID+文件ID -- forkNum(表文件還是fsm文件或者vm文件)-- 頁號

2)buf_id為buffer數組BufferBlocks[]的下標

3)state為狀態標記,包括該buffer的refcount和usagecount以及是否合法valid等待

4)wait_backend_pid:若進程A需要刪除的元組所在緩衝塊有其他進程訪問,即refcount>0時,進程A不能物理上刪除元組。系統將該進程的ID記錄在wait_backend_id上,然後對緩衝塊加pin,並阻塞自己。當refcount為1時最後一個使用該緩衝塊的進程釋放緩衝區時,會向wait_backend_id進程發送消息。

5)FreeNext為鏈表的下一個節點的下標

6)content_lock為buffer鎖,當進程訪問緩衝塊時加鎖,讀加LW_SHARE鎖,寫加LW_EXCLUSIVE鎖

二、共享緩衝區分配buffer原理

1、共享buffer的分配

圖解PostgreSQL--buffer管理

1、前期準備:

1)該buffer分配有4種情況:從hash表SharedBufHash中查找;從環形緩衝區查找;從free list查找以及驅逐策略進行分配。

2)hash表SharedBufHash同樣是共享內存全局的,所有進程公有。下面分別是兩個會話連接的server端進程打印出的hash表。

<code>(gdb) p SharedBufHash

$1 = (HTAB *) 0x87f5b04



(gdb) p SharedBufHash

$1 = (HTAB *) 0x87f5b04/<code>

該hash表同樣在InitBufferPool中進行創建:

<code>StrategyInitialize->InitBufTable(NBuffers + NUM_BUFFER_PARTITIONS)->

SharedBufHash = ShmemInitHash/<code>

3)該hash表中條目為:[BufferTag,id]即key值為物理磁盤頁的標誌,id為對應buffer的ID

3)首先需要創建一個newTag,對應物理文件的一個頁

4)通過newTag到函數BufTableHashCode中計算hash表的key值newHash

5)共有128個buffer partition鎖,通過hash的key值以輪詢的方式取鎖

6)此時對key值對應的buffer partition加LW_SHARED鎖

2、此時進入第一種獲取buffer描述符的方法:所有進程共享的SharedBufHash

1)根據newTag從hash表SharedBufHash中查找對應的buffer

2)buf_id>0則表示數據頁在hash表中找到,即對應數據頁以加載到內存

3)根據buf_id獲取buffer的描述符BufferDescriptors[buf_id)].bufferdesc

4)通過函數PinBuffer將對應buffer pin住,然後就可以將buffer的partition鎖釋放

即,將buf的state的refcount+1,usagecount根據情況+1,具體流程下文分析。

5)pin失敗,通過StartBufferIO判斷,返回TRUE,緩衝區無效,此時foundPtr為false,並返回對應buf;返回false,表示別人正在使用,直接返回對應buf。foundPtr表示是否在緩衝區命中

3、若hash表中不存在,則需要從磁盤讀取。首先釋放buf的partition鎖,進入循環。

1)StrategyGetBuffer取出一個buf描述符,具體原理見下文。

2)PinBuffer_Locked將buf的refcount+1

3)此時該buf為髒塊BM_DIRTY,則對buf->content_lock加LW_SHARED鎖,加鎖失敗釋放pin,返回1)。加鎖成功根據strategy是否為空處理。

4)使用環形緩衝區,即strategy不為空:BM_LOCKED鎖內獲取buf髒頁的lsn,根據lsn判斷其日誌是否已經刷寫到磁盤,若未則將該buf從環形緩衝區刪除;釋放buf->content_lock鎖及pin,返回1)重新循環進行選擇。

5)使用環形緩衝區且日誌已刷或者未使用環形緩衝區,則調用FlushBuffer將髒數據刷寫磁盤,最後釋放buf->content_lock鎖。

6)接著進入4,當該頁不為髒時也進入4

4、替換為自己的tag

1)先獲取buf的oldTag,是誰用過。其oldPartitionLock和newTag的newPartitionLock按順序加鎖,若同一個則只加一個鎖。LW_EXCUSIVE

2)將newTag對應的條目插入到hash表SharedBufHash

3)buf_id>=0,表示該條目已在hash表,那麼unpin、oldPartitionLock鎖釋放後,獲取老buf,pin後釋放newPartitionLock

4)pin失敗,通過StartBufferIO判斷,返回TRUE,緩衝區無效,此時foundPtr為false,並返回對應buf;返回false,表示別人正在使用,直接返回對應buf。foundPtr表示是否在緩衝區命中

5)buf_id<0,即未在hash表SharedBufHash:buf_state的refcount==1且不為BM_DIRTY,表示無人使用該buf,退出循環,將buf->tag=newTag,最後釋放相關鎖

6)否則,需要釋放相關鎖,並將newTag對應的條目從hash表刪除後,重新回到3進行選擇。

2、PinBuffer

1、若buffer的state已為BM_LOCKED即已加鎖,則需等待,該鎖是pin鎖

2、GetPrivateRefCountEntry獲取ref,若ref不為NULL,則表示別人在使用,然後TRUE。是這樣理解嗎?需要理解這個函數

3、原子操作讀取state值old_buf_state,並將之保存為buf_state

4、buf_state的refcount+1

5、默認策略下,即從free list中選擇空閒描述符,buf_state的usagecount+1;環形緩衝區策略下,buf_state的usagecount保持為1

6、通過CAS操作將buf->state的值替換為buf_state的值

7、函數返回TRUE表示該buffer的數據有效,即合法的數據已經加載到內存;返回false表示數據無效,即數據未加載到內存


圖解PostgreSQL--buffer管理

3、StartBufferIO:開啟IO,將buf狀態置為BM_IO_IN_PROGRESS

1、每個buffer都有一個IO鎖(BufferIOLWLockArray[(bdesc)->buf_id]).lock

2、獲取buf_state狀態,需要先將其置為BM_LOCKED

3、該buf此時已為BM_IO_IN_PROGRESS,即正在讀寫,需要將上面兩個鎖釋放後WaitIO等待狀態變化

4、forInput為TRUE:要向裡面寫,需要其為!BM_VALID,若是BM_VALID表示有人已經向裡寫了合法數據;FALSE:需要向外讀,若為!BM_DIRTY表示已有人刷寫了。釋放兩個鎖返回

5、將buf_state置為BM_IO_IN_PROGRESS。

6、返回TRUE,表示buf中數據無效,可以使用。False,表示別人正在使用


圖解PostgreSQL--buffer管理

4、StrategyGetBuffer

1、如果使用strategy,則從環形緩衝區取一個空閒的描述符:bufnum=strategy->buffers[strategy->current];buf = GetBufferDescriptor(bufnum - 1);,若沒有可用的則GetBufferFromRing返回NULL,否則直接返回該buf。

2、環形緩衝區取buffer失敗,則去free list取

3、StrategyControl->firstFreeBuffer>0,此時list不為空,

4、則先申請spin鎖StrategyControl->buffer_strategy_lock,再次判斷鏈表情況,若StrategyControl->firstFreeBuffer<0鏈表空了,則釋放鎖後退出循環,進入第8步

5、獲取StrategyControl->firstFreeBuffer指向的buffer描述符,並將該節點從free list刪除

6、釋放StrategyControl->buffer_strategy_lock鎖

7、該buf的refcount和usagecount都為0,則表示我們可以用,若strategy不為NULL,則現將該buf放到環形緩衝區,返回該buffer描述符;否則再次到第4步循環

8、此時free list都取了一遍,但是沒有可用的,通過時鐘算法,即循環StrategyControl->nextVictimBuffer取該buf,看其是否可用。同樣如果找到後,根據strategy是否為NULL,將其放到環形緩衝區。將所有buf都取了一遍後,仍沒有可用的話就報錯:no unpinned buffers available


圖解PostgreSQL--buffer管理

三、本地緩衝區數據結構

圖解PostgreSQL--buffer管理

1、數組LocalBufferDescriptors[]為本地緩衝塊的描述符,buf_id從-2開始,都是負數。為和共享緩衝區有區別,通過是否為負值就可以判斷是否是本地緩衝區

2、數組大小由變量num_temp_buffers控制

3、LocalBufferBlockPointers數組為本地緩衝塊的指針。這些數組的初始化由函數InitLocalBuffers完成,此時並沒有分配本地緩衝塊內存。

4、本地緩衝塊內存的申請和分配由函數GetLocalBufferStorage完成。第一次時從內存上下文LocalBufferContext中分配16個頁大小的block,然後將第一個頁地址分配給LocalBufferBlockPointers[i],並不是一下子全部分配,下次使用時再分配數組下一個頁。

5、16個頁的block用完,再次申請32個頁的block;同理32個頁的block用完,申請64個頁的block,每次都是之前的兩倍。

6、最大一次分配1個GB的block

四、本地buffer的分配

圖解PostgreSQL--buffer管理

1、本地buffer的分配由函數LocalBufferAlloc來完成,用於臨時表的讀寫。

2、同樣需要先初始化tag:newTag,唯一標記一個物理頁

3、第一次使用臨時表時,需要通過calloc創建一系列緩衝區(InitLocalBuffers完成):幾個數組,大小為num_temp_buffers,即該變量控制大小。

LocalBufferDescriptors[]:存儲本地緩衝塊的描述符

LocalBufferBlockPointers[]:本地緩衝塊指針數組,存儲指向塊的指針

LocalRefcount[]:每個描述符引用次數

LocalBufHash:用戶管理本地緩衝塊的hash表,key為tag,value為buffer的數組下標。

4、通過tag從LocalBufHash表中查找,看有沒有,是否已經加載到內存。

5、LocalBufHash表中已存在:

1)獲取其ID,然後獲取local buffer的描述符bufHdr

2)原子操作讀出bufHdr的state即buf_state

3)buf_state的usagecount保持為1?refcount+1

4)buf為BM_VALID,則foundPtr為TRUE,表示命中緩衝,否則為false

5)返回該描述符bufHdr

6、LocalBufHash不存在:

1)通過該值nextFreeLocalBuf遍歷本地緩衝區,若超過NlocBuffer值則從頭開始遍歷

2)LocalRefcount對應的引用值不為0,則返回到1);否則取出其bufHdr的state

3)usagecount=0,表示沒有人在用,則LocalRefcount=1後退出循環,進入步驟7

4)usagecount>0,則將usagecount-1後返回1)重新選擇下一個

7、buf_satate為BM_DIRTY,則需要刷髒:

1)獲取具體頁localpage,smgropen一個oreln,如打開checksum則計算checksum並寫入localpage;smgrwrite將其寫入到磁盤;最後將狀態置為非BM_DIRTY

8、如果第一次使用本地buffer,則需要調用GetLocalBufferStorage將其掛到TopMemoryContext

9、如果該buffer緩衝區的數據有效,則需要更新hash表:將該tag從LocalBufHash中刪除,並將狀態置為 ~(BM_VALID | BM_TAG_VALID),其bufHdr->tag需要清空

10、將新的tag newTag插入hash表LocalBufHash

11、將bufHdr->tag替換成newTag,此時是我們的tag了。

12、buf_state清空,並且置為BM_TAG_VALID,且usagecount加1,foundPTR為false

13、返回buf描述符bufHdr


分享到:


相關文章: