深度解讀 Curve 資源佔用之記憶體管理

語言: CN / TW / HK

1

前言

文主要是從開發者的角度來談 Curve 中的記憶體管理,不會過度強調記憶體管理理論,目的是把我們在軟體開發過程中對 Linux 記憶體管理的認知、記憶體問題分析的一些方法分享給大家。 Curve 實踐過程中遇到過幾次記憶體相關的問題,與作業系統記憶體管理相關的是以下兩次:

  • chunkserver 上記憶體無法釋放

  • mds 出現記憶體緩慢增長的現象

記憶體問題在開發階段大多很難發現,測試階段大壓力穩定性測試(持續跑7*24小時以上)、異常測試往往比較容易出問題,當然這還需要我們在測試階段足夠仔細,除了關注 io 相關指標外,還要關注服務端記憶體/ CPU /網絡卡等資源使用情況以及採集的 metric 是否符合預期。比如上述問題 mds 記憶體緩慢增長  ,如果只關注 io 是否正常,在測試階段是無法發現的。記憶體問題出現後定位也不容易,尤其在軟體規模較大的情況下。

本文主要是從開發者的角度來談 Curve 中的記憶體管理,不會過度強調記憶體管理理論,目的是把我們在軟體開發過程中對 Linux 記憶體管理的認知、記憶體問題分析的一些方法分享給大家。本文會從以下幾個方面展開:

  • 記憶體佈局。 結合 Curve 軟體說明記憶體佈局。

  • 記憶體分配策略。 說明記憶體分配器的必要性,以及需要解決的問題和具有的特點,然後通過舉例說明其中一個記憶體分配器的記憶體管理方法。

  • Curve 的記憶體管理。介紹當前 Curve 軟體記憶體分配器的選擇及原因。

Curve 是雲原生計算基金會 (CNCF) Sandbox 專案,由網易主導開源的高效能、易運維、雲原生的分散式儲存系統,由塊儲存 CurveBS 和檔案系統 CurveFS 兩部分組成。

記憶體佈局

在說記憶體管理之前,首先簡要介紹下記憶體佈局相關知識。 軟體在執行時需要佔用一定量的記憶體用來存放一些資料,但程序並不直接與存放資料的實體記憶體打交道,而是直接操作虛擬記憶體。 實體記憶體是真實的存在,就是記憶體條; 虛擬記憶體為程序隱藏了實體記憶體這一概念,為程序提供了簡潔易用的介面和更加複雜的功能。 本文說的記憶體管理是指虛擬記憶體管理。 為什麼需要抽象一層虛擬記憶體? 虛擬記憶體和實體記憶體是如何對映管理的? 物理定址是怎麼的? 這些虛擬記憶體更下層的問題不在本文討論範圍。

Linux 為每個程序維護了一個單獨的虛擬地址空間,包括兩個部分程序虛擬儲存器(使用者空間)和核心虛擬儲存器(核心空間),本文主要討論程序可操作的使用者空間,形式如下圖。

現在我們使用 

pmap 檢視執行中的 curve-mds 虛擬空間的分佈。

pmap 用於檢視程序的記憶體映像資訊,該命令讀取的是 

/proc/[pid]/maps 中的資訊。

// pmap -X {程序id} 檢視程序記憶體分佈
sudo pmap -X 2804620


// pmap 獲取的 curve-mds 記憶體分佈有很多項
Address Perm Offset Device Inode Size Rss Pss Referenced Anonymous ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked Mapping


