生產事故-記一次特殊的OOM排查

来源:https://www.cnblogs.com/mylibs/archive/2023/04/07/production-accident-0002.html
-Advertisement-
Play Games

入職多年,面對生產環境,儘管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背後,都是寶貴的經驗和教訓,都是項目成員的血淚史。為了更好地防範和遏制今後的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授 ...


入職多年,面對生產環境,儘管都是小心翼翼,慎之又慎,還是難免捅出簍子。輕則滿頭大汗,面紅耳赤。重則系統停擺,損失資金。每一個生產事故的背後,都是寶貴的經驗和教訓,都是項目成員的血淚史。為了更好地防範和遏制今後的各類事故,特開此專題,長期更新和記錄大大小小的各類事故。有些是親身經歷,有些是經人耳傳口授,但無一例外都是真實案例。

註意:為了避免不必要的麻煩和商密問題,文中提到的特定名稱都將是化名、代稱。

0x00 大綱

目錄

0x01 事故背景

2023年3月10日14時19分,C公司開發人員向A公司開發人員反映某開放介面從2023年3月10日14時許開始無法訪問和使用。該系統為某基礎數據介面服務,基於 HTTP 協議進行通信。按照慣例,首先排查網路是否異常,經運維人員檢查,證明網路連通性沒有問題。A公司開發組於2023年3月10日14時30分通知運維人員重啟應用服務,期間短暫恢復正常。但是,很快,十分鐘後,電話再次響起,告知服務又出現異常,無法訪問。為了避免影響進一步擴大,A公司決定將程式緊急回滾至上一穩定版本。回滾後,系統業務功能恢復正常。短暫松一口氣後,開始排查問題。

0x02 事故分析

讓運維拷貝和固定了更新前後的系統日誌和應用包。根據前面的故障現象,初步猜測是記憶體問題,好在應用啟停腳本中增加了參數-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/app/logs/app.dump(對於無法在生產環境上使用jstackjmap等命令直接查錯的——事實上大多數時候都不能,dump文件顯得尤為重要),果不其然,日誌目錄下出現了app.dump文件,在日誌中搜索,找到了若幹處記憶體溢出錯誤java.lang.OutOfMemoryError: Java heap space,但是令人費解的是每次出現OOM錯誤的位置居然都不一樣,事情逐漸變得複雜起來。

MAT(Memory Analyzer Tool)工具打開轉儲文件,原以為會發現某個類型對象占用大量的記憶體,結果出乎意料,Histogram(直方圖)中顯示活躍對象居然只有100多M!嘗試 Calculate Precise Retained Size(計算精確大小),計算結果與前面相差不大。檢查 Outgoing References (追蹤引用對象)和 Incoming References(追蹤被引用對象)也未見明顯異常,令人頭大。

擦擦汗,日誌已經明確提示我們java.lang.OutOfMemoryError: Java heap space,首先肯定這是一個堆記憶體空間引起的問題,可能的原因有:

  • 記憶體載入數據量過大

    例如不受行數限制的資料庫查詢語句,或者不限制位元組數的文件讀取等,事故系統顯然沒有這些情況;

  • 記憶體泄漏(資源未關閉/無法回收)

    當系統存在大量未關閉的 IO 資源,或者錯誤使用ThreadLocal等場景時也會發生OOM,經排查,也不存在這種情況;

  • 系統記憶體不足

    系統記憶體不足以支撐當前業務場景所需要的記憶體,過小的機器記憶體或者不合理的JVM記憶體參數。

如果排除所有合理選項,最不合理那個會不會就是答案呢?遂開始檢查機器的記憶體,根據運維的說法,機器記憶體為16GB,top命令查看java進程占用記憶體約為7.8GB,看起來似乎沒毛病。

但是隨後另一個同事註意到了一個事情,最後一次系統升級的時候,改動過應用啟停腳本,對比舊版本的腳本,發現差異部分就是記憶體參數:

舊版本原為:

-Xms8g -Xmx8g -Xmn3g

新版本改為:

-Xms8g -Xmx8g -Xmn8g

