postgres內存上下文

1 數據庫內存上下文

  postgresql在7.1版本引入了內存上下文機制來解決日益嚴重的內存泄漏的問題,在引入了這種“內存池”機制後,數據庫中的內存分配改爲在“內存上下文中”進行,對用戶來說,對內存的申請由原來的malloc、free變成了palloc、pfree。對內存上下文的常用操作包括:

  • 創建一個內存上下文:MemoryContextCreate
  • 在上下文中分配內存片:palloc
  • 刪除內存上下文:MemoryContextDelete
  • 重置內存上下文:MemoryContextReset

這裏引入兩個概念:內存片內存塊的概念。

內存片(CHUNK):用戶在內存上下文中申請(palloc)到的內存單位。 
內存塊(BLOCK):內存上下文在內存中申請(malloc)到的內存單位。



2 數據結構

2.1 AllocSetContext

typedef struct AllocSetContext
{
    MemoryContextData header;   /* Standard memory-context fields */
    /* Info about storage allocated in this context: */
    AllocBlock  blocks;         /* head of list of blocks in this set */
    AllocChunk  freelist[ALLOCSET_NUM_FREELISTS];       /* free chunk lists */
    /* Allocation parameters for this context: */
    Size        initBlockSize;  /* initial block size */
    Size        maxBlockSize;   /* maximum block size */
    Size        nextBlockSize;  /* next block size to allocate */
    Size        allocChunkLimit;    /* effective chunk size limit */
    AllocBlock  keeper;         /* if not NULL, keep this block over resets */
} AllocSetContext;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

AllocSetContext是內存上下文的核心的控制結構,我們在代碼中經常看到的內存上下文TopMemoryContext的定義爲:

MemoryContext TopMemoryContext = NULL;
  • 1

  可以看到這個內存上下文的類型是MemoryContext,即:

typedef struct MemoryContextData
{
    NodeTag     type;           /* identifies exact kind of context */
    MemoryContextMethods *methods;      /* virtual function table */
    MemoryContext parent;       /* NULL if no parent (toplevel context) */
    MemoryContext firstchild;   /* head of linked list of children */
    MemoryContext nextchild;    /* next child of same parent */
    char       *name;           /* context name (just for debugging) */
    bool        isReset;        /* T = no space alloced since last reset */
} MemoryContextData;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  那麼MemoryContextData和AllocSetContext是什麼樣的關係呢?請看下圖左半部分。 
  內存上下文數據結構




圖 2-1 內存上下文數據結構 
                 
  AllocSetContext結構的第一個指針用於指向MemoryContextData,也就是說TopMemoryContext實際上是一個AllocSetContext結構,但是使用時通常將類型轉換爲MemoryContextData,實際上這也是PG中最常用的技巧之一,在代碼中可以看到這樣的寫法:

AllocSet    set = (AllocSet) context;
  • 1

  由於AllocSetContext結構中的首部存放着MemoryContextData指針,所以這種轉換可以成功。這樣的使用方法有些類似與類的繼承:MemoryContextData代表父類,AllocSetContext在父類(頭部的指針)的基礎上增加了一些新的功能。實際上PG就是使用了這種機制實現了interface(MemoryContextData作爲interface),而後面的實現可以有很多種(AllocSetContext是內存上下文的一種實現)。 
  好說到這裏言歸正傳,繼續介紹MemoryContextData數據結構的功能:

  • methods:保存着內存上下文操作的函數指針(例如palloc、pfree)
  • parent、firstchild、nextchild:形成內存上下文的BTree結構
  • name:內存上下文名稱(爲了調試而存在)
  • isReset:記錄上次重置後是否有內存申請動作發生