// 為了方便展示這裡把從 Pss 後面的數值刪除了, 中間部分地址做了省略
2804620: /usr/bin/curve-mds -confPath=/etc/curve/mds.conf -mdsAddr=127.0.0.1:6666 -log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3
Address Perm Offset Device Inode Size Rss Pss Mapping
c000000000 rw-p 00000000 00:00 0 65536 1852 1852
559f0e2b9000 r-xp 00000000 41:42 37763836 9112 6296 6296 curve-mds
559f0eb9f000 r--p 008e5000 41:42 37763836 136 136 136 curve-mds
559f0ebc1000 rw-p 00907000 41:42 37763836 4 4 4 curve-mds
559f0ebc2000 rw-p 00000000 00:00 0 10040 4244 4244
559f1110a000 rw-p 00000000 00:00 0 2912 2596 2596 [heap]
7f6124000000 rw-p 00000000 00:00 0 156 156 156
7f6124027000 ---p 00000000 00:00 0 65380 0 0
7f612b7ff000 ---p 00000000 00:00 0 4 0 0
7f612b800000 rw-p 00000000 00:00 0 8192 8 8
7f612c000000 rw-p 00000000 00:00 0 132 4 4
7f612c021000 ---p 00000000 00:00 0 65404 0 0
.....
7f6188cff000 ---p 0026c000 41:42 37750237 2044 0 0
7f61895b7000 r-xp 00000000 41:42 50201214 96 96 0 libpthread-2.24.so
7f61895cf000 ---p 00018000 41:42 50201214 2044 0 0 libpthread-2.24.so
7f61897ce000 r--p 00017000 41:42 50201214 4 4 4 libpthread-2.24.so
7f61897cf000 rw-p 00018000 41:42 50201214 4 4 4 libpthread-2.24.so
7f61897d0000 rw-p 00000000 00:00 0 16 4 4
7f61897d4000 r-xp 00000000 41:42 50200647 16 16 0 libuuid.so.1.3.0
7f61897d8000 ---p 00004000 41:42 50200647 2044 0 0 libuuid.so.1.3.0
7f61899d7000 r--p 00003000 41:42 50200647 4 4 4 libuuid.so.1.3.0
7f61899d8000 rw-p 00004000 41:42 50200647 4 4 4 libuuid.so.1.3.0
7f61899d9000 r-xp 00000000 41:42 37617895 9672 8904 8904 libetcdclient.so
7f618a34b000 ---p 00972000 41:42 37617895 2048 0 0 libetcdclient.so
7f618a54b000 r--p 00972000 41:42 37617895 6556 5664 5664 libetcdclient.so
7f618abb2000 rw-p 00fd9000 41:42 37617895 292 252 252 libetcdclient.so
7f618abfb000 rw-p 00000000 00:00 0 140 60 60
7f618ac1e000 r-xp 00000000 41:42 50201195 140 136 0 ld-2.24.so
7f618ac4a000 rw-p 00000000 00:00 0 1964 1236 1236
7f618ae41000 r--p 00023000 41:42 50201195 4 4 4 ld-2.24.so
7f618ae42000 rw-p 00024000 41:42 50201195 4 4 4 ld-2.24.so
7f618ae43000 rw-p 00000000 00:00 0 4 4 4
7fffffd19000 rw-p 00000000 00:00 0 132 24 24 [stack]
7fffffdec000 r--p 00000000 00:00 0 8 0 0 [vvar]
7fffffdee000 r-xp 00000000 00:00 0 8 4 0 [vdso]
ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 [vsyscall]
======= ===== =====
1709344 42800 37113
上面輸出中程序實際佔用的空間是從 0x559f0e2b9000 開始,
  • 上面輸出中程序實際佔用的空間是從 0x559f0e2b9000 開始,不是記憶體分佈圖上畫的 0x40000000。這是因為地址空間分佈隨機化(ASLR),它的作用是隨機生成程序地址空間(例如棧、庫或者堆)的關鍵部分的起始地址,目的是增強系統安全性、避免惡意程式對已知地址攻擊。Linux 中 /proc/sys/kernel/randomize_va_space 的值為 1 或 2 表示地址空間隨機化已開啟,數值1、2的區別在於隨機化的關鍵部分不同;0表示關閉。

  • 接下來 0x559f0e2b9000 0x559f0eb9f000 0x559f0ebc1000 三個地址起始對應的檔案都是curve-mds ,但是對該檔案的擁有不同的許可權,各字母代表的許可權 r-讀 w-寫 x-可執行 p-私有 s-共享 。 curve-mds 是 elf 型別檔案,從內容的角度看,它包含程式碼段、資料段、BSS段等; 從裝載到記憶體角度看,作業系統不關心各段所包含的內容,只關心跟裝載相關的問題,主要是許可權,所以作業系統會把相同許可權的段合併在一起去載入,就是我們這裡看到的以程式碼段為代表的許可權為可讀可執行的段、以只讀資料為代表的許可權為只讀的段、以資料段和 BSS 段為代表的許可權為可讀可寫的段。

  • 再往下 0x559f1110a000 開始,對應上圖的執行時堆,執行時動態分配的記憶體會在這上面進行 。 我們發現也是在.bss 段的結束位置進行了隨機偏移。

  • 接著 0x7f6124000000 開始,對應的是上圖 mmap 記憶體對映區域,這一區域包含動態庫、使用者申請的大片記憶體等。 到這裡我們可以看到 Heap 和 Memory Mapping Region 都可以用於程式中使用 malloc 動態分配的記憶體,在下一節記憶體分配策略中會有展開,也是本文關注重點。

  • 接著 0x7fffffd19000 開始是棧空間,一般有數兆位元組。

  • 最後 vvar、vdso、vsyscall 區域是為了實現虛擬函式呼叫以加速部分系統呼叫,使得程式可以不進入核心態1直接呼叫系統呼叫。 這裡不具體展開。

