日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

 

桔妹導(dǎo)讀:線上性能問題的定位和優(yōu)化是程序員進(jìn)階的必經(jīng)之路,定位問題的方式有多種多樣,常見的有觀察線程棧、排查日志和做性能分析。性能分析(profile)作為定位性能問題的大殺器,它可以收集程序執(zhí)行過程中的具體事件,并且對程序進(jìn)行抽樣統(tǒng)計(jì),從而能更精準(zhǔn)的定位問題。本文會以 go 語言的 pprof 工具為例,分享兩個(gè)線上性能故障排查過程,希望能通過本文使大家對性能分析有更深入的理解。

在遇到線上的性能問題時(shí),面對幾百個(gè)接口、成噸的日志,如何定位具體是哪里的代碼導(dǎo)致的問題呢?這篇文章會分享一下 profiling 這個(gè)定位性能問題的利器,內(nèi)容主要有:

  • 如何通過做 profiling 來精準(zhǔn)定位故障源頭
  • 兩個(gè)工作中通過 profiling 解決性能問題的實(shí)際例子
  • 總結(jié)在做 profiling 時(shí)如何通過一些簡單的現(xiàn)象來快速定位問題的排查方向
  • 日常 golang 編碼時(shí)要避開的一些坑
  • 部分 golang 源碼解析

文章篇幅略長,也可直接翻到下面看經(jīng)驗(yàn)總結(jié)。

1.profiling是什么

profile 一般被稱為 性能分析,詞典上的翻譯是 概況(名詞)或者 描述…的概況(動詞)。對于計(jì)算機(jī)程序來說,它的 profile,就是一個(gè)程序在運(yùn)行時(shí)的各種概況信息,包括 cpu 占用情況,內(nèi)存情況,線程情況,線程阻塞情況等等。知道了程序的這些信息,也就能容易的定位程序中的問題和故障原因。

golang 對于 profiling 支持的比較好,標(biāo)準(zhǔn)庫就提供了profile庫 "runtime/pprof" 和 "net/http/pprof",而且也提供了很多好用的可視化工具來輔助開發(fā)者做 profiling。

2.兩次 profiling 線上實(shí)戰(zhàn)

紙上得來終覺淺,下面分享兩個(gè)在工作中實(shí)際遇到的線上問題,以及我是如何通過 profiling 一步一步定位到問題的。

▍cpu 占用 99%

某天早上一到公司就收到了線上 cpu 占用率過高的報(bào)警。立即去看監(jiān)控,發(fā)現(xiàn)這個(gè)故障主要有下面四個(gè)特征:

  • cpu idle 基本掉到了 0% ,內(nèi)存使用量有小幅度增長但不嚴(yán)重;
  • 故障是偶發(fā)的,不是持續(xù)存在的;
  • 故障發(fā)生時(shí)3臺機(jī)器的 cpu 幾乎是同時(shí)掉底;
  • 故障發(fā)生后,兩個(gè)小時(shí)左右能恢復(fù)正常。

現(xiàn)象如圖,上為內(nèi)存,下為cpu idle:

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

檢查完監(jiān)控之后,立即又去檢查了一下有沒有影響線上業(yè)務(wù)。看了一下線上接口返回值和延遲,基本上還都能保持正常使用,就算cpu占用 99% 時(shí)接口延時(shí)也只比平常多了幾十ms。由于不影響線上業(yè)務(wù),所以沒有選擇立即回滾,而是決定在線上定位問題(而且前一天后端也確實(shí)沒有上線新東西)。

所以給線上環(huán)境加上了 pprof,等著這個(gè)故障自己復(fù)現(xiàn)。代碼如下:

import _ "net/http/pprof"

func main() {
    go func() {
        log.Println(http.ListenAndServe("0.0.0.0:8005", nil))
    }()
    // ..... 下面業(yè)務(wù)代碼不用動
}

golang 對于 profiling 的支持比較完善,如代碼所示,只需要簡單的引入 "net/http/pprof" 這個(gè)包,然后在 main 函數(shù)里啟動一個(gè) http server 就相當(dāng)于給線上服務(wù)加上 profiling 了,通過訪問 8005 這個(gè) http 端口就可以對程序做采樣分析。

