写一个 panic blame 机器人
最近接手了一個(gè)“公共”服務(wù),負(fù)責(zé)維護(hù)它的穩(wěn)定性。代碼庫有很多人參與“維護(hù)”,其實(shí)就是各種業(yè)務(wù)方使勁往上堆邏輯。雖然入庫前我會(huì)進(jìn)行 CR,但多了之后,也看不過來,還有一些人自己偷摸就把代碼合到 master 上去了。總之,代碼質(zhì)量無法得到很好的保證。
當(dāng)然了,如果把合代碼的權(quán)限收斂到我一個(gè)人,理論上是可行的。但是,一方面,業(yè)務(wù)迭代的速度很可能就 block 在我這了;另一方面,業(yè)務(wù)方的迭代邏輯涉及很多具體的業(yè)務(wù),我也不太熟。所以,CR 的時(shí)候也只能看一些諸如 go 出去的 func 有沒有加 recover、有沒有異常使用空指針等等,對于業(yè)務(wù)相關(guān)的代碼提不出什么有用的意見。
其實(shí)有一些業(yè)務(wù)方的邏輯和其他業(yè)務(wù)方完全獨(dú)立(使用的接口和其他業(yè)務(wù)方獨(dú)立),后續(xù)會(huì)將當(dāng)前的服務(wù)完全“復(fù)制”一份出來,交給業(yè)務(wù)方自行維護(hù)。
但眼下有一個(gè)問題需要解決:報(bào)警群里時(shí)不時(shí)來一個(gè) recovered panic 的報(bào)警,我看到報(bào)警后就要登上機(jī)器看日志,執(zhí)行 “grep -C 10 panic xxx.log” 這樣的命令看 panic 發(fā)生在哪里。再執(zhí)行 git blame 看看究竟是誰寫的,再去群里 @ 他進(jìn)行處理。但很多情況下是這些 panic 是由臟數(shù)據(jù)導(dǎo)致的,發(fā)生的也不頻繁,并且 panic 被 recover 住了,所以也不太著急。
問題是業(yè)務(wù)方寫完了代碼之后,基本也不太關(guān)心服務(wù)運(yùn)行地怎么樣,但作為服務(wù)負(fù)責(zé)人得管。像前面提到的 panic 報(bào)警發(fā)生的多了,我“查日志,定位到代碼提交人再通知他處理”的事情多了之后,就想能不能寫一個(gè) panic blame 機(jī)器人來做這件事。這樣就能省不少事,而且還顯得那么優(yōu)雅。
想好了要做這件事,其實(shí)也并不困難。
最樸素的思路就是在 recover 函數(shù)里把 panic 發(fā)生時(shí)的一些信息,例如 pod-name、機(jī)器 ip、服務(wù)名、stack 等通過 HTTP 請求發(fā)送到某個(gè)服務(wù),這個(gè)服務(wù)收到 stack 后分析出 panic 的那行代碼,再請求 git 服務(wù)的某個(gè)接口,拿到提交人及提交時(shí)間。整體如下:
整體框架我們再看看具體代碼是怎么寫的。例如,Recover 函數(shù)是這樣的:
func?RecoverFromPanic(funcName?string)?{if?e?:=?recover();?e?!=?nil?{buf?:=?make([]byte,?64<<10)buf?=?buf[:runtime.Stack(buf,?false)]logs.Errorf("[%s]?func_name:?%v,?stack:?%s",?funcName,?e,?string(buf))panicError?:=?fmt.Errorf("%v",?e)panic_reporter_client.ReportPanic(panicError.Error(),?funcName,?string(buf))}return }向機(jī)器人服務(wù)端發(fā)送 panic 信息的 panic_reporter_client 代碼:
const?url?=?"http://localhost:8888/report-panic"//?為了避免造成?panic?report?服務(wù)被打掛,降低發(fā)送?http?請求頻率,進(jìn)程生命周期內(nèi)只發(fā)一次 var?panicReportOnce?sync.Oncetype?PanicReq?struct?{Service???string?`json:"service"`ErrorInfo?string?`json:"error_info"`Stack?????string?`json:"stack"`LogId?????string?`json:"log_id"`FuncName??string?`json:"func_name"`Host??????string?`json:"host"`PodName???string?`json:"pod_name"` }func?ReportPanic(errInfo,?funcName,?stack?string)?(err?error)?{panicReportOnce.Do(func()?{defer?func()?{recover()}()go?func()?{panicReq?:=?&PanicReq?{Service:???env.Service(),ErrorInfo:?errInfo,Stack:?????stack,FuncName:??funcName,Host:??????env.HostIP(),PodName:???env.PodName(),}var?jsonBytes?[]bytejsonBytes,?err?=?json.Marshal(panicReq)if?err?!=?nil?{return}var?req?*http.Requestreq,?err?=?http.NewRequest("GET",?url,?bytes.NewBuffer(jsonBytes))if?err?!=?nil?{return}req.Header.Set("Content-Type",?"application/json")client?:=?&http.Client{Timeout:?5?*?time.Second}var?resp?*http.Responseresp,?err?=?client.Do(req)if?err?!=?nil?{return}defer?resp.Body.Close()return}()})return }解析出 panic 消息的代碼也不難,我們需要看一下如何從 stack 信息中找到 panic 的那一行。
舉一個(gè)例子來說明:
package?mainimport?("fmt""runtime" )func?a()?{fmt.Println("a")b() }func?b()?{fmt.Println("b")c() }type?Student?struct?{Name?int }func?c()?{defer?RecoverFromPanic("fun?c")fmt.Println("c")var?a?*Studentfmt.Println(a.Name) }func?main()?{a() }func?RecoverFromPanic(funcName?string)?{if?e?:=?recover();?e?!=?nil?{buf?:=?make([]byte,?64<<10)buf?=?buf[:runtime.Stack(buf,?false)]fmt.Printf("[%s]?func_name:?%v,?stack:?%s",?funcName,?e,?string(buf))}return }這是一個(gè)有幾層調(diào)用關(guān)系的例子,假裝我們年幼無知直接解引用了一個(gè)空指針,導(dǎo)致 panic,但被 recover 了,輸出的調(diào)用棧信息如下:
goroutine?1?[running]: main.RecoverFromPanic(0x4c4551,?0x5)/home/raoquancheng/go/src/hello/test.go:36?+0xb5 panic(0x4a9340,?0x55b8d0)/usr/local/go/src/runtime/panic.go:679?+0x1b2 main.c()/home/raoquancheng/go/src/hello/test.go:26?+0xd4 main.b()/home/raoquancheng/go/src/hello/test.go:15?+0x7a main.a()/home/raoquancheng/go/src/hello/test.go:10?+0x7a main.main()/home/raoquancheng/go/src/hello/test.go:30?+0x20棧信息中,首先是 runtime.Stack 函數(shù)那一行;接著是 /usr/local/go/src/runtime/panic.go:679,也就是 runtime 里的 gopanic 函數(shù);下一行就是真正引起 panic 的使用空指針的那一行代碼,這是罪魁禍?zhǔn)?#xff0c;panic blame 機(jī)器人主要關(guān)注這個(gè);之后的信息就是調(diào)用鏈關(guān)系,會(huì)一直追溯到 main 函數(shù)里調(diào)用 a() 的源頭。
分析出來這些信息后,向 IM 提供的機(jī)器人 webhook 地址發(fā)送 panic 消息,并順帶 @ 剛才找到的代碼提交人,老哥,你又寫出 panic 了:
機(jī)器人通知這樣是不是就是萬事大吉了?
并不是,還有一些關(guān)鍵問題需要考慮。首先業(yè)務(wù)進(jìn)程不能阻塞在發(fā)送 panic 信息的過程中,且發(fā)送 panic 信息的代碼不能再發(fā)次發(fā)生 panic,以免給業(yè)務(wù)進(jìn)程帶來二次傷害。這樣就需要以異步的方式發(fā)送消息,并且最好是通過消息隊(duì)列或者 UDP 這種“我發(fā)完了就不管了”的姿態(tài)發(fā)送。
機(jī)器人服務(wù)端用生產(chǎn)者消費(fèi)者的形式來解析業(yè)務(wù)進(jìn)程發(fā)送上來的消息。無論業(yè)務(wù)進(jìn)程是以 HTTP,還是 UDP 或者消息隊(duì)列發(fā)過來的 panic 報(bào)告請求最終都要進(jìn)入一個(gè)“池子”,HTTP、UDP、消息隊(duì)列也就是所謂的生產(chǎn)者,消費(fèi)者協(xié)程則從“池子”里取出 panic 報(bào)告請求,解析、發(fā)送報(bào)警@人處理。
還有一個(gè)需要考慮的是機(jī)器人服務(wù)端不要被打跨了,尤其是考慮到一些業(yè)務(wù)跑在幾千個(gè)實(shí)例上的時(shí)候,更要注意了。
分別從客戶端和服務(wù)端兩方面來看。
對于客戶端,在一個(gè)進(jìn)程生命周期內(nèi),同時(shí)發(fā)生多“種” panic 的情況并不多見,因此我們只需要在進(jìn)程生命周期內(nèi)發(fā)送一次就行了,用 sync.Once。
在服務(wù)端,對同一個(gè)業(yè)務(wù)發(fā)送的請求進(jìn)行限流和聚合,例如每秒只處理同一個(gè)業(yè)務(wù)的一個(gè)請求,對被限流的請求做聚合,報(bào)告一個(gè)總的 panic 數(shù)量就行了。
另一個(gè)可能需要考慮的是如果 panic 代碼提交者離職了怎么辦?或者說我只是做了一下 format,真實(shí)的提交者并不是我,怎么辦?
我們并不能做到 100% 的準(zhǔn)確,現(xiàn)實(shí)有很多的邊角沒法解決。比如代碼提交者并沒有離職,但他轉(zhuǎn)崗了……有個(gè)可以考慮的方法是看 panic 那一行代碼附近的最近修改過代碼的人是誰,找他,或者直接找服務(wù)負(fù)責(zé)人好了。不求完美,只要能解決大部分問題就行了。
實(shí)現(xiàn)一個(gè) panic blame 機(jī)器人比較簡單,但考慮服務(wù)穩(wěn)定性的話,還是有一些點(diǎn)要注意的。
超強(qiáng)干貨來襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的写一个 panic blame 机器人的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 高并发服务遇 redis 瓶颈引发的事故
- 下一篇: [译]提案:在Go语言中增加对持久化内存