記憶體分配策略

我們平時使用 malloc 分配出來的記憶體是在 Heap 和 Memory Mapping Region 這兩個區域。mallloc 實際上由兩個系統呼叫完成:brk 和 mmap

  • brk 分配的區域對應堆 heap

  • mmap 分配的區域對應 Memory Mapping Region

如果讓每個開發者在軟體開發時都直接使用系統調 brk 和 mmap 用去分配釋放記憶體,那開發效率將會變得很低,而且也很容易出錯。一般來說我們在開發中都會直接使用記憶體管理庫,當前主流的記憶體管理器有三種:ptmalloc tcmalloc jemalloc , 都提供 malloc, free 介面,glibc 預設使用ptmalloc。這些庫的作用是管理它通過系統呼叫獲得的記憶體區域,一般來說一個優秀的通用記憶體 分配器應該具有以下特徵:

  • 額外的空間損耗量儘量少。比如應用程式只需要5k記憶體,結果分配器給他分配了10k,會造成空間的浪費。

  • 分配的速度儘可能快。

  • 儘量避免記憶體碎片。下面我們結合圖來直觀的感受下記憶體碎片。

  • 通用性、相容性、可移植性、易除錯。

我們通過下面一幅圖直觀說明下 glibc 預設的記憶體管理器 ptmalloc 在單執行緒情況下堆記憶體的回收和分配:

  • malloc(30k) 通過系統呼叫 brk 擴充套件堆頂的方式分配記憶體。

  • malloc(20k) 通過系統呼叫 brk 繼續擴充套件堆頂。

  • malloc(200k) 預設情況下請求記憶體大於 128K

    (由 M_MMAP_THRESHOLD 確定,預設大小為128K,可以調整),就利用系統呼叫 mmap分配記憶體。

  • fr ee(30k) 這部分空間並沒有歸還給系統,而是 ptmalloc 管理著。由1、2兩步的 malloc 可以看出,我們分配空間的時候呼叫 brk 進行堆頂擴充套件,那歸還空間給系統是相反操作即收縮堆頂。這裡由於第二步 malloc(20k) 的空間並未釋放,所以此時堆頂無法收縮。這部分空間是可以被再分配的,比如此時 malloc(10k),那可以從這裡分配 10k 空間,而不需要通過 brk 去申請。考慮這樣一種情況,堆頂的空間一直被佔用,堆頂向下的空間有部分被應用程式釋放但由於空間不夠沒有再被使用,就會形成 記憶體碎片。

  • free(20k) 這部分空間應用程式釋放後,ptmalloc 會把剛才的 20k 和 30k 的區域合併,如果堆頂空閒超過 M_TRIM_THREASHOLD ,會把這塊區域收縮歸還給作業系統。

  • free(200k) mmap分配出來的空間會直接歸還給系統。

