一次線上OOM問題的個人復盤

来源:https://www.cnblogs.com/codelogs/archive/2023/04/01/17278811.html
-Advertisement-
Play Games

原創:扣釘日記(微信公眾號ID:codelogs),歡迎分享,非公眾號轉載保留此聲明。 上個月,我們一個java服務上線後,偶爾會發生記憶體OOM(Out Of Memory)問題,但由於OOM導致服務不響應請求,健康檢查多次不通過,最後部署平臺kill了java進程,這導致定位這次OOM問題也變得困 ...


原創:扣釘日記(微信公眾號ID:codelogs),歡迎分享,非公眾號轉載保留此聲明。

上個月,我們一個java服務上線後,偶爾會發生記憶體OOM(Out Of Memory)問題,但由於OOM導致服務不響應請求,健康檢查多次不通過,最後部署平臺kill了java進程,這導致定位這次OOM問題也變得困難起來。

最終,在多次review代碼後發現,是SQL意外地查出大量數據導致的,如下:

<sql id="conditions">
    <where>
        <if test="outerId != null">
            and `outer_id` = #{outerId}
        </if>
        <if test="orderType != null and orderType != ''">
            and `order_type` = #{orderType}
        </if>
        ...
    </where>
</sql>

<select id="queryListByConditions" resultMap="orderResultMap">
    select * from order <include refid="conditions"/> 
</select>

查詢邏輯類似上面的示例,在Service層有個根據outer_id的查詢方法,然後直接調用了Mapper層一個通用查詢方法queryListByConditions。

但我們有個調用量極低的場景,可以不傳outer_id這個參數,導致這個通用查詢方法沒有添加這個過濾條件,導致查了全表,進而導致OOM問題。

我們內部對這個問題進行了復盤,考慮到OOM問題還是蠻常見的,所以給大家也分享下。

事前

在OOM問題發生前,為什麼測試階段沒有發現問題?

其實在編寫技術方案時,是有考慮到這個場景的,但在提測時,忘記和測試同學溝通此場景,導致遺漏了此場景的測試驗證。

關於測試用例不全面,其實不管是疏忽問題、經驗問題、質量意識問題或人手緊張問題,從人的角度來說,都很難徹底避免,人沒法像機器那樣很聽話的、不疏漏的執行任何指令。

既然人做不到,那就讓機器來做,這就是單元測試、自動化測試的優勢,通過逐步積累測試用例,可覆蓋的場景就會越來越多。

當然,實施單元測試等方案,也會增加不少成本,需要權衡質量與研發效率誰更重要,畢竟在需求不能砍的情況下,質量與效率只能二選其一,這是任何一本項目管理的書都提到過的。

事中

在感知到OOM問題發生時,由於進程被部署平臺kill,導致現場丟失,難以快速定位到問題點。

一般java裡面是推薦使用-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/home/dump/這種JVM參數來保存現場的,這兩個參數的意思是,當JVM發生OOM異常時,自動dump堆記憶體到文件中,但在我們的場景中,這個方案難以生效,如下:

  1. 在堆占滿之前,會發生很多次FGC,jvm會盡最大努力騰挪空間,導致還沒有OOM時,系統實際已經不響應了,然後被kill了,這種場景無dump文件生成。
  2. 就算有時幸運,JVM發生了OOM異常開始dump,由於dump文件過大(我們約10G),導致dump文件還沒保存完,進程就被kill了,這種場景dump文件不完整,無法使用。

為瞭解決這個問題,有如下2種方案:

方案1:利用k8s容器生命周期內的Hook

我們部署平臺是套殼k8s的,k8s提供了preStop生命周期鉤子,在容器銷毀前會先執行此鉤子,只要將jmap -dump命令放入preStop中,就可以在k8s健康檢查不通過並kill容器前將記憶體dump出來。

要註意的是,正常發佈也會調用此鉤子,需要想辦法繞過,我們的辦法是將健康檢查也做成腳本,當不通過時創建一個臨時文件,然後在preStop腳本中判斷存在此文件才dump,preStop腳本如下:

if [ -f "/tmp/health_check_failed" ]; then
    echo "Health check failed, perform dumping and cleanups...";
    pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;
    if [[ $pid ]]; then
        jmap -dump:format=b,file=/home/work/logs/applogs/heap.hprof $pid
    fi
else
    echo "No health check failure detected. Exiting gracefully.";
fi 

註:也可以考慮在堆占用高時才dump記憶體,效果應該差不多。

方案2:容器中掛腳本監控堆占用,占用高時自動dump

#!/bin/bash

while sleep 1; do
    now_time=$(date +%F_%H-%M-%S)
    pid=`ps h -o pid --sort=-pmem -C java|head -n1|xargs`;
    [[ ! $pid ]] && { unset n pre_fgc; sleep 1m; continue; }
    data=$(jstat -gcutil $pid|awk 'NR>1{print $4,$(NF-2)}');
    read old fgc <<<"$data";
    echo "$now_time: $old $fgc";
    if [[ $(echo $old|awk '$1>80{print $0}') ]]; then
        (( n++ ))
    else
        (( n=0 ))
    fi
    if [[ $n -ge 3 || $pre_fgc && $fgc -gt $pre_fgc && $n -ge 1 ]]; then
        jstack $pid > /home/dump/jstack-$now_time.log;
        if [[ "$@" =~ dump ]];then
            jmap -dump:format=b,file=/home/dump/heap-$now_time.hprof $pid;
        else
            jmap -histo $pid > /home/dump/histo-$now_time.log;
        fi
        { unset n pre_fgc; sleep 1m; continue; }
    fi
    pre_fgc=$fgc
done