MemoryContextData使內存上下文形成了一個二叉樹的結構,這樣的數據結構增加了內存上下文的易用性,即在重置或刪除內存上下文時,所有當前上下文的子節點也會被遞歸的刪除或重置,避免錯刪或漏刪上下文。methods中保存的全部爲函數指針,在內存上下文創建時,這些指針會被賦予具體函數地址。 
  下面繼續介紹AllocSetContext數據結構:

  • header:前面介紹過了
  • blocks:內存塊鏈表,內存上下文向OS申請連續大塊內存後,空間由blocks鏈表維護
  • freelist:內存片回收數組,後面具體分析
  • initBlockSize:上下文申請的第一個內存塊的大小
  • maxBlockSize: 上下文申請的最大的內存塊的大小
  • nextBlockSize: 上下文下一次申請的內存塊的大小(MemoryContextCreate函數中介紹這三個參數)
  • allocChunkLimit:申請內存片/塊的閾值
  • keeper:這個指針指向內存上下文重置時不釋放的內存塊

(20160712以上)

2.2 AllocChunkData

內存片存在於內存塊以內,是內存塊分割後形成的一段空間,內存片空間的頭部爲AllocChunkData結構體,後面跟着該內存片的空間,實際上palloc返回的就這指向這段空間首地址的指針。內存片有兩種狀態:AllocSetContext中freelist數組中存放的是內存片指針是被回收的內存片;另外一種內存片是用戶正在使用的內存片。(注意兩種狀態的內存片都存在於內存塊中,被回收只是改變內存片aset指針,形成鏈表保存在freelist中;在使用中的內存片aset指針指向所屬的AllocSetContext)

typedef struct AllocChunkData
{
    void       *aset;
    Size        size;
}   AllocChunkData;
  • 1
  • 2
  • 3
  • 4
  • 5

  在palloc時會發生兩種情況:

  • allocset會在自己維護的內存塊鏈表(blocks)中尋找空間構造內存片,然後分配給用戶。
  • 申請新的內存塊追加到blocks鏈表中,在其中分配新的內存片分配給用戶。

  內存片的數據結構相對簡單,空指針aset是一個複用的指針,當內存片正在使用時,aset指向它屬於的allocset結構,當內存片被釋放後,內存片被freelist數組回收,aset作爲實現鏈表的指針,用於形成內存片的鏈式結構。


2.3 AllocBlockData

typedef struct AllocBlockData
{
    AllocSet    aset;           /* aset that owns this block */
    AllocBlock  next;           /* next block in aset's blocks list */
    char       *freeptr;        /* start of free space in this block */
    char       *endptr;         /* end of space in this block */
}   AllocBlockData;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

  內存塊是內存上下文向操作系統申請的連續的一塊內存空間,申請後將AllocBlockData結構置於空間的首部,其中freeptr和endptr用與指向當前內存塊中空閒空間的首地址和當前內存塊的尾地址,見圖2-1中的“連續內存段(內存塊)”。aset指向控制結構AllocSetContext,next指針形成內存塊的鏈式結構。

2.4 freelist[ALLOCSET_NUM_FREELISTS]

  AllocSetContext結構中的一個重要的數組freelist,這是一個定長數組:

#define ALLOCSET_NUM_FREELISTS  11
.
.
AllocChunk  freelist[ALLOCSET_NUM_FREELISTS];       /* free chunk lists */
.
.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
這是一個存放內存片指針的數組,數組中每一個元素都是一個內存片指針,就像前面提到的,空閒內存片會形成鏈表結構,而鏈表的頭結點的指針就存放在這個數組中。從長度來看,這個數組可以保存11個內存片的鏈表,每一個鏈表都保存這特定大小的內存片: 
freelist


圖2-2描述的就是freelist數組的結構,數組下標0位置保存8字節的內存片,下標1位置保存16字節的內存片,以此類推,freelist中可以保存的最大的內存片爲8k字節。 
  相同大小的內存片會串在同一個鏈表中,放在freelist中指定的位置,數組下標的計算按照公式:log(Size)-3。例如大小爲512字節的內存片被釋放了,套用公式log(512)-3=5,那麼這個內存片就會維護到freelist[5]指向的鏈表中。(具體計算過程見AllocSetFreeIndex函數)。

