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 內存管理機製的實現比較複雜,但用起來確卻很簡單,有一種特別的美感,推薦大家了解一下。
參考資料
最後更新:2017-07-21 09:03:09