線上 Go 程序偶爾出現(xiàn)異常怎么辦?這個(gè)思路可解決你的煩惱
Go 項(xiàng)目做的比較大(主要說代碼多,參與人多)之后,可能會(huì)遇到類似下面這樣的問題:
- 程序老是半夜崩,崩了以后就重啟了,我也醒不來,現(xiàn)場(chǎng)早就丟了,不知道怎么定位
- 這壓測(cè)開壓之后,隨機(jī)出問題,可能兩小時(shí),也可能五小時(shí)以后才出問題,這我蹲點(diǎn)蹲出痔瘡都不一定能等到崩潰的那個(gè)時(shí)間點(diǎn)啊
- 有些級(jí)聯(lián)失敗,最后留下現(xiàn)場(chǎng)并不能幫助我們準(zhǔn)確地判斷問題的根因,我們需要出問題時(shí)第一時(shí)間的現(xiàn)場(chǎng)

Go 內(nèi)置的 pprof 雖然是問題定位的神器,但是沒有辦法讓你恰好在出問題的那個(gè)時(shí)間點(diǎn),把相應(yīng)的現(xiàn)場(chǎng)保存下來進(jìn)行分析。特別是一些隨機(jī)出現(xiàn)的內(nèi)存泄露、CPU 抖動(dòng),等你發(fā)現(xiàn)有泄露的時(shí)候,可能程序已經(jīng) OOM 被 kill 掉了。而 CPU 抖動(dòng),你可以蹲了一星期都不一定蹲得到。
這個(gè)問題最好的解決辦法是 continuous profiling,不過這個(gè)理念需要公司的監(jiān)控系統(tǒng)配合,在沒有達(dá)到最終目標(biāo)前,我們可以先向前邁一小步,看看怎么用比較低的成本來解決這個(gè)問題。
從現(xiàn)象上,可以將出問題的癥狀簡單分個(gè)類:
- cpu 抖動(dòng):有可能是模塊內(nèi)有一些比較冷門的邏輯,觸發(fā)概率比較低,比如半夜的定時(shí)腳本,觸發(fā)了以后你還在睡覺,這時(shí)候要定位就比較鬧心了。
- 內(nèi)存使用抖動(dòng):有很多種情況會(huì)導(dǎo)致內(nèi)存使用抖動(dòng),比如突然涌入了大量請(qǐng)求,導(dǎo)致本身創(chuàng)建了過多的對(duì)象。也可能是 goroutine 泄露。也可能是突然有鎖沖突,也可能是突然有 IO 抖動(dòng)。原因太多了,猜是沒法猜出根因的。
- goroutine 數(shù)暴漲,可能是死鎖,可能是數(shù)據(jù)生產(chǎn)完了 channel 沒關(guān)閉,也可能是 IO 抖了什么的。
CPU 使用,內(nèi)存占用和 goroutine 數(shù),都可以用數(shù)值表示,所以不管是“暴漲”還是抖動(dòng),都可以用簡單的規(guī)則來表示:
- xx 突然比正常情況下的平均值高出了 25%
- xx 超過了模塊正常情況下的最高水位線
這兩條規(guī)則可以描述大部分情況下的異常,規(guī)則一可以表示瞬時(shí)的,劇烈的抖動(dòng),之后可能迅速恢復(fù)了;規(guī)則二可以用來表示那些緩慢上升,但最終超出系統(tǒng)負(fù)荷的情況,例如 1s 泄露一兆內(nèi)存,直至幾小時(shí)后 OOM。
而與均值的 diff,在沒有歷史數(shù)據(jù)的情況下,就只能在程序內(nèi)自行收集了,比如 goroutine 的數(shù)據(jù),我們可以每 x 秒運(yùn)行一次采集,在內(nèi)存中保留最近 N 個(gè)周期的 goroutine 計(jì)數(shù),并持續(xù)與之前記錄的 goroutine 數(shù)據(jù)均值進(jìn)行 diff:

time-Page-2
比如像圖里的情況,前十個(gè)周期收集到的 goroutine 數(shù)在 1300 左右波動(dòng),而最新周期收集到的數(shù)據(jù)為 2w+,這顯然是瞬時(shí)觸發(fā)導(dǎo)致的異常情況,那么我們就可以在這個(gè)點(diǎn)自動(dòng)地去做一些事情,比如:
- 把當(dāng)前的 goroutine 棧 dump 下來
- 把當(dāng)前的 cpu profile dump 下來
- 把當(dāng)前的 off-cpu profile dump 下來
- 不怕死的話,也可以 dump 幾秒的 trace
文件保存下來,模塊掛掉也就無所謂了。之后在喝茶的時(shí)候,發(fā)現(xiàn)線上曾經(jīng)出現(xiàn)過崩潰,那再去線上機(jī)器把文件撈下來,一邊喝茶一邊分析,還是比較悠哉的。
這里面稍微麻煩一些的是 CPU 和內(nèi)存使用量的采集,現(xiàn)在的應(yīng)用可能跑在物理機(jī)上,也可能跑在 docker 中,因此在獲取用量時(shí),需要我們自己去做一些定制。物理機(jī)中的數(shù)據(jù)采集,可以使用 gopsutil[1],docker 中的數(shù)據(jù)采集,可以參考少量 cgroups[2] 中的實(shí)現(xiàn)。