前言
Curve 實踐過程當中遇到過幾回內存相關的問題,與操做系統內存管理相關的是如下兩次:html
chunkserver
上內存沒法釋放mds
出現內存緩慢增加的現象
內存問題在開發階段大多很難發現,測試階段大壓力穩定性測試(持續跑7*24小時以上)、異常測試每每比較容易出問題,固然這還須要咱們在測試階段足夠仔細,除了關注io相關指標外,還要關注服務端內存/CPU/網卡等資源使用狀況以及採集的metric
是否符合預期。好比上述問題mds 內存緩慢增加
,若是隻關注io是否正常,在測試階段是沒法發現的。內存問題出現後定位也不容易,尤爲在軟件規模較大的狀況下。node
本文主要是從開發者的角度來談 Curve 中的內存管理,不會過分強調內存管理理論,目的是把咱們在軟件開發過程當中對 Linux 內存管理的認知、內存問題分析的一些方法分享給你們。本文會從如下幾個方面展開:linux
- 內存佈局。結合 Curve 軟件說明內存佈局。
- 內存分配策略。說明內存分配器的必要性,以及須要解決的問題和具備的特色,而後經過舉例說明其中一個內存分配器的內存管理方法。
- Curve 的內存管理。介紹當前 Curve 軟件內存分配器的選擇及緣由。
內存佈局
在說內存管理以前,首先簡要介紹下內存佈局相關知識。git
軟件在運行時須要佔用必定量的內存用來存放一些數據,但進程並不直接與存放數據的物理內存打交道,而是直接操做虛擬內存。物理內存是真實的存在,就是內存條;虛擬內存爲進程隱藏了物理內存這一律念,爲進程提供了簡潔易用的接口和更加複雜的功能。本文說的內存管理是指虛擬內存管理。爲何須要抽象一層虛擬內存?虛擬內存和物理內存是如何映射管理的?物理尋址是怎麼的?這些虛擬內存更下層的問題不在本文討論範圍。github
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 開始,不是內存分佈圖上畫的 0x40000000。這是由於地址空間分佈隨機化(ASLR),它的做用是隨機生成進程地址空間(例如棧、庫或者堆)的關鍵部分的起始地址,目的是加強系統安全性、避免惡意程序對已知地址攻擊。Linux 中
/proc/sys/kernel/randomize_va_space
的值爲 1 或 2 表示地址空間隨機化已開啓,數值一、2的區別在於隨機化的關鍵部分不一樣;0表示關閉。session -
接下來 0x559f0e2b9000 0x559f0eb9f000 0x559f0ebc1000 三個地址起始對應的文件都是curve-mds ,可是對該文件的擁有不一樣的權限,各字母表明的權限
r-讀 w-寫 x-可執行 p-私有 s-共享
。curve-mds 是elf
類型文件,從內容的角度看,它包含代碼段、數據段、BSS段等;從裝載到內存角度看,操做系統不關心各段所包含的內容,只關心跟裝載相關的問題,主要是權限,因此操做系統會把相同權限的段合併在一塊兒去加載,就是咱們這裏看到的以代碼段爲表明的權限爲可讀可執行的段、以只讀數據爲表明的權限爲只讀的段、以數據段和 BSS 段爲表明的權限爲可讀可寫的段。多線程 -
再往下 0x559f1110a000 開始,對應上圖的運行時堆,運行時動態分配的內存會在這上面進行 。咱們發現也是在
.bss
段的結束位置進行了隨機偏移。app -
接着 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分配內存。free(30k)
這部分空間並無歸還給系統,而是 ptmalloc 管理着。由一、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。先申請再歸仍是爲了讓分配的這部份內存的起止地址是字節對齊的。
Curve 的內存管理
Curve 中選擇了兩種分配器:ptmalloc
和jemalloc
。其中 MDS 使用默認的 ptmalloc,Chunkserver 和 Client 端使用 jemalloc。
本文開頭提到的兩個問題在這裏進行說明。首先是MDS內存緩慢增加,現象是天天增加 3G。這個問題分析的過程以下:
- 首先是使用
pmap
查看內存分佈。咱們用 pmap 查看內存緩慢增加的 curve-mds 內存分配狀況,發如今 Memory Mapping Region 存在着大量分配的 64M 內存,且觀察一段時間後都不釋放還在一直分配。從這裏懷疑存在內存泄露。 - 而後查看應用上請求的壓力狀況。查看MDS 上相關的業務 metric,發現 MDS 上的壓力都很小,一些控制面 rpc 的 iops 在幾百左右,不該該是業務壓力較大致使的。
- 接下來查看 curve-mds 部分 64M 內存上的數據。使用
gdb -p {pid} attach
跟蹤線程,dump meemory mem.bin {addr1} {addr2}
獲取指定地址段的內存,而後查看這部份內存內容,基本肯定幾個懷疑點。 - 根據這幾個點去排查代碼,看是否有內存泄露。
Chunkserver 端不是開始就使用 jemalloc 的,最初也是用的默認的 ptmalloc。換成 jemalloc 是本文開始提到的 Chunkserver 在測試過程當中出現內存沒法釋放的問題,這個問題的現象是:chunkserver的內存在 2 個 小時內增加很快,一共增加了 50G 左右,但後面並未釋放。這個問題分析的過程以下:
-
首先分析內存中數據來源。這一點跟 MDS 不一樣,MDS 上都是控制面的請求以及一些元數據的緩存。而Chunkserver 上的內存增加通常來自兩個地方:一是用戶發送的請求,二是 copyset 的 leader 和 follower 之間同步數據。這兩個都會涉及到 brpc 模塊。
brpc 的內存管理有兩個模塊 IOBuf 和 ResourcePool。IOBuf 中的空間通常用於存放用戶數據,ResourcePool 管理 socket、bthread_id 等對象,管理的內存對象單位是 64K 。
-
查看對應模塊的一些趨勢指標。觀察這兩個模塊的metric,發現 IOBuf 和 ResourcePool 這段時間內佔用的內存都有相同的增加趨勢。
IOBuf 後面將佔用的內存歸還給 ptmalloc, ResourcePool 中管理的內存不會歸還給 ptmalloc 而是本身管理。
從這個現象咱們懷疑 IOBuf 歸還給 ptmalloc 的內存 ptmalloc 沒法釋放。
-
分析驗證。結合第二節的內存分配策略,若是堆頂的空間一直被佔用,那堆頂向下的空間也是沒法被釋放的。仍然可使用 pmap 查看當前堆上內存的大小以及內存的權限(是否有不少 —-p 權限的內存)來肯定猜測。所以後面 Chunkserver 使用了 jemalloc。這裏能夠看到在多線程狀況下,若是一部份內存被應用長期持有,使用 ptmalloc 也許就會遇到內存沒法釋放的問題。
這裏對 MDS 和 Chunkserver 出現的兩個問題進行了總結,一方面想說明 Curve 選擇不一樣內存分配器的緣由。對於一個從 0 到 1 的項目,代碼開發之初選擇內存分配器不是一個特別重要的事情,若是有比較多的開發和解決相似內存問題的經驗,開始作出評估是好的;但若是沒有,能夠先選擇一個,出現問題或者到須要優化內存性能的時候再去分析。另一方面但願能夠給遇到相似內存問題的小夥伴一些定位的思路。
做者:李小翠,網易數帆存儲團隊Curve項目攻城獅。
若有理解和描述上有疏漏或者錯誤的地方,歡迎共同交流;參考已經在參考文獻中註明,但仍有可能有疏漏的地方,有任何侵權或者不明確的地方,歡迎指出,一定及時更正或者刪除;文章供於學習交流,轉載註明出處
參考文獻
[1]ptmalloc,tcmalloc,jemalloc對比分析
[3] 深刻理解計算機系統 [美] 蘭德爾 E.布萊恩特(Randal E.·Bryant) 著,龔奕利,賀蓮 譯
相關連接