挖一挖曹大 holmes 的设计与实现
線上服務(wù)的性能分析,一直以來都是比較難的點,主要是難在無法在性能出現(xiàn)異常的當(dāng)時捕捉到現(xiàn)場信息。有人可能會說,這有什么難的,直接用 Go 工具集里的 pprof 訪問一下,進行采樣拿下來分析就行了。話雖不假,不過拋開現(xiàn)實場景談解決方案一般都會非常打臉,真的不行。
舉個現(xiàn)實中的例子,比如服務(wù)可能由于哪里的代碼寫的不好,內(nèi)存占用率一點點漲上來,表現(xiàn)出來的現(xiàn)象就是服務(wù)隔一段時間就會重啟一次(被集群重新拉起來),如果服務(wù)部署在 K8s上,那 K8s 集群更是會發(fā)現(xiàn) Pod 的資源超限后把 Pod 殺掉重新創(chuàng)建一個。即使你能 24 小時 Oncall,在收到告警的那一刻程序可能已經(jīng)重啟了,所以傳統(tǒng)的訪問 pprof 路由主動采樣在這種場景下不可取。
前段時間發(fā)表的文章學(xué)會這幾招讓 Go 程序自己監(jiān)控自己 有讀者問到,讓Go程序監(jiān)控自己進程的各項指標(biāo)有何用處,我的回答是可以做一些服務(wù)治理,或者程序內(nèi)部分析的事情。今天就借此跟大家分享一下,怎么給 Go 程序做自動采樣。
下面我?guī)е蠹蚁瓤匆豢?Go 程序怎么采樣,再剖析一下怎么讓 Go 程序進行自動采樣。
Go 的采樣工具
Go的pprof工具集,提供了Go程序內(nèi)部多種性能指標(biāo)的采樣能力,我們常會用到的性能采樣指標(biāo)有這些:
profile:CPU采樣
heap:堆中活躍對象的內(nèi)存分配情況的采樣
goroutine:當(dāng)前所有g(shù)oroutine的堆棧信息
allocs: 會采樣自程序啟動所有對象的內(nèi)存分配信息(包括已經(jīng)被GC回收的內(nèi)存)
threadcreate:采樣導(dǎo)致創(chuàng)建新系統(tǒng)線程的堆棧信息
怎么獲取采樣信息
網(wǎng)上最常見的例子是在服務(wù)端開啟端口讓客戶端通過HTTP訪問指定的路由進行各種信息的采樣
import?("net/http/pprof" )func?main()?{http.HandleFunc("/debug/pprof/heap",?pprof.Index)http.HandleFunc("/debug/pprof/profile",?pprof.Profile)http.HandleFunc("/",?index)......http.ListenAndServe(":80",?nil) }$?go?tool?pprof?http://localhost/debug/pprof/profile這種方法的弊端就是
需要客戶端主動請求特定路由進行采樣,沒法在資源出現(xiàn)尖刺的第一時間進行采樣。
會注冊多個/debug/pprof類的路由,相當(dāng)于對 Web 服務(wù)有部分侵入。
對于非 Web 服務(wù),還需在服務(wù)所在的節(jié)點上單獨開 HTTP 端口,起 Web 服務(wù)注冊 debug 路由才能進行采集,對原服務(wù)侵入性更大。
Runtime pprof
除了上面通過HTTP訪問指定路由進行采樣外,還有一種主要的方法是使用runtime.pprof 提供的Lookup方法完成各資源維度的信息采樣。
//?lookup?takes?a?profile?name pprof.Lookup("heap").WriteTo(some_file,?0) pprof.Lookup("goroutine").WriteTo(some_file,?0) pprof.Lookup("threadcreate").WriteTo(some_file,?0)CPU的采樣方式runtime/pprof提供了單獨的方法在開關(guān)時間段內(nèi)對 CPU 進行采樣
bf,?err?:=?os.OpenFile('tmp/profile.out',?os.O_RDWR?|?os.O_CREATE?|?os.O_APPEND,?0644) err?=?pprof.StartCPUProfile(bf) time.Sleep(2?*?time.Second) pprof.StopCPUProfile()這種方式是操作簡單,把采樣信息可以直接寫到文件里,不需要額外開端口,再手動通過HTTP進行采樣,但是弊端也很明顯--不停的采樣會影響性能。固定時間間隔的采樣(比如每隔五分鐘執(zhí)行一次采樣)不夠有針對性,有可能采樣的時候資源并不緊張。
適合采樣的時間點
經(jīng)過了上面的分析,現(xiàn)在看來只要讓Go進程在自己占用資源突增或者超過一定的閾值時再用pprof對程序Runtime進行采樣,才是最合適的,那么接下來我們就要想一下,到底以什么樣的規(guī)則,才能判斷出當(dāng)前周期是適合采樣的時段呢。
判斷采樣時間點的規(guī)則
CPU 使用,內(nèi)存占用和 goroutine 數(shù),都可以用數(shù)值表示,所以無論是使用率慢慢上升直到超過閾值,還是突增之后迅速回落,都可以用簡單的規(guī)則來表示,比如:
cpu/mem/goroutine數(shù) 突然比正常情況下的平均值高出了一定的比例,比如說資源占用率突增25%就是出現(xiàn)了資源尖刺。
cpu/mem/goroutine數(shù) 超過了程序正常運行情況下的閾值,比如說80%就定義為服務(wù)資源緊張。
這兩條規(guī)則可以描述大部分情況下的異常,第一個規(guī)則可以表示瞬時的,劇烈的抖動,之后可能迅速恢復(fù)正常的情況。
規(guī)則二可以用來表示那些緩慢上升,但最終超出閾值的情況,例如下圖中內(nèi)存使用率一直在慢慢上升,直到超過了設(shè)置的80%的閾值。
內(nèi)存使用率超過80%而規(guī)則一判斷資源突增,需要與歷史均值對比才行。在沒有歷史數(shù)據(jù)的情況下,就只能在程序內(nèi)自行收集了,比如進程的內(nèi)存使用率,我們可以以每 10 秒為一個周期,運行一次采集,在內(nèi)存中保留最近 5 ~ 10 個周期的內(nèi)存使用率,并持續(xù)與之前記錄的內(nèi)存使用率均值進行比較:
內(nèi)存使用率突增超過25%比如像上圖里的情況,前五個周期收集到的內(nèi)存占用率在 35% 左右波動,而最新周期收集到的數(shù)據(jù)為70%,這顯然是瞬時突增導(dǎo)致的異常情況,那么我們就可以在這個時間點,自動讓程序調(diào)用 pprof 把 mem 信息采樣到文件里,后續(xù)無論服務(wù)是否已重啟都能把采樣信息拿下來分析。
而關(guān)于怎么讓Go程序獲取本進程的CPU、Mem使用量在系統(tǒng)中的占比,我們在之前的文章 學(xué)會這幾招讓 Go 程序自己監(jiān)控自己 已經(jīng)跟大家分享過了,只需要編碼實現(xiàn)在上面描述的兩個采樣時機進行性能采樣即可。
開源的自動采樣庫
社區(qū)里其實已經(jīng)有開源庫實現(xiàn)了類似的功能,比如曹大在螞蟻的時候設(shè)計的Holmes ,其實曹大在桃花源公眾號里發(fā)文章分享過。
無人值守的自動 dump(一)
無人值守的自動 dump(二)
使用起來也比較方便,比如下面是一個對內(nèi)存使用率突增 25% 和超過閾值 80% 這兩種情況下讓程序自動進行Mem信息采樣的例子。如果你公司里的基建還沒有到把持續(xù)采樣做到統(tǒng)一平臺里的水平的話,Holmes 是一個比較好的選擇,能快速解決問題,比較適合中小型的業(yè)務(wù)快速發(fā)展的團隊。
簡單的看一個 Web 服務(wù)接入 Holmes 分析內(nèi)存占用率的例子。
package?mainimport?("net/http""time""github.com/mosn/holmes" )func?init()?{http.HandleFunc("/make1gb",?make1gbslice)go?http.ListenAndServe(":10003",?nil) }func?main()?{h,?_?:=?holmes.New(holmes.WithCollectInterval("2s"),holmes.WithCoolDown("1m"),holmes.WithDumpPath("/tmp"),holmes.WithTextDump(),holmes.WithMemDump(3,?25,?80),)h.EnableMemDump().Start()time.Sleep(time.Hour) }func?make1gbslice(wr?http.ResponseWriter,?req?*http.Request)?{var?a?=?make([]byte,?1073741824)_?=?a }WithCollectInterval("2s") ?指定 2s 為區(qū)間監(jiān)控進程的資源占用率, 線上建議設(shè)置大于10s的采樣區(qū)間。
WithMemDump(3, 25, 80) 指定進程的mem占用率超過3%后(線上建議設(shè)置成30),如果有25%突增,或者總占用率超過80%后進行采樣
通過采樣能獲取到了內(nèi)存資源突增時的程序調(diào)用棧,[1: 1073741824] 表示有一個對象消耗了1GB的內(nèi)存,通過調(diào)用棧分析我們也能快速找到找到造成資源占用的代碼位置。
heap?profile:?0:?0?[1:?1073741824]?@?heap/1048576 0:?0?[1:?1073741824]?@?0x42ba3ef?0x4252254?0x4254095?0x4254fd3?0x425128c?0x40650a1 #?0x42ba3ee?main.make1gbslice+0x3e???/Users/xargin/go/src/github.com/mosn/holmes/example/1gbslice.go:24 #?0x4252253?net/http.HandlerFunc.ServeHTTP+0x43?/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2012 #?0x4254094?net/http.(*ServeMux).ServeHTTP+0x1a4?/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2387 #?0x4254fd2?net/http.serverHandler.ServeHTTP+0xa2?/Users/xargin/sdk/go1.14.2/src/net/http/server.go:2807 #?0x425128b?net/http.(*conn).serve+0x86b??/Users/xargin/sdk/go1.14.2/src/net/http/server.go:1895關(guān)于這個庫更詳細的使用介紹可以直接看倉庫提供的Get Started 教程 https://github.com/mosn/holmes
另外我還做了個docker 方便進行試驗,鏡像已經(jīng)上傳到了Docker Hub上,大家感興趣的可以Down下來自己在電腦上快速試驗一下。
通過以下命令即可快速體驗。
docker run --name go-profile-demo -v /tmp:/tmp -p 10030:80 --rm -d kevinyan001/go-profiling
容器里Go服務(wù)提供的路由如下
Holmes 試驗容器的路由三個路由分別對應(yīng)了內(nèi)存、CPU過載、通道阻塞,可以直接壓測訪問,觀察效果。自動采樣的結(jié)果除了進到容器里去看外,還可以在本地和容器做映射的 /tmp 目錄中找到。
- END -
掃碼關(guān)注公眾號「網(wǎng)管叨bi叨」
給網(wǎng)管個星標(biāo),第一時間吸我的知識?👆
網(wǎng)管為大家整理了一本超實用的《Go 開發(fā)參考書》收集了70多條開發(fā)實踐。去公眾號回復(fù)【gocookbook】即刻領(lǐng)取!
覺得有用就點個在看? 👇👇👇
總結(jié)
以上是生活随笔為你收集整理的挖一挖曹大 holmes 的设计与实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 又一本 Go 语言力作出版了
- 下一篇: 曹大带我学 Go(12)—— 面向火焰图