你好呀,我是歪歪。
最近遇到一個生產問題,我負責的一個服務觸發了記憶體使用率預警,收到預警的時候我去看了記憶體使用率已經到了 80%,看了一眼 GC 又發現還沒有觸發 FullGC,一次都沒有。
基於這個現象,當時推測有兩種可能,一種是記憶體溢位,一種是記憶體洩漏。
好,假設現在是面試,面試官目前就給了這點資訊,他問你到底是溢位還是洩漏,你怎麼回答?
在回答之前,我們得現明確啥是溢位,啥情況又是洩漏。
記憶體溢位(OutOfMemoryError):記憶體溢位指的是程式請求的記憶體超出了 JVM 當前允許的最大記憶體容量。當 JVM 試圖為一個物件分配記憶體時,如果當前可用的堆記憶體不足以滿足需求,就會丟擲 java.lang.OutOfMemoryError 異常。這通常是因為堆空間太小或者由於某些原因導致堆空間被佔滿。 記憶體洩漏 (Memory Leak):記憶體洩漏是指不再使用的記憶體空間沒有被釋放,導致這部分記憶體無法再次被使用。雖然記憶體洩漏不會立即導致程式崩潰,但它會逐漸消耗可用記憶體,最終可能導致記憶體溢位。
雖然都與記憶體相關,但它們發生的時機和影響有所不同。記憶體溢位通常發生在程式執行時,當資料結構的大小超過預設限制時,常見的情況是你要分配一個大物件,比如一次從資料中查到了過多的資料。
而記憶體洩漏和“過多”關係不大,是一個細水長流的過程,一次記憶體洩漏的影響可能微乎其微,但隨著時間推移,多次記憶體洩漏累積起來,最終可能導致記憶體溢位。
概念就是這個概念,這兩個玩意經常被大家搞混,所以多嘴提一下。
概念明確了,回到最開始這個問題,你怎麼回答?
你回答不了。
因為這些資訊太不完整了,所以你回答不了。
面試的時候面試官就喜歡出這種全是錯誤選項的題目來迷惑你,摸摸你的底子到底怎麼樣。
首先,為什麼不能判斷,是因為前面說了:一次 FullGC 都沒有。
雖然現在記憶體使用率已經到 80% 了,萬一一次 FullGC 之後,記憶體使用率又下去了呢,說明程式沒有任何問題。
如果沒有下去,說明大機率是記憶體溢位了,需要去程式碼裡面找哪裡分配了大物件了。
那如果下去了,能說明一定沒有記憶體洩漏嗎?
也不能,因為前面又說了:記憶體洩漏是一個細水長流的過程。
關於記憶體溢位,如果監控手段齊全到位的話,你就記住左邊這個走勢圖:
一個緩慢的持續上升的記憶體趨勢圖, 最後瘋狂觸發 GC,但是並沒有記憶體被回收,最後程式直接崩掉。
記憶體洩漏,一眼定真假。
這個圖來自我去年寫的這篇文章:《雖然是我遇到的一個棘手的生產問題,但是我寫出來之後,就是你的了。》
裡面就是描述了一個記憶體洩漏的問題,透過分析 Dump 檔案的方式,最終成功定位到洩漏點,修復程式碼。
一個不論多麼複雜的記憶體洩漏問題,處理起來都是有方法論的。
不過就是 Dump 檔案分析、工具的使用以及足夠的耐心和些許的運氣罷了。
所以我不打算贅述這些東西了,我想要分享的是我這次是怎麼對應文章開始說的記憶體預警的。
我的處理方式就是:重啟服務。
是的,常規來說都是會保留現場,然後重啟服務。但是我的處理方式是:直接執行重啟服務的預案。沒有後續動作了。
我當時腦子裡面的考慮大概是這樣的。
首先,這個服務是一個邊緣服務,它所承載的資料量不多,其業務已經超過一年多沒有新增,存量資料正在慢慢的消亡。程式碼近一兩年沒啥改動,只有一些升級 jar 包,日誌埋點這類的橫向改造。
其次,我看了一下這個服務已經有超過四個月沒有重啟過了,這期間沒有任何突發流量,每天處理的資料呈遞減趨勢,記憶體走勢確實是一個緩慢上升的過程,我初步懷疑是有記憶體洩漏。
然後,這個服務是我從別的團隊那邊接手的一個服務,基於前一點,業務正在消亡這個因素,我也只是知道大概的功能,並不知道內部的細節,所以由於對系統的熟悉度不夠,如果要定位問題,會較為困難。
最後,基於公司制度,雖然我知道應該怎麼去排查問題,命令和工具我都會使用,但是我作為開發人員是沒有許可權使用運維人員的各類排查工具和排查命令的,所以如果要定位問題,我必須請求協調一個運維同事幫忙。
於是,在心裡默默的盤算了一下投入產出比,我決定直接重啟服務,不去定位問題。
按照目前的頻率,程式正常執行四五個月後可能會觸發記憶體預警,那麼大不了就每隔三個月重啟一次服務嘛,重啟一次只需要 30s。一年按照重啟 4 次算,也就是才 2 分鐘。
這個業務我們就算它要五年後才徹底消亡,那麼也就才 10 分鐘而已。
如果我要去定位到底是不是記憶體洩露,到底在哪兒洩露的,結合我對於系統的熟悉程度和公司必須有的流程,這一波時間消耗,少說點,加起來得三五個工作日吧。
10 分鐘和三五個工作日,這投入產出比,該選哪個,一目瞭然了吧?
我分享這個事情的目的,其實就是想說明我在這個事情上領悟到的一個點:在工作中,你遇到的問題,不是每一個都必須被解決的,也可以選擇繞過問題,只要最終結果是好的就行。
如果我們拋開其他因素,只是從程式設計師的本職工作來看,那麼遇到諸如記憶體洩漏的問題的時候,就是應該去定位問題、解決問題。
但是在職場中,其實還需要結合實際情況,進行分析。
什麼是實際情況呢?
我前面列出來的那個“首先,其次,然後,最後”,就是我這個問題在技術之外的實際情況。
這些實際情況,讓我決定不用去定位這個問題。
這也不是逃避問題,這是權衡利弊之後的最佳選擇。
同樣是一天的時間,我可以去定位這個“重啟就能解決”的問題,也可以去做其他的更有價值事情,敲一些業務價值更大的程式碼。
這個是需要去權衡的,一個重要的衡量標準就是前面說的:投入產出比。
關於“不是所有的問題都必須被解決的,也可以選擇繞過問題”這個事情,我再給你舉一個我遇到的真實的例子。
幾年前,我們團隊遇到一個問題,我們使用的 RPC 框架是 Dubbo,有幾個核心服務在投產期間滾動釋出的時候,流量老是弄不乾淨,導致服務已經下線了,上游系統還在呼叫。
當時安排我去調研一下解決方案。
其實這就是一個優雅下線的問題,但是當時資歷尚淺,我認真研究了一段時間,確實沒研究出問題的根本解決方案。
後來我們給出的解決方案就是做一個容錯機制,如果投產期間有因為流量不乾淨的問題導致請求處理失敗的,我們把這些資料記錄下來,然後等到投產完成後再進行重發。
沒有解決根本問題,選擇繞過了問題,但是從最終結果上看,問題是被解決了。
再後來,我們搭建了雙中心。投產之前,A,B 中心都有流量,每次投產的時候,先把所有流量從 A 中心切到 B 中心去,在 A 中心沒有任何流量的情況下,進行服務投產。B 中心反之。
這樣,從投產流程上就規避了“流量老是弄不乾淨”的問題,因為投產的時候對應的服務已經沒有在途流量了,不需要考慮優雅的問題了,從而規避了優雅下線的問題。
問題還是沒有被解決,但是問題被徹底繞過。
最後,再舉一個我在知乎上看到的一個回答,和我想要表達的觀點,有異曲同工之妙:
https://www.zhihu.com/question/634940930/answer/3336285780
這個回答下面的評論也很有意思,有興趣的可以去翻一下,我擷取兩個我覺得有意思的:
在職場上,甚至在生活中,一個雖然沒有解決方案但是可以被繞過的問題,我認為不是問題。
但是這個也得分情況,不是所有問題都能繞開的,假如是一個關鍵服務,那肯定不能置之不理,硬著頭皮也得上。
關鍵是,我在職場上和生活中遇到過好多人,遇到問題的時候,似乎只會硬著頭皮往上衝。
只會硬著頭皮往上衝和知道什麼時候應該硬著頭皮往上衝,是兩種截然不同的職場階段。
所以有時候,遇到問題的時候,不要硬上,也讓頭皮休息一下,看看能不能繞過去。