无人值守的自动 dump(二)
之前在這篇無人值守(一)[1]簡單介紹了我們針對線上抖動問題定位的工具的設計思路,思路很簡單,技術含量很低,是個人都可以想得到,但是它確實幫我們查到了很多很難定位的問題。
在本篇里,我們重點講一講這個工具[2]在生產環境幫我們發現了哪些問題。
OOM 類問題
RPC decode 未做防御性編程,導致 OOM
應用側的編解碼可能是非官方實現(如 node 之類的無官方 SDK 的項目),在一些私有協議 decode 工程中會讀諸如 list len 之類的字段,如果外部編碼實現有問題,發生了字節錯位,就可能會讀出一個很大的值。
非標準 app ----encode-------> 我們的應用 decode -----> Boom!
decoder 實現都是需要考慮這種情況的,類似這樣[3]。如果對請求方的數據完全信任,碰到對方的 bug 或者惡意攻擊,可能導致自己的服務 OOM。
在線上實際發現了一例內存瞬間飚升的 case,收到報警后,我們可以看到:
1:?1330208768?[1:?1330208768]?@?0x11b1df3?0x11b1bcb?0x119664e?0x11b1695?0x1196f77?0x11a956a?0x11a86c7?0x1196724?0x11b1695?0x11b1c29?0x119664e?0x11b1695?0x11b1c29?0x119664e?0x11b1695?0x11b1c29?0x119664e?0x11bb360?0x168f143?0x179c2fc?0x1799b70?0x179acd6?0x16d3306?0x16d1088?0xf59386?0xf59474?0xf54e5f?0xf54987?0xf539f1?0xf6043a?0xcd8c0d?0x49b481 ....下面是表示棧內容的,這不重要1: 1330208768 [1: 1330208768] 表示 inuse_objects : inuse_space [alloc_objects : alloc_space],這里可以看到一個對象就直接用掉了 1GB 內存,顯然不是正常情況,查看代碼后,發現有未進行大小判斷而完全信任用戶輸入數據包的 decode 代碼。
修復起來很簡單,像前面提到的 async-h1 一樣加個判斷就可以了。
tls 開啟后線上進程占用內存上漲,直至 OOM
線上需要做全鏈路加密,所以有開啟 tls 的需求,但開啟之后發現內存一路暴漲,直至 OOM,工具可以打印如下堆棧:
heap?profile:?1460:?27614136?[45557:?1080481472]?@?heap/1048576 727:?23822336?[730:?23920640]?@?0xc56b96?0xc591e8?0xc58e68?0xc59ed1?0xdd55ff?0xde15b8?0xde13ef?0xde09e9?0xde050c?0x13bfa13?0x13bf475?0x14c33d0?0x14c49f8?0x14cb398?0x14bffab?0x14cdf78?0xddcf90?0x45eda1 #?0xc56b95??*****mtls/crypto/tls.(*block).reserve+0x75???*****mtls/crypto/tls/conn.go:475查閱老版本的 Go 代碼,發現其 TLS 的 write buffer 會隨著寫出的數據包大小增加而逐漸擴容,其擴容邏輯比較簡單:
func?(b?*block)?reserve(n?int)?{if?cap(b.data)?>=?n?{return}m?:=?cap(b.data)if?m?==?0?{m?=?1024}for?m?<?n?{m?*=?2}data?:=?make([]byte,?len(b.data),?m)copy(data,?b.data)b.data?=?data }初始為 1024,后續不夠用每次擴容為兩倍。但是閱讀 tls 的代碼后得知,這個寫出的數據包大小最大實際上只有 16KB + 額外的一個小 header 大小左右,但老版本的實現會導致比較多的空間浪費,因為最終會擴容到 32KB。
這段比較浪費空間的邏輯在 Go1.12 之后已經進行了優化:
func?sliceForAppend(in?[]byte,?n?int)?(head,?tail?[]byte)?{if?total?:=?len(in)?+?n;?cap(in)?>=?total?{head?=?in[:total]}?else?{head?=?make([]byte,?total)copy(head,?in)}tail?=?head[len(in):]return }變成了需要多少,分配多少的樸實邏輯。所以會比老版本在這個問題上有不少緩解,不過在我們的場景下,新版本的代碼依然沒法滿足需求,所以還需要進一步優化,這就是后話了。
goroutine 暴漲類問題
本地 app GC hang 死,導致 goroutine 卡 channel send
在我們的程序中有一段和本地進程通信的邏輯,write goroutine 會向一個 channel 中寫數據,按常理來講,同物理機的兩個進程通過網絡通信成本比較低,類似下面的代碼按說不太可能出問題:
concurrently: taskChan?<-?taskconsumer: for?task?:=?range?taskChan?{//?憋一些?task?一起寫localConnection.write(task?們) }看起來問題不大,但是線上經常都有和這個 channel send 相關的抖動,我們通過工具拿到的現場:
2020-11-03?08:00:05,950?[ERROR]?[diag.goroutine]?[diagnose]?pprof?goroutine,?config_min?:?3000,?config_diff?:?25,?config_abs?:?200000,??previous?:?[41402?44257?47247?50085?52795?55509?29762?32575?35451?38460],?current?:?55509,?profile?:?goroutine?profile:?total?55513 40844?@?0x46daaf?0x4433ab?0x443381?0x443165?0xf551f7?0x12fd2e7?0x12fc94f?0x13f41d5?0x13fc45f?0xf43ee4?0xcd8c0d?0x49b481 #????****channel.Send?這是個假的棧,你理解意思就行了 #?當前憋了 5w 個 goroutine,有 4w 個卡在 channel send 上,這個 channel 的對面還是一條本地連接,令人難以接受。
但是要考慮到,線上的業務系統是 Java,Java 發生 FG? 的時候可不是鬧著玩的。對往本地連接的 write buffer 寫數據一定不會卡的假設是有問題的。
既然出問題了,說明在這里對我們的程序進行保護是必要的,修改起來也很簡單,給 channel send 加一個超時就可以了。
應用邏輯死鎖,導致連接不可用,大量 goroutine 阻塞在 lock 上
大多數網絡程序里,我們需要在發送應用層心跳,以保證在一些異常情況(比如拔網線)下,能夠把那些無效連接從連接池里剔除掉。
對于我們的場景來說,客戶端向外創建的連接,如果一直沒有請求,那么每隔一段時間會向外發送一個應用心跳請求,如果心跳連續失敗(超時) N 次,那么將該連接進行關閉。
在這個場景下會涉及到兩把鎖:
- 對連接進行操作的鎖 conn lock 
- 記錄心跳請求的 request map lock 
心跳成功的流程:收到心跳響應包,獲取 conn lock -> 獲取 request map lock 心跳失敗的流程:timer 超時,獲取 request map lock -> 需要關閉連接 -> 獲取 conn lock
可以看出來,心跳的成功和失敗流程并發時,獲取鎖的流程符合死鎖的一般定義:持有鎖、非搶占、循環等待。
這個 bug 比較難觸發,因為心跳失敗要失敗 N 次才會關閉連接,而正好在最后一次發生了心跳成功和失敗并發才會觸發上述的死鎖,線上可以通過 goroutine 短時間的上漲發現這個問題,goroutine 的現場也是可以看得到的。簡單分析就可以發現這個死鎖問題(因為后續的流程都會卡在其中一把鎖上)。
知道原因解決起來就不麻煩了,涉及到一些具體的業務邏輯,這里就不贅述了。
CPU 尖刺問題
應用邏輯導致死循環問題
國際化業務涉及到冬夏令時的切換,從夏令時切換到冬令時,會將時鐘向前拔一個月,但天級日志輪轉時,會根據輪轉前的時間計算 24 小時后的時間,并按與 24:00 的差值來進行 time.Sleep,這時會發現整個應用的 CPU 飚高。自動采樣結果發現一直在循環計算時間和重命名文件。
list 一下相關的函數,能很快地發現執行死循環的代碼位置。這里就不截真實代碼了,隨便舉個例子:
????????.??????????.?????23:func?cpuex(wr?http.ResponseWriter,?req?*http.Request)?{.??????????.?????24:?go?func()?{17.73s?????19.37s?????25:??for?{.??????????.?????26:??}.??????????.?????27:?}().??????????.?????28:}參考資料
[1]
無人值守(上): https://xargin.com/autodumper-for-go/
[2]工具: https://github.com/mosn/holmes
[3]這樣: https://github.com/http-rs/async-h1/blob/main/src/server/decode.rs#L41
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術人生總結
以上是生活随笔為你收集整理的无人值守的自动 dump(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: Go 语言能取代 Java,成为下一个
- 下一篇: 无人值守的自动 dump(一)