看到這裡,屏幕前的一眾同事都無語啊……

0x03 事故原因

為什麼-Xmn參數設置成與-Xmx參數一樣的大小會導致OOM呢?該項目使用的JDK版本為1.8,看看JDK 8的記憶體模型:

JDK8記憶體模型

不難發現,Heap Space Size = Young Space Size + Old Space Size,而-Xmn參數控制的正是 Young 區的大小,當堆區被 Young Gen 完全擠占,又有對象想要升代到 Old Gen 時,發現 Old 區空間不足,於是觸發 Full GC,觸發 Full GC 以後呢,通常又會面臨兩種情況:

  • Young 區又剛好騰出來一點空間,對象又不用放到 Old 區裡面了,皆大歡喜
  • Young 區空間還是不夠,對象還是得放到 Old 區,Old 區空間不夠,卒,喜提OOM
  • 誒,就是奔著 Old 區去的,管你 Young 不 Young,Old 區空間不夠,卒,喜提OOM

這個就解釋了為什麼系統剛剛啟動時,會有一個短時間正常工作的現象,隨後,當某段程式觸發 Old Gen 升代時,就會發生隨機的OOM錯誤。那麼什麼時候對象會進入老年代呢?這裡也很有意思,不妨結合日誌裡面出現OOM的地方,對號入座:

  • 經歷足夠多次數 GC 依然存活的對象
  • 申請一個大對象(比如超過 Eden 區一半大小)
  • GC 後 Eden 區對象大小超過 S 區之和
  • Eden 區 + S0 區 GC 後,S1 區放不下

換言之,正常情況下,-Xmn參數總是應當小於-Xmx參數,否則就會觸發OOM錯誤。我們可以構造一個簡單的例子來驗證這個場景。首先是一個簡單的SpringBoot程式:

package com.example.oom;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Random;

@SpringBootApplication
public class OomApplication {
    static final byte[] ARRAY = new byte[128 * 1024 * 1024];

    public static void main(String[] args) {
        SpringApplication.run(OomApplication.class, args);
    }

    @RestController
    public static class OomExampleController {
        @GetMapping("/oom")
        public int oom() {
            byte[] temp = new byte[128 * 1024 * 1024];
            temp[0] = (byte) 0xff;
            temp[temp.length - 1] = (byte) 0xef;
            int noise = new Random().nextInt();
            ARRAY[0] = (byte) (temp[0] + temp[temp.length - 1] + noise);
            return ARRAY[0];
        }
    }
}

使用mvn clean package命令打包後,我們用下麵的命令啟動它:

java -Xms512m -Xmx512m -Xmn512m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar oom-1.0.0-RELEASE.jar

然後藉助Apacheab.exe,完成我們的驗證測試。先是以1個併發訪問100次上面的SpringBoot介面:

ab -c 1 -n 100 http://localhost:8080/oom

你會發現,它居然是可以正常運行的,然後我們模擬用戶負載上來之後的情況,使用2個併發訪問100次:

ab -c 2 -n 100 http://localhost:8080/oom

如果前面的步驟都沒錯,此時應該在SpringBoot應用控制台看到大量的OOM錯誤,如下圖所示:

模擬OOM結果

然後在 GC 日誌裡面會看到,觸發 GC 的前後,Old 區幾乎都沒有空間,僅有的一點點還是JDK強行分配的(在啟動JVM時強制覆寫了我們的-Xmn參數):

