Go服务迁到K8s后老抽风重启? 记一次完整的线上问题解决过程
前言
之前把Go服務都遷到Kubernetes上后有些服務的某個 Pod總是時不時的重啟一下,通過查業務日志根本查不到原因,我分析了一下肯定是哪里代碼不嚴謹造成引用空指針導致Go發送運行時panic才會掛掉的,但是容器重啟后之前輸出到stderr的panic是會被清空的,所以才有了這篇文章里后面的分析和方案解決。
解決思路分析
在Go編寫的應用程序里無論是在主協程(main goroutine)還是其他子協程里,一旦出了運行時panic錯誤后,整個程序都會宕掉。一般的部署Go項目的時候都會使用supervisor監控應用程序進程,一旦應用程序發生panic停掉后supervisor會把進程再啟動起來。
那么在把項目部署到Kubernetes集群后,因為每個節點上的kubelet會對主進程崩潰的容器進行重啟,所以就再引入supervisor就有些功能重疊。但是Go的panic信息是直接寫到標準錯誤的,容器重啟后之前的panic錯誤就沒有了,沒法排查導致容器崩潰的原因。所以排查容器重啟的關鍵點就變成了:怎么把panic從stderr重定向到文件,這樣就能通過容器的volume持久化日志文件的目錄方式保留程序崩潰時的信息。
那么以前在supervisor里可以直接通過配置stderr_logfile把程序運行時的標準錯誤設置成一個文件:
[program:?go-xxx...] directory=/home/go/src... environment=... command=/home/go/src.../bin/app stderr_logfile=/home/xxx/log/..../app_err.log現在換成了Kubernetes,不再使用supervisor后就只能是想辦法在程序里實現了。針對在Go里實現記錄panic到日志文件你可能首先會考慮:在recover里把導致panic的錯誤記錄到文件里,不過引用的第三方包里也有可能panic,這個不現實。而且Go 也沒有其他語言那樣的Exception,未捕獲的異常能由全局的ExceptionHandler捕獲到的機制,實現不了用一個recover捕獲所有的panic的功能。
最后就只有一個辦法了,想辦法把程序運行時的標準錯誤替換成日志文件,這樣Go再panic的時候它還是往標準錯誤里寫,只不過我們偷偷把標準錯誤的文件描述符換成了日志文件的描述符(在系統眼里stderr也是個文件,Unix系統里一切皆文件)。
方案試錯
按照這個思路我先用下面的例子試了一下:
package?mainimport?("fmt""os" )const?stdErrFile?=?"/tmp/go-app1-stderr.log"func?RewriteStderrFile()?error?{file,?err?:=?os.OpenFile(stdErrFile,?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0666)if?err?!=?nil?{fmt.Println(err)return?err}os.Stderr?=?filereturn?nil }func?testPanic()?{panic("test?panic") }func?main()?{RewriteStderrFile()testPanic() }這個例子,我們嘗試使用 os.Stderr = file 來強制轉換,但運行程序后,發現不起作用,/tmp/go-app1-stderr.log沒有任何信息流入,panic信息照樣輸出到標準錯誤里。
最終方案
關于原因,搜索了一下,幸運的是 Rob Pike有專門對類似問題的解答,是這樣說的:
把高層包創建的變量直接賦值到底層的runtime是不行的,我們用syscall.Dup2實現替換描述符再試一次,并且增加一個全局變量對日志文件描述符的引用,避免常駐線程運行中文件描述符被GC回收掉:
var?stdErrFileHandler?*os.Filefunc?RewriteStderrFile()?error?{if?runtime.GOOS?==?"windows"?{return?nil}file,?err?:=?os.OpenFile(stdErrFile,?os.O_RDWR|os.O_CREATE|os.O_APPEND,?0666)if?err?!=?nil?{fmt.Println(err)return?err}stdErrFileHandler?=?file?//把文件句柄保存到全局變量,避免被GC回收if?err?=?syscall.Dup2(int(file.Fd()),?int(os.Stderr.Fd()));?err?!=?nil?{fmt.Println(err)return?err}//?內存回收前關閉文件描述符runtime.SetFinalizer(stdErrFileHandler,?func(fd?*os.File)?{fd.Close()})return?nil }因為Windows系統不支持的syscall.Dup2這個函數,所以我加了個判讀,Windows環境下的Go運行時加載系統的一個dll文件也能實現這里的功能,不過我們服務器環境都是Linux的,所以我認為這部分要兼容Windows是無用功,保證項目在Windows下能跑不受影響就行了。
再次運行程序后,打開日志文件/tmp/go-app1-stderr.log后就能看到剛才程序崩潰時的panic信息,以及導致panic時整個調用棧的信息:
???~?cat?/tmp/go-app1-stderr.log? panic:?test?panicgoroutine?1?[running]: main.testPanic(...)/Users/kev/Library/Application?Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:39 main.main()/Users/kev/Library/Application?Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:44?+0x3f panic:?test?panicgoroutine?1?[running]: main.testPanic(...)/Users/kev/Library/Application?Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:39 main.main()/Users/kev/Library/Application?Support/JetBrains/GoLand2020.1/scratches/scratch_4.go:44?+0x3f方案實施后的效果
目前這個方案已經在我們線上運行一個月了,已發現的Pod重啟事件都能把程序崩潰時的調用棧準確記錄到日志文件里,幫助我們定位了幾個代碼里的問題。其實問題都是空指針相關的問題,這些問題我在之前的文章《如何避免用動態語言的思維寫Go代碼》也提到過,項目一旦復雜起來誰寫的代碼也不能保證說不會發生空指針,不過我們事先做好檢查很多都是能夠避免的明顯錯誤,對于特別細微條件下引發的錯誤只能靠分析事故當時的日志來解決啦。
- END -
關注公眾號,獲取更多精選技術原創文章
總結
以上是生活随笔為你收集整理的Go服务迁到K8s后老抽风重启? 记一次完整的线上问题解决过程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: []int 能转换为 []interfa
- 下一篇: 面试官:DDD如何指导微服务拆分?90%