本文目錄一覽:
- 1、深入理解golang
- 2、如何監控 golang 程序的垃圾回收
- 3、如何用go語言抓取網絡攝像頭數據
- 4、Golang實驗性功能SetMaxHeap 固定值GC
- 5、Golang什麼時候會觸發GC
- 6、golang channel 超時如何處理
深入理解golang
最近三年,在工作中使用go開發了不少服務。深感go的便捷,以及它的runtime的複雜。我覺得需要定期的進行總結,因此決定寫這篇文章,也許更準確的,應該叫筆記。
最近終於解決了一個和cgo有關的問題。這個問題從發現到解決前後經歷了接近4個月,當然,和人手不足也有關係。而對於我個人而言,這個問題其實歷時2年!這得從頭說起。
在上一家公司的一個項目里,有一個服務做音視頻數據的提取,這個服務運行在嵌入式設備TX2上。音視頻提取這一關鍵功能主要利用nvidia基於gstreamer開發的插件,這個插件可以發揮nvidia gpu的硬件解碼功能。當時這個服務使用go和c混編的方式,問題的癥狀是服務運行一段時間後,不輸出音視頻數據。遺憾的是,由於疫情,項目停止,因此沒有機會繼續研究這個問題。
時間來到去年底。當前這個項目進行壓力測試,發現關鍵的語音處理服務運行一段時間後,會出現不拉流的情況,因此也沒有後續的結果輸出。癥狀和上一個項目非常像。雖然使用的第三方SDK不一樣,但同樣用了go和c混編的方式。一開始,焦點就放在go的運行時上,覺得可能是go和c相互調用的方式不對。經過合理猜測,並用測試進行驗證後,發現問題還是在第三方拉流的SDK上,它們的回調函數必須要快,否則有可能會阻塞它們的回調線程。當然,在go調用c的時候,如果耗時比較長,會對go的運行時造成一些副作用;在c回調go的時候,go的運行時也有可能阻塞c的回調線程。但go的運行時已經比較成熟,因此我覺得它對這個問題的貢獻不大。以上採用了假設-驗證的方法,主要的原因還是第三方的拉流SDK不開源。在定位問題的過程中,使用了gdb的gcore來生成堆棧;也搭建了灰度環境來進行壓力測試,以及完善監控,這些都是解決方法的一部分。
正是這一問題,促使我更多的了解go的運行時。而我看得越多,越覺得go的運行時是一個龐大的怪物。因此,抱着能了解一點是一點的心態,不斷的完善這篇筆記。
如何監控 golang 程序的垃圾回收
如果是本地開發環境, 可以利用 GODEBUG=gctrace=1 /path/to/binary 的方式輸出 GC 信息,然後用 gcvis 作可視化。
如何用go語言抓取網絡攝像頭數據
理論上是不行的,要想實時就必須連續不斷傳輸的視頻信號,而你的軟件是播放視頻文件的,文件的話必須有頭尾,如果做成文件格式再播放,那就不叫實時監控了。
Golang實驗性功能SetMaxHeap 固定值GC
簡單來說, SetMaxHeap 提供了一種可以設置固定觸發閾值的 GC (Garbage Collection垃圾回收)方式
官方源碼鏈接
大量臨時對象分配導致的 GC 觸發頻率過高, GC 後實際存活的對象較少,
或者機器內存較充足,希望使用剩餘內存,降低 GC 頻率的場景
GC 會 STW ( Stop The World ),對於時延敏感場景,在一個周期內連續觸發兩輪 GC ,那麼 STW 和 GC 佔用的 CPU 資源都會造成很大的影響, SetMaxHeap 並不一定是完美的,在某些場景下做了些權衡,官方也在進行相關的實驗,當前方案仍沒有合入主版本。
先看下如果沒有 SetMaxHeap ,對於如上所述的場景的解決方案
這裡簡單說下 GC 的幾個值的含義,可通過 GODEBUG=gctrace=1 獲得如下數據
這裡只關注 128-132-67 MB 135 MB goal ,
分別為 GC開始時內存使用量 – GC標記完成時內存使用量 – GC標記完成時的存活內存量 本輪GC標記完成時的 預期 內存使用量(上一輪 GC 完成時確定)
引用 GC peace設計文檔 中的一張圖來說明
對應關係如下:
簡單說下 GC pacing (信用機制)
GC pacing 有兩個目標,
那麼當一輪 GC 完成時,如何只根據本輪 GC 存活量去實現這兩個小目標呢?
這裡實際是根據當前的一些數據或狀態去 預估 “未來”,所有會存在些誤差
首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 為本輪 GC 存活量, gcpercent 默認為 100 ,可以通過環境變量 GOGC=100 或者 debug.SetGCPercent(100) 來設置
那麼默認情況下 goal = 2 * heap_marked
gc_trigger 是與 goal 相關的一個值( gc_trigger 大約為 goal 的 90% 左右),每輪 GC 標記完成時,會根據 |Ha-Hg| 和實際使用的 cpu 資源 動態調整 gc_trigger 與 goal 的差值
goal 與 gc_trigger 的差值即為,為 GC 期間分配的對象所預留的空間
GC pacing 還會預估下一輪 GC 發生時,需要掃描對象對象的總量,進而換算為下一輪 GC 所需的工作量,進而計算出 mark assist 的值
本輪 GC 觸發( gc_trigger ),到本輪的 goal 期間,需要儘力完成 GC mark 標記操作,所以當 GC 期間,某個 goroutine 分配大量內存時,就會被拉去做 mark assist 工作,先進行 GC mark 標記賺取足夠的信用值後,才能分配對應大小的對象
根據本輪 GC 存活的內存量( heap_marked )和下一輪 GC 觸發的閾值( gc_trigger )計算 sweep assist 的值,本輪 GC 完成,到下一輪 GC 觸發( gc_trigger )時,需要儘力完成 sweep 清掃操作
預估下一輪 GC 所需的工作量的方式如下:
繼續分析文章開頭的問題,如何充分利用剩餘內存,降低 GC 頻率和 GC 對 CPU 的資源消耗
如上圖可以看出, GC 後,存活的對象為 2GB 左右,如果將 gcpercent 設置為 400 ,那麼就可以將下一輪 GC 觸發閾值提升到 10GB 左右
前面一輪看起來很好,提升了 GC 觸發的閾值到 10GB ,但是如果某一輪 GC 後的存活對象到達 2.5GB 的時候,那麼下一輪 GC 觸發的閾值,將會超過內存閾值,造成 OOM ( Out of Memory ),進而導致程序崩潰。
可以通過 GOGC=off 或者 debug.SetGCPercent(-1) 來關閉 GC
可以通過進程外監控內存使用狀態,使用信號觸發的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進行堆內存使用的監測
可以通過調用 runtime.GC() 或者 debug.FreeOSMemory() 來手動進行 GC 。
這裡還需要說幾個事情來解釋這個方案所存在的問題
通過 GOGC=off 或者 debug.SetGCPercent(-1) 是如何關閉 GC 的?
gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)
通過 GC trace 可以看出,上面所說的 goal 變成了一個很詭異的值 17592186044415
實際上關閉 GC 後, Go 會將 goal 設置為一個極大值 ^uint64(0) ,那麼對應的 GC 觸發閾值也被調成了一個極大值,這種處理方式看起來也沒什麼問題,將閾值調大,預期永遠不會再觸發 GC
那麼如果在關閉 GC 的情況下,手動調用 runtime.GC() 會導致什麼呢?
由於 goal 和 gc_trigger 被設置成了極大值, mark assist 和 sweep assist 也會按照這個錯誤的值去計算,導致工作量預估錯誤,這一點可以從 trace 中進行證明
可以看到很詭異的 trace 圖,這裡不做深究,該方案與 GC pacing 信用機制不兼容
記住,不要在關閉 GC 的情況下手動觸發 GC ,至少在當前 Go1.14 版本中仍存在這個問題
SetMaxHeap 的實現原理,簡單來說是強行控制了 goal 的值
註: SetMaxHeap ,本質上是一個軟限制,並不能解決 極端場景 下的 OOM ,可以配合內存監控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆內存大小, Go 中除了堆內存還分配了如下內存,所以實際使用過程中,與實際硬件內存閾值之間需要留有一部分餘量。
對於文章開始所述問題,使用 SetMaxHeap 後,預期的 GC 過程大概是這個樣子
簡單用法1
該方法簡單粗暴,直接將 goal 設置為了固定值
註:通過上文所講,觸發 GC 實際上是 gc_trigger ,所以當閾值設置為 12GB 時,會提前一點觸發 GC ,這裡為了描述方便,近似認為 gc_trigger=goal
簡單用法2
當不關閉 GC 時, SetMaxHeap 的邏輯是, goal 仍按照 gcpercent 進行計算,當 goal 小於 SetMaxHeap 閾值時不進行處理;當 goal 大於 SetMaxHeap 閾值時,將 goal 限制為 SetMaxHeap 閾值
註:通過上文所講,觸發 GC 實際上是 gc_trigger ,所以當閾值設置為 12GB 時,會提前一點觸發 GC ,這裡為了描述方便,近似認為 gc_trigger=goal
切換到 go1.14 分支,作者選擇了 git checkout go1.14.5
選擇官方提供的 cherry-pick 方式(可能需要梯子,文件改動不多,我後面會列出具體改動)
git fetch “” refs/changes/67/227767/3 git cherry-pick FETCH_HEAD
需要重新編譯Go源碼
注意點:
下面源碼中的官方注釋說的比較清楚,在一些關鍵位置加入了中文注釋
入參bytes為要設置的閾值
notify 簡單理解為 GC 的策略 發生變化時會向 channel 發送通知,後續源碼可以看出“策略”具體指哪些內容
返回值為本次設置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
註:作者盡量用通俗易懂的語言去解釋 Go 的一些機制和 SetMaxHeap 功能,可能有些描述與實現細節不完全一致,如有錯誤還請指出
Golang什麼時候會觸發GC
Golang採用了三色標記法來進行垃圾回收,那麼在什麼場景下會觸發這個回收動作呢?
源碼主要位於文件 src/runtime/mgc.go go version 1.16
觸發條件從大方面說,可分為 手動觸發 和 系統觸發 兩種方式。手動觸發一般很少用,主要由開發者通過調用 runtime.GC() 函數來實現,而對於系統自動觸發是 運行時 根據一些條件判斷來進行的,這也正是本文要介紹的內容。
不管哪種觸發方式,底層回收機制是一樣的,所以我們先看一下手動觸發,根據它來找系統觸發的條件。
可以看到開始執行GC的是 gcStart() 函數,它有一個 gcTrigger 參數,是一個觸發條件結構體,它的結構體也很簡單。
其實在Golang 內部所有的GC都是通過 gcStart() 函數,然後指定一個 gcTrigger 的參數來開始的,而手動觸髮指定的條件值為 gcTriggerCycle 。 gcStart 是一個很複雜的函數,有興趣的可以看一下源碼實現。
對於 kind 的值有三種,分別為 gcTriggerHeap 、 gcTriggerTime 和 gcTriggerCycle 。
運行時會通過 gcTrigger.test() 函數來決定是否需要觸發GC,只要滿足上面基中一個即可。
到此我們基本明白了這三種觸發GC的條件,那麼對於系統自動觸發這種,Golang 從一個程序的開始到運行,它又是如何一步一步監控到這個條件的呢?
其實 runtime 在程序啟動時,會在一個初始化函數 init() 里啟用一個 forcegchelper() 函數,這個函數位於 proc.go 文件。
為了減少系統資源佔用,在 forcegchelper 函數里會通過 goparkunlock() 函數主動讓自己陷入休眠,以後由 sysmon() 監控線程根據條件來恢復這個gc goroutine。
可以看到 sysmon() 會在一個 for 語句里一直判斷這個 gcTriggerTime 這個條件是否滿足,如果滿足的話,會將 forcegc.g 這個 goroutine 添加到全局隊列里進行調度(這裡 forcegc 是一個全局變量)。
調度器在調度循環 runtime.schedule 中還可以通過垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 獲取並執行用於後台標記的任務。
golang channel 超時如何處理
個人理解的channel超時處理思路分享,若有錯誤或者不足,請聯繫我:qq 869329877
主程序通過go timeout()掛起一個協程,在timeout方法裡面利用select來監控邏輯處理的變化,如果請求時間過長或者連接到其他服務比如grpc、mysql等服務中斷導致的請求時間過長,則直接超時,超時要返回定義的管道數據結果,否則程序會報錯。
原創文章,作者:YSUO,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/144385.html