閱讀513 返回首頁    go 阿裏雲 go 技術社區[雲棲]


HybridDB · 源碼分析 · MemoryContext 內存管理和內存異常分析

背景

最近排查和解決了幾處 HybridDB for PostgreSQL 內存泄漏的BUG。覺得有一定通用性。
這期分享給大家一些實現細節和小技巧。

阿裏雲上的 HybridDB for PostgreSQL 是基於 PostgreSQL 開發,定位於 OLAP 場景的 MPP 架構數據庫集群。它不少的內部機製沿用了 PostgreSQL 的實現。其中就包括了內存管理機製 MemoryContext。

一:PostgreSQL 內存管理機製

PostgreSQL 對內存的使用方式主要分兩大塊

1. shared_buffer 和同類 buffer。 簡單的說 shared_buffer 用於存放數據頁麵對應數據文件中的 block,這部分內存是 PostgreSQL 中各進程共享。這部分不在本文討論。
2. MemoryContext 以功能為單位組織起來的樹形數據結構,不同的階段使用不同的 MemoryContext。

1. MemoryContext 的作用

簡單的說 MemoryContext 的存在是為了更清晰的管理內存

  • 合理管理碎片小內存。頻繁的向 OS 申請和釋放內存效率是很差的。MemoryContext 會以 trunk 為單位向 OS 申請成塊的內存,並管理起來。當程序需求小內存時從 trunk 中分配,用完後歸還給對應的 MemoryContext ,並不歸還給 OS。
  • 賦予內存功能和生命周期屬性
    • 以功能為單位管理內存。不同功能和階段使用對應的 MemoryContext。
    • TopTransactionContext:一個事務的生命周期,事務管理相關數據放在 TopTransactionContext,當一個事務提交時該上下文被整個釋放。
    • ExprContext PostgreSQL 以行為單位處理數據,每一行數據的表達式計算都會在 ExprContext 完成,每處理完一行都會重置對應的 ExprContext。
  • 樹形的 MemoryContext 結構
    • 不同功能間的 MemoryContext 是以為樹為單位組織起來的
    • 每個數據庫後端進程頂層是 TopMemoryContext
    • TopMemoryContext 下有很多子 Context
      • 緩存相關的 CacheMemoryContext;
      • 本地鎖相關的 LOCALLOCK hash;
      • 當前事務相關的 TopTransactionContext
      • 注意 CacheMemoryContext 為何不屬於 TopTransactionContext,那是由於 Cache 是獨立於事務存在的,事務提交不影響 Cache 的存在。
    • 刪除或重置一個 MemoryContext,它的子 MemoryContext 也一並被刪除或重置。

2. 不同模塊的 MemoryContext

你可能明白了,實現不同的模塊時,對待內存的方式可能區別很大。
比如:

1. 執行器在做表達式計算時,一些諸如字符串類型數據處理的函數,大多會比較隨意的使用 palloc 分配內存,但直到函數返回,卻並沒有釋放它們。
2. 在處理緩存模塊處理數據時,卻倍加小心的釋放內存。

這是由於:

1. 執行器對數據的處理是以行為單位,都在 ExprContext 中,每處理完一行,會重置 ExprContext,以此釋放相關的內存。
2. 緩存的生命周期很長,不會定期重置整個 MemoryContext。哪怕少量的內存泄漏,積攢的後果都很嚴重。這部分的實現容易出問題,也不好排查。

3. 常見的內存問題

雖然有很好的內存管理機製,但進程中內存間沒有強隔離,也可能出現內存問題。

造成內存泄漏的原因很大可能是:

1. 在較長生存周期的 MemoryContext 中正常處理流程中沒有釋放內存。
2. 由於發生了異常,跳轉到在異常處理階段沒有釋放內存。
3. 沒有使用內存管理機製,使用 OS 調用 malloc,free 處理內存(某些實現不合理的插件中可能出現)。
4. 在不正確的 MemoryContext 分配了內存,導致內存泄漏或數據丟失。
5. 寫內存越界,這是最難找的問題,很容易造成數據庫崩潰。

4. 問題排查小技巧

針對內存泄漏,常用兩種方法排查

1. valgrind 最常見的大殺器,開發人員都懂的。這裏就不詳細介紹了。

2. 使用 GDB 也能大致定位問題

2.1 這是一段腳本,我們把它保存成文本文件(pg_debug_cmd)

define sum_context_blocks
set $context = $arg0
set $block = ((AllocSet) $context)->blocks
set $size = 0
while ($block)
set $size = $size + (((AllocBlock) $block)->endptr - ((char *) $block))
set $block = ((AllocBlock) $block)->next
end
printf "%s: %d\n",((MemoryContext)$context)->name, $size
end

define walk_contexts
set $parent_$arg0 = ($arg1)
set $indent_$arg0 = ($arg0)
set $i_$arg0 = $indent_$arg0
while ($i_$arg0)
printf " "
set $i_$arg0 = $i_$arg0 - 1
end
sum_context_blocks $parent_$arg0
set $child_$arg0 = ((MemoryContext) $parent_$arg0)->firstchild
set $indent_$arg0 = $indent_$arg0 + 1
while ($child_$arg0)
walk_contexts $indent_$arg0 $child_$arg0
set $child_$arg0 = ((MemoryContext) $child_$arg0)->nextchild
end
end

walk_contexts 0 TopMemoryContext

2.2 獲得疑似內存泄漏的進程PID,定時觸發執行下麵的 shell

gdb -p $PID < pg_debug_cmd > memchek/MemoryContextInfo_$(time).log

2.3 分析日誌文件

日誌文件以 MemoryContext 樹的形式展示了一個時間點該進程的內存分配情況。根據時間的積累,可以很容易判斷出哪一些 MemoryContext 可能存在異常,從而為內存泄漏指明一個方向。

(gdb)
TopMemoryContext: 149616
 pgstat TabStatusArray lookup hash table: 8192
 TopTransactionContext: 8192
 TableSpace cache: 8192
 Type information cache: 24480
 Operator lookup cache: 24576
 MessageContext: 32768
 Operator class cache: 8192
 smgr relation table: 24576
 TransactionAbortContext: 32768
 Portal hash: 8192
 PortalMemory: 8192
  PortalHeapMemory: 1024
   ExecutorState: 24576
    SRF multi-call context: 1024
    ExprContext: 0
    ExprContext: 0
    ExprContext: 0
 Relcache by OID: 24576
 CacheMemoryContext: 1040384
  pg_toast_2619_index: 1024
  ....
  pg_authid_rolname_index: 1024
 WAL record construction: 49776
 PrivateRefCount: 8192
 MdSmgr: 8192
 LOCALLOCK hash: 8192
 Timezones: 104128
 ErrorContext: 8192

最後,文章的參考資料中也提供了一種類似的方法,供各位參考。

總結

PostgreSQL 內存管理機製的實現比較複雜,但用起來確卻很簡單,有一種特別的美感,推薦大家了解一下。

參考資料

  1. PostgreSQL Developer_FAQ

最後更新:2017-07-21 09:03:09

  上一篇:go  MySQL · 實現分析 · HybridDB for MySQL 數據壓縮
  下一篇:go  MySQL · myrocks · myrocks寫入分析