一次 HashSet 所引起的併發問題

来源:https://www.cnblogs.com/crossoverJie/archive/2018/11/08/9926760.html
-Advertisement-
Play Games

上午剛到公司,準備開始一天的摸魚之旅時突然收到了一封監控中心的郵件。 心中暗道不好,因為監控系統從來不會告訴我應用完美無 bug,其實系統挺猥瑣。 打開郵件一看,果然告知我有一個應用的線程池隊列達到閾值觸發了報警。 ...


背景

上午剛到公司,準備開始一天的摸魚之旅時突然收到了一封監控中心的郵件。

心中暗道不好,因為監控系統從來不會告訴我應用完美無 bug,其實系統挺猥瑣。

打開郵件一看,果然告知我有一個應用的線程池隊列達到閾值觸發了報警。

由於這個應用出問題非常影響用戶體驗;於是立馬讓運維保留現場 dump 線程和記憶體同時重啟應用,還好重啟之後恢復正常。於是開始著手排查問題。

分析

首先瞭解下這個應用大概是做什麼的。

簡單來說就是從 MQ 中取出數據然後丟到後面的業務線程池中做具體的業務處理。

而報警的隊列正好就是這個線程池的隊列。

跟蹤代碼發現構建線程池的方式如下:

ThreadPoolExecutor executor = new ThreadPoolExecutor(coreSize, maxSize,
              0L, TimeUnit.MILLISECONDS,
              new LinkedBlockingQueue<Runnable>());;
             put(poolName,executor);

採用的是預設的 LinkedBlockingQueue 並沒有指定大小(這也是個坑),於是這個隊列的預設大小為 Integer.MAX_VALUE

由於應用已經重啟,只能從僅存的線程快照和記憶體快照進行分析。

記憶體分析

先利用 MAT 分析了記憶體,的到瞭如下報告。

其中有兩個比較大的對象,一個就是之前線程池存放任務的 LinkedBlockingQueue,還有一個則是 HashSet

當然其中隊列占用了大量的記憶體,所以優先查看,HashSet 一會兒再看。

由於隊列的大小給的夠大,所以結合目前的情況來看應當是線程池裡的任務處理較慢,導致隊列的任務越堆越多,至少這是目前可以得出的結論。

線程分析

再來看看線程的分析,這裡利用 fastthread.io 這個網站進行線程分析。

因為從表現來看線程池裡的任務遲遲沒有執行完畢,所以主要看看它們在幹嘛。

正好他們都處於 RUNNABLE 狀態,同時堆棧如下:

發現正好就是在處理上文提到的 HashSet,看這個堆棧是在查詢 key 是否存在。通過查看 312 行的業務代碼確實也是如此。

這裡的線程名字也是個坑,讓我找了好久。

定位

分析了記憶體和線程的堆棧之後其實已經大概猜出一些問題了。

這裡其實有一個前提忘記講到:

這個告警是凌晨三點發出的郵件,但並沒有電話提醒之類的,所以大家都不知道。

到了早上上班時才發現並立即 dump 了上面的證據。

所有有一個很重要的事實:這幾個業務線程在查詢 HashSet 的時候運行了 6 7 個小時都沒有返回

通過之前的監控曲線圖也可以看出:

操作系統在之前一直處於高負載中,直到我們早上看到報警重啟之後才降低。

同時發現這個應用生產上運行的是 JDK1.7 ,所以我初步認為應該是在查詢 key 的時候進入了 HashMap 的環形鏈表導致 CPU 高負載同時也進入了死迴圈。

為了驗證這個問題再次 review 了代碼。

整理之後的偽代碼如下:

//線程池
private ExecutorService executor;

private Set<String> set = new hashSet();

private void execute(){
    
    while(true){
        //從 MQ 中獲取數據
        String key = subMQ();
        executor.excute(new Worker(key)) ;
    }
}

public class Worker extends Thread{
    private String key ;

    public Worker(String key){
        this.key = key;
    }

    @Override
    private void run(){
        if(!set.contains(key)){

            //資料庫查詢
            if(queryDB(key)){
                set.add(key);
                return;
            }
        }

        //達到某種條件時清空 set
        if(flag){
            set = null ;
        }
    }   
}

大致的流程如下:

  • 源源不斷的從 MQ 中獲取數據。
  • 將數據丟到業務線程池中。
  • 判斷數據是否已經寫入了 Set
  • 沒有則查詢資料庫。
  • 之後寫入到 Set 中。

這裡有一個很明顯的問題,那就是作為共用資源的 Set 並沒有做任何的同步處理

這裡會有多個線程併發的操作,由於 HashSet 其實本質上就是 HashMap,所以它肯定是線程不安全的,所以會出現兩個問題:

  • Set 中的數據在併發寫入時被覆蓋導致數據不准確。
  • 會在擴容的時候形成環形鏈表

第一個問題相對於第二個還能接受。

通過上文的記憶體分析我們已經知道這個 set 中的數據已經不少了。同時由於初始化時並沒有指定大小,僅僅只是預設值,所以在大量的併發寫入時候會導致頻繁的擴容,而在 1.7 的條件下又可能會形成環形鏈表

不巧的是代碼中也有查詢操作(contains()),觀察上文的堆棧情況:

發現是運行在 HashMap 的 465 行,來看看 1.7 中那裡具體在做什麼:

已經很明顯了。這裡在遍歷鏈表,同時由於形成了環形鏈表導致這個 e.next 永遠不為空,所以這個迴圈也不會退出了。

到這裡其實已經找到問題了,但還有一個疑問是為什麼線程池裡的任務隊列會越堆越多。我第一直覺是任務執行太慢導致的。

仔細查看了代碼發現只有一個地方可能會慢:也就是有一個資料庫的查詢

把這個 SQL 拿到生產環境執行發現確實不快,查看索引發現都有命中。

但我一看表中的數據發現已經快有 7000W 的數據了。同時經過運維得知 MySQL 那台伺服器的 IO 壓力也比較大。

所以這個原因也比較明顯了:

由於每消費一條數據都要去查詢一次資料庫,MySQL 本身壓力就比較大,加上數據量也很高所以導致這個 IO 響應較慢,導致整個任務處理的就比較慢了。

但還有一個原因也不能忽視;由於所有的業務線程在某個時間點都進入了死迴圈,根本沒有執行完任務的機會,而後面的數據還在源源不斷的進入,所以這個隊列只會越堆越多!

這其實是一個老應用了,可能會有人問為什麼之前沒出現問題。

這是因為之前數據量都比較少,即使是併發寫入也沒有出現併發擴容形成環形鏈表的情況。這段時間業務量的暴增正好把這個隱藏的雷給揪出來了。所以還是得信墨菲他老人家的話。

總結

至此整個排查結束,而我們後續的調整措施大概如下:

  • HashSet 不是線程安全的,換為 ConcurrentHashMap同時把 value 寫死一樣可以達到 set 的效果。
  • 根據我們後面的監控,初始化 ConcurrentHashMap 的大小儘量大一些,避免頻繁的擴容。
  • MySQL 中很多數據都已經不用了,進行冷熱處理。儘量降低單表數據量。同時後期考慮分表。
  • 查數據那裡調整為查緩存,提高查詢效率。
  • 線程池的名稱一定得取的有意義,不然是自己給自己增加難度。
  • 根據監控將線程池的隊列大小調整為一個具體值,並且要有拒絕策略。
  • 升級到 JDK1.8
  • 再一個是報警郵件酌情考慮為電話通知
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 廢話少說,見官方文檔: 他的用法是:document.getElementById('username').focus(); 這樣寫在display:block;顯示之後就可以自動激活input輸入框啦! 這並不是最有意思的地方,最有意思的地方是他可以觸發該元素的 相應 事件; 接下來來舉個慄子: ...
  • 前端進階肯定會遇到npm(包管理工具)的使用,下麵是我總結的一些比較實用的npm指令: npm install <name>安裝nodejs的依賴包 例如npm install express 就會預設安裝express的最新版本,也可以通過在後面加版本號的方式安裝指定版本,如npm install ...
  • 一步一步實現web程式信息管理系統 在web程式中特別是信息管理系統, 登陸 功能必須有而且特別重要。每一個學習程式開發或以後工作中,都會遇到實現 登陸 功能的需求。而登陸功能最終提供給客戶或展現給客戶的最基本的就是2個文本框一個 按鈕用戶名與密碼,外加一個登陸按鈕 。本篇記錄一下登陸功能的前端界面 ...
  • 我們都知道企業建網站目的得到更多的潛在用戶,那麼現在建出企業需求的、吸引潛在用戶的網站呢? 下麵搜客建站就來和大家說說:如何製作出吸引潛在用戶的網站? 一、網站頁面的風格設計 設計網站就好比我們平時評論一個女人美不美,我們要從她的衣著打扮來判斷的,那麼同樣的,我們判斷一個網站的設計風格如何,就要從它 ...
  • 官網 http://www.fhadmin.org/D 集成安全許可權框架shiro Shiro 是一個用 Java 語言實現的框架,通過一個簡單易用的 API 提供身份驗證和授權,更安全,更可靠E 集成ehcache分散式緩存 是一個純Java的進程內緩存框架,具有快速、精幹等特點,廣泛使用的開源J ...
  • ASP.net技術支撐,learun工作流開發分享 一、工作流 根據的定義,工作流就是自動運作的業務過程部分或整體,表現為參與者對文件、信息或任務按照規程採取行動,並令其在參與者之間傳遞。簡單地說,工作流就是一系列相互銜接、自動進行的業務活動或任務。 工作流是針對工作中具有固定程式的常規活動而提出的 ...
  • 原創作品,轉載請註明出處:https://www.cnblogs.com/sunshine5683/p/9927186.html 今天在工作中遇到對一個已知的一維數組取出其最大值和最小值,分別用於參與其他運算,廢話不多說,直接上代碼。 package xhq.text; public class M ...
  • 1. 關於精度: 取整 除法取整: (除數為正)被除數為正時系統除法為向下取整,被除數為負時系統除法為向上取整。 向上取整(被除數非負,除數為正): 一般寫法(有bug): 上述寫法只適用於x為正的情況,x為0時有錯誤。 正確寫法: 或 庫函數(cmath庫) : (返回值為double) 向上取整 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...