服務(wù)上開啟pprof之后,在本地電腦上使用 go tool pprof 命令,可以對線上程序發(fā)起采樣請求,golang pprof 工具會把采樣結(jié)果繪制成一個(gè)漂亮的前端頁面供人們排查問題。

等到故障再次復(fù)現(xiàn)時(shí),我們首先對 cpu 性能進(jìn)行采樣分析:

brew install graphviz # 安裝graphviz,只需要安裝一次就行了

go tool pprof -http=:1234 http://your-prd-addr:8005/debug/pprof/profile?seconds=30 

打開 terminal,輸入上面命令,把命令中的 your-prd-addr 改成線上某臺機(jī)器的地址,然后回車等待30秒后,會自動在瀏覽器中打開一個(gè)頁面,這個(gè)頁面包含了剛剛30秒內(nèi)對線上cpu占用情況的一個(gè)概要分析。點(diǎn)擊左上角的 View 選擇 Flame graph,會用火焰圖(Flame graph)來顯示cpu的占用情況:

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

分析此圖可以發(fā)現(xiàn),cpu 資源的半壁江山都被 GetLeadCallRecordByLeadId 這個(gè)函數(shù)占用了,這個(gè)函數(shù)里占用 cpu 最多的又大多是數(shù)據(jù)庫訪問相關(guān)的函數(shù)調(diào)用。由于 GetLeadCallRecordByLeadId 此函數(shù)業(yè)務(wù)邏輯較為復(fù)雜,數(shù)據(jù)庫訪問較多,不太好具體排查是哪里出的問題,所以我把這個(gè)方向的排查先暫時(shí)擱置,把注意力放到了右邊那另外半壁江山。

在火焰圖的右邊,有個(gè)讓我比較在意的點(diǎn)是 runtime.gcBgMarkWorker 函數(shù),這個(gè)函數(shù)是 golang 垃圾回收相關(guān)的函數(shù),用于標(biāo)記(mark)出所有是垃圾的對象。一般情況下此函數(shù)不會占用這么多的 cpu,出現(xiàn)這種情況一般都是內(nèi)存 gc 問題,但是剛剛的監(jiān)控上看內(nèi)存占用只比平常多了幾百M(fèi),并沒有特別高又是為什么呢?原因是影響GC性能的一般都不是內(nèi)存的占用量,而是對象的數(shù)量。舉例說明,10個(gè)100m的對象和一億個(gè)10字節(jié)的對象占用內(nèi)存幾乎一樣大,但是回收起來一億個(gè)小對象肯定會被10個(gè)大對象要慢很多。

插一段 golang 垃圾回收的知識,golang 使用“三色標(biāo)記法”作為垃圾回收算法,是“標(biāo)記-清除法”的一個(gè)改進(jìn),相比“標(biāo)記-清除法”優(yōu)點(diǎn)在于它的標(biāo)記(mark)的過程是并發(fā)的,不會Stop The World。但缺點(diǎn)是對于巨量的小對象處理起來比較不擅長,有可能出現(xiàn)垃圾的產(chǎn)生速度比收集的速度還快的情況。gcMark線程占用高很大幾率就是對象產(chǎn)生速度大于垃圾回收速度了。

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

三色標(biāo)記法

所以轉(zhuǎn)換方向,又對內(nèi)存做了一下 profiling:

[net/http.HandlerFunc.ServeHTTP/server.go:1947] _com_request_in||traceid=091d682895eda2fsdffsd0cbe3f9a95||spanid=297b2a9sdfsdfsdfb8bf739||hintCode=||hintContent=||method=GET||host=10.88.128.40:8000||uri=/lp-api/v2/leadCallRecord/getLeadCallRecord||params=leadId={"id":123123}||from=10.0.0.0||proto=HTTP/1.0

然后在瀏覽器里點(diǎn)擊左上角 VIEW-》flame graph,然后點(diǎn)擊 SAMPLE-》inuse_objects。

這樣顯示的是當(dāng)前的對象數(shù)量:

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

可以看到,還是 GetLeadCallRecordByLeadId 這個(gè)函數(shù)的問題,它自己就產(chǎn)生了1億個(gè)對象,遠(yuǎn)超其他函數(shù)。所以下一步排查問題的方向確定了是:定位為何此函數(shù)產(chǎn)生了如此多的對象。

