值得收藏的查詢進程佔用內存情況方法匯總

  • 2020 年 2 月 14 日
  • 筆記

| 作者:楊一迪,騰訊雲數據庫後台開發工程師,主要負責騰訊雲PostgreSQL、CynosDB等產品後台開發工作。


現網運維過程中,常有用戶諮詢實例的內存使用情況,故而和大家一起分享我對於內存佔用情況的理解,共同進步。

1

簡述

查看進程佔用內存情況的方式比較多,包括top命令、/proc/${pid}/smaps文件統計、cgroup統計等。但不同方式的查詢結果具體代表什麼含義,這裡通過一個測試程序,簡單驗證下這三種查詢方式如何反映進程的內存使用情況。想看結論的直接看文末的總結。本文有任何錯誤,歡迎在留言區討論指導。

1

測試程序

為了驗證進程的私有內存、共享內存使用情況,寫了個簡單的http server,主要代碼如下。

1. 申請私有內存

申請一個指定大小的數組,其中g_str為全局變量,不會在接口返回時銷毀。

func expandGlobalVar(writer http.ResponseWriter, request *http.Request) {    type Request struct {        Length    int    }    data, err := ioutil.ReadAll(request.Body)    if err != nil {        log.Printf("ioutil.ReadAll failed. err: %v", err)        writer.Write([]byte("io failed"))        return    }    req := &Request{}    json.Unmarshal(data, req)    g_str = make([]byte, req.Length)    for i:=0;i<req.Length;i++{        g_str[i]='a'    }    curLength, curCap := len(g_str), cap(g_str)    writer.Write([]byte(fmt.Sprintf("req length: %d, length: %d, cap: %d", req.Length, curLength, curCap)))    return}
2. 掛載共享內存文件

僅掛載共享內存文件,還未讀取共享內存,此時並沒有申請共享內存。

func mmapAttach(writer http.ResponseWriter, request *http.Request) {    data, err := ioutil.ReadAll(request.Body)    if err != nil {        log.Printf("ioutil.ReadAll failed. err: %v", err)        writer.Write([]byte("io failed"))        return    }    type Request struct {        Filename    string    }    req := &Request{}    json.Unmarshal(data, req)    mmapsFile, err = mmap.Open(req.Filename)    if err != nil {        writer.Write([]byte(err.Error()))    }    return}

3. 讀取共享內存

讀取指定長度的共享內存文件,此時會申請共享內存。

func mmapRead(writer http.ResponseWriter, request *http.Request) {    data, err := ioutil.ReadAll(request.Body)    if err != nil {        log.Printf("ioutil.ReadAll failed. err: %v", err)        writer.Write([]byte("io failed"))        return    }    type Request struct {        Start    int64        Length    int64    }    req := &Request{}    json.Unmarshal(data, req)      buf := make([]byte, req.Length)    length, err := mmapsFile.ReadAt(buf, req.Start)    if err != nil {        log.Printf("readat error. err: ", err)        writer.Write([]byte("readat error"))        return    }    log.Printf("length: %d", length)    return}
4. 測試步驟

1)啟動2個http server

~/code/httpMock/bin/httpMock -p 1001 &~/code/httpMock/bin/httpMock -p 1002 &

2)分別申請50M的私有內存

curl -d '{"Length":50000000}' http://127.0.0.1:1001/expandGlobalVarcurl -d '{"Length":50000000}' http://127.0.0.1:1002/expandGlobalVar

3)分別申請100M、200M的共享內存,其中有100M由進程共享

curl -d '{"Filename":"/root/code/httpMock/mmap_files/log"}' http://127.0.0.1:1001/mmapAttachcurl -d '{"Filename":"/root/code/httpMock/mmap_files/log"}' http://127.0.0.1:1002/mmapAttachcurl -d '{"Start": 0, "Length":100000000}' http://127.0.0.1:1001/mmapReadcurl -d '{"Start": 0, "Length":200000000}' http://127.0.0.1:1002/mmapRead

1

測試結果

1. /proc/${pid}/smaps

smaps文件記錄了進程中各個內存段的使用情況,按照上述測試步驟,可觀察到smaps中的內存變化情況如下:

1)啟動http server後,Rss佔用3M左右

2)申請50M的私有內存後,可以看到私有內存所在的內存段,Rss/Pss分別佔用50M左右

3)分別申請100M、200M的共享內存,其中有100M由進程共享。申請後私有內存段擴充到100M,Rss增加量=私有內存增加量+共享內存增加量,Pss=私有內存+共享內存/共享進程數。