那對於多執行緒程式,ptmalloc 又是怎麼區分配的?多執行緒情況下需要處理各執行緒間的競爭,如果還是按照之前的方式,小於 HEAP_MAX_SIZE ( 64 位系統預設大小為 64M )的空間使用 brk 擴充套件堆頂, 大於 HEAP_MAX_SIZE 的空間使用 mmap 申請,那對於執行緒數量較多的程式,如果每個執行緒上存在比較頻繁的記憶體分配操作,競爭會很激烈。ptmalloc 的方法是使用多個分配區域,包含兩種型別分配區:主分配區 和 動態分配區。

  • 主分配區:會在 Heap 和 Memory Mapping Region 這兩個區域分配記憶體;

  • 動態分配區:在 Memory Mapping Region 區域分配記憶體,在 64 位系統中預設每次申請的大小位。Main 執行緒和先執行 malloc 的執行緒使用不同的動態分配區,動態分配區的數量一旦增加就不會減少了。動態分配區的數量對於 32 位系統最多是 ( 2 number of cores + 1 ) 個,對於 64 位系統最多是( 8 number of cores + 1 )個。

舉個多執行緒的例子來看下這種情況下的空間分配:

// 共有三個執行緒
// 主執行緒:分配一次 4k 空間
// 執行緒1: 分配 100 次 4k 空間
// 執行緒2: 分配 100 次 4k 空間
# include <stdio.h>
# include <stdlib.h>
# include <pthread.h>
# include <unistd.h>
# include <sys/types.h>