之后我開始在日志中 grep '/getLeadCallRecord' lead-platform. 來一點(diǎn)一點(diǎn)翻,重點(diǎn)看 cpu 掉底那個(gè)時(shí)刻附近的日志有沒有什么異常。果然發(fā)現(xiàn)了一條比較異常的日志:

[net/http.HandlerFunc.ServeHTTP/server.go:1947] _com_request_in||traceid=091d682895eda2fsdffsd0cbe3f9a95||spanid=297b2a9sdfsdfsdfb8bf739||hintCode=||hintContent=||method=GET||host=10.88.128.40:8000||uri=/lp-api/v2/leadCallRecord/getLeadCallRecord||params=leadId={"id":123123}||from=10.0.0.0||proto=HTTP/1.0

注意看 params 那里,leadId 本應(yīng)該是一個(gè) int,但是前端給傳來一個(gè)JSON,推測應(yīng)該是前一天上線帶上去的bug。但是還有問題解釋不清楚,類型傳錯(cuò)應(yīng)該報(bào)錯(cuò),但是為何會產(chǎn)生這么多對象呢?于是我進(jìn)代碼(已簡化)里看了看:

func GetLeadCallRecord(leadId string, bizType int) ([]model.LeadCallRecords, error) {
 sql := "SELECT record.* FROM lead_call_record AS record " +
"where record.lead_id  = {{leadId}} and record.biz_type = {{bizType}}"
 conditions := make(map[string]interface{}, 2)
 conditions["leadId"] = leadId
 conditions["bizType"] = bizType
 cond, val, err := builder.NamedQuery(sql, conditions)

發(fā)現(xiàn)很尷尬的是,這段遠(yuǎn)古代碼里對于 leadId 根本沒有判斷類型,直接用 string 了,前端傳什么過來都直接當(dāng)作 sql 參數(shù)了。也不知道為什么 MySQL 很抽風(fēng)的是,雖然 lead_id 字段類型是bigint,在 sql 里條件用 string 類型傳參數(shù) WHERE leadId = 'someString' 也能查到數(shù)據(jù),而且返回的數(shù)據(jù)量很大。本身 lead_call_record 就是千萬級別的大表,這個(gè)查詢一下子返回了幾十萬條數(shù)據(jù)。又因?yàn)榇私涌诤罄m(xù)的查詢都是根據(jù)這個(gè)這個(gè)查詢返回的數(shù)據(jù)進(jìn)行查詢的,所以整個(gè)請求一下子就產(chǎn)生了上億個(gè)對象。

由于之前傳參都是正確的,所以一直沒有觸發(fā)這個(gè)問題,正好前一天前端小姐姐上線需求帶上了這個(gè)bug,一波前后端混合雙打造成了這次故障。

到此為止就終于定位到了問題所在,而且最一開始的四個(gè)現(xiàn)象也能解釋的通了:

  • cpu idle 基本掉到了 0% ,內(nèi)存使用量有小幅度增長但不嚴(yán)重;
  • 故障是偶發(fā)的,不是持續(xù)存在的;
  • 故障發(fā)生時(shí)3臺機(jī)器的 cpu 幾乎是同時(shí)掉底;
  • 故障發(fā)生后,兩個(gè)小時(shí)左右能恢復(fù)正常。

逐條解釋一下:

  • GetLeadCallRecordByLeadId 函數(shù)每次在執(zhí)行時(shí)從數(shù)據(jù)庫取回的數(shù)據(jù)量過大,大量cpu時(shí)間浪費(fèi)在反序列化構(gòu)造對象 和 gc 回收對象上。
  • 和前端確認(rèn) /lp-api/v2/leadCallRecord/getLeadCallRecord 接口并不是所有請求都會傳入json,只在某個(gè)頁面里才會有這種情況,所以故障是偶發(fā)的。
  • 因?yàn)榻涌诓]有直接掛掉報(bào)錯(cuò),而是執(zhí)行的很慢,所以應(yīng)用前面的負(fù)載均衡會超時(shí),負(fù)載均衡器會把請求打到另一臺機(jī)器上,結(jié)果每次都會導(dǎo)致三臺機(jī)器同時(shí)爆表。
  • 雖然申請了上億個(gè)對象,但 golang 的垃圾回收器是真滴靠譜,兢兢業(yè)業(yè)的回收了兩個(gè)多小時(shí)之后,就把幾億個(gè)對象全回收回去了,而且奇跡般的沒有影響線上業(yè)務(wù)。幾億個(gè)對象都扛得住,只能說厲害了我的go。