結論:smaps中記錄了進程的各個內存段,其中Rss=私有內存+共享內存,Pss=私有內存+共享內存/共享進程數,Rss中的共享內存會被重複計算。

2. top命令

top命令返回了物理內存和共享內存的使用情況,按上述測試步驟,可觀察到top命令結果變化如下:

1)啟動2個http server後,RES私有內存佔用3M左右,與smaps的RSS一致:

2)分別申請50M的私有內存,RES擴充到50M左右:

3)分別申請100M、200M的共享內存後,RES與smaps中的Rss類似,擴充了150M和250M左右,SHR擴充了100M和200M:

結論:top命令結果中,RES代表私有內存+共享內存,SHR代表共享內存,單位都為KB。top命令的RES與smaps中的RSS基本一致

3. cgroup memory子系統

cgroup memory子系統中,memory.usage_in_bytes記錄了cgroup組中的進程的內存使用情況,memory.stat記錄了各類內存的詳細使用情況,按上述測試步驟,可觀察到cgroup統計結果變化如下:

1)在同一cgroup組中啟動http server,注意需要通過cgexec啟動,保證進程啟動時就在cgroup組中。啟動後rss為2M左右,等於(3M-2M)2,即私有內存量進程數,與top命令、smaps計算的私有內存量基本一致:

cgdelete memory:httpMockcgcreate -g memroy:httpMockcgexec -g memory:httpMock ~/code/httpMock/bin/httpMock -p 1001 &cgexec -g memory:httpMock ~/code/httpMock/bin/httpMock -p 1002 &

2)分別申請50M的私有內存後,兩進程共擴充100M左右:

3)分別申請100M、200M的共享內存後,內存使用量與top命令和smaps中統計的私有內存用量基本一致:

結論:cgroup中的memory.usage_in_bytes和memory.stat的rss字段,統計的是進程的私有內存

4. cgroup的內存限制與page cache

當系統讀取文件時,會在系統緩存中緩存文件內容,以減少硬盤IO。這部分內存緩存,會統計到cgroup.stat中的cache字段。而在多個cgroup組都有讀取相同文件時,這部分緩存只會統計到第一個讀該文件的cgroup組中。經過驗證,這部分緩存不會觸發oom,在緩存+內存佔用達到內存限制時,會回收系統緩存。驗證過程如下:

1)啟動http server後,加載共享文件並讀取,可看到佔用了100M的cache:

2)調整內存上限,使其低於cache+rss,觸發了緩存回收:

[ 調整內存上限前,系統buf+cache為509M ]

[ 調整上限後觸發緩存回收 ]

3)嘗試將內存上限調整到已使用內存以下,調整失敗:

1

總結

1)smaps中記錄了進程佔用的各個內存段,每個內存段中的Rss表示私有內存+共享內存大小,其中共享內存被多個進程佔用時會被重複計算; 2)smaps中的Pss會將共享內存部分按共享進程數進行均攤,Pss表示私有內存+共享內存/共享進程數,因此計算一組進程佔用的內存總數時,累加Pss的結果更準確; 3)smaps中的Shared_Clean/Shared_dirty表示共享內存大小 4)top命令的RES表示私有內存+共享內存大小,單位為KB,其中共享內存被多個進程佔用時會被重複計算; 5)top命令的SHR表示共享內存大小,單位為KB; 6)cgroup的memory.stat中cache表示系統page cache大小,在進程讀取文件時,文件會緩存到系統內存,這部分緩存的內存就會記到cache中; 7)cgroup的memory.stat中rss表示私有內存大小,不包括共享內存部分; 8)cgroup的memroy.usage_in_bytes表示內存使用量,主要包括memory.stat的cache和rss; 9)cgroup的內存限制,主要限制rss大小,當rss+cache>內存上限時會優先觸發cache的回收。

綜上所述,當我們考慮進程的內存使用量時,如果關注是否會觸發oom,則主要看memory.stat的rss部分即可,但rss並不能反映共享內存的使用情況;如果要關注進程的私有內存+共享內存佔用情況,則可以主要看smaps中的Pss。

參考資料:

cgroup:https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt linux /proc/pid/smaps各字段含義:https://blog.csdn.net/u010902721/article/details/46446031

往期推薦

(點擊圖片即可跳轉閱讀)

開年大禮包 

↓↓更多驚喜優惠請點這兒~