每秒檢查老年代占用,3次超過80%或發生一次FGC後還超過80%,記錄jstack、jmap數據,此腳本保存為jvm_old_mon.sh文件。

然後在程式啟動腳本中加入nohup bash jvm_old_mon.sh dump &即可,添加dump參數時會執行jmap -dump導全部堆數據,不添加時執行jmap -histo導對象分佈情況。

事後

為了避免同類OOM case再次發生,可以對查詢進行兜底,在底層對查詢SQL改寫,當發現查詢沒有limit時,自動添加limit xxx,避免查詢大量數據。
優點:對資料庫友好,查詢數據量少。
缺點:添加limit後可能會導致查詢漏數據,或使得本來會OOM異常的程式,添加limit後正常返回,並執行了後面意外的處理。

我們使用了Druid連接池,使用Druid Filter實現的話,大致如下:

public class SqlLimitFilter extends FilterAdapter {
    // 匹配limit 100或limit 100,100
    private static final Pattern HAS_LIMIT_PAT = Pattern.compile(
            "LIMIT\\s+[\\d?]+(\\s*,\\s*[\\d+?])?\\s*$", Pattern.CASE_INSENSITIVE);
    private static final int MAX_ALLOW_ROWS = 20000;

    /**
     * 若查詢語句沒有limit,自動加limit
     * @return 新sql
     */
    private String rewriteSql(String sql) {
        String trimSql = StringUtils.stripToEmpty(sql);
        // 不是查詢sql,不重寫
        if (!StringUtils.lowerCase(trimSql).startsWith("select")) {
            return sql;
        }
        // 去掉尾部分號
        boolean hasSemicolon = false;
        if (trimSql.endsWith(";")) {
            hasSemicolon = true;
            trimSql = trimSql.substring(0, trimSql.length() - 1);
        }
        // 還包含分號,說明是多條sql,不重寫
        if (trimSql.contains(";")) {
            return sql;
        }
        // 有limit語句,不重寫
        int idx = StringUtils.lowerCase(trimSql).indexOf("limit");
        if (idx > -1 && HAS_LIMIT_PAT.matcher(trimSql.substring(idx)).find()) {
            return sql;
        }
        StringBuilder sqlSb = new StringBuilder();
        sqlSb.append(trimSql).append(" LIMIT ").append(MAX_ALLOW_ROWS);
        if (hasSemicolon) {
            sqlSb.append(";");
        }
        return sqlSb.toString();
    }

    @Override
    public PreparedStatementProxy connection_prepareStatement(FilterChain chain, ConnectionProxy connection, String sql)
            throws SQLException {
        String newSql = rewriteSql(sql);
        return super.connection_prepareStatement(chain, connection, newSql);
    }
    //...此處省略了其它重載方法
}

本來還想過一種方案,使用MySQL的流式查詢並攔截jdbc層ResultSet.next()方法,在此方法調用超過指定次數時拋異常,但最終發現MySQL驅動在ResultSet.close()方法調用時,還是會讀取剩餘未讀數據,查詢沒法提前終止,故放棄之。


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

-Advertisement-
Play Games
更多相關文章
  • 今天我來分享一個關於日誌的問題和解法。 問題 沒有界面的後端程式在實際運行中發生了什麼事,通常是通過日誌來探查。所以日誌非常重要。資料庫記錄了程式運行的結果,日誌記錄了程式運行的過程。但是日誌經常出現一個問題,日誌量太多,以至於把重要的日誌淹沒在裡面。未能及時地發現問題,或者當有問題出現的時候,在日 ...
  • 項目背景 在java項目部署過程中,由於內外部各種因素,可能會遇到一些感覺操作不便捷的場景,例如 jar包未隨系統自動啟動需要每次手動重啟 系統vpn堡壘機多重防禦更新繁瑣 系統無圖形化界面命令行操作複雜 等等...... 在工作中之前也總結了windows的Jar包部署工具與linux下的jar包 ...
  • Java記憶體區域 說一下 JVM 的主要組成部分及其作用? JVM包含兩個子系統和兩個組件,兩個子系統為Class loader(類裝載)、Execution engine(執行引擎);兩個組件為Runtime data area(運行時數據區)、Native Interface(本地介面)。 ●C ...
  • 在使用codeium這個AI提示插件的過程中,使用中文註釋,智能提示的提示語,會有可能展示為亂碼、方塊字 如下圖中的灰色提示語: tab以後,就展示正常了。 在中文網上搜了下,沒有相關資料,去codeium的discord頻道問了下,找到瞭解答: 解答為:將首選項->編輯器->字體從“JetBrai ...
  • 1.相關組件 |組件 | 說明 |版本地址| | | | | |Nacos |配置及註冊中心 |https://github.com/alibaba/nacos/releases| ps: SpringBoot、SpringCloud和nacos集成版本對應關係對照(版本若對應不上,應用可能會啟動報 ...
  • #案例一 列印排序好的數據 #列表方式 lst_name=['林黛玉','薛寶釵','賈元春','賈探春','史湘雲'] lst_sign=['①','②','③','④','⑤'] for i in range(5): print(lst_sign[i],lst_name[i]) print(' ...
  • 7.1 潰壩 官網 目錄:$FOAM_TUTORIALS/multiphase/interFoam/laminar/damBreak 7.1.1 介紹 本案例使用interFoam兩相演算法,基於流體體積分數(VOF)法,每個網格中的相體積分數(alpha)通過求解一個組分運輸方程確定。物理屬性基於這 ...
  • 1、MySQL 中有哪幾種鎖? (1)表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖衝突的概率最 高,併發度最低。 (2)行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖衝突的概率最 低,併發度也最高。 (3)頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表 鎖 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...