內存泄漏
web
內存泄漏(Memory Leak)是指程序中已動態分配的堆內存因爲某種緣由程序未釋放或沒法釋放,形成系統內存的浪費,致使程序運行速度減慢甚至內存溢出,系統崩潰等嚴重後果。在Go語言服務中,內存泄漏的大多數緣由是goroutine泄露。設計模式
問題發現
在巡檢過程當中突然發現某個服務模塊服務的內存瘋漲,剛剛部署時候很小,過了兩個月左右達到了驚人的200倍左右,繼續增加下去的結果就是內存溢出致使該服務的pod重啓(該服務使用k8s的deploy部署)。須要排查一下泄露問題出在哪裏,因而在本地本身模擬了一套邏輯類似的環境(自測用的,模擬了一個簡單場景)。緩存
pprof工具安全
pprof 是一個強大的性能分析工具,能夠捕捉到多維度的運行狀態的數據,便於排查程序的堆棧信息,goroutine分佈等。
bash
排查過程
一、程序中添加pprof工具
首先在程序添加監聽端口,以下:微信
而後導入pprof包:網絡
二、啓動程序
訪問本地端口6060(主要關注goroutine數量)
三、使用goTest發起請求
這裏模擬了20個併發量,一次請求。
四、使用pprof查看樣本數據
能夠看到在請求結束以後,goroutine數量依然存在,並無被回收。併發
猜想緣由:大量goroutine滯留致使棧空間沒有被釋放(影響較小),goroutine沒有被釋放,goroutine指向的heap一系列對象沒有被回收掉,heap越用越多,持續申請內存形成內存持續異常增加,也就是內存泄漏。app
點擊goroutine查看詳細信息:ide
能夠看到有四種類型的goroutine棧居高不下,grpc的出現率很高,猜想是在發起請求時建立的grpc鏈接沒有釋放掉。
gRpc的源碼沒必要追溯,也沒有完整的調用棧信息,做爲一個rpc庫,已經封裝了關閉請求流的方法。應該關注引用的它庫的位置,看看是否在應用層有暴露出來的關閉流的方法。
選擇從該處向上追溯,首先定位到該處的源碼:
每次的請求在該處都會阻塞住,是否須要在該處上游有一個釋放信號,將該context構造的goroutine樹釋放掉。
一路向上追溯源碼(不詳述),看到一個在應用層結構體實現了close方法,點到close方法,能夠看到該方法能夠釋放鏈接與緩存。因而在應用層找到合適的位置調用了close方法。
再次發起模擬請求,查看pprof工具:
能夠看到協程數量恢復如初,業務正常沒受影響,初步猜想是該處的緣由。接下來各類業務驗證不詳述...
五、併發測試進程memory
繼續單元測試。
建立20個協程,每一個協程100個請求,中間休眠一秒(爲了防止速度太快腳本沒法記錄)。
監控腳本一覽:
#!/bin/bashread -p "輸入進程的id:" processIdwhile [ 1 ]do #每隔五秒讀一次進程內存,看結束以後內存狀況 ProcessMem=`cat /proc/$processId/status |grep VmRSS|awk '{print $2,$3}'` DateTime=` date "+%H:%M:%S"` echo $DateTime "| 進程內存:"$ProcessMem >> noclose-process-mem.txt sleep 5sdone
6、測試結果
不關閉流:
內存狀況(腳本統計):16M -> 822M,至關大
貼出來一部分腳本統計數據:
09:33:11 | 進程內存:16580 kB09:33:16 | 進程內存:16580 kB09:33:21 | 進程內存:16580 kB09:33:26 | 進程內存:16580 kB09:33:31 | 進程內存:16580 kB #發起請求,內存開始暴漲09:33:36 | 進程內存:44324 kB09:33:41 | 進程內存:74400 kB09:33:46 | 進程內存:98708 kB09:33:51 | 進程內存:122612 kB09:33:56 | 進程內存:147912 kB .....10:05:57 | 進程內存:822224 kB #請求結束慢慢恢復穩定10:06:12 | 進程內存:822488 kB10:06:17 | 進程內存:822488 kB......10:06:47 | 進程內存:822488 kB10:06:52 | 進程內存:822488 kB10:06:57 | 進程內存:822488 kB#以後內存並無縮小
查看一下goroutine:
關閉流:
內存統計:16M -> 44M(優化了80%左右,請求時間也縮短)
每次請求關閉流:
09:25:44 | 進程內存:16496 kB#初始內存----往下表示發起請求,內存開始增加09:25:49 | 進程內存:35948 kB09:25:54 | 進程內存:41320 kB09:25:59 | 進程內存:41776 kB......09:29:24 | 進程內存:43976 kB#逐漸趨於穩定09:29:29 | 進程內存:43976 kB09:29:34 | 進程內存:43976 kB09:29:39 | 進程內存:44180 kB#已經穩定在44M09:29:44 | 進程內存:44180 kB
7、再次定位
定位到問題,雖然上述方法能夠解決內存泄露的問題,可是並無選擇這種方式,由於和最初的設計模式相悖,最初針對這塊設計模式是單例模式。
以後又是抓耳撓腮的讀代碼,調試,終於發現問題所在.......
查看一波本身寫的該部分代碼:
/*源代碼不能泄露,這是本地本身編寫的代碼,大概邏輯相似*/func GetClient(userName string) *Client{ //先在緩存讀,讀不到就new,存map key := userName //從map中獲取,該map是sync.map,併發安全 value, ok := Map.Load(key) if ok{ fmt.Println("讀syncmap") return value.(*Client) } client := newClient(userName) Map.Store(key,client) return client}
發現客戶端做爲單例對象,不是線程安全的,沒有併發控制機制,當初始遇到併發請求時候,就會建立大量的客戶端,請求結束沒法釋放,致使程序中大量無感的客戶端佔用內存。
最終解決方法:既然不是線程安全,加個鎖。
/*本地模擬的代碼*/func GetClient(userName string) *Client{ //加鎖保證線程安全 lock.Lock() defer lock.UnLock() key := userName
value, ok := Map.Load(key) if ok{ fmt.Println("讀syncmap") return value.(*Client) } client := newClient(userName) Map.Store(key,client) return client}
果斷修改調試.....
等待一天後......
goroutine數雖然有所減小仍是讓人抓狂,陷入自我懷疑
猜想問題是否是出在map裏面,開始一波針對性的檢查。
終於又發現問題:
首先,map在清理的時候沒有釋放掉裏面的鏈接(在第一種方案時候就定位到了)。
map清理的按期時間是可配的,讀取配置文件出錯沒有異常處理,使用了默認值(默認值很小)。
完成問題定位,最終修改完成,調試,內存雖然有增加,可是要優化了不少。
問題解決
此次問題的解決並非一路順風,這個泄露問題是好多點綜合做用的結果,期間還有不少繁瑣的點,要復現某個泄露的點真的很讓人頭大,pprof顯示的全部泄露的點調用棧都在引用的庫源碼裏,和網上的定位文章一點不同,算是在摸索着前進。最終主要的優化方式就是上述兩種方式。選擇了後者,最初設計是不能亂改的嘛,不過正由於此次問題出現,對Golang有了更深層的瞭解。
也獲得一點經驗教訓:
必定要在測試機上仔細檢查,業務驗證同時要關注服務的內存與CPU。
goroutine泄露的點主要發生在channel的阻塞上。
對於Golang錯誤處理留個心眼,萬一在沒有察覺的地方出錯了呢?
後臺回覆「加羣」,帶你進入高手如雲交流羣
推薦閱讀:
10大高性能開發利器
10T 技術資源大放送!包括但不限於:雲計算、虛擬化、微服務、大數據、網絡、Linux、Docker、Kubernetes、Python、Go、C/C++、Shell、PPT 等。在公衆號內回覆「1024」,便可免費獲取!!
本文分享自微信公衆號 - Linux雲計算網絡(cloud_dev)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。