最后捋一下整個(gè)過程:

cpu占用99% -> 發(fā)現(xiàn)GC線程占用率持續(xù)異常 -> 懷疑是內(nèi)存問題 -> 排查對象數(shù)量 -> 定位產(chǎn)生對象異常多的接口 -> 定位到某接口 -> 在日志中找到此接口的異常請求 -> 根據(jù)異常參數(shù)排查代碼中的問題 -> 定位到問題

可以發(fā)現(xiàn),有 pprof 工具在手的話,整個(gè)排查問題的過程都不會懵逼,基本上一直都照著正確的方向一步一步定位到問題根源。這就是用 profiling 的優(yōu)點(diǎn)所在。

▍內(nèi)存占用 90%

第二個(gè)例子是某天周會上有同學(xué)反饋說項(xiàng)目內(nèi)存占用達(dá)到了15個(gè)G之多,看了一下監(jiān)控現(xiàn)象如下:

  • cpu占用并不高,最低idle也有85%
  • 內(nèi)存占用呈鋸齒形持續(xù)上升,且速度很快,半個(gè)月就從2G達(dá)到了15G

如果所示:

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

鋸齒是因?yàn)闀円垢叻迤椒鍖?dǎo)致的暫時(shí)不用管,但持續(xù)上漲很明顯的是內(nèi)存泄漏的問題,有對象在持續(xù)產(chǎn)生,并且被持續(xù)引用著導(dǎo)致釋放不掉。于是上了 pprof 然后準(zhǔn)備等一晚上再排查,讓它先泄露一下再看現(xiàn)象會比較明顯。

這次重點(diǎn)看內(nèi)存的 inuse_space 圖,和 inuse_objects 圖不同的是,這個(gè)圖表示的是具體的內(nèi)存占用而不是對象數(shù),然后VIEW類型也選graph,比火焰圖更清晰。

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

這個(gè)圖可以明顯的看出來程序中92%的對象都是由于 event.GetInstance 產(chǎn)生的。然后令人在意的點(diǎn)是這個(gè)函數(shù)產(chǎn)生的對象都是一個(gè)只有 16 個(gè)字節(jié)的對象(看圖上那個(gè)16B)這個(gè)是什么原因?qū)е碌暮竺鏁忉尅?/p>

先來看這個(gè)函數(shù)的代碼吧:

var (
    firstActivationEventHandler FirstActivationEventHandler
    firstOnlineEventHandler FirstOnlineEventHandler
)
func GetInstance(eventType string) Handler {
    if eventType == FirstActivation {
        firstActivationEventHandler.ChildHandler = firstActivationEventHandler
return firstActivationEventHandler
    } else if eventType == FirstOnline {
        firstOnlineEventHandler.ChildHandler = firstOnlineEventHandler
return firstOnlineEventHandler
}
// ... 各種類似的判斷,略過
    return nil
}

這個(gè)是做一個(gè)類似單例模式的功能,根據(jù)事件類型返回不同的 Handler。但是這個(gè)函數(shù)有問題的點(diǎn)有兩個(gè):

  • firstActivationEventHandler.ChildHandler 是一個(gè) interface,在給一個(gè) interface 賦值的時(shí)候,如果等號右邊是一個(gè) struct,會進(jìn)行值傳遞,也就意味著每次賦值都會在堆上復(fù)制一個(gè)此 struct 的副本。(golang默認(rèn)都是值傳遞)
  • firstActivationEventHandler.ChildHandler = firstActivationEventHandler 是一個(gè)自己引用自己循環(huán)引用。

兩個(gè)問題導(dǎo)致了每次 GetInstance 函數(shù)在被調(diào)用的時(shí)候,都會復(fù)制一份之前的 firstActivationEventHandler 在堆上,并且讓 firstActivationEventHandler.ChildHandler 引用指向到這個(gè)副本上。

這就導(dǎo)致人為在內(nèi)存里創(chuàng)造了一個(gè)巨型的鏈表:

滴滴實(shí)戰(zhàn)分享:通過 profiling 定位 golang 性能問題 - 內(nèi)存篇

 

并且這個(gè)鏈表中所有節(jié)點(diǎn)都被之后的副本引用著,永遠(yuǎn)無法被GC當(dāng)作垃圾釋放掉。

所以解決這個(gè)問題方案也很簡單,單例模式只需要在 init 函數(shù)里初始化一次就夠了,沒必要在每次 GetInstance 的時(shí)候做初始化操作:

func init() {
    firstActivationEventHandler.ChildHandler = &firstActivationEventHandler
    firstOnlineEventHandler.ChildHandler = &firstOnlineEventHandler
// ... 略過
}

另外,可以深究一下為什么都是一個(gè) 16B 的對象呢?為什么 interface 會復(fù)制呢?這里貼一下 golang runtime 關(guān)于 interface 部分的源碼:

下面分析 golang 源碼,不感興趣可直接略過。

// interface 底層定義
type iface struct {
    tab  *itab
    data unsafe.Pointer
}
// 空 interface 底層定義
type eface struct {
    _type *_type
    data  unsafe.Pointer
}
// 將某變量轉(zhuǎn)換為interface
func convT2I(tab *itab, elem unsafe.Pointer) (i iface) {
    t := tab._type
    if raceenabled {
        raceReadObjectPC(t, elem, getcallerpc(), funcPC(convT2I))
    }
    if msanenabled {
        msanread(elem, t.size)
    }
    x := mallocgc(t.size, t, true)
    typedmemmove(t, x, elem)
    i.tab = tab
    i.data = x
    return
}

iface 這個(gè) struct 是 interface 在內(nèi)存中實(shí)際的布局。可以看到,在golang中定義一個(gè)interface,實(shí)際上在內(nèi)存中是一個(gè) tab 指針和一個(gè) data 指針,目前的機(jī)器都是64位的,一個(gè)指針占用8個(gè)字節(jié),兩個(gè)就是16B。

我們的 firstActivationEventHandler 里面只有一個(gè) ChildHandler interface,所以整個(gè) firstActivationEventHandler 占用 16 個(gè)字節(jié)也就不奇怪了。

另外看代碼第 20 行那里,可以看到每次把變量轉(zhuǎn)為 interface 時(shí)是會做一次 mallocgc(t.size, t, true) 操作的,這個(gè)操作就會在堆上分配一個(gè)副本,第 21 行 typedmemmove(t, x, elem) 會進(jìn)行復(fù)制,會復(fù)制變量到堆上的副本上。這就解釋了開頭的問題。

3.經(jīng)驗(yàn)總結(jié)

在做內(nèi)存問題相關(guān)的 profiling 時(shí):

  • 若 gc 相關(guān)函數(shù)占用異常,可重點(diǎn)排查對象數(shù)量
  • 解決速度問題(CPU占用)時(shí),關(guān)注對象數(shù)量( --inuse/alloc_objects )指標(biāo)
  • 解決內(nèi)存占用問題時(shí),關(guān)注分配空間( --inuse/alloc_space )指標(biāo)
  • inuse 代表當(dāng)前時(shí)刻的內(nèi)存情況,alloc 代表從從程序啟動到當(dāng)前時(shí)刻累計(jì)的內(nèi)存情況,一般情況下看 inuse 指標(biāo)更重要一些,但某些時(shí)候兩張圖對比著看也能有些意外發(fā)現(xiàn)。

在日常 golang 編碼時(shí):

  • 參數(shù)類型要檢查,尤其是 sql 參數(shù)要檢查(低級錯(cuò)誤)
  • 傳遞struct盡量使用指針,減少復(fù)制和內(nèi)存占用消耗(尤其對于賦值給interface,會分配到堆上,額外增加gc消耗)
  • 盡量不使用循環(huán)引用,除非邏輯真的需要
  • 能在初始化中做的事就不要放到每次調(diào)用的時(shí)候做

分享到:
標(biāo)簽:實(shí)戰(zhàn) 滴滴
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定