您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關怎么分析Curve中的內存管理,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
Curve 實踐過程中遇到過幾次內存相關的問題,與操作系統內存管理相關的是以下兩次:
chunkserver
上內存無法釋放
mds
出現內存緩慢增長的現象
內存問題在開發階段大多很難發現,測試階段大壓力穩定性測試(持續跑7*24小時以上)、異常測試往往比較容易出問題,當然這還需要我們在測試階段足夠仔細,除了關注io相關指標外,還要關注服務端內存/CPU/網卡等資源使用情況以及采集的metric
是否符合預期。比如上述問題mds 內存緩慢增長
,如果只關注io是否正常,在測試階段是無法發現的。內存問題出現后定位也不容易,尤其在軟件規模較大的情況下。
下面主要是從開發者的角度來談 Curve 中的內存管理,不會過度強調內存管理理論,目的是把我們在軟件開發過程中對 Linux 內存管理的認知、內存問題分析的一些方法分享給大家。本文會從以下幾個方面展開:
內存布局。結合 Curve 軟件說明內存布局。
內存分配策略。說明內存分配器的必要性,以及需要解決的問題和具有的特點,然后通過舉例說明其中一個內存分配器的內存管理方法。
Curve 的內存管理。介紹當前 Curve 軟件內存分配器的選擇及原因。
在說內存管理之前,首先簡要介紹下內存布局相關知識。
軟件在運行時需要占用一定量的內存用來存放一些數據,但進程并不直接與存放數據的物理內存打交道,而是直接操作虛擬內存。物理內存是真實的存在,就是內存條;虛擬內存為進程隱藏了物理內存這一概念,為進程提供了簡潔易用的接口和更加復雜的功能。本文說的內存管理是指虛擬內存管理。為什么需要抽象一層虛擬內存?虛擬內存和物理內存是如何映射管理的?物理尋址是怎么的?這些虛擬內存更下層的問題不在本文討論范圍。
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 表示地址空間隨機化已開啟,數值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分配內存。
free(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。先申請再歸還是為了讓分配的這部分內存的起止地址是字節對齊的。
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 也許就會遇到內存無法釋放的問題。
以上就是怎么分析Curve中的內存管理,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。