{Heap before GC invocations=279 (full 139):
 PSYoungGen      total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
  eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
  from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
  to   space 65024K, 0% used [0x00000000f8100000,0x00000000f8100000,0x00000000fc080000)
 ParOldGen       total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
  object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
 Metaspace       used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
  class space    used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
2023-04-07T01:44:25.348+0800: 57.446: [GC (Allocation Failure) --[PSYoungGen: 273877K->273877K(458752K)] 274384K->274384K(459264K), 0.0441401 secs] [Times: user=0.06 sys=0.30, real=0.04 secs] 
Heap after GC invocations=279 (full 139):
 PSYoungGen      total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
  eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
  from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
  to   space 65024K, 9% used [0x00000000f8100000,0x00000000f86e2070,0x00000000fc080000)
 ParOldGen       total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
  object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
 Metaspace       used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
  class space    used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
}
{Heap before GC invocations=280 (full 140):
 PSYoungGen      total 458752K, used 273877K [0x00000000e0080000, 0x0000000100000000, 0x0000000100000000)
  eden space 393728K, 69% used [0x00000000e0080000,0x00000000f0bf5798,0x00000000f8100000)
  from space 65024K, 0% used [0x00000000fc080000,0x00000000fc080000,0x0000000100000000)
  to   space 65024K, 9% used [0x00000000f8100000,0x00000000f86e2070,0x00000000fc080000)
 ParOldGen       total 512K, used 506K [0x00000000e0000000, 0x00000000e0080000, 0x00000000e0080000)
  object space 512K, 98% used [0x00000000e0000000,0x00000000e007e910,0x00000000e0080000)
 Metaspace       used 35959K, capacity 38240K, committed 38872K, reserved 1083392K
  class space    used 4533K, capacity 4953K, committed 5080K, reserved 1048576K
2023-04-07T01:44:25.392+0800: 57.490: [Full GC (Ergonomics) [PSYoungGen: 273877K->142631K(458752K)] [ParOldGen: 506K->506K(512K)] 274384K->143137K(459264K), [Metaspace: 35959K->35959K(1083392K)], 0.0248171 secs] [Times: user=0.14 sys=0.00, real=0.03 secs] 

接著無需改動任何代碼,我們調整下啟動參數,像這樣:

java -Xms512m -Xmx512m -Xmn64m -XX:+HeapDumpOnOutOfMemoryError -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC -Xloggc:gc.log -jar oom-1.0.0-RELEASE.jar

你會發現它又可以了。這是一個為了驗證而打造的極端例子,實際上生產的應用情況會比這個複雜得多,但這並不妨礙我們理解它的意圖。

0x04 事故復盤

這是一場典型的”人禍“,來源於某個同事的”調優“,比起追究責任,更重要的是帶給我們的啟發:

  • 即使是應用啟停腳本,也應該作為程式的一部分,納入測試驗證流程和上線檢查清單,禁止隨意變更;
  • 很多時候,預設的就是最好的,矯枉則常常過正。

0x05 事故影響

造成C公司關鍵業務停擺半小時,生產系統緊急回滾一次。A公司相關負責人連夜編寫事故報告一份。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 <template> 2 <el-form ref="form" :model="form" :rules="rules" label-width="100px"> 3 <div v-for="(input, index) in inputs" :key="index"> 4 <el-form- ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 瞭解 console ● 什麼是 console ? console 其實是 JavaScript 內的一個原生對象 內部存儲的方法大部分都是在瀏覽器控制台輸出一些內容 並且還提供了很多的輔助方法 ● 最常見的 console 我們在開發 ...
  • 基於 Node.js、Express.js 和 MongoDB 通過Mongoose驅動進行 REST API 開發的輕量級樣板。集成了Swagger UI、JWT、session、發送郵箱驗證、日誌管理、統一的預定義狀態碼響應格式等,對於為前端平臺構建純凈的Web API非常有用。 ...
  • 前面已經介紹過CSS偽類的知識,具體可見前文 CSS偽類知識詳解。 偽元素常常被誤解為偽類,主要在於他們的語法相似,都是對於選擇器功能的擴展,相似程度很高導致被混淆。 本文通過詳細介紹偽元素和常見的使用方法,最後也會分析下偽元素與偽類的基本區別。 基本描述 CSS偽元素也是應用於選擇器的關鍵字,允許 ...
  • MVC模式(Model-View-Controller):是一種前端和後端都廣泛應用的設計模式。它將應用程式的業務邏輯、數據表示和用戶界面分離,使得開發人員可以獨立地修改各部分而不影響其他部分。MVC設計模式有助於提高代碼的可讀性、可維護性和可重用性。 MVC是Model-View-Controll ...
  • 如何為組件添加 CSS 的 class? 傳遞一個字元串作為 className 屬性: render() { return <span className="menu navigation-menu">Menu</span> } CSS 的 class 依賴組件的 props 或 state 的情 ...
  • 過濾器模式(Filter Pattern)或標準模式(Criteria Pattern),是一種結構型模式。這種模式允許使用不同的標準條件來過濾一組對象,並通過邏輯運算的方式把各條件連接起來,它結合多個標準來獲得單一標準。 例子將創建一個 Person 對象、Criteria 介面和實現了該介面的實... ...
  • 從我們作為業務開發主要的職責深入到DDD的本質是什麼?複雜度應處理?規範設計怎麼做?本文將全方位為大家解答。 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 本文介紹一款使用 C# 與 WPF 開發的音頻播放器,其界面簡潔大方,操作體驗流暢。該播放器支持多種音頻格式(如 MP4、WMA、OGG、FLAC 等),並具備標記、實時歌詞顯示等功能。 另外,還支持換膚及多語言(中英文)切換。核心音頻處理採用 FFmpeg 組件,獲得了廣泛認可,目前 Git ...
  • OAuth2.0授權驗證-gitee授權碼模式 本文主要介紹如何筆者自己是如何使用gitee提供的OAuth2.0協議完成授權驗證並登錄到自己的系統,完整模式如圖 1、創建應用 打開gitee個人中心->第三方應用->創建應用 創建應用後在我的應用界面,查看已創建應用的Client ID和Clien ...
  • 解決了這個問題:《winForm下,fastReport.net 從.net framework 升級到.net5遇到的錯誤“Operation is not supported on this platform.”》 本文內容轉載自:https://www.fcnsoft.com/Home/Sho ...
  • 國內文章 WPF 從裸 Win 32 的 WM_Pointer 消息獲取觸摸點繪製筆跡 https://www.cnblogs.com/lindexi/p/18390983 本文將告訴大家如何在 WPF 裡面,接收裸 Win 32 的 WM_Pointer 消息,從消息裡面獲取觸摸點信息,使用觸摸點 ...
  • 前言 給大家推薦一個專為新零售快消行業打造了一套高效的進銷存管理系統。 系統不僅具備強大的庫存管理功能,還集成了高性能的輕量級 POS 解決方案,確保頁面載入速度極快,提供良好的用戶體驗。 項目介紹 Dorisoy.POS 是一款基於 .NET 7 和 Angular 4 開發的新零售快消進銷存管理 ...
  • ABP CLI常用的代碼分享 一、確保環境配置正確 安裝.NET CLI: ABP CLI是基於.NET Core或.NET 5/6/7等更高版本構建的,因此首先需要在你的開發環境中安裝.NET CLI。這可以通過訪問Microsoft官網下載並安裝相應版本的.NET SDK來實現。 安裝ABP ...
  • 問題 問題是這樣的:第三方的webapi,需要先調用登陸介面獲取Cookie,訪問其它介面時攜帶Cookie信息。 但使用HttpClient類調用登陸介面,返回的Headers中沒有找到Cookie信息。 分析 首先,使用Postman測試該登陸介面,正常返回Cookie信息,說明是HttpCli ...
  • 國內文章 關於.NET在中國為什麼工資低的分析 https://www.cnblogs.com/thinkingmore/p/18406244 .NET在中國開發者的薪資偏低,主要因市場需求、技術棧選擇和企業文化等因素所致。歷史上,.NET曾因微軟的閉源策略發展受限,儘管後來推出了跨平臺的.NET ...
  • 在WPF開發應用中,動畫不僅可以引起用戶的註意與興趣,而且還使軟體更加便於使用。前面幾篇文章講解了畫筆(Brush),形狀(Shape),幾何圖形(Geometry),變換(Transform)等相關內容,今天繼續講解動畫相關內容和知識點,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 什麼是委托? 委托可以說是把一個方法代入另一個方法執行,相當於指向函數的指針;事件就相當於保存委托的數組; 1.實例化委托的方式: 方式1:通過new創建實例: public delegate void ShowDelegate(); 或者 public delegate string ShowDe ...