3 算法

3.1 AllocSetContextCreate:創建內存上下文

MemoryContext
AllocSetContextCreate(MemoryContext parent,
                      const char *name,
                      Size minContextSize,
                      Size initBlockSize,
                      Size maxBlockSize)

內存上下文創建需要傳入幾個參數:

  • parent:當前創建內存上下文的父節點
  • name:當前創建內存上下文名稱
  • minContextSize:創建上下文時申請內存塊大小
  • initBlockSize:該上下文第一次申請內存塊大小
  • maxBlockSize:該上下文可以申請的最大內存塊大小

讓我們看幾個數據庫中最常見的上下文創建時的參數,結合具體值在說說創建時參數的作用:

這裏寫圖片描述

  • minContextSize:如果這個值設定了並超過了一定大小(一個內存塊結構體加上一個內存片結構體的大小),那麼在創建上下文時立即申請一個內存塊,大小爲minContextSize。上圖中我們可以看到大部分上下文minContextSize都爲0,那麼ErrorContext的minContextSize爲8k有什麼作用呢?在系統出現OOM時,內存空間已經耗盡,但是ereport的錯誤處理流程仍然需要申請內存空間去打印錯誤信息,但系統已經沒有內存可以申請了。這時ErrorContext中保留的8k空間可以保證最後的錯誤處理流程可以正確執行。 
      
    -initBlockSizemaxBlockSize:內存上下文中的內存塊申請的大小是由這兩個參數決定的,initBlockSize代表了第一次申請的內存塊大小,後面每一次申請都是前一次申請大小的二倍,直到申請內存大小爲maxBlockSize爲止,當達到maxBlockSize時,以後每一次申請的內存大小都等於maxBlockSize。(事實上如果多次在一個上下文申請內存,那麼很快就會到達maxBlockSize,舉個例子:TupleSort中申請內存塊的大小序列爲:8k 16k 32k 64k 128k 256k 512k 1M 2M 4M 8M 8M 8M 8M …) 
     
  • allocChunkLimit:這裏引出一個重要的參數,內存片申請閾值,這個值被開始被設爲8k字節,但是後面會適當縮小到maxBlockSize的1/8。這個參數的調整是爲了減少內存片空間的浪費(內存塊中的最後一段內存不足以放下一個內存片,所以這段空間被捨棄掉了,理論上浪費掉的空間最大爲allocChunkLimit)

申請內存的流程圖:

這裏寫圖片描述


需要重點關注的有幾點:

  • 回收當前內存塊的剩餘空間:將剩餘空間切割成freelist能保存的最大值,例如1000字節的內存片回收時首先申請512字節的內存片,然後掛在freelist[6]上,剩餘488字節申請256字節的內存片掛在freelist[5]上,剩餘232字節繼續上面處理流程,直到最後空間小於8字節爲止。
  • 在多次申請內存塊後,內存塊的大小總會等於maxBlockSize,這樣如果出現內存泄漏導致OOM時,如果某一個內存上下文非常大,可以利用這個特點分析內存問題的根因。例如每100次申請8M的內存塊時,打印一次Backtrace。

3.3 AllocSetFree

釋放內存流程圖:

這裏寫圖片描述

3.4 AllocSetRealloc

relloc流程圖:

這裏寫圖片描述


3.5 AllocSetStats

這個函數會被MemoryContextStats遞歸調用,遍歷內存上下文樹的內個節點,並獲取當前節點的信息。

GDB調試時這一個非常好用的函數,可以直接在log中打印內存上下文樹,指令:

gdb > p MemoryContextStats(TopMemoryContext)
  • 1

關於PG數據庫GDB的一些調試技巧在下篇博客中繼續介紹。


原來鏈接:http://blog.csdn.net/u014539401/article/details/51893272




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章