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


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

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

簡述

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

測試程序

為了驗證進程的私有內存、共享內存使用情況,寫了個簡單的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/expandGlobalVar
curl -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/mmapAttach
curl -d '{"Filename":"/root/code/httpMock/mmap_files/log"}' http://127.0.0.1:1002/mmapAttach
curl -d '{"Start": 0, "Length":100000000}' http://127.0.0.1:1001/mmapRead
curl -d '{"Start": 0, "Length":200000000}' http://127.0.0.1:1002/mmapRead

測試結果

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:httpMock
cgcreate -g memroy:httpMock
cgexec -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)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

往期推薦

解碼Redis最易被忽視的CPU和內存占用高問題

開年大禮包

 

 點擊優惠購買騰訊自研數據庫CynosDB

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM