1. 程式人生 > >記一次golang的記憶體洩露

記一次golang的記憶體洩露

程式功能

此程式的主要功能是將檔案中資料匯入到clickhouse資料庫中。

【問題描述】

伺服器記憶體每隔一段時間會耗盡

【問題分析】

由於使用的是go語言開發的,所以採用了業界流行的工具pprof。

參考URL:https://cizixs.com/2017/09/11/profiling-golang-program/

工具的使用與思路:
1)先修改原始碼
2)安裝工具觀察
3)根據工具抓取的現象進行分析
4)修復記憶體缺陷程式碼, 再根據分析結果修復記憶體洩漏的地方
5)釋出程式碼進行再跟蹤分析
==================================================
1)修改程式碼:
使用這個工具前需要在程式碼中寫幾行程式碼,以便能使用這個工具的來收集資料。


1 //引用pprof
2 import "net/http"
3 import_ "net/http/pprof"
4
5 //在主函式中新增埠監控程式
6 //由於我的程式碼本來就是守護程序,所以這裡採用新開一個監聽協程方式,防止阻塞
7 func main(){
8 go func(){
9 http.ListenAndServe("0.0.0.0:80", nil)
10 }()
11 //其他程式碼
12 ...
13 }

經過上面的原始碼改造後,重新部署到伺服器上,觀察記憶體狀況;
記憶體仍然可以重新持續消耗記憶體不釋放的現象。

2)在伺服器上安裝 golang pprof 程式,進行資料採集。

安裝方法:yum install golang pprof

3)使用命令對heap進行dump分析,這個工具的好處是dump後可以直接生成pdf或png

1 [root@centos ~]# go tool pprof /root/clickhouse_runner/clickhouse_mssql_e
tl http://0.0.0.0:80/debug/pprof/heap
2 Fetching profile over HTTP from http://0.0.0.0:80/debug/pprof/heap
3 Saved profile in /root/pprof/pprof.clickhouse_mssql_etl.alloc_objects.all
oc_space.inuse_objects.inuse_space.012.pb.gz
4 File: clickhouse_mssql_etl
5 Type: inuse_space
6 Time: Feb 5, 2020 at 4:15pm (CST)
7 Entering interactive mode (type "help" for commands, "o" for options)
8 (pprof) pdf
9 Generating report in profile003.pdf
10 (pprof) quit
11 [root@centos ~]



通過上面的heap 來分析,可以很明顯的看到程式碼中主要的記憶體使用地方在於clickhouse 的驅動中,呼叫clickhouse的部分在建立記憶體沒有釋放(後來仔細分析了下golang的記憶體gc邏輯是由於gc速度存在滯後現象,而匯入程式建立速度又很快,所以才導致gc越來越慢)。

4)找到記憶體洩漏的源頭,開始修改程式碼
修改前原始碼:

1 connect, err := sql.Open("clickhouse", connstring)
2 if err != nil {
3 return err
4 }
5 load_start := time.Now()
6 tx, err := connect.Begin()
7 if err != nil {
8 log.Println(full_filename, "begin err", err)
9 return err
10 }
11 stmt, err := tx.Prepare("insert ... values....")
12 if err != nil {
13 log.Println(full_filename, "preare err", err)
14 return err
15 }
16 _, er := stmt.Exec(...)
17 if er != nil {
18 log.Println("err", er)
19 }
20 er2 := tx.Commit()
21 if er2 != nil {
22 log.Println(db_view, "err", er2)
23 }
24 stmt.Close()
25 connect.Close()

//通過自己寫的程式碼與clickhouse 驅動程式碼的分析,總結可以有兩種方式來改進記憶體洩
漏:
a.修改clickhouse中的驅動程式碼,再執行完程式碼後立即進行重置記憶體,而不等gc來處理:
1 func (stmt *stmt) Close() error {
2 stmt.ch.logf("[stmt] close")
3 //新增再次回收記憶體資料
4 if stmt.ch.block != nil {
5 stmt.ch.block.Reset()
6 }
7 return nil
8 }

b. 直接釋放stmt的物件,利用gc 的自動回收(考慮後還是採用這個方式更合理些)

1 stmt.Close()
2 connect.Close()
3 //新增直接將stmt,connect物件置nil
4 //clear mem
5 stmt = nil
6 tx = nil
7 connect = nil

修改後完整的程式碼:
1 connect, err := sql.Open("clickhouse", connstring)
2 if err != nil {
3 return err
4 }
5 load_start := time.Now()
6 tx, err := connect.Begin()
7 if err != nil {
8 log.Println(full_filename, "begin err", err)
9 return err
10 }
11 stmt, err := tx.Prepare("insert ... values....")
12 if err != nil {
13 log.Println(full_filename, "preare err", err)
14 return err
15 }
16 _, er := stmt.Exec(...)
17 if er != nil {
18 log.Println("err", er)
19 }
20 er2 := tx.Commit()
21 if er2 != nil {
22 log.Println(db_view, "err", er2)
23 }
24 stmt.Close()
25 connect.Close()
26
27 //***** clear mem for gc ******
28 stmt = nil
29 tx = nil
30 connect = nil
31 //////////////////////////////////////////////////////////////////////////////////

5) 釋出修改後的程式碼,進行觀察,通過觀察發現系統記憶體可以正常回收與釋放

【結論】

經過本次golang的除錯發生,真正的原因是gc記憶體釋放不夠及時,存在滯後性(通過其他伺服器觀察發現,當壓力小的時候,記憶體是可以正常釋放的)。
所以最佳實踐還是,在涉及到golang中使用大物件或者頻繁建立記憶體的時候,要採用將物件設定能obj = nil 的方式,告知gc 我已經確實不再使用該記憶體塊了,以便gc快速的回收,減少迭代gc。
另外,這種方式是可以應用到如java,c# 等語言身上的,它們都存在類似的問題