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

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 [
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章