作者:楊一迪,騰訊雲數據庫后台開發工程師,主要負責騰訊雲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
往期推薦
開年大禮包