void* threadFunc(void* id) {
std::vector<char *> malloclist;
for (int i = 0; i < 100; i++) {
malloclist.emplace_back((char*) malloc(1024 * 4));
}


sleep(300); // 這裡等待是為檢視記憶體分佈
}
int main() {
pthread_t t1,t2;
int id1 = 1;
int id2 = 2;
void* s;
int ret;
char* addr;


addr = (char*) malloc(4 * 1024);
pthread_create(&t1, NULL, threadFunc, (void *) &id1);
pthread_create(&t2, NULL, threadFunc, (void *) &id2);


pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

我們用 pmap 檢視下該程式的記憶體分佈情況:

741545:   ./memory_test
Address Perm Offset Device Inode Size Rss Pss Mapping
56127705a000 r-xp 00000000 08:02 62259273 4 4 4 memory_test
56127725a000 r--p 00000000 08:02 62259273 4 4 4 memory_test
56127725b000 rw-p 00001000 08:02 62259273 4 4 4 memory_test
5612784b9000 rw-p 00000000 00:00 0 132 8 8 [heap]
**7f0df0000000 rw-p 00000000 00:00 0 404 404 404
7f0df0065000 ---p 00000000 00:00 0 65132 0 0
7f0df8000000 rw-p 00000000 00:00 0 404 404 404
7f0df8065000 ---p 00000000 00:00 0 65132 0 0**
7f0dff467000 ---p 00000000 00:00 0 4 0 0
7f0dff468000 rw-p 00000000 00:00 0 8192 8 8
7f0dffc68000 ---p 00000000 00:00 0 4 0 0
7f0dffc69000 rw-p 00000000 00:00 0 8192 8 8
7f0e00469000 r-xp 00000000 08:02 50856517 1620 1052 9 libc-2.24.so
7f0e005fe000 ---p 00195000 08:02 50856517 2048 0 0 libc-2.24.so
7f0e007fe000 r--p 00195000 08:02 50856517 16 16 16 libc-2.24.so
7f0e00802000 rw-p 00199000 08:02 50856517 8 8 8 libc-2.24.so
7f0e00804000 rw-p 00000000 00:00 0 16 12 12
7f0e00808000 r-xp 00000000 08:02 50856539 96 96 1 libpthread-2.24.so
7f0e00820000 ---p 00018000 08:02 50856539 2044 0 0 libpthread-2.24.so
7f0e00a1f000 r--p 00017000 08:02 50856539 4 4 4 libpthread-2.24.so
7f0e00a20000 rw-p 00018000 08:02 50856539 4 4 4 libpthread-2.24.so
7f0e00a21000 rw-p 00000000 00:00 0 16 4 4
7f0e00a25000 r-xp 00000000 08:02 50856513 140 140 1 ld-2.24.so
7f0e00c31000 rw-p 00000000 00:00 0 16 16 16
7f0e00c48000 r--p 00023000 08:02 50856513 4 4 4 ld-2.24.so
7f0e00c49000 rw-p 00024000 08:02 50856513 4 4 4 ld-2.24.so
7f0e00c4a000 rw-p 00000000 00:00 0 4 4 4
7ffe340be000 rw-p 00000000 00:00 0 132 12 12 [stack]
7ffe3415c000 r--p 00000000 00:00 0 8 0 0 [vvar]
7ffe3415e000 r-xp 00000000 00:00 0 8 4 0 [vdso]
ffffffffff600000 r-xp 00000000 00:00 0 4 0 0 [vsyscall]
====== ==== ===
153800 2224 943

關注上面加粗的部分,藍色區域加起來是 65536K,其中有 404K 是 rw-p (可讀可寫)許可權,65132K 是 —-p (不可讀寫)許可權;黃色區域類似。兩個執行緒分配的時 ptmalloc 分別給了 動態分割槽,並且每次申請 64M 記憶體,再從這 64M 中切分出一部分給應用程式。

這裡還有一個有意思的現象:我們用 strace -f -e "brk, mmap, munmap" -p {pid} 去跟蹤程式檢視下 malloc 中的系統呼叫:

mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f624a169000
strace: Process 774601 attached
[pid 774018] mmap(NULL, 8392704, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f6249968000
[pid 774601] mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7f6241968000
[pid 774601] munmap(0x7f6241968000, 40468480strace: Process 774602 attached
) = 0
[pid 774601] munmap(0x7f6248000000, 26640384) = 0
[pid 774602] mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7f623c000000
[pid 774602] munmap(0x7f6240000000, 67108864) = 0

這裡主執行緒 [774018] 要求分配了 8M+4k 空間;執行緒1 [774601] 先 mmap 了 128M 空間,再歸還了 0x7f6241968000 為起始地址的 40468480 位元組 和 0x7f6248000000 為起始地址的 26640384 位元組,那剩餘的部分是 0x7F6244000000 ~ 0x7F6248000000。先申請再歸還是為了讓分配的這部分記憶體的起止地址是位元組對齊的。

3

Curve 的記憶體管理

Curve 中選擇了兩種分配器:ptmalloc 和 jemalloc 。其中 MDS 使用預設的 ptmalloc,Chunkserver 和 Client 端使用 jemalloc。

本文開頭提到的兩個問題在這裡進行說明。首先是MDS記憶體緩慢增長,現象是每天增長 3G。這個問題分析的過程如下:

1、首先是使用 pmap 檢視記憶體分佈。 我們用 pmap 檢視記憶體緩慢增長的 curve-mds 記憶體分配情況,發現在 Memory Mapping Region 存在著大量分配的 64M 記憶體,且觀察一段時間後都不釋放還在一直分配。從這裡懷疑存在記憶體洩露。

2815659:   /usr/bin/curve-mds -confPath=/etc/curve/mds.conf -mdsAddr=*.*.*.*:6666 -log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3
Address Perm Offset Device Inode Size Rss Pss Referenced Anonymous ShmemPmdMapped Shared_Hugetlb Private_Hugetlb Swap SwapPss Locked Mapping
c000000000 rw-p 00000000 00:00 0 8192 4988 4988 4988 4988 0 0 0 0 0 0
c000800000 rw-p 00000000 00:00 0 57344 0 0 0 0 0 0 0 0 0 0
557c5abb6000 r-xp 00000000 41:42 55845493 9112 6488 6488 6488 0 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b49c000 r--p 008e5000 41:42 55845493 136 136 136 136 136 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b4be000 rw-p 00907000 41:42 55845493 4 4 4 4 4 0 0 0 0 0 0 /usr/bin/curve-mds
557c5b4bf000 rw-p 00000000 00:00 0 10040 2224 2224 2224 2224 0 0 0 0 0 0
557c5cce2000 rw-p 00000000 00:00 0 5604 5252 5252 5252 5252 0 0 0 0 0 0 [heap]
7f837f7ff000 ---p 00000000 00:00 0 4 0 0 0 0 0 0 0 0 0 0
7f837f800000 rw-p 00000000 00:00 0 8192 8 8 8 8 0 0 0 0 0 0
7f8380000000 rw-p 00000000 00:00 0 132 12 12 12 12 0 0 0 0 0 0
......
7fbcf8000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbcfc000000 rw-p 00000000 00:00 0 65536 65536 65536 65528 65536 0 0 0 0 0 0
7fbd04000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd08000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbd0c000000 rw-p 00000000 00:00 0 65536 65536 65536 65528 65536 0 0 0 0 0 0
7fbd10000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd14000000 rw-p 00000000 00:00 0 65536 65536 65536 65532 65536 0 0 0 0 0 0
7fbd18000000 rw-p 00000000 00:00 0 65536 65536 65536 65536 65536 0 0 0 0 0 0
7fbd1c000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd20000000 rw-p 00000000 00:00 0 65536 65536 65536 65524 65536 0 0 0 0 0 0
7fbd24000000 rw-p 00000000 00:00 0 65536 65536 65536 65512 65536 0 0 0 0 0 0
7fbd28000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd2c000000 rw-p 00000000 00:00 0 65536 65536 65536 65520 65536 0 0 0 0 0 0
7fbd30000000 rw-p 00000000 00:00 0 65536 65536 65536 65516 65536 0 0 0 0 0 0
......
======= ====== ====== ========== ========= ============== ============== =============== ==== ======= ======
7814504 272928 263610 272928 248772 0 0 0 0 0 0 KB

然後檢視應用上請求的壓力情況。 檢視MDS 上相關的業務 metric,發現 MDS 上的壓力都很小,一些控制面 rpc 的 iops 在幾百左右,不應該是業務壓力較大導致的。

接下來檢視 curve-mds 部分 64M 記憶體上的資料。 使用 gdb -p {pid} attach 跟蹤執行緒, dump memory mem.bin {addr1} {addr2} 獲取指定地址段的記憶體,然後檢視這部分記憶體內容,基本確定幾個懷疑點。以下是 dump 出來的部分內容,很多 01 開頭的字串,還有一些metric 資訊等資訊,基本可以分為幾塊內容。

根據這幾個點去排查程式碼,看是否有記憶體洩露。 其中有大量 01、02開頭的字串是我們對檔案資訊 FileInfo 和 對映資訊 SegmentInfo 轉化為 key-value 進行持久化中 key 的編碼。那會分配記憶體的就是 GetFileInfo、GetSegmentInfo、ListSegmentInfo 等介面,排查這些介面後發現一處記憶體洩露。MDS 中使用的 Etcd 儲存元資料,其中用 cgo 封裝了 go 版本的 etcdclient。go 中的資料傳遞給 C 時,這部分記憶體是需要由 C 管理釋放了,下面有一處寫程式碼時忘記釋放了。

記憶體洩露還有一個比較好用的工具:Vargrind 。在初期定位的時候嘗試過,由於缺乏經驗,沒有看出相應的問題。後面覆盤的時候我們又嘗試抓了一次,加入了比較多的引數,命令如下:

valgrind --tool=memcheck --leak-check=full --show-reachable=yes 
--trace-children=yes --track-origins=yes /usr/bin/curve-mds
-confPath=/etc/curve/mds.conf -mdsAddr=*.*.*.*:6666
-log_dir=/data/log/curve/mds -graceful_quit_on_sigterm=true -stderrthreshold=3

結果很長,截取了部分,在標黃區域有比較明顯的提示:

==1559781== 13,440 bytes in 40 blocks are possibly lost in loss record 2,296 of 2,367
==1559781== at 0x4C2DBC5: calloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1559781== by 0x4011E31: allocate_dtv (dl-tls.c:322)
==1559781== by 0x40127BD: _dl_allocate_tls (dl-tls.c:539)
==1559781== by 0x628A189: allocate_stack (allocatestack.c:584)
==1559781== by 0x628A189: [email protected]@GLIBC_2.2.5 (pthread_create.c:663)
==1559781== by 0x54F88D: bthread::TaskControl::add_workers(int) (in /usr/bin/curve-mds)
==1559781== by 0x54147B: bthread_setconcurrency (in /usr/bin/curve-mds)
==1559781== by 0x35A0C9: brpc::Server::Init(brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35AAD7: brpc::Server::StartInternal(in_addr const&, brpc::PortRange const&, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35BCBC: brpc::Server::Start(butil::EndPoint const&, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x35BD60: brpc::Server::Start(char const*, brpc::ServerOptions const*) (in /usr/bin/curve-mds)
==1559781== by 0x19442B: curve::mds::MDS::StartServer() (in /usr/bin/curve-mds)
==1559781== by 0x194A61: curve::mds::MDS::Run() (in /usr/bin/curve-mds)
==1559781==
**==1559781== 85,608 bytes in 4,125 blocks are definitely lost in loss record 2,333 of 2,367
==1559781== at 0x4C2BBAF: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==1559781== by 0x56D6863: _cgo_728933f4f8ea_Cfunc__Cmalloc (_cgo_export.c:502)
==1559781== by 0x51A9817: runtime.asmcgocall (/home/xuchaojie/github/curve/thirdparties/etcdclient/tmp/go/src/runtime/asm_amd64.s:635)
==1559781== by 0xC00000077F: ???
==1559781== by 0x300000005: ???**
==1559781==
==1559781== LEAK SUMMARY:
==1559781== definitely lost: 85,655 bytes in 4,128 blocks
==1559781== indirectly lost: 0 bytes in 0 blocks
==1559781== possibly lost: 94,392 bytes in 125 blocks
==1559781== still reachable: 27,012,904 bytes in 13,518 blocks
==1559781== of which reachable via heuristic:
==1559781== newarray : 3,136 bytes in 2 blocks
==1559781== multipleinheritance: 1,616 bytes in 1 blocks
==1559781== suppressed: 0 bytes in 0 blocks
==1559781== Reachable blocks (those to which a pointer was found) are not shown.
==1559781== To see them, rerun with: --leak-check=full --show-leak-kinds=all
==1559781==
==1559781== For counts of detected and suppressed errors, rerun with: -v
==1559781== ERROR SUMMARY: 10331 errors from 988 contexts (suppressed: 0 from 0)

Chunkserver 端不是開始就使用 jemalloc 的,最初也是用的預設的 ptmalloc。換成 jemalloc 是本文開始提到的 Chunkserver 在測試過程中出現記憶體無法釋放 的問題,這個問題的現象是:chunkserver的記憶體在 2 個 小時內增長很快,一共增長了 50G 左右,但後面並未釋放。這個問題分析的過程如下:

  • 首先分析記憶體中資料來源 。 這一點跟 MDS 不同,MDS 上都是控制面的請求以及一些元資料的快取。而Chunkserver 上的記憶體增長一般來自兩個地方:一是使用者傳送的請求,二是 copyset 的 leader 和 follower 之間同步資料。這兩個都會涉及到 brpc 模組。brpc 的記憶體管理有兩個模組 IOBuf 和 ResourcePool。IOBuf 中的空間一般用於存放使用者資料,ResourcePool 管理 socket、bthread_id 等物件,管理的記憶體物件單位是 64K 。這兩個模組詳細的資料結構這裡就不展開說明了,感興趣的小夥伴可以閱讀 brpc 文件:brpc-innternal, ResourcePool相關的可以看下原始碼。

  • 檢視對應模組的一些趨勢指標 。 觀察這兩個模組的metric,發現 IOBuf 和 ResourcePool 這段時間內佔用的記憶體都有相同的增長趨勢。IOBuf 後面將佔用的記憶體歸還給 ptmalloc, ResourcePool 中管理的記憶體不會歸還給 ptmalloc 而是自己管理。

  • 分析驗證 。結合第二節的記憶體分配策略,如果堆頂的空間一直被佔用,那堆頂向下的空間也是無法被釋放的。仍然可以使用 pmap 檢視當前堆上記憶體的大小以及記憶體的許可權(是否有很多 —-p 許可權的記憶體)來確定猜想。因此後面 Chunkserver 使用了 jemalloc。這裡可以看到在多執行緒情況下,如果一部分記憶體被應用長期持有,使用 ptmalloc 也許就會遇到記憶體無法釋放的問題。

這裡對 MDS 和 Chunkserver 出現的兩個問題進行了總結,一方面想說明 Curve 選擇不同記憶體分配器的原因。對於一個從 0 到 1 的專案,程式碼開發之初選擇記憶體分配器不是一個特別重要的事情,如果有比較多的開發和解決類似記憶體問題的經驗,開始做出評估是好的;但如果沒有,可以先選擇一個,出現問題或者到需要優化記憶體效能的時候再去分析;另外一方面希望可以給遇到類似記憶體問題的小夥伴一些定位的思路~

參考

ptmalloc,tcmalloc,jemalloc對比分析 

https://www.cyningsun.com/07-07-2018/memory-allocator-contrasts.html

參考

十問linux虛擬記憶體管理(glibc)

https://zhuanlan.zhihu.com/p/26137521?utm_source=wechat_session&utm_medium=social&utm_oi=552541092948684800&utm_content=sec

Go語言使用cgo時的記憶體管理筆記

https://www.pengrl.com/p/29054/

深入理解計算機系統 [美] 蘭德爾 E.布萊恩特(Randal E.·Bryant) 著,龔奕利,賀蓮 譯

<原創作者:李小翠,Curve Maintainer>

關於 Curve

Curve 是一款高效能、易運維、雲原生的開源分散式儲存系統。可應用於主流的雲原生基礎設施平臺:對接 OpenStack 平臺為雲主機提供高效能塊儲存服務; 對接  Kubernetes 為其提供 RWO、RWX 等型別的持久化儲存卷; 對接  PolarFS 作為雲原生資料庫的高效能儲存底座,完美支援雲原生資料庫的存算分離架構。

Curve 亦可作為雲端儲存中介軟體使用 S3 相容的物件儲存作為資料儲存引擎,為公有云使用者提供高性價比的共享檔案儲存。

  • GitHub :https://github.com/opencurve/curve

  • 官網: https://opencurve.io/

  • 使用者論壇 :https://ask.opencurve.io/

  • 微信群: 請掃描新增或搜尋群助手微訊號  OpenCurve_bot

歡迎 加入雲原生社群 ,點選閱讀原文了解更多。